mirror of
https://github.com/fleetdm/fleet
synced 2026-05-24 01:18:42 +00:00
Initial work on user team information storage and retrieval (#483)
There are more migrations to come, but this is a foundation for the DB changes that will be needed for Teams.
This commit is contained in:
parent
6f381de04e
commit
3286864d9d
17 changed files with 623 additions and 14 deletions
39
server/datastore/datastore_teams_test.go
Normal file
39
server/datastore/datastore_teams_test.go
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
package datastore
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/fleetdm/fleet/server/kolide"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func testTeamGetSet(t *testing.T, ds kolide.Datastore) {
|
||||
var createTests = []struct {
|
||||
name, description string
|
||||
}{
|
||||
{"foo_team", "foobar is the description"},
|
||||
{"bar_team", "were you hoping for more?"},
|
||||
}
|
||||
|
||||
for _, tt := range createTests {
|
||||
t.Run("", func(t *testing.T) {
|
||||
team, err := ds.NewTeam(&kolide.Team{
|
||||
Name: tt.name,
|
||||
Description: tt.description,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.NotZero(t, team.ID)
|
||||
|
||||
team, err = ds.Team(team.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.name, team.Name)
|
||||
assert.Equal(t, tt.description, team.Description)
|
||||
|
||||
team, err = ds.TeamByName(tt.name)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.name, team.Name)
|
||||
assert.Equal(t, tt.description, team.Description)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -94,4 +94,7 @@ var testFunctions = [...]func(*testing.T, kolide.Datastore){
|
|||
testCarveListCarves,
|
||||
testCarveCleanupCarves,
|
||||
testCarveUpdateCarve,
|
||||
testTeamGetSet,
|
||||
testUserTeams,
|
||||
testUserCreateWithTeams,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -145,3 +145,115 @@ func testListUsers(t *testing.T, ds kolide.Datastore) {
|
|||
require.Len(t, users, 1)
|
||||
assert.Equal(t, "mike@kolide.co", users[0].Email)
|
||||
}
|
||||
|
||||
func testUserTeams(t *testing.T, ds kolide.Datastore) {
|
||||
for i := 0; i < 10; i++ {
|
||||
_, err := ds.NewTeam(&kolide.Team{Name: fmt.Sprintf("%d", i)})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
users := createTestUsers(t, ds)
|
||||
|
||||
assert.Len(t, users[0].Teams, 0)
|
||||
assert.Len(t, users[1].Teams, 0)
|
||||
|
||||
// Add invalid team should fail
|
||||
users[0].Teams = []kolide.UserTeam{
|
||||
{
|
||||
Team: kolide.Team{ID: 13},
|
||||
Role: "foobar",
|
||||
},
|
||||
}
|
||||
err := ds.SaveUser(users[0])
|
||||
require.Error(t, err)
|
||||
|
||||
// Add valid team should succeed
|
||||
users[0].Teams = []kolide.UserTeam{
|
||||
{
|
||||
Team: kolide.Team{ID: 3},
|
||||
Role: "foobar",
|
||||
},
|
||||
}
|
||||
err = ds.SaveUser(users[0])
|
||||
require.NoError(t, err)
|
||||
|
||||
users, err = ds.ListUsers(kolide.ListOptions{OrderKey: "name", OrderDirection: kolide.OrderDescending})
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Len(t, users[0].Teams, 1)
|
||||
assert.Len(t, users[1].Teams, 0)
|
||||
|
||||
users[1].Teams = []kolide.UserTeam{
|
||||
{
|
||||
Team: kolide.Team{ID: 1},
|
||||
Role: "foobar",
|
||||
},
|
||||
{
|
||||
Team: kolide.Team{ID: 2},
|
||||
Role: "foobar",
|
||||
},
|
||||
{
|
||||
Team: kolide.Team{ID: 3},
|
||||
Role: "foobar",
|
||||
},
|
||||
}
|
||||
err = ds.SaveUser(users[1])
|
||||
require.NoError(t, err)
|
||||
|
||||
users, err = ds.ListUsers(kolide.ListOptions{OrderKey: "name", OrderDirection: kolide.OrderDescending})
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Len(t, users[0].Teams, 1)
|
||||
assert.Len(t, users[1].Teams, 3)
|
||||
|
||||
// Clear teams
|
||||
users[1].Teams = []kolide.UserTeam{}
|
||||
err = ds.SaveUser(users[1])
|
||||
require.NoError(t, err)
|
||||
|
||||
users, err = ds.ListUsers(kolide.ListOptions{OrderKey: "name", OrderDirection: kolide.OrderDescending})
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Len(t, users[0].Teams, 1)
|
||||
assert.Len(t, users[1].Teams, 0)
|
||||
}
|
||||
|
||||
func testUserCreateWithTeams(t *testing.T, ds kolide.Datastore) {
|
||||
for i := 0; i < 10; i++ {
|
||||
_, err := ds.NewTeam(&kolide.Team{Name: fmt.Sprintf("%d", i)})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
u := &kolide.User{
|
||||
Username: "1",
|
||||
Password: []byte("foo"),
|
||||
Teams: []kolide.UserTeam{
|
||||
{
|
||||
Team: kolide.Team{ID: 6},
|
||||
Role: "admin",
|
||||
},
|
||||
{
|
||||
Team: kolide.Team{ID: 3},
|
||||
Role: "observer",
|
||||
},
|
||||
{
|
||||
Team: kolide.Team{ID: 9},
|
||||
Role: "maintainer",
|
||||
},
|
||||
},
|
||||
}
|
||||
user, err := ds.NewUser(u)
|
||||
assert.Nil(t, err)
|
||||
assert.Len(t, user.Teams, 3)
|
||||
|
||||
user, err = ds.UserByID(user.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, user.Teams, 3)
|
||||
|
||||
assert.Equal(t, uint(3), user.Teams[0].ID)
|
||||
assert.Equal(t, "observer", user.Teams[0].Role)
|
||||
assert.Equal(t, uint(6), user.Teams[1].ID)
|
||||
assert.Equal(t, "admin", user.Teams[1].Role)
|
||||
assert.Equal(t, uint(9), user.Teams[2].ID)
|
||||
assert.Equal(t, "maintainer", user.Teams[2].Role)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,54 @@
|
|||
package tables
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func init() {
|
||||
MigrationClient.AddMigration(Up_20210315111056, Down_20210315111056)
|
||||
}
|
||||
|
||||
func Up_20210315111056(tx *sql.Tx) error {
|
||||
if _, err := tx.Exec(`CREATE TABLE IF NOT EXISTS teams (
|
||||
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
description VARCHAR(1023) NOT NULL DEFAULT '',
|
||||
UNIQUE KEY idx_name (name)
|
||||
)`); err != nil {
|
||||
return errors.Wrap(err, "create teams")
|
||||
}
|
||||
|
||||
// Users <> Teams mapping
|
||||
if _, err := tx.Exec(`CREATE TABLE IF NOT EXISTS user_teams (
|
||||
user_id INT UNSIGNED NOT NULL,
|
||||
team_id INT UNSIGNED NOT NULL,
|
||||
role VARCHAR(64) NOT NULL,
|
||||
PRIMARY KEY (user_id, team_id),
|
||||
FOREIGN KEY fk_user_id (user_id) REFERENCES users (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 user_teams")
|
||||
}
|
||||
|
||||
if _, err := tx.Exec(`ALTER TABLE hosts
|
||||
ADD team_id INT UNSIGNED DEFAULT NULL,
|
||||
ADD FOREIGN KEY fk_team_id (team_id) REFERENCES teams (id) ON DELETE SET NULL
|
||||
`); err != nil {
|
||||
return errors.Wrap(err, "alter hosts")
|
||||
}
|
||||
|
||||
if _, err := tx.Exec(`ALTER TABLE users
|
||||
ADD global_role VARCHAR(64) DEFAULT NULL
|
||||
`); err != nil {
|
||||
return errors.Wrap(err, "alter users")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func Down_20210315111056(tx *sql.Tx) error {
|
||||
return nil
|
||||
}
|
||||
69
server/datastore/mysql/teams.go
Normal file
69
server/datastore/mysql/teams.go
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
package mysql
|
||||
|
||||
import (
|
||||
"github.com/fleetdm/fleet/server/kolide"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func (d *Datastore) NewTeam(team *kolide.Team) (*kolide.Team, error) {
|
||||
query := `
|
||||
INSERT INTO teams (
|
||||
name,
|
||||
description
|
||||
) VALUES ( ?, ? )
|
||||
`
|
||||
result, err := d.db.Exec(
|
||||
query,
|
||||
team.Name,
|
||||
team.Description,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "insert team")
|
||||
}
|
||||
|
||||
id, _ := result.LastInsertId()
|
||||
team.ID = uint(id)
|
||||
return team, nil
|
||||
}
|
||||
|
||||
func (d *Datastore) Team(tid uint) (*kolide.Team, error) {
|
||||
sql := `
|
||||
SELECT * FROM teams
|
||||
WHERE id = ?
|
||||
`
|
||||
team := &kolide.Team{}
|
||||
|
||||
if err := d.db.Get(team, sql, tid); err != nil {
|
||||
return nil, errors.Wrap(err, "select team")
|
||||
}
|
||||
|
||||
return team, nil
|
||||
}
|
||||
|
||||
func (d *Datastore) TeamByName(name string) (*kolide.Team, error) {
|
||||
sql := `
|
||||
SELECT * FROM teams
|
||||
WHERE name = ?
|
||||
`
|
||||
team := &kolide.Team{}
|
||||
|
||||
if err := d.db.Get(team, sql, name); err != nil {
|
||||
return nil, errors.Wrap(err, "select team")
|
||||
}
|
||||
|
||||
return team, nil
|
||||
}
|
||||
|
||||
func (d *Datastore) SaveTeam(team *kolide.Team) (*kolide.Team, error) {
|
||||
query := `
|
||||
UPDATE teams SET
|
||||
name = ?,
|
||||
description = ?
|
||||
WHERE id = ?
|
||||
`
|
||||
_, err := d.db.Exec(query, team.Name, team.Description, team.ID)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "saving team")
|
||||
}
|
||||
return team, nil
|
||||
}
|
||||
|
|
@ -3,8 +3,10 @@ package mysql
|
|||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/fleetdm/fleet/server/kolide"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
|
|
@ -36,6 +38,11 @@ func (d *Datastore) NewUser(user *kolide.User) (*kolide.User, error) {
|
|||
|
||||
id, _ := result.LastInsertId()
|
||||
user.ID = uint(id)
|
||||
|
||||
if err := d.saveTeamsForUser(user); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
|
|
@ -56,6 +63,10 @@ func (d *Datastore) findUser(searchCol string, searchVal interface{}) (*kolide.U
|
|||
return nil, errors.Wrap(err, "find user")
|
||||
}
|
||||
|
||||
if err := d.loadTeamsForUsers([]*kolide.User{user}); err != nil {
|
||||
return nil, errors.Wrap(err, "load teams")
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
|
|
@ -80,6 +91,10 @@ func (d *Datastore) ListUsers(opt kolide.ListOptions) ([]*kolide.User, error) {
|
|||
return nil, errors.Wrap(err, "list users")
|
||||
}
|
||||
|
||||
if err := d.loadTeamsForUsers(users); err != nil {
|
||||
return nil, errors.Wrap(err, "load teams")
|
||||
}
|
||||
|
||||
return users, nil
|
||||
|
||||
}
|
||||
|
|
@ -122,5 +137,86 @@ func (d *Datastore) SaveUser(user *kolide.User) error {
|
|||
return notFound("User").WithID(user.ID)
|
||||
}
|
||||
|
||||
// REVIEW: Check if teams have been set?
|
||||
if err := d.saveTeamsForUser(user); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// loadTeamsForUsers will load the teams/roles for the provided users.
|
||||
func (d *Datastore) loadTeamsForUsers(users []*kolide.User) error {
|
||||
userIDs := make([]uint, 0, len(users)+1)
|
||||
// Make sure the slice is never empty for IN by filling a nonexistent ID
|
||||
userIDs = append(userIDs, 0)
|
||||
idToUser := make(map[uint]*kolide.User, len(users))
|
||||
for _, u := range users {
|
||||
// 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
|
||||
userIDs = append(userIDs, u.ID)
|
||||
idToUser[u.ID] = u
|
||||
}
|
||||
|
||||
sql := `
|
||||
SELECT ut.team_id AS id, ut.user_id, ut.role, t.name
|
||||
FROM user_teams ut INNER JOIN teams t ON ut.team_id = t.id
|
||||
WHERE ut.user_id IN (?)
|
||||
ORDER BY user_id, team_id
|
||||
`
|
||||
sql, args, err := sqlx.In(sql, userIDs)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "sqlx.In loadTeamsForUsers")
|
||||
}
|
||||
|
||||
var rows []struct {
|
||||
kolide.UserTeam
|
||||
UserID uint `db:"user_id"`
|
||||
}
|
||||
if err := d.db.Select(&rows, sql, args...); err != nil {
|
||||
return errors.Wrap(err, "get loadTeamsForUsers")
|
||||
}
|
||||
|
||||
// Map each row to the appropriate user
|
||||
for _, r := range rows {
|
||||
user := idToUser[r.UserID]
|
||||
user.Teams = append(user.Teams, r.UserTeam)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Datastore) saveTeamsForUser(user *kolide.User) error {
|
||||
// Do a full teams update by deleting existing teams and then inserting all
|
||||
// the current teams in a single transaction.
|
||||
if err := d.withRetryTxx(func(tx *sqlx.Tx) error {
|
||||
// Delete before insert
|
||||
sql := `DELETE FROM user_teams WHERE user_id = ?`
|
||||
if _, err := tx.Exec(sql, user.ID); err != nil {
|
||||
return errors.Wrap(err, "delete existing teams")
|
||||
}
|
||||
|
||||
if len(user.Teams) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Bulk insert
|
||||
const valueStr = "(?,?,?),"
|
||||
var args []interface{}
|
||||
for _, userTeam := range user.Teams {
|
||||
args = append(args, user.ID, userTeam.Team.ID, userTeam.Role)
|
||||
}
|
||||
sql = "INSERT INTO user_teams (user_id, team_id, role) VALUES " +
|
||||
strings.Repeat(valueStr, len(user.Teams))
|
||||
sql = strings.TrimSuffix(sql, ",")
|
||||
if _, err := tx.Exec(sql, args...); err != nil {
|
||||
return errors.Wrap(err, "insert teams")
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
return errors.Wrap(err, "save teams for user")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@ type Datastore interface {
|
|||
ScheduledQueryStore
|
||||
OsqueryOptionsStore
|
||||
CarveStore
|
||||
TeamStore
|
||||
|
||||
Name() string
|
||||
Drop() error
|
||||
// MigrateTables creates and migrates the table schemas
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ import (
|
|||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"gopkg.in/guregu/null.v3"
|
||||
)
|
||||
|
||||
type HostStatus string
|
||||
|
|
@ -139,6 +141,8 @@ type Host struct {
|
|||
LoggerTLSPeriod uint `json:"logger_tls_period" db:"logger_tls_period"`
|
||||
Additional *json.RawMessage `json:"additional,omitempty" db:"additional"`
|
||||
EnrollSecretName string `json:"enroll_secret_name" db:"enroll_secret_name"`
|
||||
|
||||
TeamID null.Int `json:"team_id" db:"team_id"`
|
||||
}
|
||||
|
||||
// HostDetail provides the full host metadata along with associated labels and
|
||||
|
|
|
|||
|
|
@ -17,4 +17,5 @@ type Service interface {
|
|||
ScheduledQueryService
|
||||
StatusService
|
||||
CarveService
|
||||
TeamService
|
||||
}
|
||||
|
|
|
|||
56
server/kolide/teams.go
Normal file
56
server/kolide/teams.go
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
package kolide
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
)
|
||||
|
||||
type TeamStore interface {
|
||||
// NewTeam creates a new Team object in the store.
|
||||
NewTeam(team *Team) (*Team, error)
|
||||
// Team retrieves the Team by ID.
|
||||
Team(tid uint) (*Team, error)
|
||||
// TeamByName retrieves the Team by Name.
|
||||
TeamByName(name string) (*Team, error)
|
||||
// SaveTeam saves any changes to the team.
|
||||
SaveTeam(team *Team) (*Team, error)
|
||||
}
|
||||
|
||||
type TeamService interface {
|
||||
NewTeam(ctx context.Context, p TeamPayload) (*Team, error)
|
||||
ModifyTeam(ctx context.Context, id uint, payload TeamPayload) (*Team, error)
|
||||
}
|
||||
|
||||
type TeamPayload struct {
|
||||
Name *string `json:"name"`
|
||||
Description *string `json:"description"`
|
||||
}
|
||||
|
||||
// Team is the data representation for the "Team" concept (group of hosts and
|
||||
// group of users that can perform operations on those hosts).
|
||||
type Team struct {
|
||||
// Directly in DB
|
||||
|
||||
// ID is the database ID.
|
||||
ID uint `json:"id" db:"id"`
|
||||
// CreatedAt is the timestamp of the label creation.
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
// Name is the human friendly name of the team.
|
||||
Name string `json:"name" db:"name"`
|
||||
// Description is an optional description for the team.
|
||||
Description string `json:"description" db:"description"`
|
||||
|
||||
// Derived from JOINs
|
||||
|
||||
// Users is the users that have a role on this team.
|
||||
Users []TeamUser `json:"users,omitempty"`
|
||||
// Hosts are the hosts assigned to the team.
|
||||
Hosts []Host `json:"hosts,omitempty"`
|
||||
}
|
||||
|
||||
type TeamUser struct {
|
||||
// User is the user object
|
||||
User
|
||||
// Role is the role the user has for the team.
|
||||
Role string `json:"role" db:"role"`
|
||||
}
|
||||
|
|
@ -7,6 +7,7 @@ import (
|
|||
"fmt"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"gopkg.in/guregu/null.v3"
|
||||
)
|
||||
|
||||
// UserStore contains methods for managing users in a datastore
|
||||
|
|
@ -99,24 +100,36 @@ type User struct {
|
|||
AdminForcedPasswordReset bool `json:"force_password_reset" db:"admin_forced_password_reset"`
|
||||
GravatarURL string `json:"gravatar_url" db:"gravatar_url"`
|
||||
Position string `json:"position,omitempty"` // job role
|
||||
// SSOEnabled if true, the single siqn on is used to log in
|
||||
SSOEnabled bool `json:"sso_enabled" db:"sso_enabled"`
|
||||
// SSOEnabled if true, the user may only log in via SSO
|
||||
SSOEnabled bool `json:"sso_enabled" db:"sso_enabled"`
|
||||
GlobalRole null.String `json:"global_role" db:"global_role"`
|
||||
|
||||
// Teams is the teams this user has roles in.
|
||||
Teams []UserTeam
|
||||
}
|
||||
|
||||
type UserTeam struct {
|
||||
// Team is the team object.
|
||||
Team
|
||||
// Role is the role the user has for the team.
|
||||
Role string `json:"role" db:"role"`
|
||||
}
|
||||
|
||||
// UserPayload is used to modify an existing user
|
||||
type UserPayload struct {
|
||||
Username *string `json:"username,omitempty"`
|
||||
Name *string `json:"name,omitempty"`
|
||||
Email *string `json:"email,omitempty"`
|
||||
Admin *bool `json:"admin,omitempty"`
|
||||
Enabled *bool `json:"enabled,omitempty"`
|
||||
Password *string `json:"password,omitempty"`
|
||||
GravatarURL *string `json:"gravatar_url,omitempty"`
|
||||
Position *string `json:"position,omitempty"`
|
||||
InviteToken *string `json:"invite_token,omitempty"`
|
||||
SSOInvite *bool `json:"sso_invite,omitempty"`
|
||||
SSOEnabled *bool `json:"sso_enabled,omitempty"`
|
||||
AdminForcedPasswordReset *bool `json:"admin_forced_password_reset,omitempty"`
|
||||
Username *string `json:"username,omitempty"`
|
||||
Name *string `json:"name,omitempty"`
|
||||
Email *string `json:"email,omitempty"`
|
||||
Admin *bool `json:"admin,omitempty"`
|
||||
Enabled *bool `json:"enabled,omitempty"`
|
||||
Password *string `json:"password,omitempty"`
|
||||
GravatarURL *string `json:"gravatar_url,omitempty"`
|
||||
Position *string `json:"position,omitempty"`
|
||||
InviteToken *string `json:"invite_token,omitempty"`
|
||||
SSOInvite *bool `json:"sso_invite,omitempty"`
|
||||
SSOEnabled *bool `json:"sso_enabled,omitempty"`
|
||||
AdminForcedPasswordReset *bool `json:"admin_forced_password_reset,omitempty"`
|
||||
Teams *[]UserTeam `json:"teams,omitempty"`
|
||||
}
|
||||
|
||||
// User creates a user from payload.
|
||||
|
|
@ -126,6 +139,7 @@ func (p UserPayload) User(keySize, cost int) (*User, error) {
|
|||
Email: *p.Email,
|
||||
Admin: falseIfNil(p.Admin),
|
||||
Enabled: true,
|
||||
Teams: []UserTeam{},
|
||||
}
|
||||
if err := user.SetPassword(*p.Password, keySize, cost); err != nil {
|
||||
return nil, err
|
||||
|
|
@ -147,6 +161,9 @@ func (p UserPayload) User(keySize, cost int) (*User, error) {
|
|||
if p.AdminForcedPasswordReset != nil {
|
||||
user.AdminForcedPasswordReset = *p.AdminForcedPasswordReset
|
||||
}
|
||||
if p.Teams != nil {
|
||||
user.Teams = *p.Teams
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ var _ kolide.Datastore = (*Store)(nil)
|
|||
|
||||
type Store struct {
|
||||
kolide.PasswordResetStore
|
||||
kolide.TeamStore
|
||||
TargetStore
|
||||
SessionStore
|
||||
CampaignStore
|
||||
|
|
|
|||
64
server/service/endpoint_teams.go
Normal file
64
server/service/endpoint_teams.go
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/fleetdm/fleet/server/kolide"
|
||||
"github.com/go-kit/kit/endpoint"
|
||||
)
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Create Team
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
type createTeamRequest struct {
|
||||
payload kolide.TeamPayload
|
||||
}
|
||||
|
||||
type createTeamResponse struct {
|
||||
Team *kolide.Team `json:"team,omitempty"`
|
||||
Err error `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
func (r createTeamResponse) error() error { return r.Err }
|
||||
|
||||
func makeCreateTeamEndpoint(svc kolide.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(createTeamRequest)
|
||||
|
||||
team, err := svc.NewTeam(ctx, req.payload)
|
||||
if err != nil {
|
||||
return createTeamResponse{Err: err}, nil
|
||||
}
|
||||
|
||||
return createTeamResponse{Team: team}, nil
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Modify Team
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
type modifyTeamRequest struct {
|
||||
ID uint
|
||||
payload kolide.TeamPayload
|
||||
}
|
||||
|
||||
type modifyTeamResponse struct {
|
||||
Team *kolide.Team `json:"team,omitempty"`
|
||||
Err error `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
func (r modifyTeamResponse) error() error { return r.Err }
|
||||
|
||||
func makeModifyTeamEndpoint(svc kolide.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(modifyTeamRequest)
|
||||
team, err := svc.ModifyTeam(ctx, req.ID, req.payload)
|
||||
if err != nil {
|
||||
return modifyTeamResponse{Err: err}, nil
|
||||
}
|
||||
|
||||
return modifyTeamResponse{Team: team}, err
|
||||
}
|
||||
}
|
||||
|
|
@ -110,6 +110,8 @@ type KolideEndpoints struct {
|
|||
GetCarve endpoint.Endpoint
|
||||
GetCarveBlock endpoint.Endpoint
|
||||
Version endpoint.Endpoint
|
||||
CreateTeam endpoint.Endpoint
|
||||
ModifyTeam endpoint.Endpoint
|
||||
}
|
||||
|
||||
// MakeKolideServerEndpoints creates the Kolide API endpoints.
|
||||
|
|
@ -212,6 +214,8 @@ func MakeKolideServerEndpoints(svc kolide.Service, jwtKey, urlPrefix string, lim
|
|||
GetCarve: authenticatedUser(jwtKey, svc, makeGetCarveEndpoint(svc)),
|
||||
GetCarveBlock: authenticatedUser(jwtKey, svc, makeGetCarveBlockEndpoint(svc)),
|
||||
Version: authenticatedUser(jwtKey, svc, makeVersionEndpoint(svc)),
|
||||
CreateTeam: authenticatedUser(jwtKey, svc, makeCreateTeamEndpoint(svc)),
|
||||
ModifyTeam: authenticatedUser(jwtKey, svc, makeModifyTeamEndpoint(svc)),
|
||||
|
||||
// Authenticated status endpoints
|
||||
StatusResultStore: authenticatedUser(jwtKey, svc, makeStatusResultStoreEndpoint(svc)),
|
||||
|
|
@ -321,6 +325,8 @@ type kolideHandlers struct {
|
|||
GetCarve http.Handler
|
||||
GetCarveBlock http.Handler
|
||||
Version http.Handler
|
||||
CreateTeam http.Handler
|
||||
ModifyTeam http.Handler
|
||||
}
|
||||
|
||||
func makeKolideKitHandlers(e KolideEndpoints, opts []kithttp.ServerOption) *kolideHandlers {
|
||||
|
|
@ -417,6 +423,8 @@ func makeKolideKitHandlers(e KolideEndpoints, opts []kithttp.ServerOption) *koli
|
|||
GetCarve: newServer(e.GetCarve, decodeGetCarveRequest),
|
||||
GetCarveBlock: newServer(e.GetCarveBlock, decodeGetCarveBlockRequest),
|
||||
Version: newServer(e.Version, decodeNoParamsRequest),
|
||||
CreateTeam: newServer(e.CreateTeam, decodeCreateTeamRequest),
|
||||
ModifyTeam: newServer(e.ModifyTeam, decodeModifyTeamRequest),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -629,6 +637,9 @@ func attachKolideAPIRoutes(r *mux.Router, h *kolideHandlers) {
|
|||
r.Handle("/api/v1/fleet/carves/{id}", h.GetCarve).Methods("GET").Name("get_carve")
|
||||
r.Handle("/api/v1/fleet/carves/{id}/block/{block_id}", h.GetCarveBlock).Methods("GET").Name("get_carve_block")
|
||||
|
||||
r.Handle("/api/v1/fleet/teams", h.CreateTeam).Methods("POST").Name("create_team")
|
||||
r.Handle("/api/v1/fleet/teams/{id}", h.ModifyTeam).Methods("PATCH").Name("modify_team")
|
||||
|
||||
r.Handle("/api/v1/osquery/enroll", h.EnrollAgent).Methods("POST").Name("enroll_agent")
|
||||
r.Handle("/api/v1/osquery/config", h.GetClientConfig).Methods("POST").Name("get_client_config")
|
||||
r.Handle("/api/v1/osquery/distributed/read", h.GetDistributedQueries).Methods("POST").Name("get_distributed_queries")
|
||||
|
|
|
|||
47
server/service/service_teams.go
Normal file
47
server/service/service_teams.go
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/fleetdm/fleet/server/kolide"
|
||||
)
|
||||
|
||||
func (svc service) NewTeam(ctx context.Context, p kolide.TeamPayload) (*kolide.Team, error) {
|
||||
team := &kolide.Team{}
|
||||
|
||||
if p.Name == nil {
|
||||
return nil, newInvalidArgumentError("name", "missing required argument")
|
||||
}
|
||||
if *p.Name == "" {
|
||||
return nil, newInvalidArgumentError("name", "may not be empty")
|
||||
}
|
||||
team.Name = *p.Name
|
||||
|
||||
if p.Description != nil {
|
||||
team.Description = *p.Description
|
||||
}
|
||||
|
||||
team, err := svc.ds.NewTeam(team)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return team, nil
|
||||
}
|
||||
|
||||
func (svc service) ModifyTeam(ctx context.Context, id uint, payload kolide.TeamPayload) (*kolide.Team, error) {
|
||||
team, err := svc.ds.Team(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if payload.Name != nil {
|
||||
if *payload.Name == "" {
|
||||
return nil, newInvalidArgumentError("name", "may not be empty")
|
||||
}
|
||||
team.Name = *payload.Name
|
||||
}
|
||||
if payload.Description != nil {
|
||||
team.Description = *payload.Description
|
||||
}
|
||||
|
||||
return svc.ds.SaveTeam(team)
|
||||
}
|
||||
|
|
@ -120,6 +120,10 @@ func (svc service) ModifyUser(ctx context.Context, userID uint, p kolide.UserPay
|
|||
user.SSOEnabled = *p.SSOEnabled
|
||||
}
|
||||
|
||||
if p.Teams != nil {
|
||||
user.Teams = *p.Teams
|
||||
}
|
||||
|
||||
err = svc.saveUser(user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
|
|||
29
server/service/transport_teams.go
Normal file
29
server/service/transport_teams.go
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func decodeCreateTeamRequest(ctx context.Context, r *http.Request) (interface{}, error) {
|
||||
var req createTeamRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req.payload); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeModifyTeamRequest(ctx context.Context, r *http.Request) (interface{}, error) {
|
||||
id, err := idFromRequest(r, "id")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var resp modifyTeamRequest
|
||||
err = json.NewDecoder(r.Body).Decode(&resp.payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp.ID = id
|
||||
return resp, nil
|
||||
}
|
||||
Loading…
Reference in a new issue