2016-09-26 18:48:55 +00:00
|
|
|
package service
|
2016-08-28 03:59:17 +00:00
|
|
|
|
|
|
|
import (
|
2017-03-15 15:55:30 +00:00
|
|
|
"context"
|
2016-08-28 03:59:17 +00:00
|
|
|
"crypto/rand"
|
|
|
|
"encoding/base64"
|
2016-12-28 16:55:03 +00:00
|
|
|
"html/template"
|
2016-09-15 14:52:17 +00:00
|
|
|
"time"
|
2016-08-28 03:59:17 +00:00
|
|
|
|
2020-11-11 17:59:12 +00:00
|
|
|
"github.com/fleetdm/fleet/server/contexts/viewer"
|
|
|
|
"github.com/fleetdm/fleet/server/kolide"
|
|
|
|
"github.com/fleetdm/fleet/server/mail"
|
2016-12-14 18:11:43 +00:00
|
|
|
"github.com/pkg/errors"
|
2016-08-28 03:59:17 +00:00
|
|
|
)
|
|
|
|
|
2020-11-05 01:06:55 +00:00
|
|
|
func (svc service) CreateUserWithInvite(ctx context.Context, p kolide.UserPayload) (*kolide.User, error) {
|
2016-12-30 01:58:12 +00:00
|
|
|
invite, err := svc.VerifyInvite(ctx, *p.InviteToken)
|
2016-09-29 02:44:05 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2016-12-21 17:34:10 +00:00
|
|
|
|
|
|
|
// set the payload Admin property based on an existing invite.
|
|
|
|
p.Admin = &invite.Admin
|
|
|
|
|
2016-11-09 17:19:07 +00:00
|
|
|
user, err := svc.newUser(p)
|
2016-08-28 03:59:17 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2017-01-04 18:18:21 +00:00
|
|
|
|
|
|
|
err = svc.ds.DeleteInvite(invite.ID)
|
2016-08-28 03:59:17 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2016-11-09 17:19:07 +00:00
|
|
|
return user, nil
|
|
|
|
}
|
|
|
|
|
2020-11-05 01:06:55 +00:00
|
|
|
func (svc service) CreateUser(ctx context.Context, p kolide.UserPayload) (*kolide.User, error) {
|
2016-11-09 17:19:07 +00:00
|
|
|
return svc.newUser(p)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (svc service) newUser(p kolide.UserPayload) (*kolide.User, error) {
|
2017-05-10 16:26:05 +00:00
|
|
|
var ssoEnabled bool
|
|
|
|
// if user is SSO generate a fake password
|
2020-11-05 01:06:55 +00:00
|
|
|
if (p.SSOInvite != nil && *p.SSOInvite) || (p.SSOEnabled != nil && *p.SSOEnabled) {
|
2017-05-10 16:26:05 +00:00
|
|
|
fakePassword, err := generateRandomText(14)
|
|
|
|
if err != nil {
|
2020-11-05 01:06:55 +00:00
|
|
|
return nil, errors.Wrap(err, "generate stand-in password")
|
2017-05-10 16:26:05 +00:00
|
|
|
}
|
|
|
|
p.Password = &fakePassword
|
|
|
|
ssoEnabled = true
|
|
|
|
}
|
2016-11-09 17:19:07 +00:00
|
|
|
user, err := p.User(svc.config.Auth.SaltKeySize, svc.config.Auth.BcryptCost)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2017-05-10 16:26:05 +00:00
|
|
|
user.SSOEnabled = ssoEnabled
|
2016-11-09 17:19:07 +00:00
|
|
|
user, err = svc.ds.NewUser(user)
|
2016-09-29 02:44:05 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2016-08-28 03:59:17 +00:00
|
|
|
return user, nil
|
|
|
|
}
|
|
|
|
|
2017-01-17 16:43:59 +00:00
|
|
|
func (svc service) ChangeUserAdmin(ctx context.Context, id uint, isAdmin bool) (*kolide.User, error) {
|
|
|
|
user, err := svc.ds.UserByID(id)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
user.Admin = isAdmin
|
|
|
|
if err = svc.saveUser(user); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return user, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (svc service) ChangeUserEnabled(ctx context.Context, id uint, isEnabled bool) (*kolide.User, error) {
|
|
|
|
user, err := svc.ds.UserByID(id)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
user.Enabled = isEnabled
|
|
|
|
if err = svc.saveUser(user); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return user, nil
|
|
|
|
}
|
|
|
|
|
2016-09-15 14:52:17 +00:00
|
|
|
func (svc service) ModifyUser(ctx context.Context, userID uint, p kolide.UserPayload) (*kolide.User, error) {
|
|
|
|
user, err := svc.User(ctx, userID)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// the method assumes that the correct authorization
|
|
|
|
// has been validated higher up the stack
|
|
|
|
if p.Username != nil {
|
|
|
|
user.Username = *p.Username
|
|
|
|
}
|
|
|
|
|
|
|
|
if p.Name != nil {
|
|
|
|
user.Name = *p.Name
|
|
|
|
}
|
|
|
|
|
|
|
|
if p.Email != nil {
|
2017-01-27 13:35:58 +00:00
|
|
|
err = svc.modifyEmailAddress(ctx, user, *p.Email, p.Password)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2016-09-15 14:52:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if p.Position != nil {
|
|
|
|
user.Position = *p.Position
|
|
|
|
}
|
|
|
|
|
|
|
|
if p.GravatarURL != nil {
|
|
|
|
user.GravatarURL = *p.GravatarURL
|
|
|
|
}
|
|
|
|
|
2017-05-17 15:58:40 +00:00
|
|
|
if p.SSOEnabled != nil {
|
|
|
|
user.SSOEnabled = *p.SSOEnabled
|
|
|
|
}
|
|
|
|
|
2021-03-18 04:59:00 +00:00
|
|
|
if p.Teams != nil {
|
|
|
|
user.Teams = *p.Teams
|
|
|
|
}
|
|
|
|
|
2016-09-15 14:52:17 +00:00
|
|
|
err = svc.saveUser(user)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2017-01-06 22:38:39 +00:00
|
|
|
return user, nil
|
2016-09-15 14:52:17 +00:00
|
|
|
}
|
|
|
|
|
2017-01-27 13:35:58 +00:00
|
|
|
func (svc service) modifyEmailAddress(ctx context.Context, user *kolide.User, email string, password *string) error {
|
|
|
|
// password requirement handled in validation middleware
|
|
|
|
if password != nil {
|
|
|
|
err := user.ValidatePassword(*password)
|
|
|
|
if err != nil {
|
2017-02-16 17:02:49 +00:00
|
|
|
return newPermissionError("password", "incorrect password")
|
2017-01-27 13:35:58 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
random, err := kolide.RandomText(svc.config.App.TokenKeySize)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
token := base64.URLEncoding.EncodeToString([]byte(random))
|
|
|
|
err = svc.ds.PendingEmailChange(user.ID, email, token)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
config, err := svc.AppConfig(ctx)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
changeEmail := kolide.Email{
|
2019-01-24 17:39:32 +00:00
|
|
|
Subject: "Confirm Fleet Email Change",
|
2017-01-27 13:35:58 +00:00
|
|
|
To: []string{email},
|
|
|
|
Config: config,
|
2020-03-30 02:22:04 +00:00
|
|
|
Mailer: &mail.ChangeEmailMailer{
|
2019-10-16 23:40:45 +00:00
|
|
|
Token: token,
|
|
|
|
BaseURL: template.URL(config.KolideServerURL + svc.config.Server.URLPrefix),
|
|
|
|
AssetURL: getAssetURL(),
|
2017-01-27 13:35:58 +00:00
|
|
|
},
|
|
|
|
}
|
2017-12-04 14:43:43 +00:00
|
|
|
return svc.mailService.SendEmail(changeEmail)
|
2017-01-27 13:35:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (svc service) ChangeUserEmail(ctx context.Context, token string) (string, error) {
|
2017-02-16 13:07:20 +00:00
|
|
|
vc, ok := viewer.FromContext(ctx)
|
|
|
|
if !ok {
|
|
|
|
return "", errNoContext
|
|
|
|
}
|
|
|
|
return svc.ds.ConfirmPendingEmailChange(vc.UserID(), token)
|
2017-01-27 13:35:58 +00:00
|
|
|
}
|
|
|
|
|
2016-09-01 04:51:38 +00:00
|
|
|
func (svc service) User(ctx context.Context, id uint) (*kolide.User, error) {
|
|
|
|
return svc.ds.UserByID(id)
|
2016-08-29 00:29:56 +00:00
|
|
|
}
|
|
|
|
|
2016-09-16 04:35:52 +00:00
|
|
|
func (svc service) AuthenticatedUser(ctx context.Context) (*kolide.User, error) {
|
2016-09-26 17:14:39 +00:00
|
|
|
vc, ok := viewer.FromContext(ctx)
|
|
|
|
if !ok {
|
|
|
|
return nil, errNoContext
|
2016-09-16 04:35:52 +00:00
|
|
|
}
|
2016-09-19 20:50:31 +00:00
|
|
|
if !vc.IsLoggedIn() {
|
2016-09-23 02:41:58 +00:00
|
|
|
return nil, permissionError{}
|
2016-09-19 20:50:31 +00:00
|
|
|
}
|
2016-09-26 17:14:39 +00:00
|
|
|
return vc.User, nil
|
2016-09-16 04:35:52 +00:00
|
|
|
}
|
|
|
|
|
2016-10-13 18:21:47 +00:00
|
|
|
func (svc service) ListUsers(ctx context.Context, opt kolide.ListOptions) ([]*kolide.User, error) {
|
2016-10-14 15:59:27 +00:00
|
|
|
return svc.ds.ListUsers(opt)
|
2016-09-15 14:52:17 +00:00
|
|
|
}
|
|
|
|
|
2016-12-14 18:11:43 +00:00
|
|
|
// setNewPassword is a helper for changing a user's password. It should be
|
|
|
|
// called to set the new password after proper authorization has been
|
|
|
|
// performed.
|
|
|
|
func (svc service) setNewPassword(ctx context.Context, user *kolide.User, password string) error {
|
|
|
|
err := user.SetPassword(password, svc.config.Auth.SaltKeySize, svc.config.Auth.BcryptCost)
|
|
|
|
if err != nil {
|
|
|
|
return errors.Wrap(err, "setting new password")
|
|
|
|
}
|
2017-05-10 16:26:05 +00:00
|
|
|
if user.SSOEnabled {
|
|
|
|
return errors.New("set password for single sign on user not allowed")
|
|
|
|
}
|
2016-12-14 18:11:43 +00:00
|
|
|
err = svc.saveUser(user)
|
|
|
|
if err != nil {
|
|
|
|
return errors.Wrap(err, "saving changed password")
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (svc service) ChangePassword(ctx context.Context, oldPass, newPass string) error {
|
|
|
|
vc, ok := viewer.FromContext(ctx)
|
|
|
|
if !ok {
|
|
|
|
return errNoContext
|
|
|
|
}
|
2017-05-10 16:26:05 +00:00
|
|
|
if vc.User.SSOEnabled {
|
|
|
|
return errors.New("change password for single sign on user not allowed")
|
|
|
|
}
|
2016-12-22 17:08:29 +00:00
|
|
|
if err := vc.User.ValidatePassword(newPass); err == nil {
|
|
|
|
return newInvalidArgumentError("new_password", "cannot reuse old password")
|
|
|
|
}
|
|
|
|
|
2016-12-14 18:11:43 +00:00
|
|
|
if err := vc.User.ValidatePassword(oldPass); err != nil {
|
2016-12-22 17:08:29 +00:00
|
|
|
return newInvalidArgumentError("old_password", "old password does not match")
|
2016-12-14 18:11:43 +00:00
|
|
|
}
|
|
|
|
|
2017-01-15 23:23:09 +00:00
|
|
|
if err := svc.setNewPassword(ctx, vc.User, newPass); err != nil {
|
|
|
|
return errors.Wrap(err, "setting new password")
|
|
|
|
}
|
|
|
|
return nil
|
2016-12-14 18:11:43 +00:00
|
|
|
}
|
|
|
|
|
2016-09-15 14:52:17 +00:00
|
|
|
func (svc service) ResetPassword(ctx context.Context, token, password string) error {
|
|
|
|
reset, err := svc.ds.FindPassswordResetByToken(token)
|
2016-08-29 00:29:56 +00:00
|
|
|
if err != nil {
|
2016-12-14 18:11:43 +00:00
|
|
|
return errors.Wrap(err, "looking up reset by token")
|
2016-08-29 00:29:56 +00:00
|
|
|
}
|
2016-09-15 14:52:17 +00:00
|
|
|
user, err := svc.User(ctx, reset.UserID)
|
|
|
|
if err != nil {
|
2016-12-14 18:11:43 +00:00
|
|
|
return errors.Wrap(err, "retrieving user")
|
2016-08-29 17:04:07 +00:00
|
|
|
}
|
2017-05-10 16:26:05 +00:00
|
|
|
if user.SSOEnabled {
|
|
|
|
return errors.New("password reset for single sign on user not allowed")
|
|
|
|
}
|
2016-09-15 14:52:17 +00:00
|
|
|
|
2016-12-22 17:08:29 +00:00
|
|
|
// prevent setting the same password
|
|
|
|
if err := user.ValidatePassword(password); err == nil {
|
|
|
|
return newInvalidArgumentError("new_password", "cannot reuse old password")
|
|
|
|
}
|
|
|
|
|
2016-12-14 18:11:43 +00:00
|
|
|
err = svc.setNewPassword(ctx, user, password)
|
2016-08-29 00:29:56 +00:00
|
|
|
if err != nil {
|
2016-12-14 18:11:43 +00:00
|
|
|
return errors.Wrap(err, "setting new password")
|
2016-09-15 14:52:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// delete password reset tokens for user
|
|
|
|
if err := svc.ds.DeletePasswordResetRequestsForUser(user.ID); err != nil {
|
2016-12-14 18:11:43 +00:00
|
|
|
return errors.Wrap(err, "deleting password reset requests")
|
2016-09-15 14:52:17 +00:00
|
|
|
}
|
|
|
|
|
2016-12-12 17:24:51 +00:00
|
|
|
// Clear sessions so that any other browsers will have to log in with
|
|
|
|
// the new password
|
|
|
|
if err := svc.DeleteSessionsForUser(ctx, user.ID); err != nil {
|
2016-12-14 18:11:43 +00:00
|
|
|
return errors.Wrap(err, "deleting user sessions")
|
2016-12-12 17:24:51 +00:00
|
|
|
}
|
|
|
|
|
2016-09-15 14:52:17 +00:00
|
|
|
return nil
|
2016-08-29 00:29:56 +00:00
|
|
|
}
|
|
|
|
|
2017-01-10 04:42:50 +00:00
|
|
|
func (svc service) PerformRequiredPasswordReset(ctx context.Context, password string) (*kolide.User, error) {
|
|
|
|
vc, ok := viewer.FromContext(ctx)
|
|
|
|
if !ok {
|
|
|
|
return nil, errNoContext
|
|
|
|
}
|
|
|
|
user := vc.User
|
2017-05-10 16:26:05 +00:00
|
|
|
if user.SSOEnabled {
|
|
|
|
return nil, errors.New("password reset for single sign on user not allowed")
|
|
|
|
}
|
2017-01-10 04:42:50 +00:00
|
|
|
if !user.AdminForcedPasswordReset {
|
|
|
|
return nil, errors.New("user does not require password reset")
|
|
|
|
}
|
|
|
|
|
|
|
|
// prevent setting the same password
|
|
|
|
if err := user.ValidatePassword(password); err == nil {
|
|
|
|
return nil, newInvalidArgumentError("new_password", "cannot reuse old password")
|
|
|
|
}
|
|
|
|
|
|
|
|
user.AdminForcedPasswordReset = false
|
|
|
|
err := svc.setNewPassword(ctx, user, password)
|
|
|
|
if err != nil {
|
|
|
|
return nil, errors.Wrap(err, "setting new password")
|
|
|
|
}
|
|
|
|
|
|
|
|
// Sessions should already have been cleared when the reset was
|
|
|
|
// required
|
|
|
|
|
|
|
|
return user, nil
|
|
|
|
}
|
|
|
|
|
2017-01-06 22:38:39 +00:00
|
|
|
func (svc service) RequirePasswordReset(ctx context.Context, uid uint, require bool) (*kolide.User, error) {
|
|
|
|
user, err := svc.ds.UserByID(uid)
|
|
|
|
if err != nil {
|
|
|
|
return nil, errors.Wrap(err, "loading user by ID")
|
|
|
|
}
|
2017-05-10 16:26:05 +00:00
|
|
|
if user.SSOEnabled {
|
|
|
|
return nil, errors.New("password reset for single sign on user not allowed")
|
|
|
|
}
|
2017-01-06 22:38:39 +00:00
|
|
|
// Require reset on next login
|
|
|
|
user.AdminForcedPasswordReset = require
|
|
|
|
if err := svc.saveUser(user); err != nil {
|
|
|
|
return nil, errors.Wrap(err, "saving user")
|
|
|
|
}
|
|
|
|
|
|
|
|
if require {
|
|
|
|
// Clear all of the existing sessions
|
|
|
|
if err := svc.DeleteSessionsForUser(ctx, user.ID); err != nil {
|
|
|
|
return nil, errors.Wrap(err, "deleting user sessions")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return user, nil
|
|
|
|
}
|
|
|
|
|
2016-09-15 14:52:17 +00:00
|
|
|
func (svc service) RequestPasswordReset(ctx context.Context, email string) error {
|
2021-03-25 02:36:30 +00:00
|
|
|
// Regardless of error, sleep until the request has taken at least 1 second.
|
|
|
|
// This means that any request to this method will take ~1s and frustrate a timing attack.
|
|
|
|
defer func(start time.Time) {
|
|
|
|
time.Sleep(time.Until(start.Add(1 * time.Second)))
|
|
|
|
}(time.Now())
|
|
|
|
|
2016-09-15 14:52:17 +00:00
|
|
|
user, err := svc.ds.UserByEmail(email)
|
2016-08-29 17:04:07 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2017-05-10 16:26:05 +00:00
|
|
|
if user.SSOEnabled {
|
|
|
|
return errors.New("password reset for single sign on user not allowed")
|
|
|
|
}
|
2016-09-15 14:52:17 +00:00
|
|
|
|
2016-12-07 15:42:58 +00:00
|
|
|
random, err := kolide.RandomText(svc.config.App.TokenKeySize)
|
2016-09-15 14:52:17 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2016-12-07 15:42:58 +00:00
|
|
|
token := base64.URLEncoding.EncodeToString([]byte(random))
|
2016-09-15 14:52:17 +00:00
|
|
|
|
|
|
|
request := &kolide.PasswordResetRequest{
|
|
|
|
ExpiresAt: time.Now().Add(time.Hour * 24),
|
|
|
|
UserID: user.ID,
|
|
|
|
Token: token,
|
|
|
|
}
|
|
|
|
request, err = svc.ds.NewPasswordResetRequest(request)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2016-12-20 21:54:30 +00:00
|
|
|
config, err := svc.AppConfig(ctx)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2016-09-15 14:52:17 +00:00
|
|
|
resetEmail := kolide.Email{
|
2019-01-24 17:39:32 +00:00
|
|
|
Subject: "Reset Your Fleet Password",
|
2016-12-20 21:54:30 +00:00
|
|
|
To: []string{user.Email},
|
|
|
|
Config: config,
|
2020-03-30 02:22:04 +00:00
|
|
|
Mailer: &mail.PasswordResetMailer{
|
2019-10-16 23:40:45 +00:00
|
|
|
BaseURL: template.URL(config.KolideServerURL + svc.config.Server.URLPrefix),
|
|
|
|
AssetURL: getAssetURL(),
|
|
|
|
Token: token,
|
2016-12-20 21:54:30 +00:00
|
|
|
},
|
2016-09-15 14:52:17 +00:00
|
|
|
}
|
|
|
|
|
2017-12-04 14:43:43 +00:00
|
|
|
return svc.mailService.SendEmail(resetEmail)
|
2016-08-29 17:04:07 +00:00
|
|
|
}
|
|
|
|
|
2016-08-29 00:29:56 +00:00
|
|
|
// saves user in datastore.
|
|
|
|
// doesn't need to be exposed to the transport
|
|
|
|
// the service should expose actions for modifying a user instead
|
2016-09-01 04:51:38 +00:00
|
|
|
func (svc service) saveUser(user *kolide.User) error {
|
|
|
|
return svc.ds.SaveUser(user)
|
2016-08-29 00:29:56 +00:00
|
|
|
}
|
|
|
|
|
2016-08-28 03:59:17 +00:00
|
|
|
// 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
|
|
|
|
}
|