Add endpoint to retrieve an invite with the invite token. (#719)

Closes #579
This commit is contained in:
Victor Vrantchan 2016-12-29 20:58:12 -05:00 committed by GitHub
parent 6cb1026d86
commit 154200db8a
14 changed files with 150 additions and 33 deletions

View file

@ -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)

View file

@ -19,6 +19,7 @@ var testFunctions = [...]func(*testing.T, kolide.Datastore){
testOrgInfo,
testCreateInvite,
testInviteByEmail,
testInviteByToken,
testListInvites,
testDeleteInvite,
testSaveInvite,

View file

@ -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()

View file

@ -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

View file

@ -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.

View file

@ -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
}
}

View file

@ -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")

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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"),

View file

@ -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 {

View file

@ -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),
)
}