From df597103828fa85ecbb195719ce6fde32bb560df Mon Sep 17 00:00:00 2001 From: klizhentas Date: Wed, 24 Feb 2016 13:19:36 -0800 Subject: [PATCH] push fixes and tests --- Godeps/Godeps.json | 4 +- lib/auth/clt.go | 2 +- lib/auth/new_web_user.go | 6 + lib/web/auth.go | 1 + lib/web/cookie.go | 4 + lib/web/multi.go | 75 +++++++--- lib/web/web_test.go | 135 +++++++++++++++++- .../gravitational/roundtrip/client.go | 32 ++++- vendor/github.com/gravitational/trace/log.go | 1 - .../github.com/gravitational/trace/trace.go | 12 +- 10 files changed, 235 insertions(+), 37 deletions(-) diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index 7a0e798cd40..4f747c65cd4 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -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", diff --git a/lib/auth/clt.go b/lib/auth/clt.go index 593aae5fd09..f4089dbdce9 100644 --- a/lib/auth/clt.go +++ b/lib/auth/clt.go @@ -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 { diff --git a/lib/auth/new_web_user.go b/lib/auth/new_web_user.go index 2f36153357e..9bcd0c31f9c 100644 --- a/lib/auth/new_web_user.go +++ b/lib/auth/new_web_user.go @@ -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 } diff --git a/lib/web/auth.go b/lib/web/auth.go index b3dcf8acdcf..8d8f50f9f14 100644 --- a/lib/web/auth.go +++ b/lib/web/auth.go @@ -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 ( diff --git a/lib/web/cookie.go b/lib/web/cookie.go index 176054dcf9b..51fad78b952 100644 --- a/lib/web/cookie.go +++ b/lib/web/cookie.go @@ -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 } diff --git a/lib/web/multi.go b/lib/web/multi.go index c2e73bde9da..9d16825754f 100644 --- a/lib/web/multi.go +++ b/lib/web/multi.go @@ -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 } diff --git a/lib/web/web_test.go b/lib/web/web_test.go index a134a9392dd..dfd33f6cdaf 100644 --- a/lib/web/web_test.go +++ b/lib/web/web_test.go @@ -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)) } diff --git a/vendor/github.com/gravitational/roundtrip/client.go b/vendor/github.com/gravitational/roundtrip/client.go index 155439d4e35..13550c19e6a 100644 --- a/vendor/github.com/gravitational/roundtrip/client.go +++ b/vendor/github.com/gravitational/roundtrip/client.go @@ -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 diff --git a/vendor/github.com/gravitational/trace/log.go b/vendor/github.com/gravitational/trace/log.go index 2df3fd59fab..d08f1ebde90 100644 --- a/vendor/github.com/gravitational/trace/log.go +++ b/vendor/github.com/gravitational/trace/log.go @@ -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) } diff --git a/vendor/github.com/gravitational/trace/trace.go b/vendor/github.com/gravitational/trace/trace.go index 93ed1e6cf21..6e901503e7a 100644 --- a/vendor/github.com/gravitational/trace/trace.go +++ b/vendor/github.com/gravitational/trace/trace.go @@ -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)