mirror of
https://github.com/fleetdm/fleet
synced 2026-05-23 08:58:41 +00:00
Add endpoint to retrieve an invite with the invite token. (#719)
Closes #579
This commit is contained in:
parent
6cb1026d86
commit
154200db8a
14 changed files with 150 additions and 33 deletions
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ var testFunctions = [...]func(*testing.T, kolide.Datastore){
|
|||
testOrgInfo,
|
||||
testCreateInvite,
|
||||
testInviteByEmail,
|
||||
testInviteByToken,
|
||||
testListInvites,
|
||||
testDeleteInvite,
|
||||
testSaveInvite,
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue