From 154200db8a75503bd8ee9b441912f59394b01cbb Mon Sep 17 00:00:00 2001 From: Victor Vrantchan Date: Thu, 29 Dec 2016 20:58:12 -0500 Subject: [PATCH] Add endpoint to retrieve an invite with the invite token. (#719) Closes #579 --- server/datastore/datastore_invites_test.go | 32 +++++++++++++++++++++ server/datastore/datastore_test.go | 1 + server/datastore/inmem/invites.go | 14 +++++++++ server/datastore/mysql/invites.go | 33 +++++++++++++++------- server/kolide/invites.go | 5 +++- server/service/endpoint_invites.go | 24 ++++++++++++++++ server/service/handler.go | 5 ++++ server/service/logging_invites.go | 11 ++++---- server/service/metrics_invites.go | 9 +++--- server/service/service_invites.go | 12 ++++---- server/service/service_users.go | 6 +--- server/service/service_users_test.go | 2 +- server/service/transport_invites.go | 10 +++++++ server/service/transport_invites_test.go | 19 ++++++++++++- 14 files changed, 150 insertions(+), 33 deletions(-) diff --git a/server/datastore/datastore_invites_test.go b/server/datastore/datastore_invites_test.go index c21aeb4782..d0e588152f 100644 --- a/server/datastore/datastore_invites_test.go +++ b/server/datastore/datastore_invites_test.go @@ -130,6 +130,38 @@ func testSaveInvite(t *testing.T, ds kolide.Datastore) { } +func testInviteByToken(t *testing.T, ds kolide.Datastore) { + setupTestInvites(t, ds) + + var inviteTests = []struct { + token string + wantErr error + }{ + { + token: "admin", + }, + { + token: "nosuchtoken", + wantErr: errors.New("Invite with token nosuchtoken was not found in the datastore"), + }, + } + + for _, tt := range inviteTests { + t.Run("", func(t *testing.T) { + invite, err := ds.InviteByToken(tt.token) + if tt.wantErr != nil { + require.NotNil(t, err) + assert.Equal(t, tt.wantErr.Error(), err.Error()) + return + } else { + require.Nil(t, err) + } + assert.NotEqual(t, invite.ID, 0) + + }) + } +} + func testInviteByEmail(t *testing.T, ds kolide.Datastore) { setupTestInvites(t, ds) diff --git a/server/datastore/datastore_test.go b/server/datastore/datastore_test.go index d67dec3421..1505f34460 100644 --- a/server/datastore/datastore_test.go +++ b/server/datastore/datastore_test.go @@ -19,6 +19,7 @@ var testFunctions = [...]func(*testing.T, kolide.Datastore){ testOrgInfo, testCreateInvite, testInviteByEmail, + testInviteByToken, testListInvites, testDeleteInvite, testSaveInvite, diff --git a/server/datastore/inmem/invites.go b/server/datastore/inmem/invites.go index 60a2a6a0b0..340743ea76 100644 --- a/server/datastore/inmem/invites.go +++ b/server/datastore/inmem/invites.go @@ -93,6 +93,20 @@ func (d *Datastore) InviteByEmail(email string) (*kolide.Invite, error) { WithMessage(fmt.Sprintf("with email %s", email)) } +// InviteByToken retrieves an invite given the invite token. +func (d *Datastore) InviteByToken(token string) (*kolide.Invite, error) { + d.mtx.Lock() + defer d.mtx.Unlock() + + for _, invite := range d.invites { + if invite.Token == token { + return invite, nil + } + } + return nil, notFound("Invite"). + WithMessage(fmt.Sprintf("with token %s", token)) +} + // SaveInvite saves an invitation in the datastore. func (d *Datastore) SaveInvite(invite *kolide.Invite) error { d.mtx.Lock() diff --git a/server/datastore/mysql/invites.go b/server/datastore/mysql/invites.go index 489149e1d2..03794ffc2f 100644 --- a/server/datastore/mysql/invites.go +++ b/server/datastore/mysql/invites.go @@ -37,7 +37,7 @@ func (d *Datastore) ListInvites(opt kolide.ListOptions) ([]*kolide.Invite, error query := appendListOptionsToSQL("SELECT * FROM invites WHERE NOT deleted", opt) err := d.db.Select(&invites, query) - if err != nil && err == sql.ErrNoRows { + if err == sql.ErrNoRows { return nil, notFound("Invite") } else if err != nil { return nil, errors.Wrap(err, "select invite by ID") @@ -47,27 +47,40 @@ func (d *Datastore) ListInvites(opt kolide.ListOptions) ([]*kolide.Invite, error // Invite returns Invite identified by id. func (d *Datastore) Invite(id uint) (*kolide.Invite, error) { - invite := &kolide.Invite{} - err := d.db.Get(invite, "SELECT * FROM invites WHERE id = ? AND NOT deleted", id) - if err != nil && err == sql.ErrNoRows { + var invite kolide.Invite + err := d.db.Get(&invite, "SELECT * FROM invites WHERE id = ? AND NOT deleted", id) + if err == sql.ErrNoRows { return nil, notFound("Invite").WithID(id) } else if err != nil { return nil, errors.Wrap(err, "select invite by ID") } - return invite, nil + return &invite, nil } // InviteByEmail finds an Invite with a particular email, if one exists. func (d *Datastore) InviteByEmail(email string) (*kolide.Invite, error) { - invite := &kolide.Invite{} - err := d.db.Get(invite, "SELECT * FROM invites WHERE email = ? AND NOT deleted", email) - if err != nil && err == sql.ErrNoRows { + var invite kolide.Invite + err := d.db.Get(&invite, "SELECT * FROM invites WHERE email = ? AND NOT deleted", email) + if err == sql.ErrNoRows { return nil, notFound("Invite"). WithMessage(fmt.Sprintf("with email %s", email)) } else if err != nil { - return nil, errors.Wrap(err, "sqlx get invite") + return nil, errors.Wrap(err, "sqlx get invite by email") } - return invite, nil + return &invite, nil +} + +// InviteByToken finds an Invite with a particular token, if one exists. +func (d *Datastore) InviteByToken(token string) (*kolide.Invite, error) { + var invite kolide.Invite + err := d.db.Get(&invite, "SELECT * FROM invites WHERE token = ? AND NOT deleted", token) + if err == sql.ErrNoRows { + return nil, notFound("Invite"). + WithMessage(fmt.Sprintf("with token %s", token)) + } else if err != nil { + return nil, errors.Wrap(err, "sqlx get invite by token") + } + return &invite, nil } // SaveInvite modifies existing Invite diff --git a/server/kolide/invites.go b/server/kolide/invites.go index dd37662289..aa68c92a58 100644 --- a/server/kolide/invites.go +++ b/server/kolide/invites.go @@ -22,6 +22,9 @@ type InviteStore interface { // InviteByEmail retrieves an invite for a specific email address. InviteByEmail(email string) (*Invite, error) + // InviteByToken retrieves and invite using the token string. + InviteByToken(token string) (*Invite, error) + // SaveInvite saves an invitation in the datastore. SaveInvite(i *Invite) error @@ -43,7 +46,7 @@ type InviteService interface { // VerifyInvite verifies that an invite exists and that it matches the // invite token. - VerifyInvite(ctx context.Context, email, token string) (err error) + VerifyInvite(ctx context.Context, token string) (invite *Invite, err error) } // InvitePayload contains fields required to create a new user invite. diff --git a/server/service/endpoint_invites.go b/server/service/endpoint_invites.go index fb15dea4ff..b12ea51fb1 100644 --- a/server/service/endpoint_invites.go +++ b/server/service/endpoint_invites.go @@ -63,6 +63,8 @@ type deleteInviteResponse struct { Err error `json:"error,omitempty"` } +func (r deleteInviteResponse) error() error { return r.Err } + func makeDeleteInviteEndpoint(svc kolide.Service) endpoint.Endpoint { return func(ctx context.Context, request interface{}) (interface{}, error) { req := request.(deleteInviteRequest) @@ -73,3 +75,25 @@ func makeDeleteInviteEndpoint(svc kolide.Service) endpoint.Endpoint { return deleteInviteResponse{}, nil } } + +type verifyInviteRequest struct { + Token string +} + +type verifyInviteResponse struct { + Invite *kolide.Invite `json:"invite"` + Err error `json:"error,omitempty"` +} + +func (r verifyInviteResponse) error() error { return r.Err } + +func makeVerifyInviteEndpoint(svc kolide.Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req := request.(verifyInviteRequest) + invite, err := svc.VerifyInvite(ctx, req.Token) + if err != nil { + return verifyInviteResponse{Err: err}, nil + } + return verifyInviteResponse{Invite: invite}, nil + } +} diff --git a/server/service/handler.go b/server/service/handler.go index 16a10b36a8..94d5b6a30c 100644 --- a/server/service/handler.go +++ b/server/service/handler.go @@ -33,6 +33,7 @@ type KolideEndpoints struct { CreateInvite endpoint.Endpoint ListInvites endpoint.Endpoint DeleteInvite endpoint.Endpoint + VerifyInvite endpoint.Endpoint GetQuery endpoint.Endpoint ListQueries endpoint.Endpoint CreateQuery endpoint.Endpoint @@ -78,6 +79,7 @@ func MakeKolideServerEndpoints(svc kolide.Service, jwtKey string) KolideEndpoint ForgotPassword: makeForgotPasswordEndpoint(svc), ResetPassword: makeResetPasswordEndpoint(svc), CreateUser: makeCreateUserEndpoint(svc), + VerifyInvite: makeVerifyInviteEndpoint(svc), // Authenticated user endpoints // Each of these endpoints should have exactly one @@ -160,6 +162,7 @@ type kolideHandlers struct { CreateInvite http.Handler ListInvites http.Handler DeleteInvite http.Handler + VerifyInvite http.Handler GetQuery http.Handler ListQueries http.Handler CreateQuery http.Handler @@ -221,6 +224,7 @@ func makeKolideKitHandlers(ctx context.Context, e KolideEndpoints, opts []kithtt CreateInvite: newServer(e.CreateInvite, decodeCreateInviteRequest), ListInvites: newServer(e.ListInvites, decodeListInvitesRequest), DeleteInvite: newServer(e.DeleteInvite, decodeDeleteInviteRequest), + VerifyInvite: newServer(e.VerifyInvite, decodeVerifyInviteRequest), GetQuery: newServer(e.GetQuery, decodeGetQueryRequest), ListQueries: newServer(e.ListQueries, decodeListQueriesRequest), CreateQuery: newServer(e.CreateQuery, decodeCreateQueryRequest), @@ -319,6 +323,7 @@ func attachKolideAPIRoutes(r *mux.Router, h *kolideHandlers) { r.Handle("/api/v1/kolide/invites", h.CreateInvite).Methods("POST").Name("create_invite") r.Handle("/api/v1/kolide/invites", h.ListInvites).Methods("GET").Name("list_invites") r.Handle("/api/v1/kolide/invites/{id}", h.DeleteInvite).Methods("DELETE").Name("delete_invite") + r.Handle("/api/v1/kolide/invites/{token}", h.VerifyInvite).Methods("GET").Name("verify_invite") r.Handle("/api/v1/kolide/queries/{id}", h.GetQuery).Methods("GET").Name("get_query") r.Handle("/api/v1/kolide/queries", h.ListQueries).Methods("GET").Name("list_queries") diff --git a/server/service/logging_invites.go b/server/service/logging_invites.go index 112ef0236a..7c726156f5 100644 --- a/server/service/logging_invites.go +++ b/server/service/logging_invites.go @@ -72,18 +72,19 @@ func (mw loggingMiddleware) ListInvites(ctx context.Context, opt kolide.ListOpti return invites, err } -func (mw loggingMiddleware) VerifyInvite(ctx context.Context, email string, token string) error { +func (mw loggingMiddleware) VerifyInvite(ctx context.Context, token string) (*kolide.Invite, error) { var ( - err error + err error + invite *kolide.Invite ) defer func(begin time.Time) { _ = mw.logger.Log( "method", "VerifyInvite", - "email", email, + "token", token, "err", err, "took", time.Since(begin), ) }(time.Now()) - err = mw.Service.VerifyInvite(ctx, email, token) - return err + invite, err = mw.Service.VerifyInvite(ctx, token) + return invite, err } diff --git a/server/service/metrics_invites.go b/server/service/metrics_invites.go index 1fce65270c..7eee009969 100644 --- a/server/service/metrics_invites.go +++ b/server/service/metrics_invites.go @@ -49,15 +49,16 @@ func (mw metricsMiddleware) ListInvites(ctx context.Context, opt kolide.ListOpti return invites, err } -func (mw metricsMiddleware) VerifyInvite(ctx context.Context, email string, token string) error { +func (mw metricsMiddleware) VerifyInvite(ctx context.Context, token string) (*kolide.Invite, error) { var ( - err error + err error + invite *kolide.Invite ) defer func(begin time.Time) { lvs := []string{"method", "VerifyInvite", "error", fmt.Sprint(err != nil)} mw.requestCount.With(lvs...).Add(1) mw.requestLatency.With(lvs...).Observe(time.Since(begin).Seconds()) }(time.Now()) - err = mw.Service.VerifyInvite(ctx, email, token) - return err + invite, err = mw.Service.VerifyInvite(ctx, token) + return invite, err } diff --git a/server/service/service_invites.go b/server/service/service_invites.go index eee3b0931e..80d5a8a85d 100644 --- a/server/service/service_invites.go +++ b/server/service/service_invites.go @@ -76,22 +76,22 @@ func (svc service) ListInvites(ctx context.Context, opt kolide.ListOptions) ([]* return svc.ds.ListInvites(opt) } -func (svc service) VerifyInvite(ctx context.Context, email, token string) error { - invite, err := svc.ds.InviteByEmail(email) +func (svc service) VerifyInvite(ctx context.Context, token string) (*kolide.Invite, error) { + invite, err := svc.ds.InviteByToken(token) if err != nil { - return err + return nil, err } if invite.Token != token { - return newInvalidArgumentError("invite_token", "Invite Token does not match Email Address.") + return nil, newInvalidArgumentError("invite_token", "Invite Token does not match Email Address.") } expiresAt := invite.CreatedAt.Add(svc.config.App.InviteTokenValidityPeriod) if svc.clock.Now().After(expiresAt) { - return newInvalidArgumentError("invite_token", "Invite token has expired.") + return nil, newInvalidArgumentError("invite_token", "Invite token has expired.") } - return nil + return invite, nil } diff --git a/server/service/service_users.go b/server/service/service_users.go index 0cf1ffbec0..5a7822e7cc 100644 --- a/server/service/service_users.go +++ b/server/service/service_users.go @@ -13,11 +13,7 @@ import ( ) func (svc service) NewUser(ctx context.Context, p kolide.UserPayload) (*kolide.User, error) { - err := svc.VerifyInvite(ctx, *p.Email, *p.InviteToken) - if err != nil { - return nil, err - } - invite, err := svc.ds.InviteByEmail(*p.Email) + invite, err := svc.VerifyInvite(ctx, *p.InviteToken) if err != nil { return nil, err } diff --git a/server/service/service_users_test.go b/server/service/service_users_test.go index 3b7fc067ff..80578ac96c 100644 --- a/server/service/service_users_test.go +++ b/server/service/service_users_test.go @@ -158,7 +158,7 @@ func TestCreateUser(t *testing.T) { NeedsPasswordReset: boolPtr(true), Admin: boolPtr(false), InviteToken: &invites["admin2@example.com"].Token, - wantErr: errors.New("Invite with email admin2@example.com was not found in the datastore"), + wantErr: errors.New("Invite with token admin2@example.com was not found in the datastore"), }, { Username: stringPtr("admin3"), diff --git a/server/service/transport_invites.go b/server/service/transport_invites.go index 8db81c177b..0bbeb79010 100644 --- a/server/service/transport_invites.go +++ b/server/service/transport_invites.go @@ -5,6 +5,7 @@ import ( "net/http" "strings" + "github.com/gorilla/mux" "golang.org/x/net/context" ) @@ -28,6 +29,15 @@ func decodeDeleteInviteRequest(ctx context.Context, r *http.Request) (interface{ return deleteInviteRequest{ID: id}, nil } +func decodeVerifyInviteRequest(ctx context.Context, r *http.Request) (interface{}, error) { + vars := mux.Vars(r) + token, ok := vars["token"] + if !ok { + return 0, errBadRoute + } + return verifyInviteRequest{Token: token}, nil +} + func decodeListInvitesRequest(ctx context.Context, r *http.Request) (interface{}, error) { opt, err := listOptionsFromRequest(r) if err != nil { diff --git a/server/service/transport_invites_test.go b/server/service/transport_invites_test.go index 2bbb4df931..7789a52848 100644 --- a/server/service/transport_invites_test.go +++ b/server/service/transport_invites_test.go @@ -2,13 +2,13 @@ package service import ( "bytes" - "golang.org/x/net/context" "net/http" "net/http/httptest" "testing" "github.com/gorilla/mux" "github.com/stretchr/testify/assert" + "golang.org/x/net/context" ) func TestDecodeCreateInviteRequest(t *testing.T) { @@ -51,3 +51,20 @@ func TestDecodeCreateInviteRequest(t *testing.T) { }) } + +func TestDecodeVerifyInviteRequest(t *testing.T) { + router := mux.NewRouter() + router.HandleFunc("/api/v1/kolide/invites/{token}", func(writer http.ResponseWriter, request *http.Request) { + r, err := decodeCreateInviteRequest(context.Background(), request) + assert.Nil(t, err) + + params := r.(verifyInviteRequest) + assert.Equal(t, "test_token", params.Token) + }).Methods("GET") + + router.ServeHTTP( + httptest.NewRecorder(), + httptest.NewRequest("GET", "/api/v1/kolide/tokens/test_token", nil), + ) + +}