package service import ( "context" "crypto/sha256" "crypto/subtle" "strings" "github.com/go-errors/errors" "github.com/google/uuid" "github.com/oapi-codegen/nullable" "github.com/phishingclub/phishingclub/data" "github.com/phishingclub/phishingclub/errs" "github.com/phishingclub/phishingclub/model" "github.com/phishingclub/phishingclub/password" "github.com/phishingclub/phishingclub/random" "github.com/phishingclub/phishingclub/repository" "github.com/phishingclub/phishingclub/validate" "github.com/phishingclub/phishingclub/vo" "github.com/pquerna/otp" "github.com/pquerna/otp/totp" "gorm.io/gorm" ) // TOTPValues is TOTP related values type TOTPValues struct { Secret string URL string RecoveryCode string } // User is a service for User type User struct { Common UserRepository *repository.User RoleRepository *repository.Role CompanyRepository *repository.Company PasswordVerifier *password.Argon2Verifier PasswordHasher *password.Argon2Hasher TOTPReplayCache *TOTPReplayCache } // Create creates a new user func (u *User) Create( ctx context.Context, session *model.Session, newUser *model.UserUpsertRequest, ) (*uuid.UUID, error) { ae := NewAuditEvent("User.Create", session) // check permissions isAuthorized, err := IsAuthorized(session, data.PERMISSION_ALLOW_GLOBAL) if err != nil && !errors.Is(err, errs.ErrAuthorizationFailed) { u.LogAuthError(err) return nil, errs.Wrap(err) } if !isAuthorized { u.AuditLogNotAuthorized(ae) return nil, errs.ErrAuthorizationFailed } // check if the username is already taken _, err = u.UserRepository.GetByUsername( ctx, &newUser.Username, &repository.UserOption{}, ) // if there is not record not found error, then thw username is already user if err == nil { u.Logger.Debugw("username is already taken", "username", newUser.Username.String()) return nil, validate.WrapErrorWithField(errors.New("not unique"), "username") } if !errors.Is(err, gorm.ErrRecordNotFound) { u.Logger.Errorw("failed to create user - failed to get user by username", "error", err) return nil, errs.Wrap(err) } // check if the email is already taken _, err = u.UserRepository.GetByEmail( ctx, &newUser.Email, &repository.UserOption{}, ) if err == nil { u.Logger.Debugw("email is already taken", "email", newUser.Email.String()) return nil, validate.WrapErrorWithField(errors.New("not unique"), "email") } if !errors.Is(err, gorm.ErrRecordNotFound) { u.Logger.Errorw("failed to create user - failed to get user by email", "error", err) return nil, errs.Wrap(err) } adminRole, err := u.RoleRepository.GetByName( ctx, data.RoleSuperAdministrator, ) if err != nil { return nil, errs.Wrap(err) } // create user := model.User{ Username: nullable.NewNullableWithValue(newUser.Username), Email: nullable.NewNullableWithValue(newUser.Email), Name: nullable.NewNullableWithValue(newUser.Fullname), RoleID: nullable.NewNullableWithValue(adminRole.ID), } passwdHash, err := u.PasswordHasher.Hash(newUser.Password.String()) if err != nil { u.Logger.Errorw("failed to create user - failed to hash password", "error", err) return nil, errs.Wrap(err) } // validate if err := user.Validate(); err != nil { u.Logger.Debugw("failed to create user - failed to validate user", "error", err) return nil, errs.Wrap(err) } // save the user id, err := u.UserRepository.Insert( ctx, &user, passwdHash, "", ) if err != nil { u.Logger.Errorw("failed to create user - failed to save user", "error", err) return nil, errs.Wrap(err) } ae.Details["id"] = id.String() u.AuditLogAuthorized(ae) return id, nil } // CreateFromSSO resolves an existing user for an SSO login. // Users are never auto provisioned, an administrator must create the user // first, so an unknown identity is rejected. Matching is done on the email // reported by the identity provider, which in a managed directory is an admin // controlled attribute the user cannot change to impersonate someone else. // Email is also portable across providers, so switching SSO method does not // lock accounts out. func (u *User) CreateFromSSO( ctx context.Context, name string, email string, externalID string, ) (*uuid.UUID, error) { ae := NewAuditEvent("User.SSOLogin", nil) ae.Details["email"] = email emailVO, err := vo.NewEmail(email) if err != nil { u.Logger.Debugw("failed SSO login: invalid email", "error", err) return nil, errs.Wrap(err) } existingUser, err := u.UserRepository.GetByEmail( ctx, emailVO, &repository.UserOption{}, ) if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { u.Logger.Debugw("failed SSO login: DB error", "error", err) return nil, errs.Wrap(err) } // users are not auto provisioned, an admin must create the user first if existingUser == nil { u.Logger.Infow("rejected SSO login for unprovisioned user", "email", email) u.AuditLogNotAuthorized(ae) return nil, errs.Wrap(errs.ErrSSOUserNotProvisioned) } uid := existingUser.ID.MustGet() // store the current provider subject id for reference, overwriting any // previous value so a change of SSO method does not lock the account out. // the password is left intact, whether password login is allowed is // governed by the exclusive SSO mode, not by destroying the password here if err := u.UserRepository.SetSSOID(ctx, &uid, externalID); err != nil { u.Logger.Errorw("failed to set SSO id on user", "error", err) return nil, errs.Wrap(err) } u.AuditLogAuthorized(ae) return &uid, nil } // GetMaskedAPIKey gets a masked API user key func (u *User) GetMaskedAPIKey( ctx context.Context, session *model.Session, userID *uuid.UUID, ) (string, error) { ae := NewAuditEvent("User.GetMaskedAPIKey", session) ae.Details["userId"] = userID.String() // check permissions isAuthorized, err := IsAuthorized(session, data.PERMISSION_ALLOW_GLOBAL) if err != nil && !errors.Is(err, errs.ErrAuthorizationFailed) { u.LogAuthError(err) return "", errs.Wrap(err) } if !isAuthorized { u.AuditLogNotAuthorized(ae) return "", errs.ErrAuthorizationFailed } // get api key apiKey, err := u.UserRepository.GetAPIKey(ctx, userID) if err != nil { u.Logger.Errorw("failed to get api key", "error", err) return "", errs.Wrap(err) } masked := "" if len(apiKey) > 4 { masked = apiKey[0:4] + strings.Repeat("*", 28) } // no audit on read return masked, nil } // GetAllAPIKeys gets all api keys as SHA256 // THIS METHOD DOES NOT HAVE AUTH, USE WITH DISCRETION func (s *User) GetAllAPIKeysSHA256( ctx context.Context, ) ([]*model.APIUser, error) { apiUsers := []*model.APIUser{} // get api key apiKeyAndIDMap, err := s.UserRepository.GetAllAPIKeys(ctx) for apiKey, userID := range apiKeyAndIDMap { hash := sha256.Sum256([]byte(apiKey)) apiUsers = append( apiUsers, &model.APIUser{ ID: userID, APIKeyHash: hash, }, ) } if err != nil { s.Logger.Errorw("failed to get all api keys", "error", err) return apiUsers, errs.Wrap(err) } return apiUsers, nil } // UpsertAPIKey creates/updates a user API key func (s *User) UpsertAPIKey( ctx context.Context, session *model.Session, userID *uuid.UUID, ) (string, error) { ae := NewAuditEvent("User.UpsertAPIKey", session) ae.Details["userId"] = userID.String() // check permissions isAuthorized, err := IsAuthorized(session, data.PERMISSION_ALLOW_GLOBAL) if err != nil && !errors.Is(err, errs.ErrAuthorizationFailed) { s.LogAuthError(err) return "", errs.Wrap(err) } if !isAuthorized { s.AuditLogNotAuthorized(ae) return "", errs.ErrAuthorizationFailed } key, err := random.GenerateRandomURLBase64Encoded(64) if err != nil { s.Logger.Errorw("failed to create api key - bad crypto", "error", err) return "", errs.Wrap(err) } // upsert api key err = s.UserRepository.UpsertAPIKey( ctx, userID, key, ) if err != nil { s.Logger.Errorw("failed set api key", "error", err) return "", errs.Wrap(err) } s.AuditLogAuthorized(ae) return key, nil } // RemoveAPIKey removes a users api key func (u *User) RemoveAPIKey( ctx context.Context, session *model.Session, userID *uuid.UUID, ) error { ae := NewAuditEvent("User.RemoveAPIKey", session) ae.Details["userId"] = userID.String() // check permissions isAuthorized, err := IsAuthorized(session, data.PERMISSION_ALLOW_GLOBAL) if err != nil && !errors.Is(err, errs.ErrAuthorizationFailed) { u.LogAuthError(err) return err } if !isAuthorized { u.AuditLogNotAuthorized(ae) return errs.ErrAuthorizationFailed } err = u.UserRepository.RemoveAPIKey(ctx, userID) if err != nil { u.Logger.Errorw("failed to remove api key", "error", err) return err } u.AuditLogAuthorized(ae) return nil } // UpdateByID updates a user by ID // values to update are email, username and fullname func (u *User) Update( ctx context.Context, session *model.Session, userID *uuid.UUID, incoming *model.User, ) error { ae := NewAuditEvent("User.Update", session) ae.Details["id"] = userID.String() // check permissions isAuthorized, err := IsAuthorized(session, data.PERMISSION_ALLOW_GLOBAL) if err != nil && !errors.Is(err, errs.ErrAuthorizationFailed) { u.LogAuthError(err) return err } if !isAuthorized { u.AuditLogNotAuthorized(ae) return errs.ErrAuthorizationFailed } // get user to be updated current, err := u.UserRepository.GetByID(ctx, userID, &repository.UserOption{}) if err != nil { u.Logger.Errorw("failed to update user - failed to get user by id", "error", err) return err } // check if the username is already taken if username, err := incoming.Username.Get(); err == nil { _, err = u.UserRepository.GetByUsername( ctx, &username, &repository.UserOption{}, ) if err == nil && current.Username.MustGet().String() != username.String() { u.Logger.Debugw("username is already taken", "username", username.String()) return validate.WrapErrorWithField(errors.New("not unique"), "username") } if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { u.Logger.Errorw("failed to update user - failed to get user by username", "error", err) return err } } // check if the is already taken if email, err := incoming.Email.Get(); err == nil { // check if the email is already taken _, err = u.UserRepository.GetByEmail( ctx, &email, &repository.UserOption{}, ) if err == nil && current.Email.MustGet().String() != incoming.Email.MustGet().String() { u.Logger.Debugw("email is already taken", "email", email.String()) return validate.WrapErrorWithField(errors.New("not unique"), "email") } if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { u.Logger.Errorw("failed to update user - failed to get user by email", "error", err) return err } } if v, err := incoming.Email.Get(); err == nil { current.Email.Set(v) } if v, err := incoming.Username.Get(); err == nil { current.Username.Set(v) } if v, err := incoming.Name.Get(); err == nil { current.Name.Set(v) } // validate if err := current.Validate(); err != nil { u.Logger.Debugw("failed to update user - failed to validate user", "error", err) return err } // update the user err = u.UserRepository.UpdateByID( ctx, userID, current, ) if err != nil { u.Logger.Errorw("failed to update user - failed to update user", "error", err) return err } u.AuditLogAuthorized(ae) return nil } // GetAll gets all users func (u *User) GetAll( ctx context.Context, session *model.Session, options *repository.UserOption, ) (*model.Result[model.User], error) { result := model.NewEmptyResult[model.User]() ae := NewAuditEvent("User.GetAll", session) // check permissions isAuthorized, err := IsAuthorized(session, data.PERMISSION_ALLOW_GLOBAL) if err != nil && !errors.Is(err, errs.ErrAuthorizationFailed) { u.LogAuthError(err) return result, errs.Wrap(err) } if !isAuthorized { u.AuditLogNotAuthorized(ae) return result, errs.ErrAuthorizationFailed } result, err = u.UserRepository.GetAll(ctx, options) if err != nil { u.Logger.Errorw("failed to get all users - failed to get all users", "error", err) return result, errs.Wrap(err) } // no audit on read return result, nil } // GetByID gets a user by ID func (s *User) GetByID( ctx context.Context, session *model.Session, userID *uuid.UUID, ) (*model.User, error) { ae := NewAuditEvent("User.GetByID", session) ae.Details["id"] = userID.String() // check permissions isAuthorized, err := IsAuthorized(session, data.PERMISSION_ALLOW_GLOBAL) if err != nil && !errors.Is(err, errs.ErrAuthorizationFailed) { s.LogAuthError(err) return nil, errs.Wrap(err) } if !isAuthorized { s.AuditLogNotAuthorized(ae) return nil, errs.ErrAuthorizationFailed } user, err := s.UserRepository.GetByID( ctx, userID, &repository.UserOption{ WithRole: true, WithCompany: true, }, ) if err != nil { s.Logger.Errorw("failed to get user by id - failed to get user by id", "error", err) return nil, errs.Wrap(err) } // no audit on read return user, nil } // GetByIDWithoutAuth gets a user by ID without requiring auth func (s *User) GetByIDWithoutAuth( ctx context.Context, userID *uuid.UUID, ) (*model.User, error) { user, err := s.UserRepository.GetByID( ctx, userID, &repository.UserOption{ WithRole: true, WithCompany: true, }, ) if err != nil { s.Logger.Errorw("failed to get user by id - failed to get user by id", "error", err) return nil, errs.Wrap(err) } // no audit on read return user, nil } // Delete deletes a user func (u *User) Delete( ctx context.Context, session *model.Session, userID *uuid.UUID, ) error { ae := NewAuditEvent("User.Delete", session) ae.Details["id"] = userID.String() // check permissions isAuthorized, err := IsAuthorized(session, data.PERMISSION_ALLOW_GLOBAL) if err != nil && !errors.Is(err, errs.ErrAuthorizationFailed) { u.LogAuthError(err) return err } if !isAuthorized { u.AuditLogNotAuthorized(ae) return errs.ErrAuthorizationFailed } if session.User.ID.MustGet().String() == userID.String() { u.Logger.Debugw("Attempted to delete own user", "userID", userID.String()) return errs.NewValidationError( errors.New("Can not delete own user"), ) } // delete the user err = u.UserRepository.DeleteByID( ctx, userID, ) if err != nil { u.Logger.Errorw("failed to delete user - failed to delete user", "error", err) return err } u.AuditLogAuthorized(ae) return nil } // SetupTOTP sets up TOTP for a user // returns secret, url and error func (u *User) SetupTOTP( ctx context.Context, session *model.Session, password *vo.ReasonableLengthPassword, ) (*TOTPValues, error) { ae := NewAuditEvent("User.SetupTOTP", session) // check permissions isAuthorized, err := IsAuthorized(session, data.PERMISSION_ALLOW_GLOBAL) if err != nil && !errors.Is(err, errs.ErrAuthorizationFailed) { u.LogAuthError(err) return nil, errs.Wrap(err) } if !isAuthorized { u.AuditLogNotAuthorized(ae) return nil, errs.ErrAuthorizationFailed } // check if the user is loaded in the session user := session.User if user == nil { u.Logger.Error("user is not loaded in session") return nil, errors.New("user is not loaded in session") } // check password username := user.Username.MustGet() hasValidPassword, err := u.CheckPassword( ctx, &username, password, ) if err != nil || !hasValidPassword { return nil, errs.ErrAuthenticationFailed } // generate OTP email := user.Email.MustGet() key, err := totp.Generate(totp.GenerateOpts{ Issuer: "Phishing Club", AccountName: email.String(), Algorithm: otp.AlgorithmSHA1, }) if err != nil { u.Logger.Errorw("failed to setup TOTP - failed to generate key", "error", err) return nil, errs.Wrap(err) } rc, err := random.GenerateRandomURLBase64Encoded(24) if err != nil { u.Logger.Errorw("failed to setup TOTP - failed to generate recovery code", "error", err) return nil, errs.Wrap(err) } // update user userID := user.ID.MustGet() err = u.UserRepository.SetupTOTP( ctx, &userID, key.Secret(), rc, key.URL(), ) if err != nil { u.Logger.Errorw("failed to setup TOTP - failed to update user", "error", err) return nil, errs.Wrap(err) } u.AuditLogAuthorized(ae) // audit log return &TOTPValues{ Secret: key.Secret(), URL: key.URL(), RecoveryCode: rc, }, nil } // SetupCheckTOTP verifies a TOTP setup func (u *User) SetupCheckTOTP( ctx context.Context, session *model.Session, token *vo.String64, ) error { ae := NewAuditEvent("User.SetupCheckTOTP", session) // check permissions isAuthorized, err := IsAuthorized(session, data.PERMISSION_ALLOW_GLOBAL) if err != nil && !errors.Is(err, errs.ErrAuthorizationFailed) { u.LogAuthError(err) return err } if !isAuthorized { u.AuditLogNotAuthorized(ae) return errs.ErrAuthorizationFailed } // get user from session user := session.User if user == nil { u.Logger.Error("user is not loaded in session") return errors.New("user is not loaded in session") } // check if the token is valid // get the secret userID := user.ID.MustGet() secret, _, err := u.UserRepository.GetTOTP( ctx, &userID, ) if err != nil { u.Logger.Errorw("failed to verify TOTP - failed to get TOTP", "error", err) return err } // verify the token u.Logger.Debug("verifying TOTP") if u.TOTPReplayCache.isUsed(userID.String(), token.String()) { u.Logger.Debug("failed to verify TOTP - token already used") return errs.ErrUserWrongTOTP } valid := totp.Validate(token.String(), secret) if !valid { u.Logger.Debug("failed to verify TOTP - invalid token") return errs.ErrUserWrongTOTP } u.TOTPReplayCache.markUsed(userID.String(), token.String()) u.Logger.Debugw("Enabling MFA TOTP for user", "userID", userID) // enable TOTP err = u.UserRepository.EnableTOTP( ctx, &userID, ) if err != nil { u.Logger.Errorw("failed to verify TOTP - failed to enable TOTP", "error", err) return err } u.AuditLogAuthorized(ae) return nil } // IsTOTPEnabled checks if TOTP is enabled func (u *User) IsTOTPEnabled( ctx context.Context, session *model.Session, ) (bool, error) { ae := NewAuditEvent("User.IsTOTPEnabled", session) // check permissions isAuthorized, err := IsAuthorized(session, data.PERMISSION_ALLOW_GLOBAL) if err != nil && !errors.Is(err, errs.ErrAuthorizationFailed) { u.LogAuthError(err) return false, errs.Wrap(err) } if !isAuthorized { u.AuditLogNotAuthorized(ae) return false, errs.ErrAuthorizationFailed } // get user from session user := session.User if user == nil { u.Logger.Error("user is not loaded in session") return false, errors.New("user is not loaded in session") } // check if TOTP is enabled userID := user.ID.MustGet() enabled, err := u.UserRepository.IsTOTPEnabled( ctx, &userID, ) if err != nil { u.Logger.Errorw("failed to check if TOTP is enabled", "error", err) return false, errs.Wrap(err) } // no audit on read return enabled, nil } // IsTOTPEnabledByUserID checks if TOTP is enabled by user ID // this method had no auth check, use with consideration func (u *User) IsTOTPEnabledByUserID( ctx context.Context, userID *uuid.UUID, ) (bool, error) { // check if TOTP is enabled enabled, err := u.UserRepository.IsTOTPEnabled( ctx, userID, ) if err != nil { u.Logger.Errorw("failed to check if TOTP is enabled", "error", err) return false, errs.Wrap(err) } return enabled, nil } // DisableTOTP disables TOTP // without checking if the user privilige, use with consideration // session may be nil when called during a pre session flow such as // login with a recovery code func (u *User) DisableTOTP( ctx context.Context, session *model.Session, userID *uuid.UUID, ) error { ae := NewAuditEvent("User.DisableTOTP", session) ae.Details["userId"] = userID.String() err := u.UserRepository.RemoveTOTP( ctx, userID, ) if err != nil { u.Logger.Errorw("failed to disable TOTP", "error", err) return err } u.AuditLogAuthorized(ae) return nil } // CheckTOTP verifies a TOTP token func (u *User) CheckTOTP( ctx context.Context, userID *uuid.UUID, token *vo.String64, ) error { if u.TOTPReplayCache.isUsed(userID.String(), token.String()) { u.Logger.Debug("failed to verify TOTP - token already used") return errs.ErrUserWrongTOTP } // get the secret secret, _, err := u.UserRepository.GetTOTP( ctx, userID, ) if err != nil { u.Logger.Errorw("failed to verify TOTP - failed to get TOTP", "error", err) return err } // verify the token valid := totp.Validate(token.String(), secret) if !valid { u.Logger.Debug("failed to verify TOTP - invalid token") return errs.ErrUserWrongTOTP } u.TOTPReplayCache.markUsed(userID.String(), token.String()) return nil } // AuthenticateUsernameWithPassword tests a username and password is correct func (u *User) AuthenticateUsernameWithPassword( ctx context.Context, username string, passwd string, ip string, ) (*model.User, error) { ae := NewAuditEvent("User.AuthenticateUsernameWithPassword", nil) ae.IP = ip ae.Details["username"] = username // check the entities are valid before doing anything usernameEntity, err := vo.NewUsername(username) if err != nil { u.Logger.Debugw("failed to authenticate - invalid username", "error", err) return nil, errs.Wrap(err) } passwordEntity, err := vo.NewReasonableLengthPassword(passwd) if err != nil { u.Logger.Debugw("failed to authenticate - invalid password", "error", err) return nil, errs.Wrap(err) } // retrieve only the password hash to minimize the timing attack window compared to // pulling the user with all relations passwordHash, err := u.UserRepository.GetPasswordHashByUsername( ctx, usernameEntity, ) errIsRecordNotFound := errors.Is(err, gorm.ErrRecordNotFound) if err != nil && !errIsRecordNotFound { u.Logger.Errorw("failed to authenticate - failed to get password hash", "error", err) return nil, errs.Wrap(err) } // if user not found - we compare with a fake fakePass hash to mitigate risk of timing attacks // or if the user is a SSO user if errIsRecordNotFound || len(passwordHash) == 0 { _, err = u.PasswordVerifier.Verify(passwd, password.DummyHash) if err != nil { u.Logger.Debugw("failed to verify dummy hash", "error", err) } return nil, gorm.ErrRecordNotFound } // veriy the hash in a constant time manner verified, err := u.PasswordVerifier.Verify(passwordEntity.String(), passwordHash) if err != nil { u.Logger.Errorw("failed to verify password hash", "error", err) return nil, errs.Wrap(err) } // if the password is not verifed, log it and return the error if !verified { u.AuditLogNotAuthorized(ae) return nil, errs.ErrUserWrongPasword } // on successful login, retrieve the user with relations and send it back user, err := u.UserRepository.GetByUsername( ctx, usernameEntity, &repository.UserOption{ WithRole: true, WithCompany: true, }, ) if err != nil { u.Logger.Errorw("failed to get user by username after verifying login", "error", err) return nil, errs.Wrap(err) } u.AuditLogAuthorized(ae) return user, nil } // CheckPassword checks if a password is correct func (u *User) CheckPassword( ctx context.Context, username *vo.Username, password *vo.ReasonableLengthPassword, ) (bool, error) { passwordHash, err := u.UserRepository.GetPasswordHashByUsername( ctx, username, ) if err != nil { u.Logger.Errorw("failed to check password - failed to get password hash", "error", err) return false, errs.Wrap(err) } verified, err := u.PasswordVerifier.Verify(password.String(), passwordHash) if err != nil { u.Logger.Errorw("failed to check password - failed to verify hash", "error", err) return false, errs.Wrap(err) } return verified, nil } // ChangePassword changes a user's password func (u *User) ChangePassword( ctx context.Context, session *model.Session, currentPassword *vo.ReasonableLengthPassword, newPassword *vo.ReasonableLengthPassword, ) error { ae := NewAuditEvent("User.ChangePassword", session) // check if the current password is correct user := session.User if user == nil { u.Logger.Error("user is not loaded in session") return errors.New("user is not loaded in session") } username := user.Username.MustGet() ae.Details["id"] = user.ID.MustGet().String() ae.Details["username"] = username.String() passwordHash, err := u.UserRepository.GetPasswordHashByUsername( ctx, &username, ) if err != nil { u.Logger.Errorw("failed to change password - failed to get password hash", "error", err) return err } verified, err := u.PasswordVerifier.Verify(currentPassword.String(), passwordHash) if err != nil { u.Logger.Errorw("failed to change password - failed to verify hash", "error", err) return err } if !verified { u.AuditLogNotAuthorized(ae) return errs.ErrUserWrongPasword } // change the password passwordHash, err = u.PasswordHasher.Hash(newPassword.String()) if err != nil { u.Logger.Errorw("failed to change password - failed to hash new password", "error", err) return err } err = u.UserRepository.UpdatePasswordHashByUsername( ctx, &username, passwordHash, ) if err != nil { u.Logger.Errorw("failed to change password - failed to update password hash", "error", err) return err } u.AuditLogAuthorized(ae) return nil } // ChangeFullname changes a users fullname func (u *User) ChangeFullname( ctx context.Context, session *model.Session, userID *uuid.UUID, newFullname *vo.UserFullname, ) (*vo.UserFullname, error) { ae := NewAuditEvent("User.ChangeFullname", session) ae.Details["id"] = userID.String() // check permissions isAuthorized, err := IsAuthorized(session, data.PERMISSION_ALLOW_GLOBAL) if err != nil && !errors.Is(err, errs.ErrAuthorizationFailed) { u.LogAuthError(err) return nil, errs.Wrap(err) } if !isAuthorized { u.AuditLogNotAuthorized(ae) return nil, errs.ErrAuthorizationFailed } // change full name _, err = u.UserRepository.GetByID(ctx, userID, &repository.UserOption{}) if errors.Is(err, gorm.ErrRecordNotFound) { u.Logger.Debugw("failed to change fullname - user not found", "error", err) return nil, errs.Wrap(err) } err = u.UserRepository.UpdateFullNameByID( ctx, userID, newFullname, ) if err != nil { u.Logger.Errorw("failed to change fullname - failed to update fullname", "error", err) return nil, errs.Wrap(err) } u.AuditLogAuthorized(ae) return newFullname, nil } // ChangeEmailAsAdministrator changes a user's email // changes a users email without validating their email func (u *User) ChangeEmailAsAdministrator( ctx context.Context, session *model.Session, userID *uuid.UUID, newEmail *vo.Email, ) (*vo.Email, error) { ae := NewAuditEvent("User.ChangeEmailAsAdministrator", session) ae.Details["id"] = userID.String() // check permissions isAuthorized, err := IsAuthorized(session, data.PERMISSION_ALLOW_GLOBAL) if err != nil && !errors.Is(err, errs.ErrAuthorizationFailed) { u.LogAuthError(err) return nil, errs.Wrap(err) } if !isAuthorized { u.AuditLogNotAuthorized(ae) return nil, errs.ErrAuthorizationFailed } current, err := u.UserRepository.GetByID(ctx, userID, &repository.UserOption{}) if errors.Is(err, gorm.ErrRecordNotFound) { u.Logger.Debug("failed to change email - user not found") return nil, errs.Wrap(err) } if err != nil { u.Logger.Debugw("failed to change email - failed to get user by id", "error", err) return nil, errs.Wrap(err) } // update current.Email.Set(*newEmail) err = u.UserRepository.UpdateByID( ctx, userID, current, ) if err != nil { u.Logger.Errorw("failed to change email - failed to update email", "error", err) return nil, errs.Wrap(err) } u.AuditLogAuthorized(ae) return newEmail, nil } // ChangeUsername changes a user's username func (u *User) ChangeUsername( ctx context.Context, session *model.Session, userID *uuid.UUID, newUsername *vo.Username, ) error { ae := NewAuditEvent("User.ChangeUsername", session) ae.Details["id"] = userID.String() // check permissions isAuthorized, err := IsAuthorized(session, data.PERMISSION_ALLOW_GLOBAL) if err != nil && !errors.Is(err, errs.ErrAuthorizationFailed) { u.LogAuthError(err) return err } if !isAuthorized { u.AuditLogNotAuthorized(ae) return errs.ErrAuthorizationFailed } current, err := u.UserRepository.GetByID(ctx, userID, &repository.UserOption{}) if err != nil { u.Logger.Debugw("failed to change username - failed to get user by id", "error", err) return err } current.Username.Set(*newUsername) err = u.UserRepository.UpdateByID( ctx, userID, current, ) if err != nil { u.Logger.Errorw("failed to change username - failed to update username", "error", err) return err } u.AuditLogAuthorized(ae) return nil } // CheckMFARecoveryCode checks if a recovery code is valid // returns true if the recovery code is valid func (u *User) CheckMFARecoveryCode( ctx context.Context, userID *uuid.UUID, recoveryCode *vo.String64, ) (bool, error) { dbRecoveryCode, err := u.UserRepository.GetMFARecoveryCode( ctx, userID, ) if err != nil { u.Logger.Errorw("failed to get recovery code", "error", err) return false, errs.Wrap(err) } if subtle.ConstantTimeCompare([]byte(recoveryCode.String()), []byte(dbRecoveryCode)) != 1 { u.Logger.Info("invalid recovery code") return false, errs.ErrUserWrongRecoveryCode } return true, nil }