user service: initial implementation (#17668)
* conceptual user service: initial commit * reflect review comments * fix i18n issues and some tests * implement get user methods * add license * reflect review comments
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
d320b50abb
Коммит
ac3bb2e811
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
181
app/user.go
181
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 {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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 == "" {
|
||||
|
||||
12
services/users/constants.go
Обычный файл
12
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
|
||||
)
|
||||
31
services/users/errors.go
Обычный файл
31
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
|
||||
}
|
||||
142
services/users/helper_test.go
Обычный файл
142
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)
|
||||
}
|
||||
}
|
||||
35
services/users/main_test.go
Обычный файл
35
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)
|
||||
}
|
||||
96
services/users/password.go
Обычный файл
96
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
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
})
|
||||
}
|
||||
197
services/users/users.go
Обычный файл
197
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
|
||||
}
|
||||
29
services/users/users_test.go
Обычный файл
29
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()
|
||||
}
|
||||
}
|
||||
69
services/users/utils.go
Обычный файл
69
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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 })
|
||||
})
|
||||
|
||||
Ссылка в новой задаче
Block a user