mirror of
https://github.com/fleetdm/fleet
synced 2026-05-24 09:28:54 +00:00
Add role storage to invites APIs (#576)
- Reorder migrations post-rebase - Fix global_role in user payload - Add teams/roles to invite entities - Add teams/roles support to invite datastore methods - Update tests - Carry over team information from invite when creating user
This commit is contained in:
parent
56d2480389
commit
08fce719e9
11 changed files with 164 additions and 108 deletions
|
|
@ -8,7 +8,6 @@ const { invites: inviteMocks } = mocks;
|
|||
|
||||
describe('Kolide - API client (invites)', () => {
|
||||
afterEach(() => {
|
||||
nock.cleanAll();
|
||||
Kolide.setBearerToken(null);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -8,14 +8,24 @@ import (
|
|||
"github.com/fleetdm/fleet/server/kolide"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gopkg.in/guregu/null.v3"
|
||||
)
|
||||
|
||||
func testCreateInvite(t *testing.T, ds kolide.Datastore) {
|
||||
for i := 0; i < 3; i++ {
|
||||
_, err := ds.NewTeam(&kolide.Team{Name: fmt.Sprintf("%d", i)})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
invite := &kolide.Invite{
|
||||
|
||||
Email: "user@foo.com",
|
||||
Name: "user",
|
||||
Token: "some_user",
|
||||
Teams: []kolide.UserTeam{
|
||||
{Role: "observer", Team: kolide.Team{ID: 1}},
|
||||
{Role: "maintainer", Team: kolide.Team{ID: 3}},
|
||||
},
|
||||
}
|
||||
|
||||
invite, err := ds.NewInvite(invite)
|
||||
|
|
@ -25,16 +35,17 @@ func testCreateInvite(t *testing.T, ds kolide.Datastore) {
|
|||
require.Nil(t, err)
|
||||
assert.Equal(t, invite.ID, verify.ID)
|
||||
assert.Equal(t, invite.Email, verify.Email)
|
||||
assert.Len(t, invite.Teams, 2)
|
||||
}
|
||||
|
||||
func setupTestInvites(t *testing.T, ds kolide.Datastore) {
|
||||
|
||||
var err error
|
||||
admin := &kolide.Invite{
|
||||
Email: "admin@foo.com",
|
||||
Admin: true,
|
||||
Name: "Xadmin",
|
||||
Token: "admin",
|
||||
Email: "admin@foo.com",
|
||||
Admin: true,
|
||||
Name: "Xadmin",
|
||||
Token: "admin",
|
||||
GlobalRole: null.StringFrom("admin"),
|
||||
}
|
||||
|
||||
admin, err = ds.NewInvite(admin)
|
||||
|
|
@ -42,11 +53,12 @@ func setupTestInvites(t *testing.T, ds kolide.Datastore) {
|
|||
|
||||
for user := 0; user < 23; user++ {
|
||||
i := kolide.Invite{
|
||||
InvitedBy: admin.ID,
|
||||
Email: fmt.Sprintf("user%d@foo.com", user),
|
||||
Admin: false,
|
||||
Name: fmt.Sprintf("User%02d", user),
|
||||
Token: fmt.Sprintf("usertoken%d", user),
|
||||
InvitedBy: admin.ID,
|
||||
Email: fmt.Sprintf("user%d@foo.com", user),
|
||||
Admin: false,
|
||||
Name: fmt.Sprintf("User%02d", user),
|
||||
Token: fmt.Sprintf("usertoken%d", user),
|
||||
GlobalRole: null.StringFrom("observer"),
|
||||
}
|
||||
|
||||
_, err := ds.NewInvite(&i)
|
||||
|
|
@ -56,12 +68,6 @@ func setupTestInvites(t *testing.T, ds kolide.Datastore) {
|
|||
}
|
||||
|
||||
func testListInvites(t *testing.T, ds kolide.Datastore) {
|
||||
// TODO: fix this for inmem
|
||||
if ds.Name() == "inmem" {
|
||||
fmt.Println("Busted test skipped for inmem")
|
||||
return
|
||||
}
|
||||
|
||||
setupTestInvites(t, ds)
|
||||
|
||||
opt := kolide.ListOptions{
|
||||
|
|
@ -77,6 +83,7 @@ func testListInvites(t *testing.T, ds kolide.Datastore) {
|
|||
assert.Equal(t, len(result), 10)
|
||||
assert.Equal(t, "User00", result[0].Name)
|
||||
assert.Equal(t, "User09", result[9].Name)
|
||||
assert.Equal(t, null.StringFrom("observer"), result[9].GlobalRole)
|
||||
|
||||
opt.Page = 2
|
||||
opt.OrderDirection = kolide.OrderDescending
|
||||
|
|
@ -105,31 +112,6 @@ func testDeleteInvite(t *testing.T, ds kolide.Datastore) {
|
|||
|
||||
}
|
||||
|
||||
func testSaveInvite(t *testing.T, ds kolide.Datastore) {
|
||||
setupTestInvites(t, ds)
|
||||
|
||||
invite, err := ds.InviteByEmail("user0@foo.com")
|
||||
assert.Nil(t, err)
|
||||
assert.NotNil(t, invite)
|
||||
|
||||
invite, err = ds.Invite(invite.ID)
|
||||
assert.Nil(t, err)
|
||||
assert.NotNil(t, invite)
|
||||
|
||||
invite.Name = "Bob"
|
||||
invite.Admin = true
|
||||
|
||||
err = ds.SaveInvite(invite)
|
||||
assert.Nil(t, err)
|
||||
|
||||
invite, err = ds.Invite(invite.ID)
|
||||
assert.Nil(t, err)
|
||||
assert.NotNil(t, invite)
|
||||
assert.Equal(t, "Bob", invite.Name)
|
||||
assert.True(t, invite.Admin)
|
||||
|
||||
}
|
||||
|
||||
func testInviteByToken(t *testing.T, ds kolide.Datastore) {
|
||||
setupTestInvites(t, ds)
|
||||
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ var testFunctions = [...]func(*testing.T, kolide.Datastore){
|
|||
testInviteByToken,
|
||||
testListInvites,
|
||||
testDeleteInvite,
|
||||
testSaveInvite,
|
||||
testDeleteQuery,
|
||||
testDeleteQueries,
|
||||
testSaveQuery,
|
||||
|
|
|
|||
|
|
@ -107,19 +107,6 @@ func (d *Datastore) InviteByToken(token string) (*kolide.Invite, error) {
|
|||
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()
|
||||
defer d.mtx.Unlock()
|
||||
|
||||
if _, ok := d.invites[invite.ID]; !ok {
|
||||
return notFound("Invite").WithID(invite.ID)
|
||||
}
|
||||
|
||||
d.invites[invite.ID] = invite
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteInvite deletes an invitation.
|
||||
func (d *Datastore) DeleteInvite(id uint) error {
|
||||
d.mtx.Lock()
|
||||
|
|
|
|||
|
|
@ -3,20 +3,22 @@ package mysql
|
|||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/fleetdm/fleet/server/kolide"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// NewInvite generates a new invitation.
|
||||
func (d *Datastore) NewInvite(i *kolide.Invite) (*kolide.Invite, error) {
|
||||
sqlStmt := `
|
||||
INSERT INTO invites ( invited_by, email, admin, name, position, token, sso_enabled)
|
||||
VALUES ( ?, ?, ?, ?, ?, ?, ?)
|
||||
INSERT INTO invites ( invited_by, email, admin, name, position, token, sso_enabled, global_role )
|
||||
VALUES ( ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`
|
||||
|
||||
result, err := d.db.Exec(sqlStmt, i.InvitedBy, i.Email, i.Admin,
|
||||
i.Name, i.Position, i.Token, i.SSOEnabled)
|
||||
i.Name, i.Position, i.Token, i.SSOEnabled, i.GlobalRole)
|
||||
if err != nil && isDuplicate(err) {
|
||||
return nil, alreadyExists("Invite", 0)
|
||||
} else if err != nil {
|
||||
|
|
@ -26,15 +28,30 @@ func (d *Datastore) NewInvite(i *kolide.Invite) (*kolide.Invite, error) {
|
|||
id, _ := result.LastInsertId()
|
||||
i.ID = uint(id)
|
||||
|
||||
if len(i.Teams) == 0 {
|
||||
return i, nil
|
||||
}
|
||||
|
||||
// Bulk insert teams
|
||||
const valueStr = "(?,?,?),"
|
||||
var args []interface{}
|
||||
for _, userTeam := range i.Teams {
|
||||
args = append(args, i.ID, userTeam.Team.ID, userTeam.Role)
|
||||
}
|
||||
sql := "INSERT INTO invite_teams (invite_id, team_id, role) VALUES " +
|
||||
strings.Repeat(valueStr, len(i.Teams))
|
||||
sql = strings.TrimSuffix(sql, ",")
|
||||
if _, err := d.db.Exec(sql, args...); err != nil {
|
||||
return nil, errors.Wrap(err, "insert teams")
|
||||
}
|
||||
|
||||
return i, nil
|
||||
}
|
||||
|
||||
// ListInvites lists all invites in the Fleet database. Supply query options
|
||||
// using the opt parameter. See kolide.ListOptions
|
||||
func (d *Datastore) ListInvites(opt kolide.ListOptions) ([]*kolide.Invite, error) {
|
||||
|
||||
invites := []*kolide.Invite{}
|
||||
|
||||
query := appendListOptionsToSQL("SELECT * FROM invites", opt)
|
||||
err := d.db.Select(&invites, query)
|
||||
if err == sql.ErrNoRows {
|
||||
|
|
@ -42,6 +59,11 @@ func (d *Datastore) ListInvites(opt kolide.ListOptions) ([]*kolide.Invite, error
|
|||
} else if err != nil {
|
||||
return nil, errors.Wrap(err, "select invite by ID")
|
||||
}
|
||||
|
||||
if err := d.loadTeamsForInvites(invites); err != nil {
|
||||
return nil, errors.Wrap(err, "load teams")
|
||||
}
|
||||
|
||||
return invites, nil
|
||||
}
|
||||
|
||||
|
|
@ -54,6 +76,11 @@ func (d *Datastore) Invite(id uint) (*kolide.Invite, error) {
|
|||
} else if err != nil {
|
||||
return nil, errors.Wrap(err, "select invite by ID")
|
||||
}
|
||||
|
||||
if err := d.loadTeamsForInvites([]*kolide.Invite{&invite}); err != nil {
|
||||
return nil, errors.Wrap(err, "load teams")
|
||||
}
|
||||
|
||||
return &invite, nil
|
||||
}
|
||||
|
||||
|
|
@ -67,6 +94,11 @@ func (d *Datastore) InviteByEmail(email string) (*kolide.Invite, error) {
|
|||
} else if err != nil {
|
||||
return nil, errors.Wrap(err, "sqlx get invite by email")
|
||||
}
|
||||
|
||||
if err := d.loadTeamsForInvites([]*kolide.Invite{&invite}); err != nil {
|
||||
return nil, errors.Wrap(err, "load teams")
|
||||
}
|
||||
|
||||
return &invite, nil
|
||||
}
|
||||
|
||||
|
|
@ -80,34 +112,56 @@ func (d *Datastore) InviteByToken(token string) (*kolide.Invite, error) {
|
|||
} else if err != nil {
|
||||
return nil, errors.Wrap(err, "sqlx get invite by token")
|
||||
}
|
||||
|
||||
if err := d.loadTeamsForInvites([]*kolide.Invite{&invite}); err != nil {
|
||||
return nil, errors.Wrap(err, "load teams")
|
||||
}
|
||||
|
||||
return &invite, nil
|
||||
}
|
||||
|
||||
// SaveInvite modifies existing Invite
|
||||
func (d *Datastore) SaveInvite(i *kolide.Invite) error {
|
||||
sql := `
|
||||
UPDATE invites SET invited_by = ?, email = ?, admin = ?,
|
||||
name = ?, position = ?, token = ?, sso_enabled = ?
|
||||
WHERE id = ?
|
||||
`
|
||||
results, err := d.db.Exec(sql, i.InvitedBy, i.Email,
|
||||
i.Admin, i.Name, i.Position, i.Token, i.SSOEnabled, i.ID,
|
||||
)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "save invite")
|
||||
}
|
||||
rowsAffected, err := results.RowsAffected()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "rows affected updating invite")
|
||||
}
|
||||
if rowsAffected == 0 {
|
||||
return notFound("Invite").WithID(i.ID)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
func (d *Datastore) DeleteInvite(id uint) error {
|
||||
return d.deleteEntity("invites", id)
|
||||
}
|
||||
|
||||
func (d *Datastore) loadTeamsForInvites(invites []*kolide.Invite) error {
|
||||
inviteIDs := make([]uint, 0, len(invites)+1)
|
||||
// Make sure the slice is never empty for IN by filling a nonexistent ID
|
||||
inviteIDs = append(inviteIDs, 0)
|
||||
idToInvite := make(map[uint]*kolide.Invite, len(invites))
|
||||
for _, u := range invites {
|
||||
// Initialize empty slice so we get an array in JSON responses instead
|
||||
// of null if it is empty
|
||||
u.Teams = []kolide.UserTeam{}
|
||||
// Track IDs for queries and matching
|
||||
inviteIDs = append(inviteIDs, u.ID)
|
||||
idToInvite[u.ID] = u
|
||||
}
|
||||
|
||||
sql := `
|
||||
SELECT ut.team_id AS id, ut.invite_id, ut.role, t.name
|
||||
FROM invite_teams ut INNER JOIN teams t ON ut.team_id = t.id
|
||||
WHERE ut.invite_id IN (?)
|
||||
ORDER BY invite_id, team_id
|
||||
`
|
||||
sql, args, err := sqlx.In(sql, inviteIDs)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "sqlx.In loadTeamsForInvites")
|
||||
}
|
||||
|
||||
var rows []struct {
|
||||
kolide.UserTeam
|
||||
InviteID uint `db:"invite_id"`
|
||||
}
|
||||
if err := d.db.Select(&rows, sql, args...); err != nil {
|
||||
return errors.Wrap(err, "get loadTeamsForInvites")
|
||||
}
|
||||
|
||||
// Map each row to the appropriate invite
|
||||
for _, r := range rows {
|
||||
invite := idToInvite[r.InviteID]
|
||||
invite.Teams = append(invite.Teams, r.UserTeam)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,37 @@
|
|||
package tables
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func init() {
|
||||
MigrationClient.AddMigration(Up_20210401164226, Down_20210401164226)
|
||||
}
|
||||
|
||||
func Up_20210401164226(tx *sql.Tx) error {
|
||||
// Invites <> Teams mapping
|
||||
if _, err := tx.Exec(`CREATE TABLE IF NOT EXISTS invite_teams (
|
||||
invite_id INT UNSIGNED NOT NULL,
|
||||
team_id INT UNSIGNED NOT NULL,
|
||||
role VARCHAR(64) NOT NULL,
|
||||
PRIMARY KEY (invite_id, team_id),
|
||||
FOREIGN KEY fk_invite_id (invite_id) REFERENCES invites (id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
FOREIGN KEY fk_team_id (team_id) REFERENCES teams (id) ON DELETE CASCADE ON UPDATE CASCADE
|
||||
)`); err != nil {
|
||||
return errors.Wrap(err, "create invite_teams")
|
||||
}
|
||||
|
||||
if _, err := tx.Exec(`ALTER TABLE invites
|
||||
ADD global_role VARCHAR(64) DEFAULT NULL
|
||||
`); err != nil {
|
||||
return errors.Wrap(err, "alter users")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func Down_20210401164226(tx *sql.Tx) error {
|
||||
return nil
|
||||
}
|
||||
|
|
@ -2,6 +2,8 @@ package kolide
|
|||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"gopkg.in/guregu/null.v3"
|
||||
)
|
||||
|
||||
// InviteStore contains the methods for
|
||||
|
|
@ -22,9 +24,6 @@ type InviteStore interface {
|
|||
// InviteByToken retrieves and invite using the token string.
|
||||
InviteByToken(token string) (*Invite, error)
|
||||
|
||||
// SaveInvite saves an invitation in the datastore.
|
||||
SaveInvite(i *Invite) error
|
||||
|
||||
// DeleteInvite deletes an invitation.
|
||||
DeleteInvite(id uint) error
|
||||
}
|
||||
|
|
@ -53,18 +52,22 @@ type InvitePayload struct {
|
|||
Admin *bool
|
||||
Name *string
|
||||
Position *string
|
||||
SSOEnabled *bool `json:"sso_enabled"`
|
||||
SSOEnabled *bool `json:"sso_enabled"`
|
||||
GlobalRole *string `json:"global_role"`
|
||||
Teams []UserTeam `json:"teams,omitempty"`
|
||||
}
|
||||
|
||||
// Invite represents an invitation for a user to join Fleet.
|
||||
type Invite struct {
|
||||
UpdateCreateTimestamps
|
||||
ID uint `json:"id"`
|
||||
InvitedBy uint `json:"invited_by" db:"invited_by"`
|
||||
Email string `json:"email"`
|
||||
Admin bool `json:"admin"`
|
||||
Name string `json:"name"`
|
||||
Position string `json:"position,omitempty"`
|
||||
Token string `json:"-"`
|
||||
SSOEnabled bool `json:"sso_enabled" db:"sso_enabled"`
|
||||
ID uint `json:"id"`
|
||||
InvitedBy uint `json:"invited_by" db:"invited_by"`
|
||||
Email string `json:"email"`
|
||||
Admin bool `json:"admin"`
|
||||
Name string `json:"name"`
|
||||
Position string `json:"position,omitempty"`
|
||||
Token string `json:"-"`
|
||||
SSOEnabled bool `json:"sso_enabled" db:"sso_enabled"`
|
||||
GlobalRole null.String `json:"global_role" db:"global_role"`
|
||||
Teams []UserTeam `json:"teams,omitempty"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -128,6 +128,7 @@ type UserPayload struct {
|
|||
InviteToken *string `json:"invite_token,omitempty"`
|
||||
SSOInvite *bool `json:"sso_invite,omitempty"`
|
||||
SSOEnabled *bool `json:"sso_enabled,omitempty"`
|
||||
GlobalRole *string `json:"global_role,omitempty"`
|
||||
AdminForcedPasswordReset *bool `json:"admin_forced_password_reset,omitempty"`
|
||||
Teams *[]UserTeam `json:"teams,omitempty"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,8 +16,6 @@ type InviteByEmailFunc func(email string) (*kolide.Invite, error)
|
|||
|
||||
type InviteByTokenFunc func(token string) (*kolide.Invite, error)
|
||||
|
||||
type SaveInviteFunc func(i *kolide.Invite) error
|
||||
|
||||
type DeleteInviteFunc func(id uint) error
|
||||
|
||||
type InviteStore struct {
|
||||
|
|
@ -36,9 +34,6 @@ type InviteStore struct {
|
|||
InviteByTokenFunc InviteByTokenFunc
|
||||
InviteByTokenFuncInvoked bool
|
||||
|
||||
SaveInviteFunc SaveInviteFunc
|
||||
SaveInviteFuncInvoked bool
|
||||
|
||||
DeleteInviteFunc DeleteInviteFunc
|
||||
DeleteInviteFuncInvoked bool
|
||||
}
|
||||
|
|
@ -68,11 +63,6 @@ func (s *InviteStore) InviteByToken(token string) (*kolide.Invite, error) {
|
|||
return s.InviteByTokenFunc(token)
|
||||
}
|
||||
|
||||
func (s *InviteStore) SaveInvite(i *kolide.Invite) error {
|
||||
s.SaveInviteFuncInvoked = true
|
||||
return s.SaveInviteFunc(i)
|
||||
}
|
||||
|
||||
func (s *InviteStore) DeleteInvite(id uint) error {
|
||||
s.DeleteInviteFuncInvoked = true
|
||||
return s.DeleteInviteFunc(id)
|
||||
|
|
|
|||
|
|
@ -21,6 +21,10 @@ func (svc service) CreateUserWithInvite(ctx context.Context, p kolide.UserPayloa
|
|||
|
||||
// set the payload Admin property based on an existing invite.
|
||||
p.Admin = &invite.Admin
|
||||
if invite.GlobalRole.Valid {
|
||||
p.GlobalRole = &invite.GlobalRole.String
|
||||
}
|
||||
p.Teams = &invite.Teams
|
||||
|
||||
user, err := svc.newUser(p)
|
||||
if err != nil {
|
||||
|
|
|
|||
Loading…
Reference in a new issue