2016-11-16 13:47:49 +00:00
|
|
|
package mysql
|
|
|
|
|
|
|
|
import (
|
2021-09-14 12:11:07 +00:00
|
|
|
"context"
|
2016-12-20 22:09:49 +00:00
|
|
|
"database/sql"
|
2016-11-16 13:47:49 +00:00
|
|
|
"fmt"
|
2021-03-18 04:59:00 +00:00
|
|
|
"strings"
|
2016-11-16 13:47:49 +00:00
|
|
|
|
2021-06-26 04:46:51 +00:00
|
|
|
"github.com/fleetdm/fleet/v4/server/fleet"
|
2021-03-18 04:59:00 +00:00
|
|
|
"github.com/jmoiron/sqlx"
|
2016-12-20 22:09:49 +00:00
|
|
|
"github.com/pkg/errors"
|
2016-11-16 13:47:49 +00:00
|
|
|
)
|
|
|
|
|
2021-03-18 00:24:34 +00:00
|
|
|
var userSearchColumns = []string{"name", "email"}
|
|
|
|
|
2016-11-16 13:47:49 +00:00
|
|
|
// NewUser creates a new user
|
2021-09-14 12:11:07 +00:00
|
|
|
func (d *Datastore) NewUser(ctx context.Context, user *fleet.User) (*fleet.User, error) {
|
2021-07-13 19:33:04 +00:00
|
|
|
if err := fleet.ValidateRole(user.GlobalRole, user.Teams); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2021-09-14 14:44:02 +00:00
|
|
|
err := d.withTx(ctx, func(tx sqlx.ExtContext) error {
|
2021-07-16 18:28:13 +00:00
|
|
|
sqlStatement := `
|
2017-05-10 16:26:05 +00:00
|
|
|
INSERT INTO users (
|
|
|
|
password,
|
|
|
|
salt,
|
|
|
|
name,
|
|
|
|
email,
|
|
|
|
admin_forced_password_reset,
|
|
|
|
gravatar_url,
|
|
|
|
position,
|
2021-03-18 23:05:46 +00:00
|
|
|
sso_enabled,
|
2021-06-17 01:11:28 +00:00
|
|
|
api_only,
|
2021-03-18 23:05:46 +00:00
|
|
|
global_role
|
2021-06-24 20:42:29 +00:00
|
|
|
) VALUES (?,?,?,?,?,?,?,?,?,?)
|
2017-05-10 16:26:05 +00:00
|
|
|
`
|
2021-09-14 14:44:02 +00:00
|
|
|
result, err := tx.ExecContext(ctx, sqlStatement,
|
2021-07-16 18:28:13 +00:00
|
|
|
user.Password,
|
|
|
|
user.Salt,
|
|
|
|
user.Name,
|
|
|
|
user.Email,
|
|
|
|
user.AdminForcedPasswordReset,
|
|
|
|
user.GravatarURL,
|
|
|
|
user.Position,
|
|
|
|
user.SSOEnabled,
|
|
|
|
user.APIOnly,
|
|
|
|
user.GlobalRole)
|
|
|
|
if err != nil {
|
|
|
|
return errors.Wrap(err, "create new user")
|
|
|
|
}
|
2016-11-16 13:47:49 +00:00
|
|
|
|
2021-07-16 18:28:13 +00:00
|
|
|
id, _ := result.LastInsertId()
|
|
|
|
user.ID = uint(id)
|
2021-03-18 04:59:00 +00:00
|
|
|
|
2021-09-14 14:44:02 +00:00
|
|
|
if err := saveTeamsForUserDB(ctx, tx, user); err != nil {
|
2021-07-16 18:28:13 +00:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
})
|
|
|
|
if err != nil {
|
2021-03-18 04:59:00 +00:00
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2016-11-16 13:47:49 +00:00
|
|
|
return user, nil
|
|
|
|
}
|
|
|
|
|
2021-09-14 12:11:07 +00:00
|
|
|
func (d *Datastore) findUser(ctx context.Context, searchCol string, searchVal interface{}) (*fleet.User, error) {
|
2016-11-16 13:47:49 +00:00
|
|
|
sqlStatement := fmt.Sprintf(
|
|
|
|
"SELECT * FROM users "+
|
2020-10-22 17:51:26 +00:00
|
|
|
"WHERE %s = ? LIMIT 1",
|
2016-11-16 13:47:49 +00:00
|
|
|
searchCol,
|
|
|
|
)
|
|
|
|
|
2021-06-06 22:07:29 +00:00
|
|
|
user := &fleet.User{}
|
2016-11-16 13:47:49 +00:00
|
|
|
|
2021-09-14 14:44:02 +00:00
|
|
|
err := sqlx.GetContext(ctx, d.reader, user, sqlStatement, searchVal)
|
2016-12-20 22:09:49 +00:00
|
|
|
if err != nil && err == sql.ErrNoRows {
|
|
|
|
return nil, notFound("User").
|
|
|
|
WithMessage(fmt.Sprintf("with %s=%v", searchCol, searchVal))
|
|
|
|
} else if err != nil {
|
|
|
|
return nil, errors.Wrap(err, "find user")
|
2016-11-16 13:47:49 +00:00
|
|
|
}
|
|
|
|
|
2021-09-14 14:44:02 +00:00
|
|
|
if err := d.loadTeamsForUsers(ctx, []*fleet.User{user}); err != nil {
|
2021-03-18 04:59:00 +00:00
|
|
|
return nil, errors.Wrap(err, "load teams")
|
|
|
|
}
|
|
|
|
|
2021-11-10 22:30:15 +00:00
|
|
|
// When SSO is enabled, we can ignore forced password resets
|
|
|
|
// However, we want to leave the db untouched, to cover cases where SSO is toggled
|
|
|
|
if user.SSOEnabled {
|
|
|
|
user.AdminForcedPasswordReset = false
|
|
|
|
}
|
|
|
|
|
2016-11-16 13:47:49 +00:00
|
|
|
return user, nil
|
|
|
|
}
|
|
|
|
|
2021-04-22 03:54:09 +00:00
|
|
|
// ListUsers lists all users with team ID, limit, sort and offset passed in with
|
|
|
|
// UserListOptions.
|
2021-09-14 12:11:07 +00:00
|
|
|
func (d *Datastore) ListUsers(ctx context.Context, opt fleet.UserListOptions) ([]*fleet.User, error) {
|
2016-11-16 13:47:49 +00:00
|
|
|
sqlStatement := `
|
2020-10-22 17:51:26 +00:00
|
|
|
SELECT * FROM users
|
2021-03-18 00:24:34 +00:00
|
|
|
WHERE TRUE
|
2016-11-16 13:47:49 +00:00
|
|
|
`
|
2021-04-22 03:54:09 +00:00
|
|
|
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)
|
|
|
|
}
|
2021-03-18 00:24:34 +00:00
|
|
|
|
2021-04-22 03:54:09 +00:00
|
|
|
sqlStatement, params = searchLike(sqlStatement, params, opt.MatchQuery, userSearchColumns...)
|
|
|
|
sqlStatement = appendListOptionsToSQL(sqlStatement, opt.ListOptions)
|
2021-06-06 22:07:29 +00:00
|
|
|
users := []*fleet.User{}
|
2016-11-16 13:47:49 +00:00
|
|
|
|
2021-09-14 14:44:02 +00:00
|
|
|
if err := sqlx.SelectContext(ctx, d.reader, &users, sqlStatement, params...); err != nil {
|
2016-12-20 22:09:49 +00:00
|
|
|
return nil, errors.Wrap(err, "list users")
|
2016-11-16 13:47:49 +00:00
|
|
|
}
|
|
|
|
|
2021-09-14 14:44:02 +00:00
|
|
|
if err := d.loadTeamsForUsers(ctx, users); err != nil {
|
2021-03-18 04:59:00 +00:00
|
|
|
return nil, errors.Wrap(err, "load teams")
|
|
|
|
}
|
|
|
|
|
2016-11-16 13:47:49 +00:00
|
|
|
return users, nil
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2021-09-14 12:11:07 +00:00
|
|
|
func (d *Datastore) UserByEmail(ctx context.Context, email string) (*fleet.User, error) {
|
|
|
|
return d.findUser(ctx, "email", email)
|
2016-11-16 13:47:49 +00:00
|
|
|
}
|
|
|
|
|
2021-09-14 12:11:07 +00:00
|
|
|
func (d *Datastore) UserByID(ctx context.Context, id uint) (*fleet.User, error) {
|
|
|
|
return d.findUser(ctx, "id", id)
|
2016-11-16 13:47:49 +00:00
|
|
|
}
|
|
|
|
|
2021-09-14 12:11:07 +00:00
|
|
|
func (d *Datastore) SaveUser(ctx context.Context, user *fleet.User) error {
|
2021-09-14 14:44:02 +00:00
|
|
|
return d.withTx(ctx, func(tx sqlx.ExtContext) error {
|
|
|
|
return saveUserDB(ctx, tx, user)
|
2021-07-16 18:28:13 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2021-09-14 12:11:07 +00:00
|
|
|
func (d *Datastore) SaveUsers(ctx context.Context, users []*fleet.User) error {
|
2021-09-14 14:44:02 +00:00
|
|
|
return d.withTx(ctx, func(tx sqlx.ExtContext) error {
|
2021-07-16 18:28:13 +00:00
|
|
|
for _, user := range users {
|
2021-09-14 14:44:02 +00:00
|
|
|
err := saveUserDB(ctx, tx, user)
|
2021-07-16 18:28:13 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2021-09-14 14:44:02 +00:00
|
|
|
func saveUserDB(ctx context.Context, tx sqlx.ExtContext, user *fleet.User) error {
|
2021-07-13 19:33:04 +00:00
|
|
|
if err := fleet.ValidateRole(user.GlobalRole, user.Teams); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2016-11-16 13:47:49 +00:00
|
|
|
sqlStatement := `
|
2017-05-10 16:26:05 +00:00
|
|
|
UPDATE users SET
|
|
|
|
password = ?,
|
|
|
|
salt = ?,
|
|
|
|
name = ?,
|
|
|
|
email = ?,
|
|
|
|
admin_forced_password_reset = ?,
|
|
|
|
gravatar_url = ?,
|
|
|
|
position = ?,
|
2021-03-18 23:05:46 +00:00
|
|
|
sso_enabled = ?,
|
2021-06-17 01:11:28 +00:00
|
|
|
api_only = ?,
|
2021-03-18 23:05:46 +00:00
|
|
|
global_role = ?
|
2017-05-10 16:26:05 +00:00
|
|
|
WHERE id = ?
|
|
|
|
`
|
2021-09-14 14:44:02 +00:00
|
|
|
result, err := tx.ExecContext(ctx, sqlStatement,
|
2021-06-24 20:42:29 +00:00
|
|
|
user.Password,
|
|
|
|
user.Salt,
|
|
|
|
user.Name,
|
|
|
|
user.Email,
|
|
|
|
user.AdminForcedPasswordReset,
|
|
|
|
user.GravatarURL,
|
|
|
|
user.Position,
|
|
|
|
user.SSOEnabled,
|
|
|
|
user.APIOnly,
|
|
|
|
user.GlobalRole,
|
|
|
|
user.ID)
|
2016-11-16 13:47:49 +00:00
|
|
|
if err != nil {
|
2016-12-20 22:09:49 +00:00
|
|
|
return errors.Wrap(err, "save user")
|
2016-11-16 13:47:49 +00:00
|
|
|
}
|
2017-03-30 22:03:48 +00:00
|
|
|
rows, err := result.RowsAffected()
|
|
|
|
if err != nil {
|
|
|
|
return errors.Wrap(err, "rows affected save user")
|
|
|
|
}
|
|
|
|
if rows == 0 {
|
|
|
|
return notFound("User").WithID(user.ID)
|
|
|
|
}
|
2016-11-16 13:47:49 +00:00
|
|
|
|
2021-03-18 04:59:00 +00:00
|
|
|
// REVIEW: Check if teams have been set?
|
2021-09-14 14:44:02 +00:00
|
|
|
if err := saveTeamsForUserDB(ctx, tx, user); err != nil {
|
2021-03-18 04:59:00 +00:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// loadTeamsForUsers will load the teams/roles for the provided users.
|
2021-09-14 14:44:02 +00:00
|
|
|
func (d *Datastore) loadTeamsForUsers(ctx context.Context, users []*fleet.User) error {
|
2021-03-18 04:59:00 +00:00
|
|
|
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)
|
2021-06-06 22:07:29 +00:00
|
|
|
idToUser := make(map[uint]*fleet.User, len(users))
|
2021-03-18 04:59:00 +00:00
|
|
|
for _, u := range users {
|
|
|
|
// Initialize empty slice so we get an array in JSON responses instead
|
|
|
|
// of null if it is empty
|
2021-06-06 22:07:29 +00:00
|
|
|
u.Teams = []fleet.UserTeam{}
|
2021-03-18 04:59:00 +00:00
|
|
|
// 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 {
|
2021-06-06 22:07:29 +00:00
|
|
|
fleet.UserTeam
|
2021-03-18 04:59:00 +00:00
|
|
|
UserID uint `db:"user_id"`
|
|
|
|
}
|
2021-09-14 14:44:02 +00:00
|
|
|
if err := sqlx.SelectContext(ctx, d.reader, &rows, sql, args...); err != nil {
|
2021-03-18 04:59:00 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2021-09-14 14:44:02 +00:00
|
|
|
func saveTeamsForUserDB(ctx context.Context, tx sqlx.ExtContext, user *fleet.User) error {
|
2021-03-18 04:59:00 +00:00
|
|
|
// Do a full teams update by deleting existing teams and then inserting all
|
|
|
|
// the current teams in a single transaction.
|
|
|
|
|
2021-07-16 18:28:13 +00:00
|
|
|
// Delete before insert
|
|
|
|
sql := `DELETE FROM user_teams WHERE user_id = ?`
|
2021-09-14 14:44:02 +00:00
|
|
|
if _, err := tx.ExecContext(ctx, sql, user.ID); err != nil {
|
2021-07-16 18:28:13 +00:00
|
|
|
return errors.Wrap(err, "delete existing teams")
|
|
|
|
}
|
2021-04-22 03:54:09 +00:00
|
|
|
|
2021-07-16 18:28:13 +00:00
|
|
|
if len(user.Teams) == 0 {
|
2021-03-18 04:59:00 +00:00
|
|
|
return nil
|
|
|
|
}
|
2021-07-16 18:28:13 +00:00
|
|
|
|
|
|
|
// 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, ",")
|
2021-09-14 14:44:02 +00:00
|
|
|
if _, err := tx.ExecContext(ctx, sql, args...); err != nil {
|
2021-07-16 18:28:13 +00:00
|
|
|
return errors.Wrap(err, "insert teams")
|
|
|
|
}
|
|
|
|
|
2016-11-16 13:47:49 +00:00
|
|
|
return nil
|
|
|
|
}
|
2021-04-08 23:53:33 +00:00
|
|
|
|
|
|
|
// DeleteUser deletes the associated user
|
2021-09-14 12:11:07 +00:00
|
|
|
func (d *Datastore) DeleteUser(ctx context.Context, id uint) error {
|
2021-09-20 17:47:06 +00:00
|
|
|
return d.deleteEntity(ctx, usersTable, id)
|
2021-04-08 23:53:33 +00:00
|
|
|
}
|