fleet/server/service/users.go
Lucas Manuel Rodriguez 40265d0e6f
Fix SMTP e-mail send when SMTP server has credentials (#10758)
#9609

This PR also fixes #10777.

The issue is: We were using `svc.AppConfig` instead of
`svc.ds.AppConfig` to retrieve the SMTP credentials.
`svc.AppConfig` obfuscates credentials, whereas `svc.ds.AppConfig` does
not.
To help prevent this from happening again I've renamed `svc.AppConfig`
to `svc.AppConfigObfuscated`.
I've also added a new test SMTP server
(https://github.com/axllent/mailpit) that supports Basic Authentication
and tests that make use of it to catch these kind of bugs (the tests are
executed when running `go test` with `MAIL_TEST=1`).

- [X] Changes file added for user-visible changes in `changes/` or
`orbit/changes/`.
See [Changes
files](https://fleetdm.com/docs/contributing/committing-changes#changes-files)
for more information.
- ~[ ] Documented any API changes (docs/Using-Fleet/REST-API.md or
docs/Contributing/API-for-contributors.md)~
- ~[ ] Documented any permissions changes~
- ~[ ] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)~
- ~[ ] Added support on fleet's osquery simulator `cmd/osquery-perf` for
new osquery data ingestion features.~
- [X] Added/updated tests
- [X] Manual QA for all new/changed functionality
  - ~For Orbit and Fleet Desktop changes:~
- ~[ ] Manual QA must be performed in the three main OSs, macOS, Windows
and Linux.~
- ~[ ] Auto-update manual QA, from released version of component to new
version (see [tools/tuf/test](../tools/tuf/test/README.md)).~
2023-03-28 15:23:15 -03:00

1036 lines
31 KiB
Go

package service
import (
"context"
"database/sql"
"encoding/base64"
"errors"
"html/template"
"net/http"
"time"
"github.com/fleetdm/fleet/v4/server"
"github.com/fleetdm/fleet/v4/server/authz"
authz_ctx "github.com/fleetdm/fleet/v4/server/contexts/authz"
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
"github.com/fleetdm/fleet/v4/server/contexts/viewer"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/mail"
"github.com/fleetdm/fleet/v4/server/ptr"
)
////////////////////////////////////////////////////////////////////////////////
// Create User
////////////////////////////////////////////////////////////////////////////////
type createUserRequest struct {
fleet.UserPayload
}
type createUserResponse struct {
User *fleet.User `json:"user,omitempty"`
Err error `json:"error,omitempty"`
}
func (r createUserResponse) error() error { return r.Err }
func createUserEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) {
req := request.(*createUserRequest)
user, err := svc.CreateUser(ctx, req.UserPayload)
if err != nil {
return createUserResponse{Err: err}, nil
}
return createUserResponse{User: user}, nil
}
func (svc *Service) CreateUser(ctx context.Context, p fleet.UserPayload) (*fleet.User, error) {
var teams []fleet.UserTeam
if p.Teams != nil {
teams = *p.Teams
}
if err := svc.authz.Authorize(ctx, &fleet.User{Teams: teams}, fleet.ActionWrite); err != nil {
return nil, err
}
if err := p.VerifyAdminCreate(); err != nil {
return nil, ctxerr.Wrap(ctx, err, "verify user payload")
}
if invite, err := svc.ds.InviteByEmail(ctx, *p.Email); err == nil && invite != nil {
return nil, ctxerr.Errorf(ctx, "%s already invited", *p.Email)
}
if p.AdminForcedPasswordReset == nil {
// By default, force password reset for users created this way.
p.AdminForcedPasswordReset = ptr.Bool(true)
}
return svc.NewUser(ctx, p)
}
////////////////////////////////////////////////////////////////////////////////
// Create User From Invite
////////////////////////////////////////////////////////////////////////////////
func createUserFromInviteEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) {
req := request.(*createUserRequest)
user, err := svc.CreateUserFromInvite(ctx, req.UserPayload)
if err != nil {
return createUserResponse{Err: err}, nil
}
return createUserResponse{User: user}, nil
}
func (svc *Service) CreateUserFromInvite(ctx context.Context, p fleet.UserPayload) (*fleet.User, error) {
// skipauth: There is no viewer context at this point. We rely on verifying
// the invite for authNZ.
svc.authz.SkipAuthorization(ctx)
if err := p.VerifyInviteCreate(); err != nil {
return nil, ctxerr.Wrap(ctx, err, "verify user payload")
}
invite, err := svc.VerifyInvite(ctx, *p.InviteToken)
if err != nil {
return nil, err
}
// set the payload role property based on an existing invite.
p.GlobalRole = invite.GlobalRole.Ptr()
p.Teams = &invite.Teams
user, err := svc.NewUser(ctx, p)
if err != nil {
return nil, err
}
err = svc.ds.DeleteInvite(ctx, invite.ID)
if err != nil {
return nil, err
}
return user, nil
}
////////////////////////////////////////////////////////////////////////////////
// List Users
////////////////////////////////////////////////////////////////////////////////
type listUsersRequest struct {
ListOptions fleet.UserListOptions `url:"user_options"`
}
type listUsersResponse struct {
Users []fleet.User `json:"users"`
Err error `json:"error,omitempty"`
}
func (r listUsersResponse) error() error { return r.Err }
func listUsersEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) {
req := request.(*listUsersRequest)
users, err := svc.ListUsers(ctx, req.ListOptions)
if err != nil {
return listUsersResponse{Err: err}, nil
}
resp := listUsersResponse{Users: []fleet.User{}}
for _, user := range users {
resp.Users = append(resp.Users, *user)
}
return resp, nil
}
func (svc *Service) ListUsers(ctx context.Context, opt fleet.UserListOptions) ([]*fleet.User, error) {
user := &fleet.User{}
if opt.TeamID != 0 {
user.Teams = []fleet.UserTeam{{Team: fleet.Team{ID: opt.TeamID}}}
}
if err := svc.authz.Authorize(ctx, user, fleet.ActionRead); err != nil {
return nil, err
}
return svc.ds.ListUsers(ctx, opt)
}
////////////////////////////////////////////////////////////////////////////////
// Me (get own current user)
////////////////////////////////////////////////////////////////////////////////
func meEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) {
user, err := svc.AuthenticatedUser(ctx)
if err != nil {
return getUserResponse{Err: err}, nil
}
availableTeams, err := svc.ListAvailableTeamsForUser(ctx, user)
if err != nil {
if errors.Is(err, fleet.ErrMissingLicense) {
availableTeams = []*fleet.TeamSummary{}
} else {
return getUserResponse{Err: err}, nil
}
}
return getUserResponse{User: user, AvailableTeams: availableTeams}, nil
}
func (svc *Service) AuthenticatedUser(ctx context.Context) (*fleet.User, error) {
vc, ok := viewer.FromContext(ctx)
if !ok {
return nil, fleet.ErrNoContext
}
if err := svc.authz.Authorize(ctx, &fleet.User{ID: vc.UserID()}, fleet.ActionRead); err != nil {
return nil, err
}
if !vc.IsLoggedIn() {
return nil, fleet.NewPermissionError("not logged in")
}
return vc.User, nil
}
////////////////////////////////////////////////////////////////////////////////
// Get User
////////////////////////////////////////////////////////////////////////////////
type getUserRequest struct {
ID uint `url:"id"`
}
type getUserResponse struct {
User *fleet.User `json:"user,omitempty"`
AvailableTeams []*fleet.TeamSummary `json:"available_teams"`
Err error `json:"error,omitempty"`
}
func (r getUserResponse) error() error { return r.Err }
func getUserEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) {
req := request.(*getUserRequest)
user, err := svc.User(ctx, req.ID)
if err != nil {
return getUserResponse{Err: err}, nil
}
availableTeams, err := svc.ListAvailableTeamsForUser(ctx, user)
if err != nil {
if errors.Is(err, fleet.ErrMissingLicense) {
availableTeams = []*fleet.TeamSummary{}
} else {
return getUserResponse{Err: err}, nil
}
}
return getUserResponse{User: user, AvailableTeams: availableTeams}, nil
}
// setAuthCheckedOnPreAuthErr can be used to set the authentication as checked
// in case of errors that happened before an auth check can be performed.
// Otherwise the endpoints return a "authentication skipped" error instead of
// the actual returned error.
func setAuthCheckedOnPreAuthErr(ctx context.Context) {
if az, ok := authz_ctx.FromContext(ctx); ok {
az.SetChecked()
}
}
func (svc *Service) User(ctx context.Context, id uint) (*fleet.User, error) {
user, err := svc.ds.UserByID(ctx, id)
if err != nil {
setAuthCheckedOnPreAuthErr(ctx)
return nil, ctxerr.Wrap(ctx, err)
}
if err := svc.authz.Authorize(ctx, user, fleet.ActionRead); err != nil {
return nil, err
}
return user, nil
}
////////////////////////////////////////////////////////////////////////////////
// Modify User
////////////////////////////////////////////////////////////////////////////////
type modifyUserRequest struct {
ID uint `json:"-" url:"id"`
fleet.UserPayload
}
type modifyUserResponse struct {
User *fleet.User `json:"user,omitempty"`
Err error `json:"error,omitempty"`
}
func (r modifyUserResponse) error() error { return r.Err }
func modifyUserEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) {
req := request.(*modifyUserRequest)
user, err := svc.ModifyUser(ctx, req.ID, req.UserPayload)
if err != nil {
return modifyUserResponse{Err: err}, nil
}
return modifyUserResponse{User: user}, nil
}
func (svc *Service) ModifyUser(ctx context.Context, userID uint, p fleet.UserPayload) (*fleet.User, error) {
user, err := svc.User(ctx, userID)
if err != nil {
setAuthCheckedOnPreAuthErr(ctx)
return nil, err
}
oldGlobalRole := user.GlobalRole
oldTeams := user.Teams
if err := svc.authz.Authorize(ctx, user, fleet.ActionWrite); err != nil {
return nil, err
}
vc, ok := viewer.FromContext(ctx)
if !ok {
return nil, ctxerr.New(ctx, "viewer not present") // should never happen, authorize would've failed
}
ownUser := vc.UserID() == userID
if err := p.VerifyModify(ownUser); err != nil {
return nil, ctxerr.Wrap(ctx, err, "verify user payload")
}
if p.GlobalRole != nil || p.Teams != nil {
if err := svc.authz.Authorize(ctx, user, fleet.ActionWriteRole); err != nil {
return nil, err
}
}
if p.NewPassword != nil {
if err := svc.authz.Authorize(ctx, user, fleet.ActionChangePassword); err != nil {
return nil, err
}
if err := fleet.ValidatePasswordRequirements(*p.NewPassword); err != nil {
return nil, ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("new_password", err.Error()))
}
if ownUser {
// when changing one's own password, user cannot reuse the same password
// and the old password must be provided (validated by p.VerifyModify above)
// and must be valid. If changed by admin, then this is not required.
if err := vc.User.ValidatePassword(*p.NewPassword); err == nil {
return nil, ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("new_password", "Cannot reuse old password"))
}
if err := vc.User.ValidatePassword(*p.Password); err != nil {
return nil, ctxerr.Wrap(ctx, fleet.NewPermissionError("incorrect password"))
}
}
}
if p.Name != nil {
user.Name = *p.Name
}
if p.Email != nil && *p.Email != user.Email {
err = svc.modifyEmailAddress(ctx, user, *p.Email, p.Password)
if err != nil {
return nil, err
}
}
if p.Position != nil {
user.Position = *p.Position
}
if p.GravatarURL != nil {
user.GravatarURL = *p.GravatarURL
}
if p.SSOEnabled != nil {
user.SSOEnabled = *p.SSOEnabled
}
currentUser := authz.UserFromContext(ctx)
if p.GlobalRole != nil && *p.GlobalRole != "" {
if currentUser.GlobalRole == nil {
return nil, authz.ForbiddenWithInternal(
"cannot edit global role as a team member",
currentUser, user, fleet.ActionWriteRole,
)
}
if p.Teams != nil && len(*p.Teams) > 0 {
return nil, fleet.NewInvalidArgumentError("teams", "may not be specified with global_role")
}
user.GlobalRole = p.GlobalRole
user.Teams = []fleet.UserTeam{}
} else if p.Teams != nil {
if !isAdminOfTheModifiedTeams(currentUser, user.Teams, *p.Teams) {
return nil, authz.ForbiddenWithInternal(
"cannot modify teams in that way",
currentUser, user, fleet.ActionWriteRole,
)
}
user.Teams = *p.Teams
user.GlobalRole = nil
}
if p.NewPassword != nil {
// setNewPassword takes care of calling saveUser
err = svc.setNewPassword(ctx, user, *p.NewPassword)
} else {
err = svc.saveUser(ctx, user)
}
if err != nil {
return nil, err
}
// load user again to get team-details like names.
user, err = svc.User(ctx, userID)
if err != nil {
return nil, err
}
adminUser := authz.UserFromContext(ctx)
if err := fleet.LogRoleChangeActivities(ctx, svc.ds, adminUser, oldGlobalRole, oldTeams, user); err != nil {
return nil, err
}
return user, nil
}
////////////////////////////////////////////////////////////////////////////////
// Delete User
////////////////////////////////////////////////////////////////////////////////
type deleteUserRequest struct {
ID uint `url:"id"`
}
type deleteUserResponse struct {
Err error `json:"error,omitempty"`
}
func (r deleteUserResponse) error() error { return r.Err }
func deleteUserEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) {
req := request.(*deleteUserRequest)
err := svc.DeleteUser(ctx, req.ID)
if err != nil {
return deleteUserResponse{Err: err}, nil
}
return deleteUserResponse{}, nil
}
func (svc *Service) DeleteUser(ctx context.Context, id uint) error {
user, err := svc.ds.UserByID(ctx, id)
if err != nil {
setAuthCheckedOnPreAuthErr(ctx)
return ctxerr.Wrap(ctx, err)
}
if err := svc.authz.Authorize(ctx, user, fleet.ActionWrite); err != nil {
return err
}
if err := svc.ds.DeleteUser(ctx, id); err != nil {
return err
}
adminUser := authz.UserFromContext(ctx)
if err := svc.ds.NewActivity(
ctx,
adminUser,
fleet.ActivityTypeDeletedUser{
UserID: user.ID,
UserName: user.Name,
UserEmail: user.Email,
},
); err != nil {
return err
}
return nil
}
////////////////////////////////////////////////////////////////////////////////
// Require Password Reset
////////////////////////////////////////////////////////////////////////////////
type requirePasswordResetRequest struct {
Require bool `json:"require"`
ID uint `json:"-" url:"id"`
}
type requirePasswordResetResponse struct {
User *fleet.User `json:"user,omitempty"`
Err error `json:"error,omitempty"`
}
func (r requirePasswordResetResponse) error() error { return r.Err }
func requirePasswordResetEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) {
req := request.(*requirePasswordResetRequest)
user, err := svc.RequirePasswordReset(ctx, req.ID, req.Require)
if err != nil {
return requirePasswordResetResponse{Err: err}, nil
}
return requirePasswordResetResponse{User: user}, nil
}
func (svc *Service) RequirePasswordReset(ctx context.Context, uid uint, require bool) (*fleet.User, error) {
if err := svc.authz.Authorize(ctx, &fleet.User{ID: uid}, fleet.ActionWrite); err != nil {
return nil, err
}
user, err := svc.ds.UserByID(ctx, uid)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "loading user by ID")
}
if user.SSOEnabled {
return nil, ctxerr.New(ctx, "password reset for single sign on user not allowed")
}
// Require reset on next login
user.AdminForcedPasswordReset = require
if err := svc.saveUser(ctx, user); err != nil {
return nil, ctxerr.Wrap(ctx, err, "saving user")
}
if require {
// Clear all of the existing sessions
if err := svc.DeleteSessionsForUser(ctx, user.ID); err != nil {
return nil, ctxerr.Wrap(ctx, err, "deleting user sessions")
}
}
return user, nil
}
////////////////////////////////////////////////////////////////////////////////
// Change Password
////////////////////////////////////////////////////////////////////////////////
type changePasswordRequest struct {
OldPassword string `json:"old_password"`
NewPassword string `json:"new_password"`
}
type changePasswordResponse struct {
Err error `json:"error,omitempty"`
}
func (r changePasswordResponse) error() error { return r.Err }
func changePasswordEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) {
req := request.(*changePasswordRequest)
err := svc.ChangePassword(ctx, req.OldPassword, req.NewPassword)
return changePasswordResponse{Err: err}, nil
}
func (svc *Service) ChangePassword(ctx context.Context, oldPass, newPass string) error {
vc, ok := viewer.FromContext(ctx)
if !ok {
return fleet.ErrNoContext
}
if err := svc.authz.Authorize(ctx, vc.User, fleet.ActionChangePassword); err != nil {
return err
}
if oldPass == "" {
return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("old_password", "Old password cannot be empty"))
}
if newPass == "" {
return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("new_password", "New password cannot be empty"))
}
if err := fleet.ValidatePasswordRequirements(newPass); err != nil {
return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("new_password", err.Error()))
}
if vc.User.SSOEnabled {
return ctxerr.New(ctx, "change password for single sign on user not allowed")
}
if err := vc.User.ValidatePassword(newPass); err == nil {
return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("new_password", "Cannot reuse old password"))
}
if err := vc.User.ValidatePassword(oldPass); err != nil {
return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("old_password", "old password does not match"))
}
if err := svc.setNewPassword(ctx, vc.User, newPass); err != nil {
return ctxerr.Wrap(ctx, err, "setting new password")
}
return nil
}
////////////////////////////////////////////////////////////////////////////////
// Get Info About Sessions For User
////////////////////////////////////////////////////////////////////////////////
type getInfoAboutSessionsForUserRequest struct {
ID uint `url:"id"`
}
type getInfoAboutSessionsForUserResponse struct {
Sessions []getInfoAboutSessionResponse `json:"sessions"`
Err error `json:"error,omitempty"`
}
func (r getInfoAboutSessionsForUserResponse) error() error { return r.Err }
func getInfoAboutSessionsForUserEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) {
req := request.(*getInfoAboutSessionsForUserRequest)
sessions, err := svc.GetInfoAboutSessionsForUser(ctx, req.ID)
if err != nil {
return getInfoAboutSessionsForUserResponse{Err: err}, nil
}
var resp getInfoAboutSessionsForUserResponse
for _, session := range sessions {
resp.Sessions = append(resp.Sessions, getInfoAboutSessionResponse{
SessionID: session.ID,
UserID: session.UserID,
CreatedAt: session.CreatedAt,
})
}
return resp, nil
}
func (svc *Service) GetInfoAboutSessionsForUser(ctx context.Context, id uint) ([]*fleet.Session, error) {
if err := svc.authz.Authorize(ctx, &fleet.Session{UserID: id}, fleet.ActionRead); err != nil {
return nil, err
}
var validatedSessions []*fleet.Session
sessions, err := svc.ds.ListSessionsForUser(ctx, id)
if err != nil {
return validatedSessions, err
}
for _, session := range sessions {
if svc.validateSession(ctx, session) == nil {
validatedSessions = append(validatedSessions, session)
}
}
return validatedSessions, nil
}
////////////////////////////////////////////////////////////////////////////////
// Delete Sessions For User
////////////////////////////////////////////////////////////////////////////////
type deleteSessionsForUserRequest struct {
ID uint `url:"id"`
}
type deleteSessionsForUserResponse struct {
Err error `json:"error,omitempty"`
}
func (r deleteSessionsForUserResponse) error() error { return r.Err }
func deleteSessionsForUserEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) {
req := request.(*deleteSessionsForUserRequest)
err := svc.DeleteSessionsForUser(ctx, req.ID)
if err != nil {
return deleteSessionsForUserResponse{Err: err}, nil
}
return deleteSessionsForUserResponse{}, nil
}
func (svc *Service) DeleteSessionsForUser(ctx context.Context, id uint) error {
if err := svc.authz.Authorize(ctx, &fleet.Session{UserID: id}, fleet.ActionWrite); err != nil {
return err
}
return svc.ds.DestroyAllSessionsForUser(ctx, id)
}
////////////////////////////////////////////////////////////////////////////////
// Change user email
////////////////////////////////////////////////////////////////////////////////
type changeEmailRequest struct {
Token string `url:"token"`
}
type changeEmailResponse struct {
NewEmail string `json:"new_email"`
Err error `json:"error,omitempty"`
}
func (r changeEmailResponse) error() error { return r.Err }
func changeEmailEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) {
req := request.(*changeEmailRequest)
newEmailAddress, err := svc.ChangeUserEmail(ctx, req.Token)
if err != nil {
return changeEmailResponse{Err: err}, nil
}
return changeEmailResponse{NewEmail: newEmailAddress}, nil
}
func (svc *Service) ChangeUserEmail(ctx context.Context, token string) (string, error) {
vc, ok := viewer.FromContext(ctx)
if !ok {
return "", fleet.ErrNoContext
}
if err := svc.authz.Authorize(ctx, &fleet.User{ID: vc.UserID()}, fleet.ActionWrite); err != nil {
return "", err
}
return svc.ds.ConfirmPendingEmailChange(ctx, vc.UserID(), token)
}
// isAdminOfTheModifiedTeams checks whether the current user is allowed to modify the user
// roles in the teams.
//
// TODO: End-goal is to move all this logic to policy.rego.
func isAdminOfTheModifiedTeams(currentUser *fleet.User, originalUserTeams, newUserTeams []fleet.UserTeam) bool {
// Global admins can modify all user teams roles.
if currentUser.GlobalRole != nil && *currentUser.GlobalRole == fleet.RoleAdmin {
return true
}
// Otherwise, make a map of the original and resulting teams.
newTeams := make(map[uint]string)
for _, team := range newUserTeams {
newTeams[team.ID] = team.Role
}
originalTeams := make(map[uint]struct{})
for _, team := range originalUserTeams {
originalTeams[team.ID] = struct{}{}
}
// See which ones were removed or changed from the original.
teamsAffected := make(map[uint]struct{})
for _, team := range originalUserTeams {
if newTeams[team.ID] != team.Role {
teamsAffected[team.ID] = struct{}{}
}
}
// See which ones of the new are not in the original.
for _, team := range newUserTeams {
if _, ok := originalTeams[team.ID]; !ok {
teamsAffected[team.ID] = struct{}{}
}
}
// Then gather the teams the current user is admin for.
currentUserTeamAdmin := make(map[uint]struct{})
for _, team := range currentUser.Teams {
if team.Role == fleet.RoleAdmin {
currentUserTeamAdmin[team.ID] = struct{}{}
}
}
// And finally, let's check that the teams that were either removed
// or changed are also teams this user is an admin of.
for teamID := range teamsAffected {
if _, ok := currentUserTeamAdmin[teamID]; !ok {
return false
}
}
return true
}
func (svc *Service) modifyEmailAddress(ctx context.Context, user *fleet.User, email string, password *string) error {
// password requirement handled in validation middleware
if password != nil {
err := user.ValidatePassword(*password)
if err != nil {
return fleet.NewPermissionError("incorrect password")
}
}
random, err := server.GenerateRandomText(svc.config.App.TokenKeySize)
if err != nil {
return err
}
token := base64.URLEncoding.EncodeToString([]byte(random))
switch _, err = svc.ds.UserByEmail(ctx, email); {
case err == nil:
return ctxerr.Wrap(ctx, newAlreadyExistsError())
case errors.Is(err, sql.ErrNoRows):
// OK
default:
return ctxerr.Wrap(ctx, err)
}
switch _, err = svc.ds.InviteByEmail(ctx, email); {
case err == nil:
return ctxerr.Wrap(ctx, newAlreadyExistsError())
case errors.Is(err, sql.ErrNoRows):
// OK
default:
return ctxerr.Wrap(ctx, err)
}
err = svc.ds.PendingEmailChange(ctx, user.ID, email, token)
if err != nil {
return err
}
config, err := svc.ds.AppConfig(ctx)
if err != nil {
return err
}
changeEmail := fleet.Email{
Subject: "Confirm Fleet Email Change",
To: []string{email},
Config: config,
Mailer: &mail.ChangeEmailMailer{
Token: token,
BaseURL: template.URL(config.ServerSettings.ServerURL + svc.config.Server.URLPrefix),
AssetURL: getAssetURL(),
},
}
return svc.mailService.SendEmail(changeEmail)
}
// saves user in datastore.
// doesn't need to be exposed to the transport
// the service should expose actions for modifying a user instead
func (svc *Service) saveUser(ctx context.Context, user *fleet.User) error {
return svc.ds.SaveUser(ctx, user)
}
////////////////////////////////////////////////////////////////////////////////
// Perform Required Password Reset
////////////////////////////////////////////////////////////////////////////////
type performRequiredPasswordResetRequest struct {
Password string `json:"new_password"`
ID uint `json:"id"`
}
type performRequiredPasswordResetResponse struct {
User *fleet.User `json:"user,omitempty"`
Err error `json:"error,omitempty"`
}
func (r performRequiredPasswordResetResponse) error() error { return r.Err }
func performRequiredPasswordResetEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) {
req := request.(*performRequiredPasswordResetRequest)
user, err := svc.PerformRequiredPasswordReset(ctx, req.Password)
if err != nil {
return performRequiredPasswordResetResponse{Err: err}, nil
}
return performRequiredPasswordResetResponse{User: user}, nil
}
func (svc *Service) PerformRequiredPasswordReset(ctx context.Context, password string) (*fleet.User, error) {
vc, ok := viewer.FromContext(ctx)
if !ok {
return nil, fleet.ErrNoContext
}
if !vc.CanPerformPasswordReset() {
return nil, fleet.NewPermissionError("cannot reset password")
}
user := vc.User
if err := svc.authz.Authorize(ctx, user, fleet.ActionChangePassword); err != nil {
return nil, err
}
if user.SSOEnabled {
return nil, ctxerr.New(ctx, "password reset for single sign on user not allowed")
}
if !user.IsAdminForcedPasswordReset() {
return nil, ctxerr.New(ctx, "user does not require password reset")
}
// prevent setting the same password
if err := user.ValidatePassword(password); err == nil {
return nil, fleet.NewInvalidArgumentError("new_password", "Cannot reuse old password")
}
if err := fleet.ValidatePasswordRequirements(password); err != nil {
return nil, fleet.NewInvalidArgumentError("new_password", "Password does not meet required criteria")
}
user.AdminForcedPasswordReset = false
err := svc.setNewPassword(ctx, user, password)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "setting new password")
}
// Sessions should already have been cleared when the reset was
// required
return user, nil
}
// 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 *fleet.User, password string) error {
err := user.SetPassword(password, svc.config.Auth.SaltKeySize, svc.config.Auth.BcryptCost)
if err != nil {
return ctxerr.Wrap(ctx, err, "setting new password")
}
if user.SSOEnabled {
return ctxerr.New(ctx, "set password for single sign on user not allowed")
}
err = svc.saveUser(ctx, user)
if err != nil {
return ctxerr.Wrap(ctx, err, "saving changed password")
}
return nil
}
////////////////////////////////////////////////////////////////////////////////
// Reset Password
////////////////////////////////////////////////////////////////////////////////
type resetPasswordRequest struct {
PasswordResetToken string `json:"password_reset_token"`
NewPassword string `json:"new_password"`
}
type resetPasswordResponse struct {
Err error `json:"error,omitempty"`
}
func (r resetPasswordResponse) error() error { return r.Err }
func resetPasswordEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) {
req := request.(*resetPasswordRequest)
err := svc.ResetPassword(ctx, req.PasswordResetToken, req.NewPassword)
return resetPasswordResponse{Err: err}, nil
}
func (svc *Service) ResetPassword(ctx context.Context, token, password string) error {
// skipauth: No viewer context available. The user is locked out of their
// account and authNZ is performed entirely by providing a valid password
// reset token.
svc.authz.SkipAuthorization(ctx)
if token == "" {
return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("token", "Token cannot be empty field"))
}
if password == "" {
return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("new_password", "New password cannot be empty field"))
}
if err := fleet.ValidatePasswordRequirements(password); err != nil {
return ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("new_password", err.Error()))
}
reset, err := svc.ds.FindPasswordResetByToken(ctx, token)
if err != nil {
return ctxerr.Wrap(ctx, fleet.NewAuthFailedError(err.Error()), "find password reset request by token")
}
user, err := svc.ds.UserByID(ctx, reset.UserID)
if err != nil {
return ctxerr.Wrap(ctx, fleet.NewAuthFailedError(err.Error()), "find user by id")
}
if user.SSOEnabled {
return ctxerr.New(ctx, "password reset for single sign on user not allowed")
}
// prevent setting the same password
if err := user.ValidatePassword(password); err == nil {
return fleet.NewInvalidArgumentError("new_password", "Cannot reuse old password")
}
// password requirements are validated as part of `setNewPassword``
err = svc.setNewPassword(ctx, user, password)
if err != nil {
return fleet.NewInvalidArgumentError("new_password", err.Error())
}
// delete password reset tokens for user
if err := svc.ds.DeletePasswordResetRequestsForUser(ctx, user.ID); err != nil {
return ctxerr.Wrap(ctx, err, "delete password reset requests")
}
// Clear sessions so that any other browsers will have to log in with
// the new password
if err := svc.ds.DestroyAllSessionsForUser(ctx, user.ID); err != nil {
return ctxerr.Wrap(ctx, err, "delete user sessions")
}
return nil
}
////////////////////////////////////////////////////////////////////////////////
// Forgot Password
////////////////////////////////////////////////////////////////////////////////
type forgotPasswordRequest struct {
Email string `json:"email"`
}
type forgotPasswordResponse struct {
Err error `json:"error,omitempty"`
}
func (r forgotPasswordResponse) error() error { return r.Err }
func (r forgotPasswordResponse) Status() int { return http.StatusAccepted }
func forgotPasswordEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) {
req := request.(*forgotPasswordRequest)
// Any error returned by the service should not be returned to the
// client to prevent information disclosure (it will be logged in the
// server logs).
_ = svc.RequestPasswordReset(ctx, req.Email)
return forgotPasswordResponse{}, nil
}
func (svc *Service) RequestPasswordReset(ctx context.Context, email string) error {
// skipauth: No viewer context available. The user is locked out of their
// account and trying to reset their password.
svc.authz.SkipAuthorization(ctx)
// 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())
user, err := svc.ds.UserByEmail(ctx, email)
if err != nil {
return err
}
if user.SSOEnabled {
return ctxerr.New(ctx, "password reset for single sign on user not allowed")
}
random, err := server.GenerateRandomText(svc.config.App.TokenKeySize)
if err != nil {
return err
}
token := base64.URLEncoding.EncodeToString([]byte(random))
request := &fleet.PasswordResetRequest{
UserID: user.ID,
Token: token,
}
_, err = svc.ds.NewPasswordResetRequest(ctx, request)
if err != nil {
return err
}
config, err := svc.ds.AppConfig(ctx)
if err != nil {
return err
}
resetEmail := fleet.Email{
Subject: "Reset Your Fleet Password",
To: []string{user.Email},
Config: config,
Mailer: &mail.PasswordResetMailer{
BaseURL: template.URL(config.ServerSettings.ServerURL + svc.config.Server.URLPrefix),
AssetURL: getAssetURL(),
Token: token,
},
}
return svc.mailService.SendEmail(resetEmail)
}
func (svc *Service) ListAvailableTeamsForUser(ctx context.Context, user *fleet.User) ([]*fleet.TeamSummary, error) {
// skipauth: No authorization check needed due to implementation returning
// only license error.
svc.authz.SkipAuthorization(ctx)
return nil, fleet.ErrMissingLicense
}