push fixes and tests

This commit is contained in:
klizhentas 2016-02-24 13:19:36 -08:00
parent db8117b676
commit df59710382
10 changed files with 235 additions and 37 deletions

4
Godeps/Godeps.json generated
View file

@ -65,11 +65,11 @@
},
{
"ImportPath": "github.com/gravitational/roundtrip",
"Rev": "348c0b57968dcd9fef02239abf9b7688881d65e6"
"Rev": "6f5b466bba024cee2c0a549bf8844d98f07fd5a3"
},
{
"ImportPath": "github.com/gravitational/trace",
"Rev": "bdd69590106bbc949e797d0ed545489ec6f36eca"
"Rev": "af77d5facfcfa8cdccdae73865728c8c0ac3dbf6"
},
{
"ImportPath": "github.com/jonboulle/clockwork",

View file

@ -606,7 +606,7 @@ func (c *Client) CreateUserWithToken(token, password, hotpToken string) (*Sessio
if err := json.Unmarshal(out.Bytes(), &sess); err != nil {
return nil, trace.Wrap(err)
}
return nil, trace.Wrap(err)
return sess, nil
}
type chunkRW struct {

View file

@ -198,6 +198,12 @@ func (s *AuthServer) CreateUserWithToken(token, password, hotpToken string) (*Se
if err != nil {
return nil, trace.Wrap(err)
}
err = s.UpsertWebSession(tokenData.User, sess, WebSessionTTL)
if err != nil {
return nil, trace.Wrap(err)
}
sess.WS.Priv = nil
return sess, nil
}

View file

@ -13,6 +13,7 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package web
import (

View file

@ -20,6 +20,8 @@ import (
"encoding/hex"
"encoding/json"
"net/http"
log "github.com/Sirupsen/logrus"
)
// SessionCookie stores information about active user and session
@ -29,6 +31,7 @@ type SessionCookie struct {
}
func EncodeCookie(user, sid string) (string, error) {
log.Infof("Encod: %v %v", user, sid)
bytes, err := json.Marshal(SessionCookie{User: user, SID: sid})
if err != nil {
return "", err
@ -45,6 +48,7 @@ func DecodeCookie(b string) (*SessionCookie, error) {
if err := json.Unmarshal(bytes, &c); err != nil {
return nil, err
}
log.Infof("DEncod: %v %v", c.User, c.SID)
return c, nil
}

View file

@ -87,6 +87,8 @@ func NewMultiSiteHandler(cfg MultiSiteConfig) (http.Handler, error) {
// SSH proxy web login
h.POST("/sshlogin", h.loginSSHProxy)
h.GET("/webapi/sites", h.needsAuth(h.getSites))
// Forward all requests to site handler
sh := h.needsAuth(h.siteHandler)
h.GET("/webapi/sites/:site/*path", sh)
@ -248,6 +250,42 @@ func (m *MultiSiteHandler) createNewUser(w http.ResponseWriter, r *http.Request,
}, nil
}
type getSitesResponse struct {
Sites []site `json:"sites"`
}
type site struct {
Name string `json:"name"`
LastConnected time.Time `json:"last_connected"`
Status string `json:"status"`
}
func convertSites(rs []reversetunnel.RemoteSite) []site {
out := make([]site, len(rs))
for i := range rs {
out[i] = site{
Name: rs[i].GetName(),
LastConnected: rs[i].GetLastConnected(),
Status: rs[i].GetStatus(),
}
}
return out
}
// getSites returns a list of sites
//
// GET /v1/webapi/sites
//
// Sucessful response:
//
// {"sites": {"name": "localhost", "last_connected": "RFC3339 time", "status": "active"}}
//
func (h *MultiSiteHandler) getSites(w http.ResponseWriter, r *http.Request, _ httprouter.Params, c Context) (interface{}, error) {
return getSitesResponse{
Sites: convertSites(h.cfg.Tun.GetSites()),
}, nil
}
func message(msg string) interface{} {
return map[string]interface{}{"message": msg}
}
@ -480,23 +518,32 @@ type contextHandler func(w http.ResponseWriter, r *http.Request, p httprouter.Pa
func (h *MultiSiteHandler) needsAuth(fn contextHandler) httprouter.Handle {
return httplib.MakeHandler(func(w http.ResponseWriter, r *http.Request, p httprouter.Params) (interface{}, error) {
logger := log.WithFields(log.Fields{
"request": fmt.Sprintf("%v %v", r.Method, r.URL.String()),
})
logger.Infof("incoming request")
cookie, err := r.Cookie("session")
if err != nil {
logger.Warningf("missing cookie: %v", err)
return nil, trace.Wrap(teleport.AccessDenied("missing cookie"))
}
d, err := DecodeCookie(cookie.Value)
if err != nil {
logger.Warningf("failed to decode cookie: %v", err)
return nil, trace.Wrap(teleport.AccessDenied("failed to decode cookie"))
}
ctx, err := h.auth.ValidateSession(d.User, d.SID)
if err != nil {
return nil, trace.Wrap(err)
}
creds, err := roundtrip.ParseAuthHeaders(r)
if err != nil {
return nil, trace.Wrap(err)
logger.Warningf("no auth headers %v", err)
return nil, trace.Wrap(teleport.AccessDenied("need auth"))
}
ctx, err := h.auth.ValidateSession(d.User, d.SID)
if err != nil {
logger.Warningf("invalid session: %v", err)
return nil, trace.Wrap(teleport.AccessDenied("need auth"))
}
if creds.Password != d.SID {
logger.Warningf("bad auth token")
return nil, trace.Wrap(teleport.AccessDenied("missing auth token"))
}
return fn(w, r, p, ctx)
@ -532,24 +579,6 @@ func New(addr utils.NetAddr, cfg MultiSiteConfig) (*Server, error) {
return srv, nil
}
type site struct {
Name string `json:"name"`
LastConnected time.Time `json:"last_connected"`
Status string `json:"status"`
}
func sitesResponse(rs []reversetunnel.RemoteSite) []site {
out := make([]site, len(rs))
for i := range rs {
out[i] = site{
Name: rs[i].GetName(),
LastConnected: rs[i].GetLastConnected(),
Status: rs[i].GetStatus(),
}
}
return out
}
func CreateSignupLink(hostPort string, token string) string {
return "http://" + hostPort + "/web/newuser/" + token
}

View file

@ -19,6 +19,7 @@ package web
import (
"encoding/json"
"fmt"
"net/http/cookiejar"
"net/http/httptest"
"net/url"
"os/user"
@ -34,6 +35,7 @@ import (
"github.com/gravitational/teleport/lib/backend/encryptedbk"
"github.com/gravitational/teleport/lib/backend/encryptedbk/encryptor"
"github.com/gravitational/teleport/lib/events/boltlog"
"github.com/gravitational/teleport/lib/httplib"
"github.com/gravitational/teleport/lib/limiter"
"github.com/gravitational/teleport/lib/recorder/boltrec"
"github.com/gravitational/teleport/lib/reversetunnel"
@ -43,6 +45,7 @@ import (
"github.com/gravitational/teleport/lib/sshutils"
"github.com/gravitational/teleport/lib/utils"
"github.com/gokyle/hotp"
"github.com/gravitational/roundtrip"
"golang.org/x/crypto/ssh"
. "gopkg.in/check.v1"
@ -198,12 +201,20 @@ func (s *WebSuite) SetUpTest(c *C) {
s.webServer = httptest.NewServer(handler)
}
func (s *WebSuite) client() *roundtrip.Client {
clt, err := roundtrip.NewClient("http://"+s.webServer.Listener.Addr().String(), "v1")
func (s *WebSuite) url() *url.URL {
u, err := url.Parse("http://" + s.webServer.Listener.Addr().String())
if err != nil {
panic(err)
}
return clt
return u
}
func (s *WebSuite) client(opts ...roundtrip.ClientParam) *testClient {
clt, err := roundtrip.NewClient(s.url().String(), "v1", opts...)
if err != nil {
panic(err)
}
return &testClient{clt}
}
func (s *WebSuite) TearDownTest(c *C) {
@ -224,4 +235,122 @@ func (s *WebSuite) TestNewUser(c *C) {
c.Assert(json.Unmarshal(re.Bytes(), &out), IsNil)
c.Assert(out.User, Equals, "bob")
c.Assert(out.InviteToken, Equals, token)
_, _, hotpValues, err := s.roleAuth.GetSignupTokenData(token)
c.Assert(err, IsNil)
tempPass := "abc123"
re, err = clt.PostJSON(clt.Endpoint("webapi", "users"), createNewUserReq{
InviteToken: token,
Pass: tempPass,
SecondFactorToken: hotpValues[0],
})
c.Assert(err, IsNil)
var sess *createSessionResponse
c.Assert(json.Unmarshal(re.Bytes(), &sess), IsNil)
cookies := re.Cookies()
c.Assert(len(cookies), Equals, 1)
// now make sure we are logged in by calling authenticated method
// we need to supply both session cookie and bearer token for
// request to succeed
jar, err := cookiejar.New(nil)
c.Assert(err, IsNil)
clt = s.client(roundtrip.BearerAuth(sess.Token), roundtrip.CookieJar(jar))
jar.SetCookies(s.url(), re.Cookies())
re, err = clt.Get(clt.Endpoint("webapi", "sites"), url.Values{})
c.Assert(err, IsNil)
var sites *getSitesResponse
c.Assert(json.Unmarshal(re.Bytes(), &sites), IsNil)
// in absense of session cookie or bearer auth the same request fill fail
// no session cookie:
clt = s.client(roundtrip.BearerAuth(sess.Token))
re, err = clt.Get(clt.Endpoint("webapi", "sites"), url.Values{})
c.Assert(err, NotNil)
c.Assert(teleport.IsAccessDenied(err), Equals, true)
// no bearer token:
clt = s.client(roundtrip.CookieJar(jar))
re, err = clt.Get(clt.Endpoint("webapi", "sites"), url.Values{})
c.Assert(err, NotNil)
c.Assert(teleport.IsAccessDenied(err), Equals, true)
}
type authPack struct {
user string
pass string
otp *hotp.HOTP
session *createSessionResponse
clt *testClient
}
// authPack returns new authenticated package consisting
// of created valid user, hotp token, created web session and
// authenticated client
func (s *WebSuite) authPack(c *C) *authPack {
user := "bob"
pass := "abc123"
hotpURL, _, err := s.roleAuth.UpsertPassword(user, []byte(pass))
c.Assert(err, IsNil)
otp, _, err := hotp.FromURL(hotpURL)
c.Assert(err, IsNil)
otp.Increment()
clt := s.client()
re, err := clt.PostJSON(clt.Endpoint("webapi", "sessions"), createSessionReq{
User: user,
Pass: pass,
SecondFactorToken: otp.OTP(),
})
c.Assert(err, IsNil)
var sess *createSessionResponse
c.Assert(json.Unmarshal(re.Bytes(), &sess), IsNil)
jar, err := cookiejar.New(nil)
c.Assert(err, IsNil)
clt = s.client(roundtrip.BearerAuth(sess.Token), roundtrip.CookieJar(jar))
jar.SetCookies(s.url(), re.Cookies())
return &authPack{
user: user,
pass: pass,
session: sess,
clt: clt,
}
}
func (s *WebSuite) TestWebSessionsCRUD(c *C) {
pack := s.authPack(c)
// make sure we can use client to make authenticated requests
re, err := pack.clt.Get(pack.clt.Endpoint("webapi", "sites"), url.Values{})
c.Assert(err, IsNil)
var sites *getSitesResponse
c.Assert(json.Unmarshal(re.Bytes(), &sites), IsNil)
}
type testClient struct {
*roundtrip.Client
}
func (t *testClient) PostJSON(
endpoint string, val interface{}) (*roundtrip.Response, error) {
return httplib.ConvertResponse(t.Client.PostJSON(endpoint, val))
}
func (t *testClient) Get(
endpoint string, val url.Values) (*roundtrip.Response, error) {
return httplib.ConvertResponse(t.Client.Get(endpoint, val))
}

View file

@ -48,6 +48,7 @@ import (
"strings"
)
// ClientParam specifies functional argument for client
type ClientParam func(c *Client) error
// HTTPClient is a functional parameter that sets the internal
@ -67,7 +68,7 @@ func BasicAuth(username, password string) ClientParam {
}
}
// BearerAuth sets username and token for HTTP client
// BearerAuth sets token for HTTP client
func BearerAuth(token string) ClientParam {
return func(c *Client) error {
c.auth = &bearerAuth{token: token}
@ -75,6 +76,14 @@ func BearerAuth(token string) ClientParam {
}
}
// CookieJar sets http cookie jar for this client
func CookieJar(jar http.CookieJar) ClientParam {
return func(c *Client) error {
c.jar = jar
return nil
}
}
// Client is a wrapper holding HTTP client. It hold target server address and a version prefix,
// and provides common features for building HTTP client wrappers.
type Client struct {
@ -84,9 +93,10 @@ type Client struct {
v string
// client is a private http.Client instance
client *http.Client
// auth tells client to use HTTP auth on every request
auth fmt.Stringer
// jar is a set of cookies passed with requests
jar http.CookieJar
}
// NewClient returns a new instance of roundtrip.Client, or nil and error
@ -100,13 +110,16 @@ func NewClient(addr, v string, params ...ClientParam) (*Client, error) {
c := &Client{
addr: addr,
v: v,
client: http.DefaultClient,
client: &http.Client{},
}
for _, p := range params {
if err := p(c); err != nil {
return nil, err
}
}
if c.jar != nil {
c.client.Jar = c.jar
}
return c, nil
}
@ -288,7 +301,12 @@ func (c *Client) RoundTrip(fn RoundTripFn) (*Response, error) {
if err != nil {
return nil, err
}
return &Response{code: re.StatusCode, headers: re.Header, body: buf}, nil
return &Response{
code: re.StatusCode,
headers: re.Header,
body: buf,
cookies: re.Cookies(),
}, nil
}
// SetAuthHeader sets client's authorization headers if client
@ -310,6 +328,12 @@ type Response struct {
code int
headers http.Header
body *bytes.Buffer
cookies []*http.Cookie
}
// Cookies returns a list of cookies set by server
func (r *Response) Cookies() []*http.Cookie {
return r.cookies
}
// Code returns HTTP response status code

View file

@ -48,7 +48,6 @@ func (tf *TextFormatter) Format(e *log.Entry) ([]byte, error) {
if frameNo := findFrame(); frameNo != -1 {
t := newTrace(runtime.Caller(frameNo - 1))
e.Data[FileField] = t.String()
e.Data[FunctionField] = t.Func
}
return (&tf.TextFormatter).Format(e)
}

View file

@ -23,13 +23,19 @@ import (
"path/filepath"
"runtime"
"strings"
"sync/atomic"
)
var debug bool
var debug int32
// EnableDebug turns on debugging mode, that causes Fatalf to panic
func EnableDebug() {
debug = true
atomic.StoreInt32(&debug, 1)
}
// IsDebug returns true if debug mode is on, false otherwize
func IsDebug() bool {
return atomic.LoadInt32(&debug) == 1
}
// Wrap takes the original error and wraps it into the Trace struct
@ -63,7 +69,7 @@ func Errorf(format string, args ...interface{}) error {
// Fatalf - If debug is false Fatalf calls Errorf. If debug is
// true Fatalf calls panic
func Fatalf(format string, args ...interface{}) error {
if debug {
if IsDebug() {
panic(fmt.Sprintf(format, args))
} else {
return Errorf(format, args)