diff --git a/app/app_iface.go b/app/app_iface.go index 14eb05c897..9bafa8b839 100644 --- a/app/app_iface.go +++ b/app/app_iface.go @@ -230,8 +230,6 @@ type AppIface interface { InstallPlugin(pluginFile io.ReadSeeker, replace bool) (*model.Manifest, *model.AppError) // InstallPluginWithSignature verifies and installs plugin. InstallPluginWithSignature(pluginFile, signature io.ReadSeeker) (*model.Manifest, *model.AppError) - // IsUsernameTaken checks if the username is already used by another user. Return false if the username is invalid. - IsUsernameTaken(name string) bool // LimitedClientConfigWithComputed gets the configuration in a format suitable for sending to the client. LimitedClientConfigWithComputed() map[string]string // LogAuditRec logs an audit record using default LvlAuditCLI. @@ -326,6 +324,8 @@ type AppIface interface { // the member's group memberships and the configuration of those groups to the syncable. This method should only // be invoked on group-synced (aka group-constrained) syncables. SyncSyncableRoles(syncableID string, syncableType model.GroupSyncableType) *model.AppError + // TODO: migrate this after the user service implementation is completed + GetSanitizeOptions(asAdmin bool) map[string]bool // TeamMembersMinusGroupMembers returns the set of users on the given team minus the set of users in the given // groups. // @@ -710,7 +710,6 @@ type AppIface interface { GetSamlCertificateStatus() *model.SamlCertificateStatus GetSamlMetadata() (string, *model.AppError) GetSamlMetadataFromIdp(idpMetadataUrl string) (*model.SamlMetadataResponse, *model.AppError) - GetSanitizeOptions(asAdmin bool) map[string]bool GetScheme(id string) (*model.Scheme, *model.AppError) GetSchemeByName(name string) (*model.Scheme, *model.AppError) GetSchemeRolesForTeam(teamID string) (string, string, string, *model.AppError) diff --git a/app/authentication.go b/app/authentication.go index 98f1bf9500..b52e47084d 100644 --- a/app/authentication.go +++ b/app/authentication.go @@ -4,13 +4,14 @@ package app import ( + "errors" "net/http" "strings" "github.com/mattermost/mattermost-server/v5/app/request" "github.com/mattermost/mattermost-server/v5/model" + "github.com/mattermost/mattermost-server/v5/services/users" "github.com/mattermost/mattermost-server/v5/shared/mfa" - "github.com/mattermost/mattermost-server/v5/utils" ) type TokenLocation int @@ -49,7 +50,17 @@ func (a *App) IsPasswordValid(password string) *model.AppError { return nil } - return utils.IsPasswordValidWithSettings(password, &a.Config().PasswordSettings) + if err := users.IsPasswordValidWithSettings(password, &a.Config().PasswordSettings); err != nil { + var invErr *users.ErrInvalidPassword + switch { + case errors.As(err, &invErr): + return model.NewAppError("User.IsValid", invErr.Id(), map[string]interface{}{"Min": *a.Config().PasswordSettings.MinimumLength}, "", http.StatusBadRequest) + default: + return model.NewAppError("User.IsValid", "app.valid_password_generic.app_error", nil, err.Error(), http.StatusInternalServerError) + } + } + + return nil } func (a *App) CheckPasswordAndAllCriteria(user *model.User, password string, mfaToken string) *model.AppError { @@ -57,14 +68,20 @@ func (a *App) CheckPasswordAndAllCriteria(user *model.User, password string, mfa return err } - if err := a.checkUserPassword(user, password); err != nil { + if err := users.CheckUserPassword(user, password); err != nil { if passErr := a.Srv().Store.User().UpdateFailedPasswordAttempts(user.Id, user.FailedAttempts+1); passErr != nil { return model.NewAppError("CheckPasswordAndAllCriteria", "app.user.update_failed_pwd_attempts.app_error", nil, passErr.Error(), http.StatusInternalServerError) } a.InvalidateCacheForUser(user.Id) - return err + var invErr *users.ErrInvalidPassword + switch { + case errors.As(err, &invErr): + return model.NewAppError("checkUserPassword", "api.user.check_user_password.invalid.app_error", nil, "user_id="+user.Id, http.StatusUnauthorized) + default: + return model.NewAppError("checkUserPassword", "app.valid_password_generic.app_error", nil, err.Error(), http.StatusInternalServerError) + } } if err := a.CheckUserMfa(user, mfaToken); err != nil { @@ -100,14 +117,20 @@ func (a *App) DoubleCheckPassword(user *model.User, password string) *model.AppE return err } - if err := a.checkUserPassword(user, password); err != nil { + if err := users.CheckUserPassword(user, password); err != nil { if passErr := a.Srv().Store.User().UpdateFailedPasswordAttempts(user.Id, user.FailedAttempts+1); passErr != nil { return model.NewAppError("DoubleCheckPassword", "app.user.update_failed_pwd_attempts.app_error", nil, passErr.Error(), http.StatusInternalServerError) } a.InvalidateCacheForUser(user.Id) - return err + var invErr *users.ErrInvalidPassword + switch { + case errors.As(err, &invErr): + return model.NewAppError("DoubleCheckPassword", "api.user.check_user_password.invalid.app_error", nil, "user_id="+user.Id, http.StatusUnauthorized) + default: + return model.NewAppError("DoubleCheckPassword", "app.valid_password_generic.app_error", nil, err.Error(), http.StatusInternalServerError) + } } if passErr := a.Srv().Store.User().UpdateFailedPasswordAttempts(user.Id, 0); passErr != nil { @@ -119,14 +142,6 @@ func (a *App) DoubleCheckPassword(user *model.User, password string) *model.AppE return nil } -func (a *App) checkUserPassword(user *model.User, password string) *model.AppError { - if !model.ComparePassword(user.Password, password) { - return model.NewAppError("checkUserPassword", "api.user.check_user_password.invalid.app_error", nil, "user_id="+user.Id, http.StatusUnauthorized) - } - - return nil -} - func (a *App) checkLdapUserPasswordAndAllCriteria(c *request.Context, ldapId *string, password string, mfaToken string) (*model.User, *model.AppError) { if a.Ldap() == nil || ldapId == nil { err := model.NewAppError("doLdapAuthentication", "api.user.login_ldap.not_available.app_error", nil, "", http.StatusNotImplemented) diff --git a/app/channel_test.go b/app/channel_test.go index 6cd0107bfd..58f4ae8e1e 100644 --- a/app/channel_test.go +++ b/app/channel_test.go @@ -17,6 +17,7 @@ import ( "github.com/stretchr/testify/require" "github.com/mattermost/mattermost-server/v5/model" + "github.com/mattermost/mattermost-server/v5/services/users" "github.com/mattermost/mattermost-server/v5/store/storetest/mocks" ) @@ -1967,9 +1968,9 @@ func TestMarkChannelsAsViewedPanic(t *testing.T) { "userID": 1, } mockChannelStore.On("UpdateLastViewedAt", []string{"channelID"}, "userID", false).Return(times, nil) + th.App.srv.userService = users.New(&mockUserStore, th.App.srv.Config) mockPreferenceStore := mocks.PreferenceStore{} mockPreferenceStore.On("Get", mock.AnythingOfType("string"), mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(&model.Preference{Value: "test"}, nil) - mockStore.On("User").Return(&mockUserStore) mockStore.On("Channel").Return(&mockChannelStore) mockStore.On("Preference").Return(&mockPreferenceStore) diff --git a/app/import_functions.go b/app/import_functions.go index bcf5764572..0e679d3d7b 100644 --- a/app/import_functions.go +++ b/app/import_functions.go @@ -17,6 +17,7 @@ import ( "github.com/mattermost/mattermost-server/v5/app/request" "github.com/mattermost/mattermost-server/v5/model" + "github.com/mattermost/mattermost-server/v5/services/users" "github.com/mattermost/mattermost-server/v5/shared/mlog" "github.com/mattermost/mattermost-server/v5/store" "github.com/mattermost/mattermost-server/v5/utils" @@ -481,30 +482,57 @@ func (a *App) importUser(data *UserImportData, dryRun bool) *model.AppError { } var savedUser *model.User - var err *model.AppError + var err error if user.Id == "" { - if savedUser, err = a.createUser(user); err != nil { - return err + if savedUser, err = a.srv.userService.CreateUser(user, users.UserCreateOptions{FromImport: true}); err != nil { + var appErr *model.AppError + var invErr *store.ErrInvalidInput + switch { + case errors.As(err, &appErr): + return appErr + case errors.Is(err, users.AcceptedDomainError): + return model.NewAppError("importUser", "api.user.create_user.accepted_domain.app_error", nil, "", http.StatusBadRequest) + case errors.Is(err, users.UserCountError): + return model.NewAppError("importUser", "app.user.get_total_users_count.app_error", nil, nErr.Error(), http.StatusInternalServerError) + case errors.As(err, &invErr): + switch invErr.Field { + case "email": + return model.NewAppError("importUser", "app.user.save.email_exists.app_error", nil, invErr.Error(), http.StatusBadRequest) + case "username": + return model.NewAppError("importUser", "app.user.save.username_exists.app_error", nil, invErr.Error(), http.StatusBadRequest) + default: + return model.NewAppError("importUser", "app.user.save.existing.app_error", nil, invErr.Error(), http.StatusBadRequest) + } + default: + return model.NewAppError("importUser", "app.user.save.app_error", nil, nErr.Error(), http.StatusInternalServerError) + } } + + pref := model.Preference{UserId: savedUser.Id, Category: model.PREFERENCE_CATEGORY_TUTORIAL_STEPS, Name: savedUser.Id, Value: "0"} + if err := a.Srv().Store.Preference().Save(&model.Preferences{pref}); err != nil { + mlog.Warn("Encountered error saving tutorial preference", mlog.Err(err)) + } + } else { + var appErr *model.AppError if hasUserChanged { - if savedUser, err = a.UpdateUser(user, false); err != nil { - return err + if savedUser, appErr = a.UpdateUser(user, false); appErr != nil { + return appErr } } if hasUserRolesChanged { - if savedUser, err = a.UpdateUserRoles(user.Id, roles, false); err != nil { - return err + if savedUser, appErr = a.UpdateUserRoles(user.Id, roles, false); appErr != nil { + return appErr } } if hasNotifyPropsChanged { - if savedUser, err = a.UpdateUserNotifyProps(user.Id, user.NotifyProps, false); err != nil { - return err + if savedUser, appErr = a.UpdateUserNotifyProps(user.Id, user.NotifyProps, false); appErr != nil { + return appErr } } if password != "" { - if err = a.UpdatePassword(user, password); err != nil { - return err + if appErr = a.UpdatePassword(user, password); appErr != nil { + return appErr } } else { if hasUserAuthDataChanged { diff --git a/app/opentracing/opentracing_layer.go b/app/opentracing/opentracing_layer.go index 51558bdcbb..30400a602d 100644 --- a/app/opentracing/opentracing_layer.go +++ b/app/opentracing/opentracing_layer.go @@ -10813,23 +10813,6 @@ func (a *OpenTracingAppLayer) IsUserSignUpAllowed() *model.AppError { return resultVar0 } -func (a *OpenTracingAppLayer) IsUsernameTaken(name string) bool { - origCtx := a.ctx - span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.IsUsernameTaken") - - a.ctx = newCtx - a.app.Srv().Store.SetContext(newCtx) - defer func() { - a.app.Srv().Store.SetContext(origCtx) - a.ctx = origCtx - }() - - defer span.Finish() - resultVar0 := a.app.IsUsernameTaken(name) - - return resultVar0 -} - func (a *OpenTracingAppLayer) JoinChannel(c *request.Context, channel *model.Channel, userID string) *model.AppError { origCtx := a.ctx span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.JoinChannel") diff --git a/app/server.go b/app/server.go index 55f7b22f29..6761e91c30 100644 --- a/app/server.go +++ b/app/server.go @@ -57,6 +57,7 @@ import ( "github.com/mattermost/mattermost-server/v5/services/timezones" "github.com/mattermost/mattermost-server/v5/services/tracing" "github.com/mattermost/mattermost-server/v5/services/upgrader" + "github.com/mattermost/mattermost-server/v5/services/users" "github.com/mattermost/mattermost-server/v5/shared/filestore" "github.com/mattermost/mattermost-server/v5/shared/i18n" "github.com/mattermost/mattermost-server/v5/shared/mail" @@ -158,6 +159,7 @@ type Server struct { limitedClientConfig atomic.Value telemetryService *telemetry.TelemetryService + userService *users.UserService serviceMux sync.RWMutex remoteClusterService remotecluster.RemoteClusterServiceIFace @@ -407,6 +409,8 @@ func NewServer(options ...Option) (*Server, error) { return nil, errors.Wrap(err, "cannot create store") } + s.userService = users.New(s.Store.User(), s.Config) + s.configListenerId = s.AddConfigListener(func(_, _ *model.Config) { s.configOrLicenseListener() diff --git a/app/user.go b/app/user.go index b0217969e6..d0049e20e4 100644 --- a/app/user.go +++ b/app/user.go @@ -33,6 +33,7 @@ import ( "github.com/mattermost/mattermost-server/v5/einterfaces" "github.com/mattermost/mattermost-server/v5/model" "github.com/mattermost/mattermost-server/v5/plugin" + "github.com/mattermost/mattermost-server/v5/services/users" "github.com/mattermost/mattermost-server/v5/shared/i18n" "github.com/mattermost/mattermost-server/v5/shared/mfa" "github.com/mattermost/mattermost-server/v5/shared/mlog" @@ -244,37 +245,58 @@ func (a *App) CreateGuest(c *request.Context, user *model.User) (*model.User, *m } func (a *App) createUserOrGuest(c *request.Context, user *model.User, guest bool) (*model.User, *model.AppError) { - user.Roles = model.SYSTEM_USER_ROLE_ID - if guest { - user.Roles = model.SYSTEM_GUEST_ROLE_ID + ruser, nErr := a.srv.userService.CreateUser(user, users.UserCreateOptions{Guest: guest}) + if nErr != nil { + var appErr *model.AppError + var invErr *store.ErrInvalidInput + var nfErr *users.ErrInvalidPassword + switch { + case errors.As(nErr, &appErr): + return nil, appErr + case errors.Is(nErr, users.AcceptedDomainError): + return nil, model.NewAppError("createUserOrGuest", "api.user.create_user.accepted_domain.app_error", nil, "", http.StatusBadRequest) + case errors.As(nErr, &nfErr): + return nil, model.NewAppError("createUserOrGuest", "api.user.check_user_password.invalid.app_error", nil, "", http.StatusBadRequest) + case errors.Is(nErr, users.UserCountError): + return nil, model.NewAppError("createUserOrGuest", "app.user.get_total_users_count.app_error", nil, nErr.Error(), http.StatusInternalServerError) + case errors.As(nErr, &invErr): + switch invErr.Field { + case "email": + return nil, model.NewAppError("createUserOrGuest", "app.user.save.email_exists.app_error", nil, invErr.Error(), http.StatusBadRequest) + case "username": + return nil, model.NewAppError("createUserOrGuest", "app.user.save.username_exists.app_error", nil, invErr.Error(), http.StatusBadRequest) + default: + return nil, model.NewAppError("createUserOrGuest", "app.user.save.existing.app_error", nil, invErr.Error(), http.StatusBadRequest) + } + default: + return nil, model.NewAppError("createUserOrGuest", "app.user.save.app_error", nil, nErr.Error(), http.StatusInternalServerError) + } } - if !user.IsLDAPUser() && !user.IsSAMLUser() && !user.IsGuest() && !CheckUserDomain(user, *a.Config().TeamSettings.RestrictCreationToDomains) { - return nil, model.NewAppError("CreateUser", "api.user.create_user.accepted_domain.app_error", nil, "", http.StatusBadRequest) + if user.EmailVerified { + a.InvalidateCacheForUser(ruser.Id) + + nUser, err := a.srv.userService.GetUser(ruser.Id) + if err != nil { + var nfErr *store.ErrNotFound + switch { + case errors.As(err, &nfErr): + return nil, model.NewAppError("createUserOrGuest", MissingAccountError, nil, nfErr.Error(), http.StatusNotFound) + default: + return nil, model.NewAppError("createUserOrGuest", "app.user.get.app_error", nil, err.Error(), http.StatusInternalServerError) + } + } + + a.sendUpdatedUserEvent(*nUser) } - if !user.IsLDAPUser() && !user.IsSAMLUser() && user.IsGuest() && !CheckUserDomain(user, *a.Config().GuestAccountsSettings.RestrictCreationToDomains) { - return nil, model.NewAppError("CreateUser", "api.user.create_user.accepted_domain.app_error", nil, "", http.StatusBadRequest) + pref := model.Preference{UserId: ruser.Id, Category: model.PREFERENCE_CATEGORY_TUTORIAL_STEPS, Name: ruser.Id, Value: "0"} + if err := a.Srv().Store.Preference().Save(&model.Preferences{pref}); err != nil { + mlog.Warn("Encountered error saving tutorial preference", mlog.Err(err)) } - // Below is a special case where the first user in the entire - // system is granted the system_admin role - count, err := a.Srv().Store.User().Count(model.UserCountOptions{IncludeDeleted: true}) - if err != nil { - return nil, model.NewAppError("createUserOrGuest", "app.user.get_total_users_count.app_error", nil, err.Error(), http.StatusInternalServerError) - } - if count <= 0 { - user.Roles = model.SYSTEM_ADMIN_ROLE_ID + " " + model.SYSTEM_USER_ROLE_ID - } + go a.UpdateViewedProductNoticesForNewUser(ruser.Id) - if _, ok := i18n.GetSupportedLocales()[user.Locale]; !ok { - user.Locale = *a.Config().LocalizationSettings.DefaultClientLocale - } - - ruser, appErr := a.createUser(user) - if appErr != nil { - return nil, appErr - } // This message goes to everyone, so the teamID, channelID and userID are irrelevant message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_NEW_USER, "", "", "", nil) message.Add("user_id", ruser.Id) @@ -293,53 +315,6 @@ func (a *App) createUserOrGuest(c *request.Context, user *model.User, guest bool return ruser, nil } -func (a *App) createUser(user *model.User) (*model.User, *model.AppError) { - user.MakeNonNil() - - if err := a.IsPasswordValid(user.Password); user.AuthService == "" && err != nil { - return nil, err - } - - ruser, nErr := a.Srv().Store.User().Save(user) - if nErr != nil { - var appErr *model.AppError - var invErr *store.ErrInvalidInput - switch { - case errors.As(nErr, &appErr): - return nil, appErr - case errors.As(nErr, &invErr): - switch invErr.Field { - case "email": - return nil, model.NewAppError("createUser", "app.user.save.email_exists.app_error", nil, invErr.Error(), http.StatusBadRequest) - case "username": - return nil, model.NewAppError("createUser", "app.user.save.username_exists.app_error", nil, invErr.Error(), http.StatusBadRequest) - default: - return nil, model.NewAppError("createUser", "app.user.save.existing.app_error", nil, invErr.Error(), http.StatusBadRequest) - } - default: - return nil, model.NewAppError("createUser", "app.user.save.app_error", nil, nErr.Error(), http.StatusInternalServerError) - } - } - - if user.EmailVerified { - if err := a.VerifyUserEmail(ruser.Id, user.Email); err != nil { - mlog.Warn("Failed to set email verified", mlog.Err(err)) - } - } - - pref := model.Preference{UserId: ruser.Id, Category: model.PREFERENCE_CATEGORY_TUTORIAL_STEPS, Name: ruser.Id, Value: "0"} - if err := a.Srv().Store.Preference().Save(&model.Preferences{pref}); err != nil { - mlog.Warn("Encountered error saving tutorial preference", mlog.Err(err)) - } - - go a.UpdateViewedProductNoticesForNewUser(ruser.Id) - ruser.Sanitize(map[string]bool{}) - - // Determine whether to send the created user a welcome email - ruser.DisableWelcomeEmail = user.DisableWelcomeEmail - return ruser, nil -} - func (a *App) CreateOAuthUser(c *request.Context, service string, userData io.Reader, teamID string, tokenUser *model.User) (*model.User, *model.AppError) { if !*a.Config().TeamSettings.EnableUserCreation { return nil, model.NewAppError("CreateOAuthUser", "api.user.create_user.disabled.app_error", nil, "", http.StatusNotImplemented) @@ -360,7 +335,7 @@ func (a *App) CreateOAuthUser(c *request.Context, service string, userData io.Re found := true count := 0 for found { - if found = a.IsUsernameTaken(user.Username); found { + if found = a.srv.userService.IsUsernameTaken(user.Username); found { user.Username = user.Username + strconv.Itoa(count) count++ } @@ -430,28 +405,15 @@ func CheckUserDomain(user *model.User, domains string) bool { return CheckEmailDomain(user.Email, domains) } -// IsUsernameTaken checks if the username is already used by another user. Return false if the username is invalid. -func (a *App) IsUsernameTaken(name string) bool { - if !model.IsValidUsername(name) { - return false - } - - if _, err := a.Srv().Store.User().GetByUsername(name); err != nil { - return false - } - - return true -} - func (a *App) GetUser(userID string) (*model.User, *model.AppError) { - user, err := a.Srv().Store.User().Get(context.Background(), userID) + user, err := a.srv.userService.GetUser(userID) if err != nil { var nfErr *store.ErrNotFound switch { case errors.As(err, &nfErr): return nil, model.NewAppError("GetUser", MissingAccountError, nil, nfErr.Error(), http.StatusNotFound) default: - return nil, model.NewAppError("GetUser", "app.user.get.app_error", nil, err.Error(), http.StatusInternalServerError) + return nil, model.NewAppError("GetUser", "app.user.get_by_username.app_error", nil, err.Error(), http.StatusInternalServerError) } } @@ -459,7 +421,7 @@ func (a *App) GetUser(userID string) (*model.User, *model.AppError) { } func (a *App) GetUserByUsername(username string) (*model.User, *model.AppError) { - result, err := a.Srv().Store.User().GetByUsername(username) + result, err := a.srv.userService.GetUserByUsername(username) if err != nil { var nfErr *store.ErrNotFound switch { @@ -473,7 +435,7 @@ func (a *App) GetUserByUsername(username string) (*model.User, *model.AppError) } func (a *App) GetUserByEmail(email string) (*model.User, *model.AppError) { - user, err := a.Srv().Store.User().GetByEmail(email) + user, err := a.srv.userService.GetUserByEmail(email) if err != nil { var nfErr *store.ErrNotFound switch { @@ -487,7 +449,7 @@ func (a *App) GetUserByEmail(email string) (*model.User, *model.AppError) { } func (a *App) GetUserByAuth(authData *string, authService string) (*model.User, *model.AppError) { - user, err := a.Srv().Store.User().GetByAuth(authData, authService) + user, err := a.srv.userService.GetUserByAuth(authData, authService) if err != nil { var invErr *store.ErrInvalidInput var nfErr *store.ErrNotFound @@ -505,7 +467,7 @@ func (a *App) GetUserByAuth(authData *string, authService string) (*model.User, } func (a *App) GetUsers(options *model.UserGetOptions) ([]*model.User, *model.AppError) { - users, err := a.Srv().Store.User().GetAllProfiles(options) + users, err := a.srv.userService.GetUsers(options) if err != nil { return nil, model.NewAppError("GetUsers", "app.user.get_profiles.app_error", nil, err.Error(), http.StatusInternalServerError) } @@ -514,20 +476,20 @@ func (a *App) GetUsers(options *model.UserGetOptions) ([]*model.User, *model.App } func (a *App) GetUsersPage(options *model.UserGetOptions, asAdmin bool) ([]*model.User, *model.AppError) { - users, err := a.GetUsers(options) + users, err := a.srv.userService.GetUsersPage(options, asAdmin) if err != nil { - return nil, err + return nil, model.NewAppError("GetUsersPage", "app.user.get_profiles.app_error", nil, err.Error(), http.StatusInternalServerError) } - return a.sanitizeProfiles(users, asAdmin), nil + return users, nil } func (a *App) GetUsersEtag(restrictionsHash string) string { - return fmt.Sprintf("%v.%v.%v.%v", a.Srv().Store.User().GetEtagForAllProfiles(), a.Config().PrivacySettings.ShowFullName, a.Config().PrivacySettings.ShowEmailAddress, restrictionsHash) + return a.srv.userService.GetUsersEtag(restrictionsHash) } func (a *App) GetUsersInTeam(options *model.UserGetOptions) ([]*model.User, *model.AppError) { - users, err := a.Srv().Store.User().GetProfiles(options) + users, err := a.srv.userService.GetUsersInTeam(options) if err != nil { return nil, model.NewAppError("GetUsersInTeam", "app.user.get_profiles.app_error", nil, err.Error(), http.StatusInternalServerError) } @@ -536,7 +498,7 @@ func (a *App) GetUsersInTeam(options *model.UserGetOptions) ([]*model.User, *mod } func (a *App) GetUsersNotInTeam(teamID string, groupConstrained bool, offset int, limit int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, *model.AppError) { - users, err := a.Srv().Store.User().GetProfilesNotInTeam(teamID, groupConstrained, offset, limit, viewRestrictions) + users, err := a.srv.userService.GetUsersNotInTeam(teamID, groupConstrained, offset, limit, viewRestrictions) if err != nil { return nil, model.NewAppError("GetUsersNotInTeam", "app.user.get_profiles.app_error", nil, err.Error(), http.StatusInternalServerError) } @@ -545,29 +507,29 @@ func (a *App) GetUsersNotInTeam(teamID string, groupConstrained bool, offset int } func (a *App) GetUsersInTeamPage(options *model.UserGetOptions, asAdmin bool) ([]*model.User, *model.AppError) { - users, err := a.GetUsersInTeam(options) + users, err := a.srv.userService.GetUsersInTeamPage(options, asAdmin) if err != nil { - return nil, err + return nil, model.NewAppError("GetUsersInTeamPage", "app.user.get_profiles.app_error", nil, err.Error(), http.StatusInternalServerError) } return a.sanitizeProfiles(users, asAdmin), nil } func (a *App) GetUsersNotInTeamPage(teamID string, groupConstrained bool, page int, perPage int, asAdmin bool, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, *model.AppError) { - users, err := a.GetUsersNotInTeam(teamID, groupConstrained, page*perPage, perPage, viewRestrictions) + users, err := a.srv.userService.GetUsersNotInTeamPage(teamID, groupConstrained, page*perPage, perPage, asAdmin, viewRestrictions) if err != nil { - return nil, err + return nil, model.NewAppError("GetUsersNotInTeamPage", "app.user.get_profiles.app_error", nil, err.Error(), http.StatusInternalServerError) } return a.sanitizeProfiles(users, asAdmin), nil } func (a *App) GetUsersInTeamEtag(teamID string, restrictionsHash string) string { - return fmt.Sprintf("%v.%v.%v.%v", a.Srv().Store.User().GetEtagForProfiles(teamID), a.Config().PrivacySettings.ShowFullName, a.Config().PrivacySettings.ShowEmailAddress, restrictionsHash) + return a.srv.userService.GetUsersInTeamEtag(teamID, restrictionsHash) } func (a *App) GetUsersNotInTeamEtag(teamID string, restrictionsHash string) string { - return fmt.Sprintf("%v.%v.%v.%v", a.Srv().Store.User().GetEtagForProfilesNotInTeam(teamID), a.Config().PrivacySettings.ShowFullName, a.Config().PrivacySettings.ShowEmailAddress, restrictionsHash) + return a.srv.userService.GetUsersNotInTeamEtag(teamID, restrictionsHash) } func (a *App) GetUsersInChannel(options *model.UserGetOptions) ([]*model.User, *model.AppError) { @@ -655,16 +617,16 @@ func (a *App) GetUsersNotInChannelPage(teamID string, channelID string, groupCon } func (a *App) GetUsersWithoutTeamPage(options *model.UserGetOptions, asAdmin bool) ([]*model.User, *model.AppError) { - users, err := a.GetUsersWithoutTeam(options) + users, err := a.srv.userService.GetUsersWithoutTeamPage(options, asAdmin) if err != nil { - return nil, err + return nil, model.NewAppError("GetUsersWithoutTeamPage", "app.user.get_profiles.app_error", nil, err.Error(), http.StatusInternalServerError) } return a.sanitizeProfiles(users, asAdmin), nil } func (a *App) GetUsersWithoutTeam(options *model.UserGetOptions) ([]*model.User, *model.AppError) { - users, err := a.Srv().Store.User().GetProfilesWithoutTeam(options) + users, err := a.srv.userService.GetUsersWithoutTeam(options) if err != nil { return nil, model.NewAppError("GetUsersWithoutTeam", "app.user.get_profiles.app_error", nil, err.Error(), http.StatusInternalServerError) } @@ -693,14 +655,12 @@ func (a *App) GetChannelGroupUsers(channelID string) ([]*model.User, *model.AppE } func (a *App) GetUsersByIds(userIDs []string, options *store.UserGetByIdsOpts) ([]*model.User, *model.AppError) { - allowFromCache := options.ViewRestrictions == nil - - users, err := a.Srv().Store.User().GetProfileByIds(context.Background(), userIDs, options, allowFromCache) + users, err := a.srv.userService.GetUsersByIds(userIDs, options) if err != nil { return nil, model.NewAppError("GetUsersByIds", "app.user.get_profiles.app_error", nil, err.Error(), http.StatusInternalServerError) } - return a.sanitizeProfiles(users, options.IsAdmin), nil + return users, nil } func (a *App) GetUsersByGroupChannelIds(c *request.Context, channelIDs []string, asAdmin bool) (map[string][]*model.User, *model.AppError) { @@ -1125,6 +1085,7 @@ func (a *App) DeactivateGuests(c *request.Context) *model.AppError { return nil } +// TODO: migrate this after the user service implementation is completed func (a *App) GetSanitizeOptions(asAdmin bool) map[string]bool { options := a.Config().GetSanitizeOptions() if asAdmin { diff --git a/app/user_test.go b/app/user_test.go index 7ab4a8e1cb..e6034b9d63 100644 --- a/app/user_test.go +++ b/app/user_test.go @@ -27,27 +27,6 @@ import ( "github.com/mattermost/mattermost-server/v5/utils/testutils" ) -func TestIsUsernameTaken(t *testing.T) { - th := Setup(t).InitBasic() - defer th.TearDown() - - user := th.BasicUser - taken := th.App.IsUsernameTaken(user.Username) - - if !taken { - t.Logf("the username '%v' should be taken", user.Username) - t.FailNow() - } - - newUsername := "randomUsername" - taken = th.App.IsUsernameTaken(newUsername) - - if taken { - t.Logf("the username '%v' should not be taken", newUsername) - t.FailNow() - } -} - func TestCheckUserDomain(t *testing.T) { th := Setup(t).InitBasic() defer th.TearDown() diff --git a/i18n/en.json b/i18n/en.json index 5bc5f81c97..2792959212 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -6494,6 +6494,10 @@ "id": "app.user_terms_of_service.save.app_error", "translation": "Unable to save terms of service." }, + { + "id": "app.valid_password_generic.app_error", + "translation": "Password is not valid" + }, { "id": "app.webhooks.analytics_incoming_count.app_error", "translation": "Unable to count the incoming webhooks." diff --git a/model/user.go b/model/user.go index 2ae62bae8a..7c6789feaf 100644 --- a/model/user.go +++ b/model/user.go @@ -854,6 +854,7 @@ func HashPassword(password string) string { } // ComparePassword compares the hash +// This function is deprecated and will be removed in a future release. func ComparePassword(hash string, password string) bool { if password == "" || hash == "" { diff --git a/services/users/constants.go b/services/users/constants.go new file mode 100644 index 0000000000..eb2d28d0fb --- /dev/null +++ b/services/users/constants.go @@ -0,0 +1,12 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package users + +const ( + TokenTypePasswordRecovery = "password_recovery" + TokenTypeVerifyEmail = "verify_email" + TokenTypeTeamInvitation = "team_invitation" + TokenTypeGuestInvitation = "guest_invitation" + InvitationExpiryTime = 1000 * 60 * 60 * 48 // 48 hours +) diff --git a/services/users/errors.go b/services/users/errors.go new file mode 100644 index 0000000000..54c4c07464 --- /dev/null +++ b/services/users/errors.go @@ -0,0 +1,31 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package users + +import "errors" + +var ( + AcceptedDomainError = errors.New("the email provided does not belong to an accepted domain") + VerifyUserError = errors.New("could not update verify email field") + UserCountError = errors.New("could not get the total number of the users.") +) + +// ErrInvalidPassword indicates an error against the password settings +type ErrInvalidPassword struct { + id string +} + +func NewErrInvalidPassword(id string) *ErrInvalidPassword { + return &ErrInvalidPassword{ + id: id, + } +} + +func (e *ErrInvalidPassword) Error() string { + return "invalid password" +} + +func (e *ErrInvalidPassword) Id() string { + return e.id +} diff --git a/services/users/helper_test.go b/services/users/helper_test.go new file mode 100644 index 0000000000..8a509214d6 --- /dev/null +++ b/services/users/helper_test.go @@ -0,0 +1,142 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package users + +import ( + "bytes" + "io/ioutil" + "os" + "path/filepath" + "sync" + "testing" + + "github.com/mattermost/mattermost-server/v5/app/request" + "github.com/mattermost/mattermost-server/v5/config" + "github.com/mattermost/mattermost-server/v5/model" + "github.com/mattermost/mattermost-server/v5/store" +) + +var initBasicOnce sync.Once + +type TestHelper struct { + service *UserService + configStore *config.Store + dbStore store.Store + workspace string + + Context *request.Context + BasicUser *model.User + BasicUser2 *model.User + + SystemAdminUser *model.User + LogBuffer *bytes.Buffer +} + +func Setup(tb testing.TB) *TestHelper { + if testing.Short() { + tb.SkipNow() + } + dbStore := mainHelper.GetStore() + dbStore.DropAllTables() + dbStore.MarkSystemRanUnitTests() + mainHelper.PreloadMigrations() + + return setupTestHelper(dbStore, false, tb) +} + +func setupTestHelper(s store.Store, includeCacheLayer bool, tb testing.TB) *TestHelper { + tempWorkspace, err := ioutil.TempDir("", "userservicetest") + if err != nil { + panic(err) + } + + configStore := config.NewTestMemoryStore() + + config := configStore.Get() + *config.PluginSettings.Directory = filepath.Join(tempWorkspace, "plugins") + *config.PluginSettings.ClientDirectory = filepath.Join(tempWorkspace, "webapp") + *config.PluginSettings.AutomaticPrepackagedPlugins = false + *config.LogSettings.EnableSentry = false // disable error reporting during tests + *config.AnnouncementSettings.AdminNoticesEnabled = false + *config.AnnouncementSettings.UserNoticesEnabled = false + *config.TeamSettings.MaxUsersPerTeam = 50 + *config.RateLimitSettings.Enable = false + *config.TeamSettings.EnableOpenServer = true + // Disable strict password requirements for test + *config.PasswordSettings.MinimumLength = 5 + *config.PasswordSettings.Lowercase = false + *config.PasswordSettings.Uppercase = false + *config.PasswordSettings.Symbol = false + *config.PasswordSettings.Number = false + configStore.Set(config) + + buffer := &bytes.Buffer{} + + return &TestHelper{ + service: &UserService{store: s.User(), config: configStore.Get}, + Context: &request.Context{}, + configStore: configStore, + dbStore: s, + LogBuffer: buffer, + workspace: tempWorkspace, + } +} + +func (th *TestHelper) InitBasic() *TestHelper { + // create users once and cache them because password hashing is slow + initBasicOnce.Do(func() { + th.SystemAdminUser = th.CreateUser() + th.SystemAdminUser, _ = th.service.GetUser(th.SystemAdminUser.Id) + + th.BasicUser = th.CreateUser() + th.BasicUser, _ = th.service.GetUser(th.BasicUser.Id) + + th.BasicUser2 = th.CreateUser() + th.BasicUser2, _ = th.service.GetUser(th.BasicUser2.Id) + }) + + return th +} + +func (th *TestHelper) CreateUser() *model.User { + return th.CreateUserOrGuest(false) +} + +func (th *TestHelper) CreateGuest() *model.User { + return th.CreateUserOrGuest(true) +} + +func (th *TestHelper) CreateUserOrGuest(guest bool) *model.User { + id := model.NewId() + + user := &model.User{ + Email: "success+" + id + "@simulator.amazonses.com", + Username: "un_" + id, + Nickname: "nn_" + id, + Password: "Password1", + EmailVerified: true, + } + + var err error + if guest { + if user, err = th.service.CreateUser(user, UserCreateOptions{Guest: true}); err != nil { + panic(err) + } + } else { + if user, err = th.service.CreateUser(user, UserCreateOptions{}); err != nil { + panic(err) + } + } + return user +} + +func (th *TestHelper) TearDown() { + th.configStore.Close() + + th.dbStore.Close() + + if th.workspace != "" { + os.RemoveAll(th.workspace) + } +} diff --git a/services/users/main_test.go b/services/users/main_test.go new file mode 100644 index 0000000000..253707abab --- /dev/null +++ b/services/users/main_test.go @@ -0,0 +1,35 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package users + +import ( + "flag" + "testing" + + "github.com/mattermost/mattermost-server/v5/shared/mlog" + "github.com/mattermost/mattermost-server/v5/testlib" +) + +var mainHelper *testlib.MainHelper +var replicaFlag bool + +func TestMain(m *testing.M) { + if f := flag.Lookup("mysql-replica"); f == nil { + flag.BoolVar(&replicaFlag, "mysql-replica", false, "") + flag.Parse() + } + + var options = testlib.HelperOptions{ + EnableStore: true, + EnableResources: true, + WithReadReplica: replicaFlag, + } + + mlog.DisableZap() + + mainHelper = testlib.NewMainHelperWithOptions(&options) + defer mainHelper.Close() + + mainHelper.Main(m) +} diff --git a/services/users/password.go b/services/users/password.go new file mode 100644 index 0000000000..b0b3603e3d --- /dev/null +++ b/services/users/password.go @@ -0,0 +1,96 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package users + +import ( + "errors" + "strings" + + "github.com/mattermost/mattermost-server/v5/model" + "golang.org/x/crypto/bcrypt" +) + +func CheckUserPassword(user *model.User, password string) error { + if err := ComparePassword(user.Password, password); err != nil { + return NewErrInvalidPassword("") + } + + return nil +} + +// HashPassword generates a hash using the bcrypt.GenerateFromPassword +func HashPassword(password string) string { + hash, err := bcrypt.GenerateFromPassword([]byte(password), 10) + if err != nil { + panic(err) + } + + return string(hash) +} + +func ComparePassword(hash string, password string) error { + if password == "" || hash == "" { + return errors.New("empty password or hash") + } + + return bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) +} + +func (us *UserService) isPasswordValid(password string) error { + + if *us.config().ServiceSettings.EnableDeveloper { + return nil + } + + return IsPasswordValidWithSettings(password, &us.config().PasswordSettings) +} + +// IsPasswordValidWithSettings is a utility functions that checks if the given password +// comforms to the password settings. It returns the error id as error value. +func IsPasswordValidWithSettings(password string, settings *model.PasswordSettings) error { + id := "model.user.is_valid.pwd" + isError := false + + if len(password) < *settings.MinimumLength || len(password) > model.PASSWORD_MAXIMUM_LENGTH { + isError = true + } + + if *settings.Lowercase { + if !strings.ContainsAny(password, model.LOWERCASE_LETTERS) { + isError = true + } + + id = id + "_lowercase" + } + + if *settings.Uppercase { + if !strings.ContainsAny(password, model.UPPERCASE_LETTERS) { + isError = true + } + + id = id + "_uppercase" + } + + if *settings.Number { + if !strings.ContainsAny(password, model.NUMBERS) { + isError = true + } + + id = id + "_number" + } + + if *settings.Symbol { + if !strings.ContainsAny(password, model.SYMBOLS) { + isError = true + } + + id = id + "_symbol" + } + + if isError { + return NewErrInvalidPassword(id + ".app_error") + } + + return nil +} diff --git a/utils/password_test.go b/services/users/password_test.go similarity index 89% rename from utils/password_test.go rename to services/users/password_test.go index 29b1fb9cb5..a5de5a8a5e 100644 --- a/utils/password_test.go +++ b/services/users/password_test.go @@ -1,17 +1,25 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -package utils +package users import ( "strings" "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/mattermost/mattermost-server/v5/model" ) +func TestComparePassword(t *testing.T) { + hash := HashPassword("Test") + + assert.NoError(t, ComparePassword(hash, "Test"), "Passwords don't match") + assert.Error(t, ComparePassword(hash, "Test2"), "Passwords should not have matched") +} + func TestIsPasswordValidWithSettings(t *testing.T) { for name, tc := range map[string]struct { Password string @@ -121,9 +129,11 @@ func TestIsPasswordValidWithSettings(t *testing.T) { tc.Settings.SetDefaults() t.Run(name, func(t *testing.T) { if err := IsPasswordValidWithSettings(tc.Password, tc.Settings); tc.ExpectedError == "" { - assert.Nil(t, err) + assert.NoError(t, err) } else { - assert.Equal(t, tc.ExpectedError, err.Id) + invErr, ok := err.(*ErrInvalidPassword) + require.True(t, ok) + assert.Equal(t, tc.ExpectedError, invErr.Id()) } }) } diff --git a/services/users/users.go b/services/users/users.go new file mode 100644 index 0000000000..2a4ce4ca98 --- /dev/null +++ b/services/users/users.go @@ -0,0 +1,197 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package users + +import ( + "context" + "fmt" + + "github.com/mattermost/mattermost-server/v5/model" + "github.com/mattermost/mattermost-server/v5/shared/i18n" + "github.com/mattermost/mattermost-server/v5/shared/mlog" + "github.com/mattermost/mattermost-server/v5/store" +) + +type UserService struct { + store store.UserStore + config func() *model.Config +} + +type UserCreateOptions struct { + Guest bool + FromImport bool +} + +func New(s store.UserStore, cfgFn func() *model.Config) *UserService { + return &UserService{ + store: s, + config: cfgFn, + } +} + +// CreateUser creates a user +func (us *UserService) CreateUser(user *model.User, opts UserCreateOptions) (*model.User, error) { + user.Roles = model.SYSTEM_USER_ROLE_ID + if opts.Guest { + user.Roles = model.SYSTEM_GUEST_ROLE_ID + } + + if !user.IsLDAPUser() && !user.IsSAMLUser() && !user.IsGuest() && !checkUserDomain(user, *us.config().TeamSettings.RestrictCreationToDomains) { + return nil, AcceptedDomainError + } + + if !user.IsLDAPUser() && !user.IsSAMLUser() && user.IsGuest() && !checkUserDomain(user, *us.config().GuestAccountsSettings.RestrictCreationToDomains) { + return nil, AcceptedDomainError + } + + // Below is a special case where the first user in the entire + // system is granted the system_admin role + count, err := us.store.Count(model.UserCountOptions{IncludeDeleted: true}) + if err != nil { + return nil, UserCountError + } + if count <= 0 && !opts.FromImport { + user.Roles = model.SYSTEM_ADMIN_ROLE_ID + " " + model.SYSTEM_USER_ROLE_ID + } + + if _, ok := i18n.GetSupportedLocales()[user.Locale]; !ok { + user.Locale = *us.config().LocalizationSettings.DefaultClientLocale + } + + ruser, err := us.createUser(user) + if err != nil { + return nil, err + } + + return ruser, nil +} + +func (us *UserService) createUser(user *model.User) (*model.User, error) { + user.MakeNonNil() + + if err := us.isPasswordValid(user.Password); user.AuthService == "" && err != nil { + return nil, err + } + + ruser, err := us.store.Save(user) + if err != nil { + return nil, err + } + + if user.EmailVerified { + if err := us.verifyUserEmail(ruser.Id, user.Email); err != nil { + mlog.Warn("Failed to set email verified", mlog.Err(err)) + } + } + + // Determine whether to send the created user a welcome email + ruser.DisableWelcomeEmail = user.DisableWelcomeEmail + ruser.Sanitize(map[string]bool{}) + + return ruser, nil +} + +func (us *UserService) verifyUserEmail(userID, email string) error { + if _, err := us.store.VerifyEmail(userID, email); err != nil { + return VerifyUserError + } + + return nil +} + +func (us *UserService) GetUser(userID string) (*model.User, error) { + return us.store.Get(context.Background(), userID) +} + +func (us *UserService) GetUserByUsername(username string) (*model.User, error) { + return us.store.GetByUsername(username) +} + +func (us *UserService) GetUserByEmail(email string) (*model.User, error) { + return us.store.GetByEmail(email) +} + +func (us *UserService) GetUserByAuth(authData *string, authService string) (*model.User, error) { + return us.store.GetByAuth(authData, authService) +} + +func (us *UserService) GetUsers(options *model.UserGetOptions) ([]*model.User, error) { + return us.store.GetAllProfiles(options) +} + +func (us *UserService) GetUsersPage(options *model.UserGetOptions, asAdmin bool) ([]*model.User, error) { + users, err := us.GetUsers(options) + if err != nil { + return nil, err + } + + return us.sanitizeProfiles(users, asAdmin), nil +} + +func (us *UserService) GetUsersEtag(restrictionsHash string) string { + return fmt.Sprintf("%v.%v.%v.%v", us.store.GetEtagForAllProfiles(), us.config().PrivacySettings.ShowFullName, us.config().PrivacySettings.ShowEmailAddress, restrictionsHash) +} + +func (us *UserService) GetUsersByIds(userIDs []string, options *store.UserGetByIdsOpts) ([]*model.User, error) { + allowFromCache := options.ViewRestrictions == nil + + users, err := us.store.GetProfileByIds(context.Background(), userIDs, options, allowFromCache) + if err != nil { + return nil, err + } + + return us.sanitizeProfiles(users, options.IsAdmin), nil +} + +func (us *UserService) GetUsersInTeam(options *model.UserGetOptions) ([]*model.User, error) { + return us.store.GetProfiles(options) +} + +func (us *UserService) GetUsersNotInTeam(teamID string, groupConstrained bool, offset int, limit int, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error) { + return us.store.GetProfilesNotInTeam(teamID, groupConstrained, offset, limit, viewRestrictions) +} + +func (us *UserService) GetUsersInTeamPage(options *model.UserGetOptions, asAdmin bool) ([]*model.User, error) { + users, err := us.GetUsersInTeam(options) + if err != nil { + return nil, err + } + + return us.sanitizeProfiles(users, asAdmin), nil +} + +func (us *UserService) GetUsersNotInTeamPage(teamID string, groupConstrained bool, page int, perPage int, asAdmin bool, viewRestrictions *model.ViewUsersRestrictions) ([]*model.User, error) { + users, err := us.GetUsersNotInTeam(teamID, groupConstrained, page*perPage, perPage, viewRestrictions) + if err != nil { + return nil, err + } + + return us.sanitizeProfiles(users, asAdmin), nil +} + +func (us *UserService) GetUsersInTeamEtag(teamID string, restrictionsHash string) string { + return fmt.Sprintf("%v.%v.%v.%v", us.store.GetEtagForProfiles(teamID), us.config().PrivacySettings.ShowFullName, us.config().PrivacySettings.ShowEmailAddress, restrictionsHash) +} + +func (us *UserService) GetUsersNotInTeamEtag(teamID string, restrictionsHash string) string { + return fmt.Sprintf("%v.%v.%v.%v", us.store.GetEtagForProfilesNotInTeam(teamID), us.config().PrivacySettings.ShowFullName, us.config().PrivacySettings.ShowEmailAddress, restrictionsHash) +} + +func (us *UserService) GetUsersWithoutTeamPage(options *model.UserGetOptions, asAdmin bool) ([]*model.User, error) { + users, err := us.GetUsersWithoutTeam(options) + if err != nil { + return nil, err + } + + return us.sanitizeProfiles(users, asAdmin), nil +} + +func (us *UserService) GetUsersWithoutTeam(options *model.UserGetOptions) ([]*model.User, error) { + users, err := us.store.GetProfilesWithoutTeam(options) + if err != nil { + return nil, err + } + + return users, nil +} diff --git a/services/users/users_test.go b/services/users/users_test.go new file mode 100644 index 0000000000..06896134f5 --- /dev/null +++ b/services/users/users_test.go @@ -0,0 +1,29 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package users + +import ( + "testing" +) + +func TestIsUsernameTaken(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + + user := th.BasicUser + taken := th.service.IsUsernameTaken(user.Username) + + if !taken { + t.Logf("the username '%v' should be taken", user.Username) + t.FailNow() + } + + newUsername := "randomUsername" + taken = th.service.IsUsernameTaken(newUsername) + + if taken { + t.Logf("the username '%v' should not be taken", newUsername) + t.FailNow() + } +} diff --git a/services/users/utils.go b/services/users/utils.go new file mode 100644 index 0000000000..972b806201 --- /dev/null +++ b/services/users/utils.go @@ -0,0 +1,69 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package users + +import ( + "strings" + + "github.com/mattermost/mattermost-server/v5/model" +) + +// checkUserDomain checks that a user's email domain matches a list of space-delimited domains as a string. +func checkUserDomain(user *model.User, domains string) bool { + return checkEmailDomain(user.Email, domains) +} + +// checkEmailDomain checks that an email domain matches a list of space-delimited domains as a string. +func checkEmailDomain(email string, domains string) bool { + if domains == "" { + return true + } + + domainArray := strings.Fields(strings.TrimSpace(strings.ToLower(strings.Replace(strings.Replace(domains, "@", " ", -1), ",", " ", -1)))) + + for _, d := range domainArray { + if strings.HasSuffix(strings.ToLower(email), "@"+d) { + return true + } + } + + return false +} + +func (us *UserService) sanitizeProfiles(users []*model.User, asAdmin bool) []*model.User { + for _, u := range users { + us.SanitizeProfile(u, asAdmin) + } + + return users +} + +func (us *UserService) SanitizeProfile(user *model.User, asAdmin bool) { + options := us.GetSanitizeOptions(asAdmin) + + user.SanitizeProfile(options) +} + +func (us *UserService) GetSanitizeOptions(asAdmin bool) map[string]bool { + options := us.config().GetSanitizeOptions() + if asAdmin { + options["email"] = true + options["fullname"] = true + options["authservice"] = true + } + return options +} + +// IsUsernameTaken checks if the username is already used by another user. Return false if the username is invalid. +func (us *UserService) IsUsernameTaken(name string) bool { + if !model.IsValidUsername(name) { + return false + } + + if _, err := us.store.GetByUsername(name); err != nil { + return false + } + + return true +} diff --git a/utils/password.go b/utils/password.go deleted file mode 100644 index 9e05a8abea..0000000000 --- a/utils/password.go +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package utils - -import ( - "net/http" - "strings" - - "github.com/mattermost/mattermost-server/v5/model" -) - -func IsPasswordValidWithSettings(password string, settings *model.PasswordSettings) *model.AppError { - id := "model.user.is_valid.pwd" - isError := false - - if len(password) < *settings.MinimumLength || len(password) > model.PASSWORD_MAXIMUM_LENGTH { - isError = true - } - - if *settings.Lowercase { - if !strings.ContainsAny(password, model.LOWERCASE_LETTERS) { - isError = true - } - - id = id + "_lowercase" - } - - if *settings.Uppercase { - if !strings.ContainsAny(password, model.UPPERCASE_LETTERS) { - isError = true - } - - id = id + "_uppercase" - } - - if *settings.Number { - if !strings.ContainsAny(password, model.NUMBERS) { - isError = true - } - - id = id + "_number" - } - - if *settings.Symbol { - if !strings.ContainsAny(password, model.SYMBOLS) { - isError = true - } - - id = id + "_symbol" - } - - if isError { - return model.NewAppError("User.IsValid", id+".app_error", map[string]interface{}{"Min": *settings.MinimumLength}, "", http.StatusBadRequest) - } - - return nil -} diff --git a/web/webhook_test.go b/web/webhook_test.go index 1018ca2f46..eb84792948 100644 --- a/web/webhook_test.go +++ b/web/webhook_test.go @@ -150,7 +150,7 @@ func TestIncomingWebhook(t *testing.T) { resp, err = http.Post(adminUrl, "application/json", strings.NewReader(fmt.Sprintf("{\"text\":\"this is a test\", \"channel\":\"%s\"}", model.DEFAULT_CHANNEL))) require.NoError(t, err) - assert.True(t, resp.StatusCode == http.StatusOK) + assert.Equal(t, http.StatusOK, resp.StatusCode) th.App.UpdateConfig(func(cfg *model.Config) { *cfg.TeamSettings.ExperimentalTownSquareIsReadOnly = false }) })