Adds endpoints to invite new users to the application. (#235)
User service checks that tokens are valid on new user signups. Closes #230
This commit is contained in:
+8
-11
@@ -85,7 +85,7 @@ the way that the kolide server works.
|
||||
// Bootstrap a few users when using the in-memory database.
|
||||
// Each user's default password will just be their username.
|
||||
users := []kolide.User{
|
||||
kolide.User{
|
||||
{
|
||||
Name: "Admin User",
|
||||
Username: "admin",
|
||||
Email: "admin@kolide.co",
|
||||
@@ -93,7 +93,7 @@ the way that the kolide server works.
|
||||
Admin: true,
|
||||
Enabled: true,
|
||||
},
|
||||
kolide.User{
|
||||
{
|
||||
Name: "Normal User",
|
||||
Username: "user",
|
||||
Email: "user@kolide.co",
|
||||
@@ -104,15 +104,12 @@ the way that the kolide server works.
|
||||
}
|
||||
|
||||
for _, user := range users {
|
||||
_, err := svc.NewUser(ctx, kolide.UserPayload{
|
||||
Name: &user.Name,
|
||||
Username: &user.Username,
|
||||
Password: &user.Username,
|
||||
Email: &user.Email,
|
||||
Enabled: &user.Enabled,
|
||||
Position: &user.Position,
|
||||
Admin: &user.Admin,
|
||||
})
|
||||
user := user
|
||||
err := user.SetPassword(user.Username, config.Auth.SaltKeySize, config.Auth.BcryptCost)
|
||||
if err != nil {
|
||||
initFatal(err, "creating bootstrap user")
|
||||
}
|
||||
_, err = ds.NewUser(&user)
|
||||
if err != nil {
|
||||
initFatal(err, "creating bootstrap user")
|
||||
}
|
||||
|
||||
+16
-5
@@ -38,7 +38,10 @@ type AuthConfig struct {
|
||||
|
||||
// AppConfig defines configs related to HTTP
|
||||
type AppConfig struct {
|
||||
WebAddress string
|
||||
WebAddress string
|
||||
TokenKeySize int
|
||||
TokenKey string
|
||||
InviteTokenValidityPeriod time.Duration
|
||||
}
|
||||
|
||||
// SMTPConfig defines configs related to SMTP email
|
||||
@@ -47,7 +50,6 @@ type SMTPConfig struct {
|
||||
Username string
|
||||
Password string
|
||||
PoolConnections int
|
||||
TokenKeySize int
|
||||
}
|
||||
|
||||
// SessionConfig defines configs related to user sessions
|
||||
@@ -107,13 +109,15 @@ func (man Manager) addConfigs() {
|
||||
|
||||
// App
|
||||
man.addConfigString("app.web_address", "0.0.0.0:8080")
|
||||
man.addConfigString("app.token_key", "CHANGEME")
|
||||
man.addConfigDuration("app.invite_token_validity_period", 5*24*time.Hour)
|
||||
man.addConfigInt("app.token_key_size", 24)
|
||||
|
||||
// SMTP
|
||||
man.addConfigString("smtp.server", "0.0.0.0:1025")
|
||||
man.addConfigString("smtp.username", "")
|
||||
man.addConfigString("smtp.password", "")
|
||||
man.addConfigInt("smtp.pool_connections", 4)
|
||||
man.addConfigInt("smtp.token_key_size", 24)
|
||||
|
||||
// Session
|
||||
man.addConfigInt("session.key_size", 64)
|
||||
@@ -154,14 +158,16 @@ func (man Manager) LoadConfig() KolideConfig {
|
||||
SaltKeySize: man.getConfigInt("auth.salt_key_size"),
|
||||
},
|
||||
App: AppConfig{
|
||||
WebAddress: man.getConfigString("app.web_address"),
|
||||
WebAddress: man.getConfigString("app.web_address"),
|
||||
TokenKeySize: man.getConfigInt("app.token_key_size"),
|
||||
TokenKey: man.getConfigString("app.token_key"),
|
||||
InviteTokenValidityPeriod: man.getConfigDuration("app.invite_token_validity_period"),
|
||||
},
|
||||
SMTP: SMTPConfig{
|
||||
Server: man.getConfigString("smtp.server"),
|
||||
Username: man.getConfigString("smtp.username"),
|
||||
Password: man.getConfigString("smtp.password"),
|
||||
PoolConnections: man.getConfigInt("smtp.pool_connections"),
|
||||
TokenKeySize: man.getConfigInt("smtp.token_key_size"),
|
||||
},
|
||||
Session: SessionConfig{
|
||||
KeySize: man.getConfigInt("session.key_size"),
|
||||
@@ -350,6 +356,10 @@ func (man Manager) loadConfigFile() {
|
||||
// Individual tests may want to override some of the values provided.
|
||||
func TestConfig() KolideConfig {
|
||||
return KolideConfig{
|
||||
App: AppConfig{
|
||||
TokenKey: "CHANGEME",
|
||||
InviteTokenValidityPeriod: 5 * 24 * time.Hour,
|
||||
},
|
||||
Auth: AuthConfig{
|
||||
JwtKey: "CHANGEME",
|
||||
BcryptCost: 6, // Low cost keeps tests fast
|
||||
@@ -370,5 +380,6 @@ func TestConfig() KolideConfig {
|
||||
Debug: true,
|
||||
DisableBanner: true,
|
||||
},
|
||||
SMTP: SMTPConfig{},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,6 +75,7 @@ func New(driver, conn string, opts ...DBOption) (kolide.Datastore, error) {
|
||||
users: make(map[uint]*kolide.User),
|
||||
sessions: make(map[uint]*kolide.Session),
|
||||
passwordResets: make(map[uint]*kolide.PasswordResetRequest),
|
||||
invites: make(map[uint]*kolide.Invite),
|
||||
}
|
||||
return ds, nil
|
||||
default:
|
||||
|
||||
@@ -35,6 +35,7 @@ var tables = [...]interface{}{
|
||||
&kolide.Query{},
|
||||
&kolide.DistributedQueryExecution{},
|
||||
&kolide.OrgInfo{},
|
||||
&kolide.Invite{},
|
||||
}
|
||||
|
||||
type gormDB struct {
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package datastore
|
||||
|
||||
import "github.com/kolide/kolide-ose/server/kolide"
|
||||
|
||||
func (orm gormDB) NewInvite(invite *kolide.Invite) (*kolide.Invite, error) {
|
||||
err := orm.DB.Create(invite).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return invite, nil
|
||||
}
|
||||
|
||||
func (orm gormDB) InviteByEmail(email string) (*kolide.Invite, error) {
|
||||
invite := &kolide.Invite{
|
||||
Email: email,
|
||||
}
|
||||
err := orm.DB.Where("email = ?", email).First(invite).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return invite, nil
|
||||
}
|
||||
|
||||
func (orm gormDB) Invites() ([]*kolide.Invite, error) {
|
||||
var invites []*kolide.Invite
|
||||
err := orm.DB.Find(&invites).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return invites, nil
|
||||
}
|
||||
|
||||
func (orm gormDB) Invite(id uint) (*kolide.Invite, error) {
|
||||
invite := &kolide.Invite{ID: id}
|
||||
err := orm.DB.Where(invite).First(invite).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return invite, nil
|
||||
}
|
||||
|
||||
func (orm gormDB) SaveInvite(invite *kolide.Invite) error {
|
||||
return orm.DB.Save(invite).Error
|
||||
}
|
||||
|
||||
func (orm gormDB) DeleteInvite(invite *kolide.Invite) error {
|
||||
return orm.DB.Delete(invite).Error
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package datastore
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/kolide/kolide-ose/server/kolide"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestCreateInvite(t *testing.T) {
|
||||
var ds kolide.Datastore
|
||||
address := os.Getenv("MYSQL_ADDR")
|
||||
if address == "" {
|
||||
ds = setup(t)
|
||||
} else {
|
||||
ds = setupMySQLGORM(t)
|
||||
defer teardownMySQLGORM(t, ds)
|
||||
}
|
||||
|
||||
invite := &kolide.Invite{}
|
||||
|
||||
invite, err := ds.NewInvite(invite)
|
||||
assert.Nil(t, err)
|
||||
|
||||
verify, err := ds.InviteByEmail(invite.Email)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, invite.ID, verify.ID)
|
||||
assert.Equal(t, invite.Email, verify.Email)
|
||||
}
|
||||
@@ -13,6 +13,7 @@ type inmem struct {
|
||||
users map[uint]*kolide.User
|
||||
sessions map[uint]*kolide.Session
|
||||
passwordResets map[uint]*kolide.PasswordResetRequest
|
||||
invites map[uint]*kolide.Invite
|
||||
orginfo *kolide.OrgInfo
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
package datastore
|
||||
|
||||
import "github.com/kolide/kolide-ose/server/kolide"
|
||||
|
||||
// NewInvite creates and stores a new invitation in a DB.
|
||||
func (orm *inmem) NewInvite(invite *kolide.Invite) (*kolide.Invite, error) {
|
||||
orm.mtx.Lock()
|
||||
defer orm.mtx.Unlock()
|
||||
|
||||
for _, in := range orm.invites {
|
||||
if in.Email == invite.Email {
|
||||
return nil, ErrExists
|
||||
}
|
||||
}
|
||||
|
||||
invite.ID = uint(len(orm.invites) + 1)
|
||||
orm.invites[invite.ID] = invite
|
||||
return invite, nil
|
||||
}
|
||||
|
||||
// Invites lists all invites in the datastore.
|
||||
func (orm *inmem) Invites() ([]*kolide.Invite, error) {
|
||||
orm.mtx.Lock()
|
||||
defer orm.mtx.Unlock()
|
||||
|
||||
var invites []*kolide.Invite
|
||||
for _, invite := range orm.invites {
|
||||
invites = append(invites, invite)
|
||||
}
|
||||
|
||||
return invites, nil
|
||||
}
|
||||
|
||||
func (orm *inmem) Invite(id uint) (*kolide.Invite, error) {
|
||||
orm.mtx.Lock()
|
||||
defer orm.mtx.Unlock()
|
||||
if invite, ok := orm.invites[id]; ok {
|
||||
return invite, nil
|
||||
}
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
|
||||
// InviteByEmail retrieves an invite for a specific email address.
|
||||
func (orm *inmem) InviteByEmail(email string) (*kolide.Invite, error) {
|
||||
orm.mtx.Lock()
|
||||
defer orm.mtx.Unlock()
|
||||
|
||||
for _, invite := range orm.invites {
|
||||
if invite.Email == email {
|
||||
return invite, nil
|
||||
}
|
||||
}
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
|
||||
// SaveInvite saves an invitation in the datastore.
|
||||
func (orm *inmem) SaveInvite(invite *kolide.Invite) error {
|
||||
orm.mtx.Lock()
|
||||
defer orm.mtx.Unlock()
|
||||
|
||||
if _, ok := orm.invites[invite.ID]; !ok {
|
||||
return ErrNotFound
|
||||
}
|
||||
|
||||
orm.invites[invite.ID] = invite
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteInvite deletes an invitation.
|
||||
func (orm *inmem) DeleteInvite(invite *kolide.Invite) error {
|
||||
orm.mtx.Lock()
|
||||
defer orm.mtx.Unlock()
|
||||
|
||||
if _, ok := orm.invites[invite.ID]; !ok {
|
||||
return ErrNotFound
|
||||
}
|
||||
delete(orm.invites, invite.ID)
|
||||
return nil
|
||||
}
|
||||
@@ -41,7 +41,6 @@ func (orm *inmem) Users() ([]*kolide.User, error) {
|
||||
}
|
||||
|
||||
return users, nil
|
||||
|
||||
}
|
||||
|
||||
func (orm *inmem) UserByEmail(email string) (*kolide.User, error) {
|
||||
|
||||
@@ -10,6 +10,7 @@ type Datastore interface {
|
||||
PasswordResetStore
|
||||
SessionStore
|
||||
AppConfigStore
|
||||
InviteStore
|
||||
Name() string
|
||||
Drop() error
|
||||
Migrate() error
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
package kolide
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"html/template"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
// InviteStore contains the methods for
|
||||
// managing user invites in a datastore.
|
||||
type InviteStore interface {
|
||||
// NewInvite creates and stores a new invitation in a DB.
|
||||
NewInvite(i *Invite) (*Invite, error)
|
||||
|
||||
// Invites lists all invites in the datastore.
|
||||
Invites() ([]*Invite, error)
|
||||
|
||||
// Invite retrieves an invite by it's ID.
|
||||
Invite(id uint) (*Invite, error)
|
||||
|
||||
// InviteByEmail retrieves an invite for a specific email address.
|
||||
InviteByEmail(email string) (*Invite, error)
|
||||
|
||||
// SaveInvite saves an invitation in the datastore.
|
||||
SaveInvite(i *Invite) error
|
||||
|
||||
// DeleteInvite deletes an invitation.
|
||||
DeleteInvite(i *Invite) error
|
||||
}
|
||||
|
||||
// InviteService contains methods for a service which deals with
|
||||
// user invites.
|
||||
type InviteService interface {
|
||||
// InviteNewUser creates an invite for a new user to join Kolide.
|
||||
InviteNewUser(ctx context.Context, payload InvitePayload) (invite *Invite, err error)
|
||||
|
||||
// DeleteInvite removes an invite.
|
||||
DeleteInvite(ctx context.Context, id uint) (err error)
|
||||
|
||||
// Invites returns a list of all invites.
|
||||
Invites(ctx context.Context) (invites []*Invite, err error)
|
||||
|
||||
// VerifyInvite verifies that an invite exists and that it matches the
|
||||
// invite token.
|
||||
VerifyInvite(ctx context.Context, email, token string) (err error)
|
||||
}
|
||||
|
||||
// InvitePayload contains fields required to create a new user invite.
|
||||
type InvitePayload struct {
|
||||
InvitedBy *uint `json:"invited_by"`
|
||||
Email *string
|
||||
Admin *bool
|
||||
Name *string
|
||||
Position *string
|
||||
}
|
||||
|
||||
// Invite represents an invitation for a user to join Kolide.
|
||||
type Invite struct {
|
||||
ID uint `gorm:"primary_key"`
|
||||
CreatedAt time.Time
|
||||
InvitedBy uint `gorm:"not null"`
|
||||
Email string `gorm:"not null;unique_index:idx_invite_unique_email"`
|
||||
Admin bool
|
||||
Name string
|
||||
Position string
|
||||
Token string `gorm:"not null;unique_index:idx_invite_unique_key"`
|
||||
}
|
||||
|
||||
// TODO: fixme
|
||||
// this is not the right way to generate emails at all
|
||||
const inviteEmailTempate = `
|
||||
{{.InvitedBy}} invited you to join Kolide.,
|
||||
http://localhost:8080/signup?token={{.Token}}
|
||||
`
|
||||
|
||||
func (i Invite) Message() ([]byte, error) {
|
||||
var msg bytes.Buffer
|
||||
var err error
|
||||
t := template.New(inviteEmailTempate)
|
||||
if t, err = t.Parse(inviteEmailTempate); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = t.Execute(&msg, i); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return msg.Bytes(), nil
|
||||
}
|
||||
@@ -9,4 +9,5 @@ type Service interface {
|
||||
OsqueryService
|
||||
HostService
|
||||
AppConfigService
|
||||
InviteService
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package kolide
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
@@ -75,6 +77,32 @@ type UserPayload struct {
|
||||
Password *string `json:"password"`
|
||||
GravatarURL *string `json:"gravatar_url"`
|
||||
Position *string `json:"position"`
|
||||
InviteToken *string `json:"invite_token"`
|
||||
}
|
||||
|
||||
// User creates a user from payload.
|
||||
func (p UserPayload) User(keySize, cost int) (*User, error) {
|
||||
|
||||
user := &User{
|
||||
Username: *p.Username,
|
||||
Email: *p.Email,
|
||||
Admin: falseIfNil(p.Admin),
|
||||
AdminForcedPasswordReset: falseIfNil(p.AdminForcedPasswordReset),
|
||||
Enabled: true,
|
||||
}
|
||||
if err := user.SetPassword(*p.Password, keySize, cost); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// add optional fields
|
||||
if p.Name != nil {
|
||||
user.Name = *p.Name
|
||||
}
|
||||
if p.Position != nil {
|
||||
user.Position = *p.Position
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// ValidatePassword accepts a potential password for a given user and attempts
|
||||
@@ -84,3 +112,39 @@ func (u *User) ValidatePassword(password string) error {
|
||||
saltAndPass := []byte(fmt.Sprintf("%s%s", password, u.Salt))
|
||||
return bcrypt.CompareHashAndPassword(u.Password, saltAndPass)
|
||||
}
|
||||
|
||||
func (u *User) SetPassword(plaintext string, keySize, cost int) error {
|
||||
salt, err := generateRandomText(keySize)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
withSalt := []byte(fmt.Sprintf("%s%s", plaintext, salt))
|
||||
hashed, err := bcrypt.GenerateFromPassword(withSalt, cost)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
u.Salt = salt
|
||||
u.Password = hashed
|
||||
return nil
|
||||
}
|
||||
|
||||
// generateRandomText return a string generated by filling in keySize bytes with
|
||||
// random data and then base64 encoding those bytes
|
||||
func generateRandomText(keySize int) (string, error) {
|
||||
key := make([]byte, keySize)
|
||||
_, err := rand.Read(key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return base64.StdEncoding.EncodeToString(key), nil
|
||||
}
|
||||
|
||||
// helper to convert a bool pointer false
|
||||
func falseIfNil(b *bool) bool {
|
||||
if b == nil {
|
||||
return false
|
||||
}
|
||||
return *b
|
||||
}
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"github.com/go-kit/kit/endpoint"
|
||||
"github.com/kolide/kolide-ose/server/kolide"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
type createInviteRequest struct {
|
||||
payload kolide.InvitePayload
|
||||
}
|
||||
|
||||
type createInviteResponse struct {
|
||||
ID uint `json:"id"`
|
||||
InvitedBy uint `json:"invited_by"`
|
||||
Email string `json:"email"`
|
||||
Name string `json:"name"`
|
||||
Admin bool `json:"admin"`
|
||||
Position string `json:"position,omitempty"`
|
||||
Err error `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
func (r createInviteResponse) error() error { return r.Err }
|
||||
|
||||
func makeCreateInviteEndpoint(svc kolide.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(createInviteRequest)
|
||||
invite, err := svc.InviteNewUser(ctx, req.payload)
|
||||
if err != nil {
|
||||
return createInviteResponse{Err: err}, nil
|
||||
}
|
||||
return createInviteResponse{
|
||||
ID: invite.ID,
|
||||
InvitedBy: invite.InvitedBy,
|
||||
Email: invite.Email,
|
||||
Name: invite.Name,
|
||||
Position: invite.Position,
|
||||
Admin: invite.Admin,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
type inviteResponse struct {
|
||||
ID uint `json:"id"`
|
||||
InvitedBy uint `json:"invited_by"`
|
||||
Email string `json:"email"`
|
||||
Name string `json:"name"`
|
||||
Admin bool `json:"admin"`
|
||||
Position string `json:"position,omitempty"`
|
||||
}
|
||||
|
||||
type listInvitesResponse struct {
|
||||
Invites []inviteResponse `json:"invites"`
|
||||
Err error `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
func (r listInvitesResponse) error() error { return r.Err }
|
||||
|
||||
func makeListInvitesEndpoint(svc kolide.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
invites, err := svc.Invites(ctx)
|
||||
if err != nil {
|
||||
return listInvitesResponse{Err: err}, nil
|
||||
}
|
||||
var resp listInvitesResponse
|
||||
for _, invite := range invites {
|
||||
resp.Invites = append(resp.Invites, inviteResponse{
|
||||
ID: invite.ID,
|
||||
InvitedBy: invite.InvitedBy,
|
||||
Email: invite.Email,
|
||||
Name: invite.Name,
|
||||
Admin: invite.Admin,
|
||||
Position: invite.Position,
|
||||
})
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
}
|
||||
|
||||
type deleteInviteRequest struct {
|
||||
ID uint
|
||||
}
|
||||
|
||||
type deleteInviteResponse struct {
|
||||
Err error `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
func makeDeleteInviteEndpoint(svc kolide.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(deleteInviteRequest)
|
||||
err := svc.DeleteInvite(ctx, req.ID)
|
||||
if err != nil {
|
||||
return deleteInviteResponse{Err: err}, nil
|
||||
}
|
||||
return deleteInviteResponse{}, nil
|
||||
}
|
||||
}
|
||||
@@ -5,9 +5,9 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/go-kit/kit/endpoint"
|
||||
"github.com/kolide/kolide-ose/server/contexts/viewer"
|
||||
"github.com/kolide/kolide-ose/server/datastore"
|
||||
"github.com/kolide/kolide-ose/server/kolide"
|
||||
"github.com/kolide/kolide-ose/server/contexts/viewer"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
|
||||
@@ -28,6 +28,9 @@ type KolideEndpoints struct {
|
||||
DeleteSession endpoint.Endpoint
|
||||
GetAppConfig endpoint.Endpoint
|
||||
ModifyAppConfig endpoint.Endpoint
|
||||
CreateInvite endpoint.Endpoint
|
||||
ListInvites endpoint.Endpoint
|
||||
DeleteInvite endpoint.Endpoint
|
||||
GetQuery endpoint.Endpoint
|
||||
GetAllQueries endpoint.Endpoint
|
||||
CreateQuery endpoint.Endpoint
|
||||
@@ -50,8 +53,8 @@ func MakeKolideServerEndpoints(svc kolide.Service, jwtKey string) KolideEndpoint
|
||||
Logout: makeLogoutEndpoint(svc),
|
||||
ForgotPassword: makeForgotPasswordEndpoint(svc),
|
||||
ResetPassword: makeResetPasswordEndpoint(svc),
|
||||
CreateUser: makeCreateUserEndpoint(svc),
|
||||
Me: authenticated(jwtKey, svc, makeGetSessionUserEndpoint(svc)),
|
||||
CreateUser: authenticated(jwtKey, svc, mustBeAdmin(makeCreateUserEndpoint(svc))),
|
||||
GetUser: authenticated(jwtKey, svc, canReadUser(makeGetUserEndpoint(svc))),
|
||||
ListUsers: authenticated(jwtKey, svc, canPerformActions(makeListUsersEndpoint(svc))),
|
||||
ModifyUser: authenticated(jwtKey, svc, validateModifyUserRequest(makeModifyUserEndpoint(svc))),
|
||||
@@ -61,6 +64,9 @@ func MakeKolideServerEndpoints(svc kolide.Service, jwtKey string) KolideEndpoint
|
||||
DeleteSession: authenticated(jwtKey, svc, mustBeAdmin(makeDeleteSessionEndpoint(svc))),
|
||||
GetAppConfig: authenticated(jwtKey, svc, makeGetAppConfigEndpoint(svc)),
|
||||
ModifyAppConfig: authenticated(jwtKey, svc, mustBeAdmin(makeModifyAppConfigRequest(svc))),
|
||||
CreateInvite: authenticated(jwtKey, svc, mustBeAdmin(makeCreateInviteEndpoint(svc))),
|
||||
ListInvites: authenticated(jwtKey, svc, mustBeAdmin(makeListInvitesEndpoint(svc))),
|
||||
DeleteInvite: authenticated(jwtKey, svc, mustBeAdmin(makeDeleteInviteEndpoint(svc))),
|
||||
GetQuery: authenticated(jwtKey, svc, makeGetQueryEndpoint(svc)),
|
||||
GetAllQueries: authenticated(jwtKey, svc, makeGetAllQueriesEndpoint(svc)),
|
||||
CreateQuery: authenticated(jwtKey, svc, makeCreateQueryEndpoint(svc)),
|
||||
@@ -93,6 +99,9 @@ type kolideHandlers struct {
|
||||
DeleteSession *kithttp.Server
|
||||
GetAppConfig *kithttp.Server
|
||||
ModifyAppConfig *kithttp.Server
|
||||
CreateInvite *kithttp.Server
|
||||
ListInvites *kithttp.Server
|
||||
DeleteInvite *kithttp.Server
|
||||
GetQuery *kithttp.Server
|
||||
GetAllQueries *kithttp.Server
|
||||
CreateQuery *kithttp.Server
|
||||
@@ -128,6 +137,9 @@ func makeKolideKitHandlers(ctx context.Context, e KolideEndpoints, opts []kithtt
|
||||
DeleteSession: newServer(e.DeleteSession, decodeDeleteSessionRequest),
|
||||
GetAppConfig: newServer(e.GetAppConfig, decodeNoParamsRequest),
|
||||
ModifyAppConfig: newServer(e.ModifyAppConfig, decodeModifyAppConfigRequest),
|
||||
CreateInvite: newServer(e.CreateInvite, decodeCreateInviteRequest),
|
||||
ListInvites: newServer(e.ListInvites, decodeNoParamsRequest),
|
||||
DeleteInvite: newServer(e.DeleteInvite, decodeDeleteInviteRequest),
|
||||
GetQuery: newServer(e.GetQuery, decodeGetQueryRequest),
|
||||
GetAllQueries: newServer(e.GetAllQueries, decodeGetQueryRequest),
|
||||
CreateQuery: newServer(e.CreateQuery, decodeCreateQueryRequest),
|
||||
@@ -182,6 +194,9 @@ func attachKolideAPIRoutes(r *mux.Router, h kolideHandlers) {
|
||||
r.Handle("/api/v1/kolide/sessions/{id}", h.DeleteSession).Methods("DELETE")
|
||||
r.Handle("/api/v1/kolide/config", h.GetAppConfig).Methods("GET")
|
||||
r.Handle("/api/v1/kolide/config", h.ModifyAppConfig).Methods("PATCH")
|
||||
r.Handle("/api/v1/kolide/invites", h.CreateInvite).Methods("POST")
|
||||
r.Handle("/api/v1/kolide/invites", h.ListInvites).Methods("GET")
|
||||
r.Handle("/api/v1/kolide/invites/{id}", h.DeleteInvite).Methods("DELETE")
|
||||
r.Handle("/api/v1/kolide/queries/{id}", h.GetQuery).Methods("GET")
|
||||
r.Handle("/api/v1/kolide/queries", h.GetAllQueries).Methods("GET")
|
||||
r.Handle("/api/v1/kolide/queries", h.CreateQuery).Methods("POST")
|
||||
|
||||
@@ -71,6 +71,18 @@ func TestAPIRoutes(t *testing.T) {
|
||||
verb: "PATCH",
|
||||
uri: "/api/v1/kolide/config",
|
||||
},
|
||||
{
|
||||
verb: "GET",
|
||||
uri: "/api/v1/kolide/invites",
|
||||
},
|
||||
{
|
||||
verb: "POST",
|
||||
uri: "/api/v1/kolide/invites",
|
||||
},
|
||||
{
|
||||
verb: "DELETE",
|
||||
uri: "/api/v1/kolide/invites/1",
|
||||
},
|
||||
{
|
||||
verb: "GET",
|
||||
uri: "/api/v1/kolide/queries/1",
|
||||
|
||||
@@ -15,9 +15,7 @@ import (
|
||||
kitlog "github.com/go-kit/kit/log"
|
||||
kithttp "github.com/go-kit/kit/transport/http"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/kolide/kolide-ose/server/config"
|
||||
"github.com/kolide/kolide-ose/server/datastore"
|
||||
"github.com/kolide/kolide-ose/server/kolide"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/net/context"
|
||||
@@ -26,7 +24,7 @@ import (
|
||||
func TestLogin(t *testing.T) {
|
||||
ds, _ := datastore.New("inmem", "")
|
||||
svc, _ := newTestService(ds)
|
||||
createTestUsers(t, ds)
|
||||
users := createTestUsers(t, ds)
|
||||
logger := kitlog.NewLogfmtLogger(os.Stdout)
|
||||
|
||||
opts := []kithttp.ServerOption{
|
||||
@@ -54,7 +52,7 @@ func TestLogin(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
username: "admin1",
|
||||
password: *testUsers["admin1"].Password,
|
||||
password: testUsers["admin1"].PlaintextPassword,
|
||||
status: http.StatusOK,
|
||||
},
|
||||
{
|
||||
@@ -70,21 +68,13 @@ func TestLogin(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, tt := range loginTests {
|
||||
p, ok := testUsers[tt.username]
|
||||
if !ok {
|
||||
p = kolide.UserPayload{
|
||||
Username: stringPtr(tt.username),
|
||||
Password: stringPtr("foobar"),
|
||||
Email: stringPtr("admin1@example.com"),
|
||||
Admin: boolPtr(true),
|
||||
}
|
||||
var shouldBeAdmin bool
|
||||
if u, ok := testUsers[tt.username]; ok {
|
||||
shouldBeAdmin = u.IsAdmin
|
||||
}
|
||||
|
||||
// test sessions
|
||||
testUser, err := ds.User(tt.username)
|
||||
if err != nil {
|
||||
assert.Equal(t, datastore.ErrNotFound, err)
|
||||
}
|
||||
testUser, _ := users[tt.username]
|
||||
|
||||
params := loginRequest{
|
||||
Username: tt.username,
|
||||
@@ -117,7 +107,7 @@ func TestLogin(t *testing.T) {
|
||||
continue // skip remaining tests
|
||||
}
|
||||
|
||||
assert.Equal(t, falseIfNil(p.Admin), jsn.Admin)
|
||||
assert.Equal(t, shouldBeAdmin, jsn.Admin)
|
||||
|
||||
// ensure that a session was created for our test user and stored
|
||||
sessions, err := ds.FindAllSessionsForUser(testUser.ID)
|
||||
@@ -144,36 +134,17 @@ func TestLogin(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func createTestUsers(t *testing.T, ds kolide.Datastore) {
|
||||
svc := svcWithNoValidation(ds, kitlog.NewNopLogger())
|
||||
ctx := context.Background()
|
||||
for _, tt := range testUsers {
|
||||
payload := kolide.UserPayload{
|
||||
Username: tt.Username,
|
||||
Password: tt.Password,
|
||||
Email: tt.Email,
|
||||
Admin: tt.Admin,
|
||||
AdminForcedPasswordReset: tt.AdminForcedPasswordReset,
|
||||
}
|
||||
_, err := svc.NewUser(ctx, payload)
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
}
|
||||
|
||||
func svcWithNoValidation(ds kolide.Datastore, logger kitlog.Logger) kolide.Service {
|
||||
var svc kolide.Service
|
||||
svc = service{
|
||||
ds: ds,
|
||||
logger: logger,
|
||||
config: config.TestConfig(),
|
||||
}
|
||||
|
||||
return svc
|
||||
}
|
||||
|
||||
// an io.ReadCloser for new request body
|
||||
type nopCloser struct {
|
||||
io.Reader
|
||||
}
|
||||
|
||||
func (nopCloser) Close() error { return nil }
|
||||
|
||||
// helper to convert a bool pointer false
|
||||
func falseIfNil(b *bool) bool {
|
||||
if b == nil {
|
||||
return false
|
||||
}
|
||||
return *b
|
||||
}
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/kolide/kolide-ose/server/contexts/viewer"
|
||||
"github.com/kolide/kolide-ose/server/kolide"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
func (mw loggingMiddleware) InviteNewUser(ctx context.Context, payload kolide.InvitePayload) (*kolide.Invite, error) {
|
||||
var (
|
||||
invite *kolide.Invite
|
||||
err error
|
||||
)
|
||||
|
||||
vc, ok := viewer.FromContext(ctx)
|
||||
if !ok {
|
||||
return nil, errNoContext
|
||||
}
|
||||
defer func(begin time.Time) {
|
||||
_ = mw.logger.Log(
|
||||
"method", "InviteNewUser",
|
||||
"created_by", vc.Username(),
|
||||
"err", err,
|
||||
"took", time.Since(begin),
|
||||
)
|
||||
}(time.Now())
|
||||
|
||||
invite, err = mw.Service.InviteNewUser(ctx, payload)
|
||||
return invite, err
|
||||
}
|
||||
|
||||
func (mw loggingMiddleware) DeleteInvite(ctx context.Context, id uint) error {
|
||||
var (
|
||||
err error
|
||||
)
|
||||
vc, ok := viewer.FromContext(ctx)
|
||||
if !ok {
|
||||
return errNoContext
|
||||
}
|
||||
defer func(begin time.Time) {
|
||||
_ = mw.logger.Log(
|
||||
"method", "DeleteInvite",
|
||||
"deleted_by", vc.Username(),
|
||||
"err", err,
|
||||
"took", time.Since(begin),
|
||||
)
|
||||
}(time.Now())
|
||||
err = mw.Service.DeleteInvite(ctx, id)
|
||||
return err
|
||||
}
|
||||
|
||||
func (mw loggingMiddleware) Invites(ctx context.Context) ([]*kolide.Invite, error) {
|
||||
var (
|
||||
invites []*kolide.Invite
|
||||
err error
|
||||
)
|
||||
vc, ok := viewer.FromContext(ctx)
|
||||
if !ok {
|
||||
return nil, errNoContext
|
||||
}
|
||||
defer func(begin time.Time) {
|
||||
_ = mw.logger.Log(
|
||||
"method", "Invites",
|
||||
"called_by", vc.Username(),
|
||||
"err", err,
|
||||
"took", time.Since(begin),
|
||||
)
|
||||
}(time.Now())
|
||||
invites, err = mw.Service.Invites(ctx)
|
||||
return invites, err
|
||||
}
|
||||
|
||||
func (mw loggingMiddleware) VerifyInvite(ctx context.Context, email string, token string) error {
|
||||
var (
|
||||
err error
|
||||
)
|
||||
defer func(begin time.Time) {
|
||||
_ = mw.logger.Log(
|
||||
"method", "VerifyInvite",
|
||||
"email", email,
|
||||
"err", err,
|
||||
"took", time.Since(begin),
|
||||
)
|
||||
}(time.Now())
|
||||
err = mw.Service.VerifyInvite(ctx, email, token)
|
||||
return err
|
||||
}
|
||||
@@ -3,28 +3,29 @@ package service
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/kolide/kolide-ose/server/kolide"
|
||||
"github.com/kolide/kolide-ose/server/contexts/viewer"
|
||||
"github.com/kolide/kolide-ose/server/kolide"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
func (mw loggingMiddleware) NewUser(ctx context.Context, p kolide.UserPayload) (*kolide.User, error) {
|
||||
var (
|
||||
user *kolide.User
|
||||
err error
|
||||
username = "none"
|
||||
user *kolide.User
|
||||
err error
|
||||
username = "none"
|
||||
loggedInUser = "unauthenticated"
|
||||
)
|
||||
|
||||
vc, ok := viewer.FromContext(ctx)
|
||||
if !ok {
|
||||
return nil, errNoContext
|
||||
if ok {
|
||||
loggedInUser = vc.Username()
|
||||
}
|
||||
|
||||
defer func(begin time.Time) {
|
||||
_ = mw.logger.Log(
|
||||
"method", "NewUser",
|
||||
"user", username,
|
||||
"created_by", vc.Username(),
|
||||
"created_by", loggedInUser,
|
||||
"err", err,
|
||||
"took", time.Since(begin),
|
||||
)
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/kolide/kolide-ose/server/kolide"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
func (mw metricsMiddleware) InviteNewUser(ctx context.Context, payload kolide.InvitePayload) (*kolide.Invite, error) {
|
||||
var (
|
||||
invite *kolide.Invite
|
||||
err error
|
||||
)
|
||||
defer func(begin time.Time) {
|
||||
lvs := []string{"method", "InviteNewUser", "error", fmt.Sprint(err != nil)}
|
||||
mw.requestCount.With(lvs...).Add(1)
|
||||
mw.requestLatency.With(lvs...).Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
invite, err = mw.Service.InviteNewUser(ctx, payload)
|
||||
return invite, err
|
||||
}
|
||||
|
||||
func (mw metricsMiddleware) DeleteInvite(ctx context.Context, id uint) error {
|
||||
var (
|
||||
err error
|
||||
)
|
||||
defer func(begin time.Time) {
|
||||
lvs := []string{"method", "DeleteInvite", "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.DeleteInvite(ctx, id)
|
||||
return err
|
||||
}
|
||||
|
||||
func (mw metricsMiddleware) Invites(ctx context.Context) ([]*kolide.Invite, error) {
|
||||
var (
|
||||
invites []*kolide.Invite
|
||||
err error
|
||||
)
|
||||
defer func(begin time.Time) {
|
||||
lvs := []string{"method", "Invites", "error", fmt.Sprint(err != nil)}
|
||||
mw.requestCount.With(lvs...).Add(1)
|
||||
mw.requestLatency.With(lvs...).Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
invites, err = mw.Service.Invites(ctx)
|
||||
return invites, err
|
||||
}
|
||||
|
||||
func (mw metricsMiddleware) VerifyInvite(ctx context.Context, email string, token string) error {
|
||||
var (
|
||||
err error
|
||||
)
|
||||
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
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
// package service holds the implementation of the kolide service interface and the HTTP endpoints
|
||||
// Package service holds the implementation of the kolide service interface and the HTTP endpoints
|
||||
// for the API
|
||||
package service
|
||||
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
jwt "github.com/dgrijalva/jwt-go"
|
||||
"github.com/kolide/kolide-ose/server/datastore"
|
||||
"github.com/kolide/kolide-ose/server/kolide"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
func (svc service) InviteNewUser(ctx context.Context, payload kolide.InvitePayload) (*kolide.Invite, error) {
|
||||
// verify that the user with the given email does not already exist
|
||||
_, err := svc.ds.UserByEmail(*payload.Email)
|
||||
if err == nil {
|
||||
return nil, newInvalidArgumentError("email", "a user with this account already exists")
|
||||
}
|
||||
if err != datastore.ErrNotFound {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// find the user who created the invite
|
||||
inviter, err := svc.User(ctx, *payload.InvitedBy)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
token, err := jwt.New(jwt.SigningMethodHS256).SignedString([]byte(svc.config.App.TokenKey))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
invite := &kolide.Invite{
|
||||
Email: *payload.Email,
|
||||
Admin: *payload.Admin,
|
||||
InvitedBy: inviter.ID,
|
||||
CreatedAt: svc.clock.Now(),
|
||||
Token: token,
|
||||
}
|
||||
if payload.Position != nil {
|
||||
invite.Position = *payload.Position
|
||||
}
|
||||
if payload.Name != nil {
|
||||
invite.Name = *payload.Name
|
||||
}
|
||||
|
||||
invite, err = svc.ds.NewInvite(invite)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
inviteEmail := kolide.Email{
|
||||
From: "no-reply@kolide.co",
|
||||
To: []string{invite.Email},
|
||||
Msg: invite,
|
||||
}
|
||||
err = svc.mailService.SendEmail(inviteEmail)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return invite, nil
|
||||
}
|
||||
|
||||
func (svc service) Invites(ctx context.Context) ([]*kolide.Invite, error) {
|
||||
return svc.ds.Invites()
|
||||
}
|
||||
|
||||
func (svc service) VerifyInvite(ctx context.Context, email, token string) error {
|
||||
invite, err := svc.ds.InviteByEmail(email)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if invite.Token != token {
|
||||
return 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 errors.New("expired invite token")
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
func (svc service) DeleteInvite(ctx context.Context, id uint) error {
|
||||
invite, err := svc.ds.Invite(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return svc.ds.DeleteInvite(invite)
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/WatchBeam/clock"
|
||||
"github.com/kolide/kolide-ose/server/config"
|
||||
"github.com/kolide/kolide-ose/server/datastore"
|
||||
"github.com/kolide/kolide-ose/server/kolide"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestInviteNewUser(t *testing.T) {
|
||||
ds, err := datastore.New("inmem", "")
|
||||
createTestUsers(t, ds)
|
||||
assert.Nil(t, err)
|
||||
nosuchAdminID := uint(999)
|
||||
adminID := uint(1)
|
||||
mailer := &mockMailService{SendEmailFn: func(e kolide.Email) error { return nil }}
|
||||
svc := validationMiddleware{service{
|
||||
ds: ds,
|
||||
config: config.TestConfig(),
|
||||
mailService: mailer,
|
||||
clock: clock.NewMockClock(),
|
||||
}}
|
||||
|
||||
var inviteTests = []struct {
|
||||
payload kolide.InvitePayload
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
wantErr: &invalidArgumentError{
|
||||
{name: "email", reason: "missing required argument"},
|
||||
{name: "invited_by", reason: "missing required argument"},
|
||||
{name: "admin", reason: "missing required argument"},
|
||||
},
|
||||
},
|
||||
{
|
||||
payload: kolide.InvitePayload{
|
||||
Email: stringPtr("nosuchuser@example.com"),
|
||||
InvitedBy: &nosuchAdminID,
|
||||
Admin: boolPtr(false),
|
||||
},
|
||||
wantErr: datastore.ErrNotFound,
|
||||
},
|
||||
{
|
||||
payload: kolide.InvitePayload{
|
||||
Email: stringPtr("admin1@example.com"),
|
||||
InvitedBy: &adminID,
|
||||
Admin: boolPtr(false),
|
||||
},
|
||||
wantErr: &invalidArgumentError{
|
||||
{name: "email", reason: "a user with this account already exists"}},
|
||||
},
|
||||
{
|
||||
payload: kolide.InvitePayload{
|
||||
Email: stringPtr("nosuchuser@example.com"),
|
||||
InvitedBy: &adminID,
|
||||
Admin: boolPtr(false),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range inviteTests {
|
||||
invite, err := svc.InviteNewUser(context.Background(), tt.payload)
|
||||
assert.Equal(t, err, tt.wantErr)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
assert.Equal(t, *tt.payload.InvitedBy, invite.InvitedBy)
|
||||
}
|
||||
}
|
||||
@@ -14,25 +14,33 @@ import (
|
||||
const bcryptCost = 6
|
||||
|
||||
func TestAuthenticate(t *testing.T) {
|
||||
svc, payload, user := setupLoginTests(t)
|
||||
ds, err := datastore.New("gorm-sqlite3", ":memory:")
|
||||
require.Nil(t, err)
|
||||
svc, err := newTestService(ds)
|
||||
require.Nil(t, err)
|
||||
users := createTestUsers(t, ds)
|
||||
|
||||
var loginTests = []struct {
|
||||
username string
|
||||
password string
|
||||
user kolide.User
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
username: *payload.Username,
|
||||
password: *payload.Password,
|
||||
user: users["admin1"],
|
||||
username: testUsers["admin1"].Username,
|
||||
password: testUsers["admin1"].PlaintextPassword,
|
||||
},
|
||||
{
|
||||
username: *payload.Email,
|
||||
password: *payload.Password,
|
||||
user: users["user1"],
|
||||
username: testUsers["user1"].Email,
|
||||
password: testUsers["user1"].PlaintextPassword,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range loginTests {
|
||||
t.Run(tt.username, func(st *testing.T) {
|
||||
svc, _, user = setupLoginTests(t)
|
||||
user := tt.user
|
||||
ctx := context.Background()
|
||||
loggedIn, token, err := svc.Login(ctx, tt.username, tt.password)
|
||||
require.Nil(st, err, "login unsuccesful")
|
||||
@@ -50,22 +58,3 @@ func TestAuthenticate(t *testing.T) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func setupLoginTests(t *testing.T) (kolide.Service, kolide.UserPayload, kolide.User) {
|
||||
ds, err := datastore.New("gorm-sqlite3", ":memory:")
|
||||
assert.Nil(t, err)
|
||||
|
||||
svc, err := newTestService(ds)
|
||||
assert.Nil(t, err)
|
||||
payload := kolide.UserPayload{
|
||||
Username: stringPtr("foo"),
|
||||
Password: stringPtr("bar"),
|
||||
Email: stringPtr("foo@kolide.co"),
|
||||
Admin: boolPtr(false),
|
||||
}
|
||||
ctx := context.Background()
|
||||
user, err := svc.NewUser(ctx, payload)
|
||||
assert.Nil(t, err)
|
||||
assert.NotZero(t, user.ID)
|
||||
return svc, payload, *user
|
||||
}
|
||||
|
||||
@@ -3,17 +3,23 @@ package service
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/kolide/kolide-ose/server/kolide"
|
||||
"github.com/kolide/kolide-ose/server/contexts/viewer"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"github.com/kolide/kolide-ose/server/kolide"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
func (svc service) NewUser(ctx context.Context, p kolide.UserPayload) (*kolide.User, error) {
|
||||
user, err := userFromPayload(p, svc.config.Auth.SaltKeySize, svc.config.Auth.BcryptCost)
|
||||
err := svc.VerifyInvite(ctx, *p.Email, *p.InviteToken)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
invite, err := svc.ds.InviteByEmail(*p.Email)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
user, err := p.User(svc.config.Auth.SaltKeySize, svc.config.Auth.BcryptCost)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -21,6 +27,10 @@ func (svc service) NewUser(ctx context.Context, p kolide.UserPayload) (*kolide.U
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = svc.ds.DeleteInvite(invite)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return user, nil
|
||||
}
|
||||
|
||||
@@ -68,7 +78,7 @@ func (svc service) ModifyUser(ctx context.Context, userID uint, p kolide.UserPay
|
||||
}
|
||||
|
||||
if p.Password != nil {
|
||||
hashed, salt, err := hashPassword(
|
||||
err := user.SetPassword(
|
||||
*p.Password,
|
||||
svc.config.Auth.SaltKeySize,
|
||||
svc.config.Auth.BcryptCost,
|
||||
@@ -76,8 +86,6 @@ func (svc service) ModifyUser(ctx context.Context, userID uint, p kolide.UserPay
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
user.Password = hashed
|
||||
user.Salt = salt
|
||||
user.AdminForcedPasswordReset = false
|
||||
}
|
||||
|
||||
@@ -119,12 +127,10 @@ func (svc service) ResetPassword(ctx context.Context, token, password string) er
|
||||
return err
|
||||
}
|
||||
|
||||
hashed, salt, err := hashPassword(password, svc.config.Auth.SaltKeySize, svc.config.Auth.BcryptCost)
|
||||
err = user.SetPassword(password, svc.config.Auth.SaltKeySize, svc.config.Auth.BcryptCost)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
user.Salt = salt
|
||||
user.Password = hashed
|
||||
if err := svc.saveUser(user); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -160,7 +166,8 @@ func (svc service) RequestPasswordReset(ctx context.Context, email string) error
|
||||
}
|
||||
}
|
||||
|
||||
token, err := generateRandomText(svc.config.SMTP.TokenKeySize)
|
||||
// TODO: change this to jwt key
|
||||
token, err := generateRandomText(svc.config.App.TokenKeySize)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -201,48 +208,6 @@ func (svc service) saveUser(user *kolide.User) error {
|
||||
return svc.ds.SaveUser(user)
|
||||
}
|
||||
|
||||
func userFromPayload(p kolide.UserPayload, keySize, cost int) (*kolide.User, error) {
|
||||
hashed, salt, err := hashPassword(*p.Password, keySize, cost)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user := &kolide.User{
|
||||
Username: *p.Username,
|
||||
Email: *p.Email,
|
||||
Admin: falseIfNil(p.Admin),
|
||||
AdminForcedPasswordReset: falseIfNil(p.AdminForcedPasswordReset),
|
||||
Salt: salt,
|
||||
Enabled: true,
|
||||
Password: hashed,
|
||||
}
|
||||
|
||||
// add optional fields
|
||||
if p.Name != nil {
|
||||
user.Name = *p.Name
|
||||
}
|
||||
if p.Position != nil {
|
||||
user.Position = *p.Position
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func hashPassword(plaintext string, keySize, cost int) ([]byte, string, error) {
|
||||
salt, err := generateRandomText(keySize)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
withSalt := []byte(fmt.Sprintf("%s%s", plaintext, salt))
|
||||
hashed, err := bcrypt.GenerateFromPassword(withSalt, cost)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
return hashed, salt, nil
|
||||
}
|
||||
|
||||
// generateRandomText return a string generated by filling in keySize bytes with
|
||||
// random data and then base64 encoding those bytes
|
||||
func generateRandomText(keySize int) (string, error) {
|
||||
@@ -253,11 +218,3 @@ func generateRandomText(keySize int) (string, error) {
|
||||
}
|
||||
return base64.StdEncoding.EncodeToString(key), nil
|
||||
}
|
||||
|
||||
// helper to convert a bool pointer false
|
||||
func falseIfNil(b *bool) bool {
|
||||
if b == nil {
|
||||
return false
|
||||
}
|
||||
return *b
|
||||
}
|
||||
|
||||
@@ -6,10 +6,11 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/WatchBeam/clock"
|
||||
"github.com/kolide/kolide-ose/server/config"
|
||||
"github.com/kolide/kolide-ose/server/contexts/viewer"
|
||||
"github.com/kolide/kolide-ose/server/datastore"
|
||||
"github.com/kolide/kolide-ose/server/kolide"
|
||||
"github.com/kolide/kolide-ose/server/contexts/viewer"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/net/context"
|
||||
@@ -114,6 +115,7 @@ func TestRequestPasswordReset(t *testing.T) {
|
||||
func TestCreateUser(t *testing.T) {
|
||||
ds, _ := datastore.New("inmem", "")
|
||||
svc, _ := newTestService(ds)
|
||||
invites := setupInvites(t, ds, []string{"admin2@example.com"})
|
||||
ctx := context.Background()
|
||||
|
||||
var createUserTests = []struct {
|
||||
@@ -122,48 +124,70 @@ func TestCreateUser(t *testing.T) {
|
||||
Email *string
|
||||
NeedsPasswordReset *bool
|
||||
Admin *bool
|
||||
InviteToken *string
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
Username: stringPtr("admin1"),
|
||||
Username: stringPtr("admin2"),
|
||||
Password: stringPtr("foobar"),
|
||||
InviteToken: &invites["admin2@example.com"].Token,
|
||||
wantErr: &invalidArgumentError{invalidArgument{name: "email", reason: "missing required argument"}},
|
||||
},
|
||||
{
|
||||
Username: stringPtr("admin2"),
|
||||
Password: stringPtr("foobar"),
|
||||
wantErr: invalidArgumentError{invalidArgument{name: "email", reason: "missing required argument"}},
|
||||
Email: stringPtr("admin2@example.com"),
|
||||
wantErr: &invalidArgumentError{invalidArgument{name: "invite_token", reason: "missing required argument"}},
|
||||
},
|
||||
{
|
||||
Username: stringPtr("admin1"),
|
||||
Username: stringPtr("admin2"),
|
||||
Password: stringPtr("foobar"),
|
||||
Email: stringPtr("admin1@example.com"),
|
||||
Email: stringPtr("admin2@example.com"),
|
||||
NeedsPasswordReset: boolPtr(true),
|
||||
Admin: boolPtr(false),
|
||||
InviteToken: &invites["admin2@example.com"].Token,
|
||||
},
|
||||
{ // should return ErrNotFound because the invite is deleted
|
||||
// after a user signs up
|
||||
Username: stringPtr("admin2"),
|
||||
Password: stringPtr("foobar"),
|
||||
Email: stringPtr("admin2@example.com"),
|
||||
NeedsPasswordReset: boolPtr(true),
|
||||
Admin: boolPtr(false),
|
||||
InviteToken: &invites["admin2@example.com"].Token,
|
||||
wantErr: datastore.ErrNotFound,
|
||||
},
|
||||
{
|
||||
Username: stringPtr("admin1"),
|
||||
Username: stringPtr("admin3"),
|
||||
Password: stringPtr("foobar"),
|
||||
Email: stringPtr("admin1@example.com"),
|
||||
Email: &invites["expired"].Email,
|
||||
NeedsPasswordReset: boolPtr(true),
|
||||
Admin: boolPtr(false),
|
||||
wantErr: datastore.ErrExists,
|
||||
InviteToken: &invites["expired"].Token,
|
||||
wantErr: errors.New("expired invite token"),
|
||||
},
|
||||
{
|
||||
Username: stringPtr("@admin1"),
|
||||
Username: stringPtr("@admin2"),
|
||||
Password: stringPtr("foobar"),
|
||||
Email: stringPtr("admin1@example.com"),
|
||||
Email: stringPtr("admin2@example.com"),
|
||||
NeedsPasswordReset: boolPtr(true),
|
||||
Admin: boolPtr(false),
|
||||
wantErr: invalidArgumentError{invalidArgument{name: "username", reason: "'@' character not allowed in usernames"}},
|
||||
InviteToken: &invites["admin2@example.com"].Token,
|
||||
wantErr: &invalidArgumentError{invalidArgument{name: "username", reason: "'@' character not allowed in usernames"}},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range createUserTests {
|
||||
for i, tt := range createUserTests {
|
||||
payload := kolide.UserPayload{
|
||||
Username: tt.Username,
|
||||
Password: tt.Password,
|
||||
Email: tt.Email,
|
||||
Admin: tt.Admin,
|
||||
Username: tt.Username,
|
||||
Password: tt.Password,
|
||||
Email: tt.Email,
|
||||
Admin: tt.Admin,
|
||||
InviteToken: tt.InviteToken,
|
||||
AdminForcedPasswordReset: tt.NeedsPasswordReset,
|
||||
}
|
||||
user, err := svc.NewUser(ctx, payload)
|
||||
require.Equal(t, tt.wantErr, err)
|
||||
require.Equal(t, tt.wantErr, err, strconv.Itoa(i))
|
||||
if err != nil {
|
||||
// skip rest of the test if error is not nil
|
||||
continue
|
||||
@@ -183,6 +207,32 @@ func TestCreateUser(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func setupInvites(t *testing.T, ds kolide.Datastore, emails []string) map[string]*kolide.Invite {
|
||||
invites := make(map[string]*kolide.Invite)
|
||||
users := createTestUsers(t, ds)
|
||||
mockClock := clock.NewMockClock()
|
||||
for _, e := range emails {
|
||||
invite, err := ds.NewInvite(&kolide.Invite{
|
||||
InvitedBy: users["admin1"].ID,
|
||||
Token: e,
|
||||
Email: e,
|
||||
CreatedAt: mockClock.Now(),
|
||||
})
|
||||
require.Nil(t, err)
|
||||
invites[e] = invite
|
||||
}
|
||||
// add an expired invitation
|
||||
invite, err := ds.NewInvite(&kolide.Invite{
|
||||
InvitedBy: users["admin1"].ID,
|
||||
Token: "expired",
|
||||
Email: "expiredinvite@gmail.com",
|
||||
CreatedAt: mockClock.Now().AddDate(-1, 0, 0),
|
||||
})
|
||||
require.Nil(t, err)
|
||||
invites["expired"] = invite
|
||||
return invites
|
||||
}
|
||||
|
||||
func TestChangeUserPassword(t *testing.T) {
|
||||
ds, _ := datastore.New("inmem", "")
|
||||
svc, _ := newTestService(ds)
|
||||
@@ -203,11 +253,11 @@ func TestChangeUserPassword(t *testing.T) {
|
||||
},
|
||||
{ // missing token
|
||||
newPassword: "123cat!",
|
||||
wantErr: invalidArgumentError{invalidArgument{name: "token", reason: "cannot be empty field"}},
|
||||
wantErr: &invalidArgumentError{invalidArgument{name: "token", reason: "cannot be empty field"}},
|
||||
},
|
||||
{ // missing password
|
||||
token: "abcd",
|
||||
wantErr: invalidArgumentError{invalidArgument{name: "new_password", reason: "cannot be empty field"}},
|
||||
wantErr: &invalidArgumentError{invalidArgument{name: "new_password", reason: "cannot be empty field"}},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -227,40 +277,3 @@ func TestChangeUserPassword(t *testing.T) {
|
||||
assert.Equal(t, tt.wantErr, serr, strconv.Itoa(i))
|
||||
}
|
||||
}
|
||||
|
||||
type mockMailService struct {
|
||||
SendEmailFn func(e kolide.Email) error
|
||||
Invoked bool
|
||||
}
|
||||
|
||||
func (svc *mockMailService) SendEmail(e kolide.Email) error {
|
||||
svc.Invoked = true
|
||||
return svc.SendEmailFn(e)
|
||||
}
|
||||
|
||||
var testUsers = map[string]kolide.UserPayload{
|
||||
"admin1": {
|
||||
Username: stringPtr("admin1"),
|
||||
Password: stringPtr("foobar"),
|
||||
Email: stringPtr("admin1@example.com"),
|
||||
Admin: boolPtr(true),
|
||||
},
|
||||
"user1": {
|
||||
Username: stringPtr("user1"),
|
||||
Password: stringPtr("foobar"),
|
||||
Email: stringPtr("user1@example.com"),
|
||||
},
|
||||
"user2": {
|
||||
Username: stringPtr("user2"),
|
||||
Password: stringPtr("bazfoo"),
|
||||
Email: stringPtr("user2@example.com"),
|
||||
},
|
||||
}
|
||||
|
||||
func stringPtr(s string) *string {
|
||||
return &s
|
||||
}
|
||||
|
||||
func boolPtr(b bool) *bool {
|
||||
return &b
|
||||
}
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"github.com/WatchBeam/clock"
|
||||
kitlog "github.com/go-kit/kit/log"
|
||||
"github.com/kolide/kolide-ose/server/config"
|
||||
"github.com/kolide/kolide-ose/server/kolide"
|
||||
)
|
||||
|
||||
func newTestService(ds kolide.Datastore) (kolide.Service, error) {
|
||||
return NewService(ds, kitlog.NewNopLogger(), config.TestConfig(), nil, clock.C)
|
||||
}
|
||||
|
||||
func newTestServiceWithClock(ds kolide.Datastore, c clock.Clock) (kolide.Service, error) {
|
||||
return NewService(ds, kitlog.NewNopLogger(), config.TestConfig(), nil, c)
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
func decodeCreateInviteRequest(ctx context.Context, r *http.Request) (interface{}, error) {
|
||||
var req createInviteRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req.payload); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeDeleteInviteRequest(ctx context.Context, r *http.Request) (interface{}, error) {
|
||||
id, err := idFromRequest(r, "id")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var req deleteInviteRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.ID = id
|
||||
return req, nil
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestDecodeCreateInviteRequest(t *testing.T) {
|
||||
router := mux.NewRouter()
|
||||
router.HandleFunc("/api/v1/kolide/invites", func(writer http.ResponseWriter, request *http.Request) {
|
||||
r, err := decodeCreateInviteRequest(context.Background(), request)
|
||||
assert.Nil(t, err)
|
||||
|
||||
params := r.(createInviteRequest)
|
||||
assert.Equal(t, "foo", *params.payload.Name)
|
||||
assert.Equal(t, "foo@kolide.co", *params.payload.Email)
|
||||
assert.Equal(t, uint(1), *params.payload.InvitedBy)
|
||||
}).Methods("POST")
|
||||
|
||||
var body bytes.Buffer
|
||||
body.Write([]byte(`{
|
||||
"name": "foo",
|
||||
"email": "foo@kolide.co",
|
||||
"invited_by": 1
|
||||
}`))
|
||||
|
||||
router.ServeHTTP(
|
||||
httptest.NewRecorder(),
|
||||
httptest.NewRequest("POST", "/api/v1/kolide/invites", &body),
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/WatchBeam/clock"
|
||||
kitlog "github.com/go-kit/kit/log"
|
||||
"github.com/kolide/kolide-ose/server/config"
|
||||
"github.com/kolide/kolide-ose/server/kolide"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func newTestService(ds kolide.Datastore) (kolide.Service, error) {
|
||||
return NewService(ds, kitlog.NewNopLogger(), config.TestConfig(), nil, clock.C)
|
||||
}
|
||||
|
||||
func newTestServiceWithClock(ds kolide.Datastore, c clock.Clock) (kolide.Service, error) {
|
||||
return NewService(ds, kitlog.NewNopLogger(), config.TestConfig(), nil, c)
|
||||
}
|
||||
|
||||
func createTestUsers(t *testing.T, ds kolide.Datastore) map[string]kolide.User {
|
||||
users := make(map[string]kolide.User)
|
||||
for _, u := range testUsers {
|
||||
user := &kolide.User{
|
||||
Username: u.Username,
|
||||
Email: u.Email,
|
||||
Admin: u.IsAdmin,
|
||||
Enabled: u.Enabled,
|
||||
}
|
||||
err := user.SetPassword(u.PlaintextPassword, 10, 10)
|
||||
require.Nil(t, err)
|
||||
user, err = ds.NewUser(user)
|
||||
require.Nil(t, err)
|
||||
users[user.Username] = *user
|
||||
}
|
||||
return users
|
||||
}
|
||||
|
||||
var testUsers = map[string]struct {
|
||||
Username string
|
||||
Email string
|
||||
PlaintextPassword string
|
||||
IsAdmin bool
|
||||
Enabled bool
|
||||
}{
|
||||
"admin1": {
|
||||
Username: "admin1",
|
||||
PlaintextPassword: "foobar",
|
||||
Email: "admin1@example.com",
|
||||
IsAdmin: true,
|
||||
Enabled: true,
|
||||
},
|
||||
"user1": {
|
||||
Username: "user1",
|
||||
PlaintextPassword: "foobar",
|
||||
Email: "user1@example.com",
|
||||
Enabled: true,
|
||||
},
|
||||
"user2": {
|
||||
Username: "user2",
|
||||
PlaintextPassword: "bazfoo",
|
||||
Email: "user2@example.com",
|
||||
Enabled: true,
|
||||
},
|
||||
"disabled1": {
|
||||
Username: "disabled1",
|
||||
PlaintextPassword: "bazfoo",
|
||||
Email: "disabled1@example.com",
|
||||
Enabled: false,
|
||||
},
|
||||
}
|
||||
|
||||
type mockMailService struct {
|
||||
SendEmailFn func(e kolide.Email) error
|
||||
Invoked bool
|
||||
}
|
||||
|
||||
func (svc *mockMailService) SendEmail(e kolide.Email) error {
|
||||
svc.Invoked = true
|
||||
return svc.SendEmailFn(e)
|
||||
}
|
||||
|
||||
func stringPtr(s string) *string {
|
||||
return &s
|
||||
}
|
||||
|
||||
func boolPtr(b bool) *bool {
|
||||
return &b
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"github.com/kolide/kolide-ose/server/kolide"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
func (mw validationMiddleware) InviteNewUser(ctx context.Context, payload kolide.InvitePayload) (*kolide.Invite, error) {
|
||||
invalid := &invalidArgumentError{}
|
||||
if payload.Email == nil {
|
||||
invalid.Append("email", "missing required argument")
|
||||
}
|
||||
if payload.InvitedBy == nil {
|
||||
invalid.Append("invited_by", "missing required argument")
|
||||
}
|
||||
if payload.Admin == nil {
|
||||
invalid.Append("admin", "missing required argument")
|
||||
}
|
||||
if invalid.HasErrors() {
|
||||
return nil, invalid
|
||||
}
|
||||
return mw.Service.InviteNewUser(ctx, payload)
|
||||
}
|
||||
@@ -13,37 +13,40 @@ type validationMiddleware struct {
|
||||
}
|
||||
|
||||
func (mw validationMiddleware) NewUser(ctx context.Context, p kolide.UserPayload) (*kolide.User, error) {
|
||||
var invalid []invalidArgument
|
||||
invalid := &invalidArgumentError{}
|
||||
if p.Username == nil {
|
||||
invalid = append(invalid, invalidArgument{name: "username", reason: "missing required argument"})
|
||||
invalid.Append("username", "missing required argument")
|
||||
}
|
||||
if p.Username != nil {
|
||||
if strings.Contains(*p.Username, "@") {
|
||||
invalid = append(invalid, invalidArgument{name: "username", reason: "'@' character not allowed in usernames"})
|
||||
invalid.Append("username", "'@' character not allowed in usernames")
|
||||
}
|
||||
}
|
||||
if p.Password == nil {
|
||||
invalid = append(invalid, invalidArgument{name: "password", reason: "missing required argument"})
|
||||
invalid.Append("password", "missing required argument")
|
||||
}
|
||||
if p.Email == nil {
|
||||
invalid = append(invalid, invalidArgument{name: "email", reason: "missing required argument"})
|
||||
invalid.Append("email", "missing required argument")
|
||||
}
|
||||
if len(invalid) != 0 {
|
||||
return nil, invalidArgumentError(invalid)
|
||||
if p.InviteToken == nil {
|
||||
invalid.Append("invite_token", "missing required argument")
|
||||
}
|
||||
if invalid.HasErrors() {
|
||||
return nil, invalid
|
||||
}
|
||||
return mw.Service.NewUser(ctx, p)
|
||||
}
|
||||
|
||||
func (mw validationMiddleware) ResetPassword(ctx context.Context, token, password string) error {
|
||||
var invalid []invalidArgument
|
||||
invalid := &invalidArgumentError{}
|
||||
if token == "" {
|
||||
invalid = append(invalid, invalidArgument{name: "token", reason: "cannot be empty field"})
|
||||
invalid.Append("token", "cannot be empty field")
|
||||
}
|
||||
if password == "" {
|
||||
invalid = append(invalid, invalidArgument{name: "new_password", reason: "cannot be empty field"})
|
||||
invalid.Append("new_password", "cannot be empty field")
|
||||
}
|
||||
if len(invalid) != 0 {
|
||||
return invalidArgumentError(invalid)
|
||||
if invalid.HasErrors() {
|
||||
return invalid
|
||||
}
|
||||
return mw.Service.ResetPassword(ctx, token, password)
|
||||
}
|
||||
@@ -54,6 +57,28 @@ type invalidArgument struct {
|
||||
reason string
|
||||
}
|
||||
|
||||
// newInvalidArgumentError returns a invalidArgumentError with at least
|
||||
// one error.
|
||||
func newInvalidArgumentError(name, reason string) *invalidArgumentError {
|
||||
var invalid invalidArgumentError
|
||||
invalid = append(invalid, invalidArgument{
|
||||
name: name,
|
||||
reason: reason,
|
||||
})
|
||||
return &invalid
|
||||
}
|
||||
|
||||
func (e *invalidArgumentError) Append(name, reason string) {
|
||||
*e = append(*e, invalidArgument{
|
||||
name: name,
|
||||
reason: reason,
|
||||
})
|
||||
}
|
||||
|
||||
func (e *invalidArgumentError) HasErrors() bool {
|
||||
return len(*e) != 0
|
||||
}
|
||||
|
||||
// invalidArgumentError is returned when one or more arguments are invalid.
|
||||
func (e invalidArgumentError) Error() string {
|
||||
switch len(e) {
|
||||
|
||||
Reference in New Issue
Block a user