From e8f4860d512e39a8b224b13544fc133bce2be568 Mon Sep 17 00:00:00 2001 From: Zach Wasserman Date: Wed, 21 Apr 2021 20:54:09 -0700 Subject: [PATCH] Add team user management (#672) - Add list team users endpoint. - Add add/delete team users endpoints. - Update list users to support filter by team. --- server/datastore/datastore.go | 1 + server/datastore/datastore_teams.go | 58 +++++++++++++++++++++++++ server/datastore/datastore_users.go | 24 ++++++++--- server/datastore/inmem/users.go | 6 +-- server/datastore/mysql/teams.go | 66 +++++++++++++++++++++++++++++ server/datastore/mysql/users.go | 16 ++++--- server/kolide/teams.go | 48 ++++++++++++++++++++- server/kolide/users.go | 12 +++++- server/mock/datastore_users.go | 4 +- server/service/endpoint_teams.go | 60 ++++++++++++++++++++++++++ server/service/endpoint_users.go | 2 +- server/service/handler.go | 25 ++++++++--- server/service/logging_users.go | 2 +- server/service/metrics_users.go | 2 +- server/service/service_teams.go | 64 ++++++++++++++++++++++++++++ server/service/service_users.go | 2 +- server/service/transport.go | 31 ++++++++++++++ server/service/transport_teams.go | 26 ++++++++++++ server/service/transport_users.go | 2 +- 19 files changed, 422 insertions(+), 29 deletions(-) diff --git a/server/datastore/datastore.go b/server/datastore/datastore.go index 85ad715afd..7df4d4f862 100644 --- a/server/datastore/datastore.go +++ b/server/datastore/datastore.go @@ -98,6 +98,7 @@ var TestFunctions = [...]func(*testing.T, kolide.Datastore){ testCarveCleanupCarves, testCarveUpdateCarve, testTeamGetSetDelete, + testTeamUsers, testUserTeams, testUserCreateWithTeams, } diff --git a/server/datastore/datastore_teams.go b/server/datastore/datastore_teams.go index fdbfea3105..91c2d6f7aa 100644 --- a/server/datastore/datastore_teams.go +++ b/server/datastore/datastore_teams.go @@ -43,3 +43,61 @@ func testTeamGetSetDelete(t *testing.T, ds kolide.Datastore) { }) } } + +func testTeamUsers(t *testing.T, ds kolide.Datastore) { + users := createTestUsers(t, ds) + user1 := kolide.User{Name: users[0].Name, Email: users[0].Email, ID: users[0].ID} + user2 := kolide.User{Name: users[1].Name, Email: users[1].Email, ID: users[1].ID} + + team1, err := ds.NewTeam(&kolide.Team{Name: "team1"}) + require.NoError(t, err) + team2, err := ds.NewTeam(&kolide.Team{Name: "team2"}) + require.NoError(t, err) + _ = team2 + + team1, err = ds.Team(team1.ID) + require.NoError(t, err) + assert.Len(t, team1.Users, 0) + + team1Users := []kolide.TeamUser{ + {User: user1, Role: "maintainer"}, + {User: user2, Role: "observer"}, + } + team1.Users = team1Users + team1, err = ds.SaveTeam(team1) + require.NoError(t, err) + + team1, err = ds.Team(team1.ID) + require.NoError(t, err) + require.ElementsMatch(t, team1Users, team1.Users) + // Ensure team 2 not effected + team2, err = ds.Team(team2.ID) + require.NoError(t, err) + assert.Len(t, team2.Users, 0) + + team1Users = []kolide.TeamUser{ + {User: user2, Role: "maintainer"}, + } + team1.Users = team1Users + team1, err = ds.SaveTeam(team1) + require.NoError(t, err) + team1, err = ds.Team(team1.ID) + require.NoError(t, err) + assert.ElementsMatch(t, team1Users, team1.Users) + + team2Users := []kolide.TeamUser{ + {User: user2, Role: "observer"}, + } + team2.Users = team2Users + team1, err = ds.SaveTeam(team1) + require.NoError(t, err) + team1, err = ds.Team(team1.ID) + require.NoError(t, err) + assert.ElementsMatch(t, team1Users, team1.Users) + team2, err = ds.SaveTeam(team2) + require.NoError(t, err) + team2, err = ds.Team(team2.ID) + require.NoError(t, err) + assert.ElementsMatch(t, team2Users, team2.Users) + +} diff --git a/server/datastore/datastore_users.go b/server/datastore/datastore_users.go index a87321cbea..45120b51c9 100644 --- a/server/datastore/datastore_users.go +++ b/server/datastore/datastore_users.go @@ -130,16 +130,16 @@ func testUserGlobalRole(t *testing.T, ds kolide.Datastore, users []*kolide.User) func testListUsers(t *testing.T, ds kolide.Datastore) { createTestUsers(t, ds) - users, err := ds.ListUsers(kolide.ListOptions{}) + users, err := ds.ListUsers(kolide.UserListOptions{}) assert.NoError(t, err) require.Len(t, users, 2) - users, err = ds.ListUsers(kolide.ListOptions{MatchQuery: "jason"}) + users, err = ds.ListUsers(kolide.UserListOptions{ListOptions: kolide.ListOptions{MatchQuery: "jason"}}) assert.NoError(t, err) require.Len(t, users, 1) assert.Equal(t, "jason@kolide.co", users[0].Email) - users, err = ds.ListUsers(kolide.ListOptions{MatchQuery: "paia"}) + users, err = ds.ListUsers(kolide.UserListOptions{ListOptions: kolide.ListOptions{MatchQuery: "paia"}}) assert.NoError(t, err) require.Len(t, users, 1) assert.Equal(t, "mike@kolide.co", users[0].Email) @@ -176,7 +176,11 @@ func testUserTeams(t *testing.T, ds kolide.Datastore) { err = ds.SaveUser(users[0]) require.NoError(t, err) - users, err = ds.ListUsers(kolide.ListOptions{OrderKey: "name", OrderDirection: kolide.OrderDescending}) + users, err = ds.ListUsers( + kolide.UserListOptions{ + ListOptions: kolide.ListOptions{OrderKey: "name", OrderDirection: kolide.OrderDescending}, + }, + ) require.NoError(t, err) assert.Len(t, users[0].Teams, 1) @@ -199,7 +203,11 @@ func testUserTeams(t *testing.T, ds kolide.Datastore) { err = ds.SaveUser(users[1]) require.NoError(t, err) - users, err = ds.ListUsers(kolide.ListOptions{OrderKey: "name", OrderDirection: kolide.OrderDescending}) + users, err = ds.ListUsers( + kolide.UserListOptions{ + ListOptions: kolide.ListOptions{OrderKey: "name", OrderDirection: kolide.OrderDescending}, + }, + ) require.NoError(t, err) assert.Len(t, users[0].Teams, 1) @@ -210,7 +218,11 @@ func testUserTeams(t *testing.T, ds kolide.Datastore) { err = ds.SaveUser(users[1]) require.NoError(t, err) - users, err = ds.ListUsers(kolide.ListOptions{OrderKey: "name", OrderDirection: kolide.OrderDescending}) + users, err = ds.ListUsers( + kolide.UserListOptions{ + ListOptions: kolide.ListOptions{OrderKey: "name", OrderDirection: kolide.OrderDescending}, + }, + ) require.NoError(t, err) assert.Len(t, users[0].Teams, 1) diff --git a/server/datastore/inmem/users.go b/server/datastore/inmem/users.go index 7578a7f39e..7cd768d37c 100644 --- a/server/datastore/inmem/users.go +++ b/server/datastore/inmem/users.go @@ -37,7 +37,7 @@ func (d *Datastore) User(username string) (*kolide.User, error) { WithMessage(fmt.Sprintf("with username %s", username)) } -func (d *Datastore) ListUsers(opt kolide.ListOptions) ([]*kolide.User, error) { +func (d *Datastore) ListUsers(opt kolide.UserListOptions) ([]*kolide.User, error) { d.mtx.Lock() defer d.mtx.Unlock() @@ -66,13 +66,13 @@ func (d *Datastore) ListUsers(opt kolide.ListOptions) ([]*kolide.User, error) { "enabled": "Enabled", "position": "Position", } - if err := sortResults(users, opt, fields); err != nil { + if err := sortResults(users, opt.ListOptions, fields); err != nil { return nil, err } } // Apply limit/offset - low, high := d.getLimitOffsetSliceBounds(opt, len(users)) + low, high := d.getLimitOffsetSliceBounds(opt.ListOptions, len(users)) users = users[low:high] return users, nil diff --git a/server/datastore/mysql/teams.go b/server/datastore/mysql/teams.go index f9f0e123d6..2d45599be8 100644 --- a/server/datastore/mysql/teams.go +++ b/server/datastore/mysql/teams.go @@ -1,7 +1,10 @@ package mysql import ( + "strings" + "github.com/fleetdm/fleet/server/kolide" + "github.com/jmoiron/sqlx" "github.com/pkg/errors" ) @@ -39,6 +42,10 @@ func (d *Datastore) Team(tid uint) (*kolide.Team, error) { return nil, errors.Wrap(err, "select team") } + if err := d.loadUsersForTeam(team); err != nil { + return nil, err + } + return team, nil } @@ -60,9 +67,63 @@ func (d *Datastore) TeamByName(name string) (*kolide.Team, error) { return nil, errors.Wrap(err, "select team") } + if err := d.loadUsersForTeam(team); err != nil { + return nil, err + } + return team, nil } +func (d *Datastore) loadUsersForTeam(team *kolide.Team) error { + sql := ` + SELECT u.name, u.id, u.email, ut.role + FROM user_teams ut JOIN users u ON (ut.user_id = u.id) + WHERE ut.team_id = ? + ` + rows := []kolide.TeamUser{} + if err := d.db.Select(&rows, sql, team.ID); err != nil { + return errors.Wrap(err, "load users for team") + } + + team.Users = rows + return nil +} + +func (d *Datastore) saveUsersForTeam(team *kolide.Team) error { + // Do a full user update by deleting existing users and then inserting all + // the current users in a single transaction. + if err := d.withRetryTxx(func(tx *sqlx.Tx) error { + // Delete before insert + sql := `DELETE FROM user_teams WHERE team_id = ?` + if _, err := tx.Exec(sql, team.ID); err != nil { + return errors.Wrap(err, "delete existing users") + } + + if len(team.Users) == 0 { + return nil + } + + // Bulk insert + const valueStr = "(?,?,?)," + var args []interface{} + for _, teamUser := range team.Users { + args = append(args, teamUser.User.ID, team.ID, teamUser.Role) + } + sql = "INSERT INTO user_teams (user_id, team_id, role) VALUES " + + strings.Repeat(valueStr, len(team.Users)) + sql = strings.TrimSuffix(sql, ",") + if _, err := tx.Exec(sql, args...); err != nil { + return errors.Wrap(err, "insert users") + } + + return nil + }); err != nil { + return errors.Wrap(err, "save users for team") + } + + return nil +} + func (d *Datastore) SaveTeam(team *kolide.Team) (*kolide.Team, error) { query := ` UPDATE teams SET @@ -74,6 +135,11 @@ func (d *Datastore) SaveTeam(team *kolide.Team) (*kolide.Team, error) { if err != nil { return nil, errors.Wrap(err, "saving team") } + + if err := d.saveUsersForTeam(team); err != nil { + return nil, err + } + return team, nil } diff --git a/server/datastore/mysql/users.go b/server/datastore/mysql/users.go index b574eb2989..ae01e991da 100644 --- a/server/datastore/mysql/users.go +++ b/server/datastore/mysql/users.go @@ -75,16 +75,21 @@ func (d *Datastore) User(username string) (*kolide.User, error) { return d.findUser("username", username) } -// ListUsers lists all users with limit, sort and offset passed in with -// kolide.ListOptions -func (d *Datastore) ListUsers(opt kolide.ListOptions) ([]*kolide.User, error) { +// ListUsers lists all users with team ID, limit, sort and offset passed in with +// UserListOptions. +func (d *Datastore) ListUsers(opt kolide.UserListOptions) ([]*kolide.User, error) { sqlStatement := ` SELECT * FROM users WHERE TRUE ` + var params []interface{} + if opt.TeamID != 0 { + sqlStatement += " AND id IN (SELECT user_id FROM user_teams WHERE team_id = ?)" + params = append(params, opt.TeamID) + } - sqlStatement, params := searchLike(sqlStatement, nil, opt.MatchQuery, userSearchColumns...) - sqlStatement = appendListOptionsToSQL(sqlStatement, opt) + sqlStatement, params = searchLike(sqlStatement, params, opt.MatchQuery, userSearchColumns...) + sqlStatement = appendListOptionsToSQL(sqlStatement, opt.ListOptions) users := []*kolide.User{} if err := d.db.Select(&users, sqlStatement, params...); err != nil { @@ -214,6 +219,7 @@ func (d *Datastore) saveTeamsForUser(user *kolide.User) error { 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") diff --git a/server/kolide/teams.go b/server/kolide/teams.go index 496a81b617..473a995278 100644 --- a/server/kolide/teams.go +++ b/server/kolide/teams.go @@ -26,11 +26,17 @@ type TeamService interface { NewTeam(ctx context.Context, p TeamPayload) (*Team, error) // ModifyTeam modifies an existing team. ModifyTeam(ctx context.Context, id uint, payload TeamPayload) (*Team, error) + // AddTeamUsers adds users to an existing team. + AddTeamUsers(ctx context.Context, teamID uint, users []TeamUser) (*Team, error) + // DeleteTeamUsers deletes users from an existing team. + DeleteTeamUsers(ctx context.Context, teamID uint, users []TeamUser) (*Team, error) // DeleteTeam deletes an existing team. DeleteTeam(ctx context.Context, id uint) error // ListTeams lists teams with the ordering and filters in the provided // options. ListTeams(ctx context.Context, opt ListOptions) ([]*Team, error) + // ListTeams lists users on the team with the provided list options. + ListTeamUsers(ctx context.Context, teamID uint, opt ListOptions) ([]*User, error) } type TeamPayload struct { @@ -64,9 +70,49 @@ type Team struct { Hosts []Host `json:"hosts,omitempty"` } +// TeamUser is a user mapped to a team with a role. type TeamUser struct { - // User is the user object + // User is the user object. At least ID must be specified for most uses. User // Role is the role the user has for the team. Role string `json:"role" db:"role"` } + +var teamRoles = map[string]bool{ + "observer": true, + "maintainer": true, +} + +// ValidTeamRole returns whether the role provided is valid for a team user. +func ValidTeamRole(role string) bool { + return teamRoles[role] +} + +// ValidTeamRoles returns the list of valid roles for a team user. +func ValidTeamRoles() []string { + var roles []string + for role, _ := range teamRoles { + roles = append(roles, role) + } + return roles +} + +var globalRoles = map[string]bool{ + "observer": true, + "maintainer": true, + "admin": true, +} + +// ValidGlobalRole returns whether the role provided is valid for a global user. +func ValidGlobalRole(role string) bool { + return globalRoles[role] +} + +// ValidGlobalRoles returns the list of valid roles for a global user. +func ValidGlobalRoles() []string { + var roles []string + for role, _ := range globalRoles { + roles = append(roles, role) + } + return roles +} diff --git a/server/kolide/users.go b/server/kolide/users.go index 55bc06cd51..413aa32d6d 100644 --- a/server/kolide/users.go +++ b/server/kolide/users.go @@ -14,7 +14,7 @@ import ( type UserStore interface { NewUser(user *User) (*User, error) User(username string) (*User, error) - ListUsers(opt ListOptions) ([]*User, error) + ListUsers(opt UserListOptions) ([]*User, error) UserByEmail(email string) (*User, error) UserByID(id uint) (*User, error) SaveUser(user *User) error @@ -47,7 +47,7 @@ type UserService interface { AuthenticatedUser(ctx context.Context) (user *User, err error) // ListUsers returns all users. - ListUsers(ctx context.Context, opt ListOptions) (users []*User, err error) + ListUsers(ctx context.Context, opt UserListOptions) (users []*User, err error) // ChangePassword validates the existing password, and sets the new // password. User is retrieved from the viewer context. @@ -115,6 +115,14 @@ type UserTeam struct { Role string `json:"role" db:"role"` } +// UserListOptions is additional options that can be set for listing users. +type UserListOptions struct { + ListOptions + + // TeamID, if set, indicates to only return members of the identified team. + TeamID uint +} + // UserPayload is used to modify an existing user type UserPayload struct { Username *string `json:"username,omitempty"` diff --git a/server/mock/datastore_users.go b/server/mock/datastore_users.go index 99d3ed8e74..5af3e4ebdf 100644 --- a/server/mock/datastore_users.go +++ b/server/mock/datastore_users.go @@ -10,7 +10,7 @@ type NewUserFunc func(user *kolide.User) (*kolide.User, error) type UserFunc func(username string) (*kolide.User, error) -type ListUsersFunc func(opt kolide.ListOptions) ([]*kolide.User, error) +type ListUsersFunc func(opt kolide.UserListOptions) ([]*kolide.User, error) type UserByEmailFunc func(email string) (*kolide.User, error) @@ -63,7 +63,7 @@ func (s *UserStore) User(username string) (*kolide.User, error) { return s.UserFunc(username) } -func (s *UserStore) ListUsers(opt kolide.ListOptions) ([]*kolide.User, error) { +func (s *UserStore) ListUsers(opt kolide.UserListOptions) ([]*kolide.User, error) { s.ListUsersFuncInvoked = true return s.ListUsersFunc(opt) } diff --git a/server/service/endpoint_teams.go b/server/service/endpoint_teams.go index 5cc7b316c3..95bc9235c6 100644 --- a/server/service/endpoint_teams.go +++ b/server/service/endpoint_teams.go @@ -118,3 +118,63 @@ func makeDeleteTeamEndpoint(svc kolide.Service) endpoint.Endpoint { return deleteTeamResponse{}, nil } } + +//////////////////////////////////////////////////////////////////////////////// +// List Team Users +//////////////////////////////////////////////////////////////////////////////// + +type listTeamUsersRequest struct { + TeamID uint + ListOptions kolide.ListOptions +} + +func makeListTeamUsersEndpoint(svc kolide.Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req := request.(listTeamUsersRequest) + users, err := svc.ListTeamUsers(ctx, req.TeamID, req.ListOptions) + if err != nil { + return listUsersResponse{Err: err}, nil + } + + resp := listUsersResponse{Users: []kolide.User{}} + for _, user := range users { + resp.Users = append(resp.Users, *user) + } + return resp, nil + } +} + +//////////////////////////////////////////////////////////////////////////////// +// Add / Delete Team Users +//////////////////////////////////////////////////////////////////////////////// + +type modifyTeamUsersRequest struct { + TeamID uint // From request path + // User ID and role must be specified for add users, user ID must be + // specified for delete users. + Users []kolide.TeamUser `json:"users"` +} + +func makeAddTeamUsersEndpoint(svc kolide.Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req := request.(modifyTeamUsersRequest) + team, err := svc.AddTeamUsers(ctx, req.TeamID, req.Users) + if err != nil { + return modifyTeamResponse{Err: err}, nil + } + + return modifyTeamResponse{Team: team}, err + } +} + +func makeDeleteTeamUsersEndpoint(svc kolide.Service) endpoint.Endpoint { + return func(ctx context.Context, request interface{}) (interface{}, error) { + req := request.(modifyTeamUsersRequest) + team, err := svc.DeleteTeamUsers(ctx, req.TeamID, req.Users) + if err != nil { + return modifyTeamResponse{Err: err}, nil + } + + return modifyTeamResponse{Team: team}, err + } +} diff --git a/server/service/endpoint_users.go b/server/service/endpoint_users.go index f54844d2d0..dfffb93fd5 100644 --- a/server/service/endpoint_users.go +++ b/server/service/endpoint_users.go @@ -113,7 +113,7 @@ func makeGetSessionUserEndpoint(svc kolide.Service) endpoint.Endpoint { //////////////////////////////////////////////////////////////////////////////// type listUsersRequest struct { - ListOptions kolide.ListOptions + ListOptions kolide.UserListOptions } type listUsersResponse struct { diff --git a/server/service/handler.go b/server/service/handler.go index abaeb22fdd..016dffbdf0 100644 --- a/server/service/handler.go +++ b/server/service/handler.go @@ -114,6 +114,9 @@ type KolideEndpoints struct { ModifyTeam endpoint.Endpoint DeleteTeam endpoint.Endpoint ListTeams endpoint.Endpoint + ListTeamUsers endpoint.Endpoint + AddTeamUsers endpoint.Endpoint + DeleteTeamUsers endpoint.Endpoint } // MakeKolideServerEndpoints creates the Kolide API endpoints. @@ -217,10 +220,13 @@ func MakeKolideServerEndpoints(svc kolide.Service, jwtKey, urlPrefix string, lim GetCarveBlock: authenticatedUser(jwtKey, svc, makeGetCarveBlockEndpoint(svc)), Version: authenticatedUser(jwtKey, svc, makeVersionEndpoint(svc)), // TODO permissions for teams endpoints - CreateTeam: authenticatedUser(jwtKey, svc, makeCreateTeamEndpoint(svc)), - ModifyTeam: authenticatedUser(jwtKey, svc, makeModifyTeamEndpoint(svc)), - DeleteTeam: authenticatedUser(jwtKey, svc, makeDeleteTeamEndpoint(svc)), - ListTeams: authenticatedUser(jwtKey, svc, makeListTeamsEndpoint(svc)), + CreateTeam: authenticatedUser(jwtKey, svc, makeCreateTeamEndpoint(svc)), + ModifyTeam: authenticatedUser(jwtKey, svc, makeModifyTeamEndpoint(svc)), + DeleteTeam: authenticatedUser(jwtKey, svc, makeDeleteTeamEndpoint(svc)), + ListTeams: authenticatedUser(jwtKey, svc, makeListTeamsEndpoint(svc)), + ListTeamUsers: authenticatedUser(jwtKey, svc, makeListTeamUsersEndpoint(svc)), + AddTeamUsers: authenticatedUser(jwtKey, svc, makeAddTeamUsersEndpoint(svc)), + DeleteTeamUsers: authenticatedUser(jwtKey, svc, makeDeleteTeamUsersEndpoint(svc)), // Authenticated status endpoints StatusResultStore: authenticatedUser(jwtKey, svc, makeStatusResultStoreEndpoint(svc)), @@ -334,6 +340,9 @@ type kolideHandlers struct { ModifyTeam http.Handler DeleteTeam http.Handler ListTeams http.Handler + ListTeamUsers http.Handler + AddTeamUsers http.Handler + DeleteTeamUsers http.Handler } func makeKolideKitHandlers(e KolideEndpoints, opts []kithttp.ServerOption) *kolideHandlers { @@ -434,6 +443,9 @@ func makeKolideKitHandlers(e KolideEndpoints, opts []kithttp.ServerOption) *koli ModifyTeam: newServer(e.ModifyTeam, decodeModifyTeamRequest), DeleteTeam: newServer(e.DeleteTeam, decodeDeleteTeamRequest), ListTeams: newServer(e.ListTeams, decodeListTeamsRequest), + ListTeamUsers: newServer(e.ListTeamUsers, decodeListTeamUsersRequest), + AddTeamUsers: newServer(e.AddTeamUsers, decodeModifyTeamUsersRequest), + DeleteTeamUsers: newServer(e.DeleteTeamUsers, decodeModifyTeamUsersRequest), } } @@ -650,6 +662,9 @@ func attachKolideAPIRoutes(r *mux.Router, h *kolideHandlers) { r.Handle("/api/v1/fleet/teams", h.ListTeams).Methods("GET").Name("list_teams") r.Handle("/api/v1/fleet/teams/{id}", h.ModifyTeam).Methods("PATCH").Name("modify_team") r.Handle("/api/v1/fleet/teams/{id}", h.DeleteTeam).Methods("DELETE").Name("delete_team") + r.Handle("/api/v1/fleet/teams/{id}/users", h.ListTeamUsers).Methods("GET").Name("team_users") + r.Handle("/api/v1/fleet/teams/{id}/users", h.AddTeamUsers).Methods("PATCH").Name("add_team_users") + r.Handle("/api/v1/fleet/teams/{id}/users", h.DeleteTeamUsers).Methods("DELETE").Name("delete_team_users") 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") @@ -721,7 +736,7 @@ func RedirectLoginToSetup(svc kolide.Service, logger kitlog.Logger, next http.Ha // RequireSetup checks to see if the service has been setup. func RequireSetup(svc kolide.Service) (bool, error) { ctx := context.Background() - users, err := svc.ListUsers(ctx, kolide.ListOptions{Page: 0, PerPage: 1}) + users, err := svc.ListUsers(ctx, kolide.UserListOptions{ListOptions: kolide.ListOptions{Page: 0, PerPage: 1}}) if err != nil { return false, err } diff --git a/server/service/logging_users.go b/server/service/logging_users.go index 90af4e3727..520c1d5b66 100644 --- a/server/service/logging_users.go +++ b/server/service/logging_users.go @@ -69,7 +69,7 @@ func (mw loggingMiddleware) CreateUser(ctx context.Context, p kolide.UserPayload return user, err } -func (mw loggingMiddleware) ListUsers(ctx context.Context, opt kolide.ListOptions) ([]*kolide.User, error) { +func (mw loggingMiddleware) ListUsers(ctx context.Context, opt kolide.UserListOptions) ([]*kolide.User, error) { var ( users []*kolide.User err error diff --git a/server/service/metrics_users.go b/server/service/metrics_users.go index 6607cfe9f3..f09daada7b 100644 --- a/server/service/metrics_users.go +++ b/server/service/metrics_users.go @@ -71,7 +71,7 @@ func (mw metricsMiddleware) User(ctx context.Context, id uint) (*kolide.User, er return user, err } -func (mw metricsMiddleware) ListUsers(ctx context.Context, opt kolide.ListOptions) ([]*kolide.User, error) { +func (mw metricsMiddleware) ListUsers(ctx context.Context, opt kolide.UserListOptions) ([]*kolide.User, error) { var ( users []*kolide.User diff --git a/server/service/service_teams.go b/server/service/service_teams.go index a380c83e20..e33b70421b 100644 --- a/server/service/service_teams.go +++ b/server/service/service_teams.go @@ -2,6 +2,7 @@ package service import ( "context" + "fmt" "github.com/fleetdm/fleet/server/kolide" ) @@ -46,6 +47,69 @@ func (svc service) ModifyTeam(ctx context.Context, id uint, payload kolide.TeamP return svc.ds.SaveTeam(team) } +func (svc service) AddTeamUsers(ctx context.Context, teamID uint, users []kolide.TeamUser) (*kolide.Team, error) { + idMap := make(map[uint]kolide.TeamUser) + for _, user := range users { + if !kolide.ValidTeamRole(user.Role) { + return nil, newInvalidArgumentError("users", fmt.Sprintf("%s is not a valid role for a team user", user.Role)) + } + idMap[user.ID] = user + } + + team, err := svc.ds.Team(teamID) + if err != nil { + return nil, err + } + + // Replace existing + for i, existingUser := range team.Users { + if user, ok := idMap[existingUser.ID]; ok { + team.Users[i] = user + delete(idMap, user.ID) + } + } + + // Add new (that have not already been replaced) + for _, user := range idMap { + team.Users = append(team.Users, user) + } + + return svc.ds.SaveTeam(team) +} + +func (svc service) DeleteTeamUsers(ctx context.Context, teamID uint, users []kolide.TeamUser) (*kolide.Team, error) { + idMap := make(map[uint]bool) + for _, user := range users { + idMap[user.ID] = true + } + + team, err := svc.ds.Team(teamID) + if err != nil { + return nil, err + } + + newUsers := []kolide.TeamUser{} + // Delete existing + for _, existingUser := range team.Users { + if _, ok := idMap[existingUser.ID]; !ok { + // Only add non-deleted users + newUsers = append(newUsers, existingUser) + } + } + team.Users = newUsers + + return svc.ds.SaveTeam(team) +} + +func (svc service) ListTeamUsers(ctx context.Context, teamID uint, opt kolide.ListOptions) ([]*kolide.User, error) { + team, err := svc.ds.Team(teamID) + if err != nil { + return nil, err + } + + return svc.ds.ListUsers(kolide.UserListOptions{ListOptions: opt, TeamID: team.ID}) +} + func (svc service) ListTeams(ctx context.Context, opt kolide.ListOptions) ([]*kolide.Team, error) { return svc.ds.ListTeams(opt) } diff --git a/server/service/service_users.go b/server/service/service_users.go index 121960ccf2..9db618f4ab 100644 --- a/server/service/service_users.go +++ b/server/service/service_users.go @@ -179,7 +179,7 @@ func (svc service) AuthenticatedUser(ctx context.Context) (*kolide.User, error) return vc.User, nil } -func (svc service) ListUsers(ctx context.Context, opt kolide.ListOptions) ([]*kolide.User, error) { +func (svc service) ListUsers(ctx context.Context, opt kolide.UserListOptions) ([]*kolide.User, error) { return svc.ds.ListUsers(opt) } diff --git a/server/service/transport.go b/server/service/transport.go index d18f8e1ac2..912f0f1b76 100644 --- a/server/service/transport.go +++ b/server/service/transport.go @@ -197,6 +197,25 @@ func hostListOptionsFromRequest(r *http.Request) (kolide.HostListOptions, error) return hopt, nil } +func userListOptionsFromRequest(r *http.Request) (kolide.UserListOptions, error) { + opt, err := listOptionsFromRequest(r) + if err != nil { + return kolide.UserListOptions{}, err + } + + uopt := kolide.UserListOptions{ListOptions: opt} + + if tid := r.URL.Query().Get("team_id"); tid != "" { + teamID, err := strconv.ParseUint(tid, 10, 64) + if err != nil { + return uopt, errors.Wrap(err, "parse team_id as int") + } + uopt.TeamID = uint(teamID) + } + + return uopt, nil +} + func decodeNoParamsRequest(ctx context.Context, r *http.Request) (interface{}, error) { return nil, nil } @@ -214,3 +233,15 @@ func decodeGetGenericSpecRequest(ctx context.Context, r *http.Request) (interfac req.Name = name return req, nil } + +type genericIDListRequest struct { + IDs []uint `json:"ids"` +} + +func decodeGenericIDListRequest(ctx context.Context, r *http.Request) (interface{}, error) { + var req genericIDListRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + return nil, err + } + return req, nil +} diff --git a/server/service/transport_teams.go b/server/service/transport_teams.go index 020fc4a4c1..b9edec25ee 100644 --- a/server/service/transport_teams.go +++ b/server/service/transport_teams.go @@ -43,3 +43,29 @@ func decodeDeleteTeamRequest(ctx context.Context, r *http.Request) (interface{}, } return deleteTeamRequest{ID: id}, nil } + +func decodeListTeamUsersRequest(ctx context.Context, r *http.Request) (interface{}, error) { + id, err := idFromRequest(r, "id") + if err != nil { + return nil, err + } + opt, err := listOptionsFromRequest(r) + if err != nil { + return nil, err + } + return listTeamUsersRequest{TeamID: id, ListOptions: opt}, nil +} + +func decodeModifyTeamUsersRequest(ctx context.Context, r *http.Request) (interface{}, error) { + id, err := idFromRequest(r, "id") + if err != nil { + return nil, err + } + var req modifyTeamUsersRequest + err = json.NewDecoder(r.Body).Decode(&req) + if err != nil { + return nil, err + } + req.TeamID = id + return req, nil +} diff --git a/server/service/transport_users.go b/server/service/transport_users.go index 9927ae612f..0284f48c6b 100644 --- a/server/service/transport_users.go +++ b/server/service/transport_users.go @@ -39,7 +39,7 @@ func decodeGetUserRequest(ctx context.Context, r *http.Request) (interface{}, er } func decodeListUsersRequest(ctx context.Context, r *http.Request) (interface{}, error) { - opt, err := listOptionsFromRequest(r) + opt, err := userListOptionsFromRequest(r) if err != nil { return nil, err }