mirror of
https://github.com/mattermost/mattermost.git
synced 2026-05-28 04:35:04 -04:00
parent
2102391672
commit
eb967b6b6d
23 changed files with 800 additions and 28 deletions
|
|
@ -3266,3 +3266,25 @@
|
|||
$ref: "#/components/responses/Unauthorized"
|
||||
"403":
|
||||
$ref: "#/components/responses/Forbidden"
|
||||
"/api/v4/users/{user_id}/reset_failed_attempts":
|
||||
post:
|
||||
tags:
|
||||
- users
|
||||
summary: Reset the failed password attempts for a user
|
||||
description: |
|
||||
Reset the FailedAttempts field for a user to 0. This will only work for ldap and email/password users.
|
||||
|
||||
##### Permissions
|
||||
|
||||
Requires `sysconsole_write_user_management_users` permission.
|
||||
|
||||
operationId: resetPasswordFailedAttempts
|
||||
responses:
|
||||
"200":
|
||||
description: User's thread update successful
|
||||
"400":
|
||||
$ref: "#/components/responses/BadRequest"
|
||||
"401":
|
||||
$ref: "#/components/responses/Unauthorized"
|
||||
"404":
|
||||
$ref: "#/components/responses/NotFound"
|
||||
|
|
|
|||
|
|
@ -469,6 +469,7 @@ const defaultServerConfig: AdminConfig = {
|
|||
EnableSync: false,
|
||||
LdapServer: '',
|
||||
LdapPort: 389,
|
||||
MaximumLoginAttempts: 10,
|
||||
ConnectionSecurity: '',
|
||||
BaseDN: '',
|
||||
BindUsername: '',
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ func (api *API) InitUser() {
|
|||
api.BaseRoutes.User.Handle("/email/verify/member", api.APISessionRequired(verifyUserEmailWithoutToken)).Methods(http.MethodPost)
|
||||
api.BaseRoutes.User.Handle("/terms_of_service", api.APISessionRequired(saveUserTermsOfService)).Methods(http.MethodPost)
|
||||
api.BaseRoutes.User.Handle("/terms_of_service", api.APISessionRequired(getUserTermsOfService)).Methods(http.MethodGet)
|
||||
api.BaseRoutes.User.Handle("/reset_failed_attempts", api.APISessionRequired(resetPasswordFailedAttempts)).Methods(http.MethodPost)
|
||||
|
||||
api.BaseRoutes.User.Handle("/auth", api.APISessionRequiredTrustRequester(updateUserAuth)).Methods(http.MethodPut)
|
||||
|
||||
|
|
@ -1861,6 +1862,7 @@ func login(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||
"api.user.check_user_login_attempts.too_many.app_error",
|
||||
"app.team.join_user_to_team.max_accounts.app_error",
|
||||
"store.sql_user.save.max_accounts.app_error",
|
||||
"api.user.check_user_login_attempts.too_many_ldap.app_error",
|
||||
}
|
||||
|
||||
maskError := true
|
||||
|
|
@ -3525,3 +3527,46 @@ func getUsersWithInvalidEmails(c *Context, w http.ResponseWriter, r *http.Reques
|
|||
c.Logger.Warn("Error writing response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func resetPasswordFailedAttempts(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.RequireUserId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
errParams := map[string]any{"userID": c.Params.UserId}
|
||||
|
||||
auditRec := c.MakeAuditRecord("resetPasswordFailedAttempts", audit.Fail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
|
||||
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteUserManagementUsers) {
|
||||
c.Err = model.NewAppError("resetPasswordFailedAttempts", "api.user.reset_password_failed_attempts.permissions.app_error", errParams, "", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
user, err := c.App.GetUser(c.Params.UserId)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
auditRec.AddEventPriorState(user)
|
||||
auditRec.AddEventObjectType("user")
|
||||
|
||||
if user.IsSystemAdmin() && !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
|
||||
c.SetPermissionError(model.PermissionManageSystem)
|
||||
return
|
||||
}
|
||||
|
||||
if user.AuthService != model.UserAuthServiceLdap && user.AuthService != "" {
|
||||
c.Err = model.NewAppError("resetPasswordFailedAttempts", "api.user.reset_password_failed_attempts.ldap_and_email_only.app_error", errParams, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if err := c.App.ResetPasswordFailedAttempts(c.AppContext, user); err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
|
||||
ReturnStatusOK(w)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/dgryski/dgoogauth"
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
|
@ -8973,6 +8974,221 @@ func TestRevokeAllSessionsForUser(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestResetPasswordFailedAttempts(t *testing.T) {
|
||||
th := SetupEnterprise(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
th.SetupLdapConfig()
|
||||
|
||||
th.App.Srv().SetLicense(model.NewTestLicense("ldap"))
|
||||
|
||||
t.Run("Reset password failed attempts for regular user", func(t *testing.T) {
|
||||
client := th.CreateClient()
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.ServiceSettings.MaximumLoginAttempts = 10
|
||||
})
|
||||
maxAttempts := th.App.Config().ServiceSettings.MaximumLoginAttempts
|
||||
|
||||
user := th.CreateUser()
|
||||
|
||||
for i := 0; i < *maxAttempts; i++ {
|
||||
_, _, err := client.Login(context.Background(), user.Email, "wrongpassword")
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
user, resp, err := th.SystemAdminClient.GetUser(context.Background(), user.Id, "")
|
||||
require.NoError(t, err)
|
||||
CheckOKStatus(t, resp)
|
||||
require.Equal(t, *maxAttempts, user.FailedAttempts)
|
||||
|
||||
resp, err = th.SystemAdminClient.ResetFailedAttempts(context.Background(), user.Id)
|
||||
require.NoError(t, err)
|
||||
CheckOKStatus(t, resp)
|
||||
|
||||
user, resp, err = th.SystemAdminClient.GetUser(context.Background(), user.Id, "")
|
||||
require.NoError(t, err)
|
||||
CheckOKStatus(t, resp)
|
||||
require.Equal(t, int(0), user.FailedAttempts)
|
||||
})
|
||||
|
||||
t.Run("Reset password failed attempts for ldap user", func(t *testing.T) {
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.LdapSettings.MaximumLoginAttempts = 5
|
||||
})
|
||||
|
||||
mockCtrl := gomock.NewController(t)
|
||||
defer mockCtrl.Finish()
|
||||
|
||||
mockLdap := &mocks.LdapInterface{}
|
||||
|
||||
username := GenerateTestUsername()
|
||||
|
||||
ldapUser := &model.User{
|
||||
Email: "foobar+testdomainrestriction@mattermost.org",
|
||||
Username: username,
|
||||
AuthService: "ldap",
|
||||
AuthData: &username,
|
||||
EmailVerified: true,
|
||||
}
|
||||
ldapUser, appErr := th.App.CreateUser(th.Context, ldapUser)
|
||||
require.Nil(t, appErr)
|
||||
|
||||
client := th.CreateClient()
|
||||
mockLdap.Mock.On("GetUser", mock.AnythingOfType("*request.Context"), mock.AnythingOfType("string")).Return(ldapUser, nil).Times(5)
|
||||
|
||||
th.App.Channels().Ldap = mockLdap
|
||||
|
||||
for i := 0; i < 5; i++ {
|
||||
mockedLdapUser := ldapUser
|
||||
mockedLdapUser.FailedAttempts = i
|
||||
mockLdap.Mock.On("DoLogin", mock.AnythingOfType("*request.Context"), mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(mockedLdapUser, &model.AppError{Id: "ent.ldap.do_login.invalid_password.app_error"})
|
||||
_, _, err := client.LoginByLdap(context.Background(), *ldapUser.AuthData, "wrongpassword")
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
user, resp, err := th.SystemAdminClient.GetUser(context.Background(), ldapUser.Id, "")
|
||||
require.NoError(t, err)
|
||||
CheckOKStatus(t, resp)
|
||||
require.Equal(t, int(5), user.FailedAttempts)
|
||||
|
||||
resp, err = th.SystemAdminClient.ResetFailedAttempts(context.Background(), ldapUser.Id)
|
||||
require.NoError(t, err)
|
||||
CheckOKStatus(t, resp)
|
||||
|
||||
user, resp, err = th.SystemAdminClient.GetUser(context.Background(), ldapUser.Id, "")
|
||||
require.NoError(t, err)
|
||||
CheckOKStatus(t, resp)
|
||||
require.Equal(t, int(0), user.FailedAttempts)
|
||||
})
|
||||
|
||||
t.Run("Regular user unable to reset failed attempts", func(t *testing.T) {
|
||||
client := th.CreateClient()
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.ServiceSettings.MaximumLoginAttempts = 10
|
||||
})
|
||||
maxAttempts := th.App.Config().ServiceSettings.MaximumLoginAttempts
|
||||
|
||||
user := th.CreateUser()
|
||||
|
||||
for i := 0; i < *maxAttempts; i++ {
|
||||
_, _, err := client.Login(context.Background(), user.Email, "wrongpassword")
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
user, resp, err := th.SystemAdminClient.GetUser(context.Background(), user.Id, "")
|
||||
require.NoError(t, err)
|
||||
CheckOKStatus(t, resp)
|
||||
require.Equal(t, *maxAttempts, user.FailedAttempts)
|
||||
|
||||
resp, err = th.Client.ResetFailedAttempts(context.Background(), user.Id)
|
||||
require.Error(t, err)
|
||||
CheckForbiddenStatus(t, resp)
|
||||
|
||||
user, resp, err = th.SystemAdminClient.GetUser(context.Background(), user.Id, "")
|
||||
require.NoError(t, err)
|
||||
CheckOKStatus(t, resp)
|
||||
require.Equal(t, *maxAttempts, user.FailedAttempts)
|
||||
})
|
||||
|
||||
t.Run("Reset password failed attempts when user has PermissionSysconsoleWriteUserManagementUsers", func(t *testing.T) {
|
||||
th.AddPermissionToRole(model.PermissionSysconsoleWriteUserManagementUsers.Id, model.SystemUserRoleId)
|
||||
defer th.RemovePermissionFromRole(model.PermissionSysconsoleWriteUserManagementUsers.Id, model.SystemUserRoleId)
|
||||
|
||||
client := th.CreateClient()
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.ServiceSettings.MaximumLoginAttempts = 10
|
||||
})
|
||||
maxAttempts := th.App.Config().ServiceSettings.MaximumLoginAttempts
|
||||
|
||||
user := th.CreateUser()
|
||||
|
||||
for i := 0; i < *maxAttempts; i++ {
|
||||
_, _, err := client.Login(context.Background(), user.Email, "wrongpassword")
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
fetchedUser, resp, err := th.SystemAdminClient.GetUser(context.Background(), user.Id, "")
|
||||
require.NoError(t, err)
|
||||
CheckOKStatus(t, resp)
|
||||
require.Equal(t, *maxAttempts, fetchedUser.FailedAttempts)
|
||||
|
||||
resp, err = th.Client.ResetFailedAttempts(context.Background(), user.Id)
|
||||
require.NoError(t, err)
|
||||
CheckOKStatus(t, resp)
|
||||
|
||||
fetchedUser, resp, err = th.SystemAdminClient.GetUser(context.Background(), user.Id, "")
|
||||
require.NoError(t, err)
|
||||
CheckOKStatus(t, resp)
|
||||
require.Equal(t, int(0), fetchedUser.FailedAttempts)
|
||||
})
|
||||
|
||||
t.Run("Unable to reset password failed attempts for sysadmin when user has PermissionSysconsoleWriteUserManagementUsers", func(t *testing.T) {
|
||||
th.AddPermissionToRole(model.PermissionSysconsoleWriteUserManagementUsers.Id, model.SystemUserRoleId)
|
||||
defer th.RemovePermissionFromRole(model.PermissionSysconsoleWriteUserManagementUsers.Id, model.SystemUserRoleId)
|
||||
|
||||
client := th.CreateClient()
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.ServiceSettings.MaximumLoginAttempts = 10
|
||||
})
|
||||
maxAttempts := th.App.Config().ServiceSettings.MaximumLoginAttempts
|
||||
|
||||
// create sysadmin user
|
||||
sysadmin := th.CreateUser()
|
||||
_, appErr := th.App.UpdateUserRoles(th.Context, sysadmin.Id, model.SystemUserRoleId+" "+model.SystemAdminRoleId, false)
|
||||
require.Nil(t, appErr)
|
||||
|
||||
for i := 0; i < *maxAttempts; i++ {
|
||||
_, _, err := client.Login(context.Background(), sysadmin.Email, "wrongpassword")
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
sysadminUser, resp, err := th.SystemAdminClient.GetUser(context.Background(), sysadmin.Id, "")
|
||||
require.NoError(t, err)
|
||||
CheckOKStatus(t, resp)
|
||||
require.Equal(t, *maxAttempts, sysadminUser.FailedAttempts)
|
||||
|
||||
resp, err = th.Client.ResetFailedAttempts(context.Background(), sysadminUser.Id)
|
||||
require.Error(t, err)
|
||||
CheckForbiddenStatus(t, resp)
|
||||
|
||||
sysadminUser, resp, err = th.SystemAdminClient.GetUser(context.Background(), sysadminUser.Id, "")
|
||||
require.NoError(t, err)
|
||||
CheckOKStatus(t, resp)
|
||||
require.Equal(t, int(10), sysadminUser.FailedAttempts)
|
||||
})
|
||||
|
||||
t.Run("Reset password failed attempts for sysadmin", func(t *testing.T) {
|
||||
client := th.CreateClient()
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.ServiceSettings.MaximumLoginAttempts = 10
|
||||
})
|
||||
maxAttempts := th.App.Config().ServiceSettings.MaximumLoginAttempts
|
||||
|
||||
sysadmin := th.CreateUser()
|
||||
_, appErr := th.App.UpdateUserRoles(th.Context, sysadmin.Id, model.SystemUserRoleId+" "+model.SystemAdminRoleId, false)
|
||||
require.Nil(t, appErr)
|
||||
|
||||
for i := 0; i < *maxAttempts; i++ {
|
||||
_, _, err := client.Login(context.Background(), sysadmin.Email, "wrongpassword")
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
sysadminUser, resp, err := th.SystemAdminClient.GetUser(context.Background(), sysadmin.Id, "")
|
||||
require.NoError(t, err)
|
||||
CheckOKStatus(t, resp)
|
||||
require.Equal(t, *maxAttempts, sysadminUser.FailedAttempts)
|
||||
|
||||
resp, err = th.SystemAdminClient.ResetFailedAttempts(context.Background(), sysadminUser.Id)
|
||||
require.NoError(t, err)
|
||||
CheckOKStatus(t, resp)
|
||||
|
||||
sysadminUser, resp, err = th.SystemAdminClient.GetUser(context.Background(), sysadminUser.Id, "")
|
||||
require.NoError(t, err)
|
||||
CheckOKStatus(t, resp)
|
||||
require.Equal(t, int(0), sysadminUser.FailedAttempts)
|
||||
})
|
||||
}
|
||||
func TestSearchUsersWithMfaEnforced(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
|
|
|||
|
|
@ -64,8 +64,8 @@ func (a *App) IsPasswordValid(rctx request.CTX, password string) *model.AppError
|
|||
func (a *App) CheckPasswordAndAllCriteria(rctx request.CTX, userID string, password string, mfaToken string) *model.AppError {
|
||||
// MM-37585
|
||||
// Use locks to avoid concurrently checking AND updating the failed login attempts.
|
||||
a.ch.loginAttemptsMut.Lock()
|
||||
defer a.ch.loginAttemptsMut.Unlock()
|
||||
a.ch.emailLoginAttemptsMut.Lock()
|
||||
defer a.ch.emailLoginAttemptsMut.Unlock()
|
||||
|
||||
user, err := a.GetUser(userID)
|
||||
if err != nil {
|
||||
|
|
@ -149,31 +149,86 @@ func (a *App) DoubleCheckPassword(rctx request.CTX, user *model.User, password s
|
|||
return nil
|
||||
}
|
||||
|
||||
func (a *App) checkLdapUserPasswordAndAllCriteria(rctx request.CTX, ldapId *string, password string, mfaToken string) (*model.User, *model.AppError) {
|
||||
if a.Ldap() == nil || ldapId == nil {
|
||||
func (a *App) checkLdapUserPasswordAndAllCriteria(rctx request.CTX, user *model.User, password, mfaToken string) (*model.User, *model.AppError) {
|
||||
// MM-37585: Use locks to avoid concurrently checking AND updating the failed login attempts.
|
||||
a.ch.ldapLoginAttemptsMut.Lock()
|
||||
defer a.ch.ldapLoginAttemptsMut.Unlock()
|
||||
|
||||
// We need to get the latest value of the user from the database after we acquire the lock. user is nil for first-time LDAP users.
|
||||
if user.Id != "" {
|
||||
var err *model.AppError
|
||||
user, err = a.GetUser(user.Id)
|
||||
if err != nil {
|
||||
if err.Id != MissingAccountError {
|
||||
err.StatusCode = http.StatusInternalServerError
|
||||
return nil, err
|
||||
}
|
||||
err.StatusCode = http.StatusBadRequest
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
ldapID := user.AuthData
|
||||
|
||||
if a.Ldap() == nil || ldapID == nil {
|
||||
err := model.NewAppError("doLdapAuthentication", "api.user.login_ldap.not_available.app_error", nil, "", http.StatusNotImplemented)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ldapUser, err := a.Ldap().DoLogin(rctx, *ldapId, password)
|
||||
// First time LDAP users will not have a userID
|
||||
if user.Id != "" {
|
||||
if err := checkUserLoginAttempts(user, *a.Config().LdapSettings.MaximumLoginAttempts); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
ldapUser, err := a.Ldap().DoLogin(rctx, *ldapID, password)
|
||||
if err != nil {
|
||||
// If this is a new LDAP user, we need to get the user from the database because DoLogin will have created the user.
|
||||
if user.Id == "" {
|
||||
var getUserByAuthErr *model.AppError
|
||||
ldapUser, getUserByAuthErr = a.GetUserByAuth(ldapID, model.UserAuthServiceLdap)
|
||||
if getUserByAuthErr != nil {
|
||||
return nil, getUserByAuthErr
|
||||
}
|
||||
} else {
|
||||
ldapUser = user
|
||||
}
|
||||
|
||||
// Log a info to make it easier to admin to spot that a user tried to log in with a legitimate user name.
|
||||
if err.Id == "ent.ldap.do_login.invalid_password.app_error" {
|
||||
rctx.Logger().LogM(mlog.MlvlLDAPInfo, "A user tried to sign in, which matched an LDAP account, but the password was incorrect.", mlog.String("ldap_id", *ldapId))
|
||||
rctx.Logger().LogM(mlog.MlvlLDAPInfo, "A user tried to sign in, which matched an LDAP account, but the password was incorrect.", mlog.String("ldap_id", *ldapID))
|
||||
|
||||
if passErr := a.Srv().Store().User().UpdateFailedPasswordAttempts(ldapUser.Id, ldapUser.FailedAttempts+1); passErr != nil {
|
||||
return nil, model.NewAppError("CheckPasswordAndAllCriteria", "app.user.update_failed_pwd_attempts.app_error", nil, "", http.StatusInternalServerError).Wrap(passErr)
|
||||
}
|
||||
}
|
||||
|
||||
err.StatusCode = http.StatusUnauthorized
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := a.CheckUserMfa(rctx, ldapUser, mfaToken); err != nil {
|
||||
if err = a.CheckUserMfa(rctx, ldapUser, mfaToken); err != nil {
|
||||
// If the mfaToken is not set, we assume the client used this as a pre-flight request to query the server
|
||||
// about the MFA state of the user in question
|
||||
if mfaToken != "" && ldapUser.Id != "" {
|
||||
if passErr := a.Srv().Store().User().UpdateFailedPasswordAttempts(ldapUser.Id, ldapUser.FailedAttempts+1); passErr != nil {
|
||||
return nil, model.NewAppError("CheckPasswordAndAllCriteria", "app.user.update_failed_pwd_attempts.app_error", nil, "", http.StatusInternalServerError).Wrap(passErr)
|
||||
}
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := checkUserNotDisabled(ldapUser); err != nil {
|
||||
if err = checkUserNotDisabled(ldapUser); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if ldapUser.FailedAttempts > 0 {
|
||||
if passErr := a.Srv().Store().User().UpdateFailedPasswordAttempts(ldapUser.Id, 0); passErr != nil {
|
||||
return nil, model.NewAppError("CheckPasswordAndAllCriteria", "app.user.update_failed_pwd_attempts.app_error", nil, "", http.StatusInternalServerError).Wrap(passErr)
|
||||
}
|
||||
}
|
||||
|
||||
// user successfully authenticated
|
||||
return ldapUser, nil
|
||||
}
|
||||
|
|
@ -286,6 +341,9 @@ func (a *App) MFARequired(rctx request.CTX) *model.AppError {
|
|||
|
||||
func checkUserLoginAttempts(user *model.User, max int) *model.AppError {
|
||||
if user.FailedAttempts >= max {
|
||||
if user.AuthService == model.UserAuthServiceLdap {
|
||||
return model.NewAppError("checkUserLoginAttempts", "api.user.check_user_login_attempts.too_many_ldap.app_error", nil, "user_id="+user.Id, http.StatusUnauthorized)
|
||||
}
|
||||
return model.NewAppError("checkUserLoginAttempts", "api.user.check_user_login_attempts.too_many.app_error", nil, "user_id="+user.Id, http.StatusUnauthorized)
|
||||
}
|
||||
|
||||
|
|
@ -316,7 +374,7 @@ func (a *App) authenticateUser(rctx request.CTX, user *model.User, password, mfa
|
|||
return user, err
|
||||
}
|
||||
|
||||
ldapUser, err := a.checkLdapUserPasswordAndAllCriteria(rctx, user.AuthData, password, mfaToken)
|
||||
ldapUser, err := a.checkLdapUserPasswordAndAllCriteria(rctx, user, password, mfaToken)
|
||||
if err != nil {
|
||||
err.StatusCode = http.StatusUnauthorized
|
||||
return user, err
|
||||
|
|
|
|||
|
|
@ -13,9 +13,11 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/dgryski/dgoogauth"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/v8/einterfaces/mocks"
|
||||
)
|
||||
|
||||
func TestParseAuthTokenFromRequest(t *testing.T) {
|
||||
|
|
@ -153,3 +155,211 @@ func TestCheckPasswordAndAllCriteria(t *testing.T) {
|
|||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestCheckLdapUserPasswordAndAllCriteria(t *testing.T) {
|
||||
th := SetupEnterprise(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
// update config
|
||||
const maxFailedLoginAttempts = 3
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.LdapSettings.MaximumLoginAttempts = maxFailedLoginAttempts
|
||||
*cfg.ServiceSettings.EnableMultifactorAuthentication = true
|
||||
})
|
||||
|
||||
mockLdap := &mocks.LdapInterface{}
|
||||
th.App.Channels().Ldap = mockLdap
|
||||
|
||||
authData := model.NewRandomString(32)
|
||||
|
||||
// create an ldap user by calling createUser
|
||||
ldapUser := &model.User{
|
||||
Email: "ldapuser@mattermost-customer.com",
|
||||
Username: "ldapuser",
|
||||
AuthService: model.UserAuthServiceLdap,
|
||||
AuthData: &authData,
|
||||
EmailVerified: true,
|
||||
}
|
||||
user, appErr := th.App.CreateUser(th.Context, ldapUser)
|
||||
require.Nil(t, appErr)
|
||||
user.AuthData = &authData
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
password string
|
||||
expectedErrID string
|
||||
mockDoLogin func()
|
||||
}{
|
||||
{
|
||||
name: "valid password",
|
||||
password: "password",
|
||||
expectedErrID: "",
|
||||
mockDoLogin: func() {
|
||||
mockLdap.Mock.On("DoLogin", th.Context, authData, "password").Return(user, nil)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid password",
|
||||
password: "wrongpassword",
|
||||
expectedErrID: "api.user.check_user_password.invalid.app_error",
|
||||
mockDoLogin: func() {
|
||||
mockLdap.Mock.On("DoLogin", th.Context, authData, "wrongpassword").Return(nil, &model.AppError{Id: "ent.ldap.do_login.invalid_password.app_error"})
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "too many login attempts",
|
||||
password: "wrongpassword",
|
||||
expectedErrID: "api.user.check_user_login_attempts.too_many_ldap.app_error",
|
||||
mockDoLogin: func() {
|
||||
mockLdap.Mock.On("DoLogin", th.Context, authData, "wrongpassword").Return(nil, &model.AppError{Id: "ent.ldap.do_login.invalid_password.app_error"}).Once()
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
// Reset login attempts
|
||||
err := th.App.Srv().Store().User().UpdateFailedPasswordAttempts(user.Id, 0)
|
||||
require.NoError(t, err)
|
||||
|
||||
tc.mockDoLogin()
|
||||
|
||||
ldapUser := user
|
||||
|
||||
// Simulate failed login attempts if necessary
|
||||
if tc.expectedErrID == "api.user.check_user_login_attempts.too_many_ldap.app_error" {
|
||||
for i := 0; i < maxFailedLoginAttempts-1; i++ {
|
||||
_, appErr = th.App.checkLdapUserPasswordAndAllCriteria(th.Context, ldapUser, "wrongpassword", "")
|
||||
require.NotNil(t, appErr)
|
||||
require.Equal(t, "ent.ldap.do_login.invalid_password.app_error", appErr.Id)
|
||||
}
|
||||
}
|
||||
// Call the method with the test case parameters
|
||||
_, appErr := th.App.checkLdapUserPasswordAndAllCriteria(th.Context, ldapUser, tc.password, "")
|
||||
|
||||
// Verify the returned error matches the expected error
|
||||
if tc.expectedErrID == "" {
|
||||
require.Nil(t, appErr)
|
||||
} else {
|
||||
require.NotNil(t, appErr)
|
||||
}
|
||||
|
||||
if tc.expectedErrID == "api.user.check_user_login_attempts.too_many_ldap.app_error" {
|
||||
updatedUser, err := th.App.GetUser(ldapUser.Id)
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, maxFailedLoginAttempts, updatedUser.FailedAttempts)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckLdapUserPasswordConcurrency(t *testing.T) {
|
||||
th := SetupEnterprise(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
// update config
|
||||
const maxFailedLoginAttempts = 1
|
||||
const concurrentAttempts = 10
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.LdapSettings.MaximumLoginAttempts = maxFailedLoginAttempts
|
||||
*cfg.ServiceSettings.EnableMultifactorAuthentication = true
|
||||
})
|
||||
|
||||
authData := model.NewRandomString(32)
|
||||
|
||||
// create an ldap user by calling createUser
|
||||
ldapUser := &model.User{
|
||||
Email: "ldapuser@mattermost-customer.com",
|
||||
Username: "ldapuser",
|
||||
AuthService: model.UserAuthServiceLdap,
|
||||
AuthData: &authData,
|
||||
EmailVerified: true,
|
||||
}
|
||||
user, appErr := th.App.CreateUser(th.Context, ldapUser)
|
||||
require.Nil(t, appErr)
|
||||
|
||||
// setup MFA
|
||||
secret, appErr := th.App.GenerateMfaSecret(user.Id)
|
||||
require.Nil(t, appErr)
|
||||
err := th.Server.Store().User().UpdateMfaActive(user.Id, true)
|
||||
require.NoError(t, err)
|
||||
err = th.Server.Store().User().UpdateMfaSecret(user.Id, secret.Secret)
|
||||
require.NoError(t, err)
|
||||
|
||||
user, appErr = th.App.GetUser(user.Id)
|
||||
require.Nil(t, appErr)
|
||||
user.AuthData = &authData
|
||||
|
||||
t.Run("validate concurrent failed attempts to bypass checks", func(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
password string
|
||||
mfaToken string
|
||||
expectedErrID string
|
||||
doLoginExpectedErrID string
|
||||
}{
|
||||
{
|
||||
name: "should not breach max. login attempts when password is wrong",
|
||||
password: "wrong password",
|
||||
mfaToken: "",
|
||||
doLoginExpectedErrID: "ent.ldap.do_login.invalid_password.app_error",
|
||||
expectedErrID: "ent.ldap.do_login.invalid_password.app_error",
|
||||
},
|
||||
{
|
||||
name: "should not breach max. login attempts when MFA is wrong",
|
||||
password: "password",
|
||||
mfaToken: "123456",
|
||||
doLoginExpectedErrID: "",
|
||||
expectedErrID: "api.user.check_user_mfa.bad_code.app_error",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
mockLdap := &mocks.LdapInterface{}
|
||||
th.App.Channels().Ldap = mockLdap
|
||||
// Reset login attempts
|
||||
err := th.App.Srv().Store().User().UpdateFailedPasswordAttempts(user.Id, 0)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Capture all concurrent errors
|
||||
appErrs := make([]*model.AppError, concurrentAttempts)
|
||||
|
||||
// Wait to complete the test
|
||||
var completeWG sync.WaitGroup
|
||||
completeWG.Add(concurrentAttempts)
|
||||
|
||||
for i := 0; i < concurrentAttempts; i++ {
|
||||
go func(i int) {
|
||||
defer completeWG.Done()
|
||||
|
||||
if tc.doLoginExpectedErrID == "ent.ldap.do_login.invalid_password.app_error" {
|
||||
mockLdap.Mock.On("DoLogin", mock.AnythingOfType("*request.Context"), mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(nil, &model.AppError{Id: tc.doLoginExpectedErrID})
|
||||
} else {
|
||||
mockLdap.Mock.On("DoLogin", mock.AnythingOfType("*request.Context"), mock.AnythingOfType("string"), tc.password).Return(user, nil)
|
||||
}
|
||||
_, appErrs[i] = th.App.checkLdapUserPasswordAndAllCriteria(th.Context, user, tc.password, tc.mfaToken)
|
||||
}(i)
|
||||
}
|
||||
|
||||
completeWG.Wait()
|
||||
|
||||
expectedErrsCount := 0
|
||||
for i := 0; i < concurrentAttempts; i++ {
|
||||
if appErrs[i].Id == tc.expectedErrID {
|
||||
expectedErrsCount++
|
||||
continue
|
||||
}
|
||||
|
||||
if appErrs[i] != nil {
|
||||
require.Equal(t, "api.user.check_user_login_attempts.too_many_ldap.app_error", appErrs[i].Id, "All other errors should be of too many login attempts only.")
|
||||
}
|
||||
}
|
||||
|
||||
// Password/MFA failure attempts should not breach the maxFailedAttempts
|
||||
// even during concurrent access by the same user.
|
||||
require.Equal(t, maxFailedLoginAttempts, expectedErrsCount)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -80,10 +80,11 @@ type Channels struct {
|
|||
postReminderMut sync.Mutex
|
||||
postReminderTask *model.ScheduledTask
|
||||
|
||||
interruptQuitChan chan struct{}
|
||||
scheduledPostMut sync.Mutex
|
||||
scheduledPostTask *model.ScheduledTask
|
||||
loginAttemptsMut sync.Mutex
|
||||
interruptQuitChan chan struct{}
|
||||
scheduledPostMut sync.Mutex
|
||||
scheduledPostTask *model.ScheduledTask
|
||||
emailLoginAttemptsMut sync.Mutex
|
||||
ldapLoginAttemptsMut sync.Mutex
|
||||
}
|
||||
|
||||
func NewChannels(s *Server) (*Channels, error) {
|
||||
|
|
|
|||
|
|
@ -153,11 +153,8 @@ func (a *App) SwitchLdapToEmail(c request.CTX, ldapPassword, code, email, newPas
|
|||
return "", model.NewAppError("SwitchLdapToEmail", "api.user.ldap_to_email.not_available.app_error", nil, "", http.StatusNotImplemented)
|
||||
}
|
||||
|
||||
if err := ldapInterface.CheckPasswordAuthData(c, *user.AuthData, ldapPassword); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err := a.CheckUserMfa(c, user, code); err != nil {
|
||||
user, err = a.checkLdapUserPasswordAndAllCriteria(c, user, ldapPassword, code)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2922,3 +2922,12 @@ func (a *App) UserIsFirstAdmin(rctx request.CTX, user *model.User) bool {
|
|||
|
||||
return true
|
||||
}
|
||||
|
||||
func (a *App) ResetPasswordFailedAttempts(c request.CTX, user *model.User) *model.AppError {
|
||||
err := a.Srv().Store().User().UpdateFailedPasswordAttempts(user.Id, 0)
|
||||
if err != nil {
|
||||
return model.NewAppError("ResetPasswordFailedAttempts", "app.user.reset_password_failed_attempts.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3930,6 +3930,10 @@
|
|||
"id": "api.user.check_user_login_attempts.too_many.app_error",
|
||||
"translation": "Your account is locked because of too many failed password attempts. Please reset your password."
|
||||
},
|
||||
{
|
||||
"id": "api.user.check_user_login_attempts.too_many_ldap.app_error",
|
||||
"translation": "Your account is locked because of too many failed password attempts. Please contact your System Administrator."
|
||||
},
|
||||
{
|
||||
"id": "api.user.check_user_mfa.bad_code.app_error",
|
||||
"translation": "Invalid MFA token."
|
||||
|
|
@ -4218,6 +4222,14 @@
|
|||
"id": "api.user.reset_password.token_parse.error",
|
||||
"translation": "Unable to parse the reset password token"
|
||||
},
|
||||
{
|
||||
"id": "api.user.reset_password_failed_attempts.ldap_and_email_only.app_error",
|
||||
"translation": "User auth service must be LDAP or Email."
|
||||
},
|
||||
{
|
||||
"id": "api.user.reset_password_failed_attempts.permissions.app_error",
|
||||
"translation": "You do not have permission to update this resource."
|
||||
},
|
||||
{
|
||||
"id": "api.user.saml.not_available.app_error",
|
||||
"translation": "SAML 2.0 is not configured or supported on this server."
|
||||
|
|
@ -7264,6 +7276,10 @@
|
|||
"id": "app.user.promote_guest.user_update.app_error",
|
||||
"translation": "Failed to update the user."
|
||||
},
|
||||
{
|
||||
"id": "app.user.reset_password_failed_attempts.app_error",
|
||||
"translation": "Failed to reset login attempts."
|
||||
},
|
||||
{
|
||||
"id": "app.user.save.app_error",
|
||||
"translation": "Unable to save the account."
|
||||
|
|
@ -8048,6 +8064,10 @@
|
|||
"id": "ent.ldap.do_login.x509.app_error",
|
||||
"translation": "Error creating key pair"
|
||||
},
|
||||
{
|
||||
"id": "ent.ldap.get_user_by_auth.app_error",
|
||||
"translation": "Failed to get user."
|
||||
},
|
||||
{
|
||||
"id": "ent.ldap.no.users.checkcertificate",
|
||||
"translation": "No LDAP users found, check your user filter and certificates."
|
||||
|
|
@ -8980,6 +9000,10 @@
|
|||
"id": "model.config.is_valid.ldap_login_id",
|
||||
"translation": "AD/LDAP field \"Login ID Attribute\" is required."
|
||||
},
|
||||
{
|
||||
"id": "model.config.is_valid.ldap_max_login_attempts.app_error",
|
||||
"translation": "Invalid maximum login attempts for ldap settings. Must be a positive number."
|
||||
},
|
||||
{
|
||||
"id": "model.config.is_valid.ldap_max_page_size.app_error",
|
||||
"translation": "Invalid max page size value."
|
||||
|
|
|
|||
|
|
@ -1684,6 +1684,16 @@ func (c *Client4) UpdateUserActive(ctx context.Context, userId string, active bo
|
|||
return BuildResponse(r), nil
|
||||
}
|
||||
|
||||
// ResetFailedAttempts resets the number of failed attempts for a user.
|
||||
func (c *Client4) ResetFailedAttempts(ctx context.Context, userId string) (*Response, error) {
|
||||
r, err := c.DoAPIPost(ctx, c.userRoute(userId)+"/reset_failed_attempts", "")
|
||||
if err != nil {
|
||||
return BuildResponse(r), err
|
||||
}
|
||||
defer closeBody(r)
|
||||
return BuildResponse(r), nil
|
||||
}
|
||||
|
||||
// DeleteUser deactivates a user in the system based on the provided user id string.
|
||||
func (c *Client4) DeleteUser(ctx context.Context, userId string) (*Response, error) {
|
||||
r, err := c.DoAPIDelete(ctx, c.userRoute(userId))
|
||||
|
|
|
|||
|
|
@ -158,6 +158,7 @@ const (
|
|||
LdapSettingsDefaultGroupDisplayNameAttribute = ""
|
||||
LdapSettingsDefaultGroupIdAttribute = ""
|
||||
LdapSettingsDefaultPictureAttribute = ""
|
||||
LdapSettingsDefaultMaximumLoginAttempts = 10
|
||||
|
||||
SamlSettingsDefaultIdAttribute = ""
|
||||
SamlSettingsDefaultGuestAttribute = ""
|
||||
|
|
@ -2415,14 +2416,15 @@ type ClientRequirements struct {
|
|||
|
||||
type LdapSettings struct {
|
||||
// Basic
|
||||
Enable *bool `access:"authentication_ldap"`
|
||||
EnableSync *bool `access:"authentication_ldap"`
|
||||
LdapServer *string `access:"authentication_ldap"` // telemetry: none
|
||||
LdapPort *int `access:"authentication_ldap"` // telemetry: none
|
||||
ConnectionSecurity *string `access:"authentication_ldap"`
|
||||
BaseDN *string `access:"authentication_ldap"` // telemetry: none
|
||||
BindUsername *string `access:"authentication_ldap"` // telemetry: none
|
||||
BindPassword *string `access:"authentication_ldap"` // telemetry: none
|
||||
Enable *bool `access:"authentication_ldap"`
|
||||
EnableSync *bool `access:"authentication_ldap"`
|
||||
LdapServer *string `access:"authentication_ldap"` // telemetry: none
|
||||
LdapPort *int `access:"authentication_ldap"` // telemetry: none
|
||||
ConnectionSecurity *string `access:"authentication_ldap"`
|
||||
BaseDN *string `access:"authentication_ldap"` // telemetry: none
|
||||
BindUsername *string `access:"authentication_ldap"` // telemetry: none
|
||||
BindPassword *string `access:"authentication_ldap"` // telemetry: none
|
||||
MaximumLoginAttempts *int `access:"authentication_ldap"` // telemetry: none
|
||||
|
||||
// Filtering
|
||||
UserFilter *string `access:"authentication_ldap"` // telemetry: none
|
||||
|
|
@ -2510,6 +2512,10 @@ func (s *LdapSettings) SetDefaults() {
|
|||
s.BindPassword = NewPointer("")
|
||||
}
|
||||
|
||||
if s.MaximumLoginAttempts == nil {
|
||||
s.MaximumLoginAttempts = NewPointer(LdapSettingsDefaultMaximumLoginAttempts)
|
||||
}
|
||||
|
||||
if s.UserFilter == nil {
|
||||
s.UserFilter = NewPointer("")
|
||||
}
|
||||
|
|
@ -4100,6 +4106,10 @@ func (s *LdapSettings) isValid() *AppError {
|
|||
return NewAppError("Config.IsValid", "model.config.is_valid.ldap_server", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
if *s.MaximumLoginAttempts <= 0 {
|
||||
return NewAppError("Config.IsValid", "model.config.is_valid.ldap_max_login_attempts.app_error", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
if *s.BaseDN == "" {
|
||||
return NewAppError("Config.IsValid", "model.config.is_valid.ldap_basedn", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -150,7 +150,7 @@ func (u *UserReportOptions) IsValid() *AppError {
|
|||
}
|
||||
|
||||
func (u *UserReportQuery) ToReport() *UserReport {
|
||||
u.ClearNonProfileFields(false)
|
||||
u.ClearNonProfileFields(true)
|
||||
return &UserReport{
|
||||
User: u.User,
|
||||
UserPostStats: u.UserPostStats,
|
||||
|
|
|
|||
|
|
@ -709,10 +709,10 @@ func (u *User) ClearNonProfileFields(asAdmin bool) {
|
|||
u.EmailVerified = false
|
||||
u.AllowMarketing = false
|
||||
u.LastPasswordUpdate = 0
|
||||
u.FailedAttempts = 0
|
||||
|
||||
if !asAdmin {
|
||||
u.NotifyProps = StringMap{}
|
||||
u.FailedAttempts = 0
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3469,6 +3469,19 @@ const AdminDefinition: AdminDefinitionType = {
|
|||
),
|
||||
),
|
||||
},
|
||||
{
|
||||
type: 'number',
|
||||
key: 'LdapSettings.MaximumLoginAttempts',
|
||||
label: defineMessage({id: 'admin.ldap.maximumLoginAttemptsTitle', defaultMessage: 'Maximum Login Attempts:'}),
|
||||
help_text: defineMessage({id: 'admin.ldap.maximumLoginAttemptsDesc', defaultMessage: 'The maximum number of login attempts before the Mattermost account is locked. You can unlock the account in system console on the users page. Setting this value lower than your LDAP maximum login attempts ensures that the users won\'t be locked out of your LDAP server because of failed login attempts in Mattermost.'}),
|
||||
isDisabled: it.any(
|
||||
it.not(it.userHasWritePermissionOnResource(RESOURCE_KEYS.AUTHENTICATION.LDAP)),
|
||||
it.all(
|
||||
it.stateIsFalse('LdapSettings.Enable'),
|
||||
it.stateIsFalse('LdapSettings.EnableSync'),
|
||||
),
|
||||
),
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
key: 'LdapSettings.LdapServer',
|
||||
|
|
|
|||
|
|
@ -0,0 +1,78 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
import {useDispatch} from 'react-redux';
|
||||
|
||||
import type {ServerError} from '@mattermost/types/errors';
|
||||
import type {UserProfile} from '@mattermost/types/users';
|
||||
|
||||
import {resetFailedAttempts} from 'mattermost-redux/actions/users';
|
||||
|
||||
import ConfirmModalRedux from 'components/confirm_modal_redux';
|
||||
|
||||
type Props = {
|
||||
user: UserProfile;
|
||||
onError: (error: ServerError) => void;
|
||||
onSuccess: () => void;
|
||||
onExited: () => void;
|
||||
}
|
||||
|
||||
export default function ConfirmResetFailedAttemptsModal({user, onSuccess, onError, onExited}: Props) {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
async function confirm() {
|
||||
const {error} = await dispatch(resetFailedAttempts(user.id));
|
||||
if (error) {
|
||||
onError(error);
|
||||
}
|
||||
onSuccess();
|
||||
}
|
||||
|
||||
const title = (
|
||||
<FormattedMessage
|
||||
id='confirm_reset_failed_attempts_modal.title'
|
||||
defaultMessage='Reset failed login attempts for {username} and unlock account'
|
||||
values={{
|
||||
username: user.username,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
const message = (
|
||||
<FormattedMessage
|
||||
id='confirm_reset_failed_attempts_modal.desc'
|
||||
defaultMessage="You're about to reset the failed login attempts for {username} and unlock their account. Are you sure you want to continue?"
|
||||
values={{
|
||||
username: user.username,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
const createGroupMembershipsButton = (
|
||||
<FormattedMessage
|
||||
id='confirm_reset_failed_attempts_modal.create'
|
||||
defaultMessage='Yes'
|
||||
/>
|
||||
);
|
||||
|
||||
const cancelGroupMembershipsButton = (
|
||||
<FormattedMessage
|
||||
id='confirm_reset_failed_attempts_modal.cancel'
|
||||
defaultMessage='No'
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<ConfirmModalRedux
|
||||
title={title}
|
||||
message={message}
|
||||
confirmButtonClass='btn btn-danger'
|
||||
cancelButtonText={cancelGroupMembershipsButton}
|
||||
confirmButtonText={createGroupMembershipsButton}
|
||||
onConfirm={confirm}
|
||||
onExited={onExited}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -35,6 +35,7 @@ import Constants, {ModalIdentifiers} from 'utils/constants';
|
|||
import type {GlobalState} from 'types/store';
|
||||
|
||||
import ConfirmManageUserSettingsModal from './confirm_manage_user_settings_modal';
|
||||
import ConfirmResetFailedAttemptsModal from './confirm_reset_failed_attempts_modal';
|
||||
import CreateGroupSyncablesMembershipsModal from './create_group_syncables_membership_modal';
|
||||
import DeactivateMemberModal from './deactivate_member_modal';
|
||||
import DemoteToGuestModal from './demote_to_guest_modal';
|
||||
|
|
@ -303,6 +304,24 @@ export function SystemUsersListAction({user, currentUser, tableId, rowIndex, onE
|
|||
);
|
||||
}, [user, updateUser, onError]);
|
||||
|
||||
const handleResetAttemptsClick = useCallback(() => {
|
||||
function onResetAttemptsSuccess() {
|
||||
updateUser({failed_attempts: 0});
|
||||
}
|
||||
|
||||
dispatch(
|
||||
openModal({
|
||||
modalId: ModalIdentifiers.CONFIRM_RESET_FAILED_ATTEMPTS_MODAL,
|
||||
dialogType: ConfirmResetFailedAttemptsModal,
|
||||
dialogProps: {
|
||||
user,
|
||||
onError,
|
||||
onSuccess: onResetAttemptsSuccess,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}, [user, updateUser, onError]);
|
||||
|
||||
const disableActivationToggle = user.auth_service === Constants.LDAP_SERVICE;
|
||||
|
||||
const getManagedByLDAPText = (managedByLDAP: boolean) => {
|
||||
|
|
@ -314,6 +333,18 @@ export function SystemUsersListAction({user, currentUser, tableId, rowIndex, onE
|
|||
} : {};
|
||||
};
|
||||
|
||||
const showResetFailedAttempts = useCallback(() => {
|
||||
if (user.failed_attempts === undefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (user.auth_service !== Constants.LDAP_SERVICE && user.auth_service !== '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}, [user]);
|
||||
|
||||
return (
|
||||
<Menu.Container
|
||||
menuButton={{
|
||||
|
|
@ -414,6 +445,18 @@ export function SystemUsersListAction({user, currentUser, tableId, rowIndex, onE
|
|||
onClick={handleResetPasswordClick}
|
||||
/>
|
||||
}
|
||||
{showResetFailedAttempts() && (
|
||||
<Menu.Item
|
||||
id={`${menuItemIdPrefix}-resetAttempts`}
|
||||
labels={
|
||||
<FormattedMessage
|
||||
id='admin.system_users.list.actions.menu.resetAttempts'
|
||||
defaultMessage='Reset login attempts'
|
||||
/>
|
||||
}
|
||||
onClick={handleResetAttemptsClick}
|
||||
/>
|
||||
)}
|
||||
{user.mfa_active && config.ServiceSettings?.EnableMultifactorAuthentication &&
|
||||
<Menu.Item
|
||||
id={`${menuItemIdPrefix}-removeMFA`}
|
||||
|
|
|
|||
|
|
@ -1335,6 +1335,8 @@
|
|||
"admin.ldap.loginNameDesc": "The placeholder text that appears in the login field on the login page. Defaults to \"AD/LDAP Username\".",
|
||||
"admin.ldap.loginNameEx": "E.g.: \"AD/LDAP Username\"",
|
||||
"admin.ldap.loginNameTitle": "Login Field Name:",
|
||||
"admin.ldap.maximumLoginAttemptsDesc": "The maximum number of login attempts before the Mattermost account is locked. You can unlock the account in system console on the users page. Setting this value lower than your LDAP maximum login attempts ensures that the users won't be locked out of your LDAP server because of failed login attempts in Mattermost.",
|
||||
"admin.ldap.maximumLoginAttemptsTitle": "Maximum Login Attempts:",
|
||||
"admin.ldap.maxPageSizeEx": "E.g.: \"2000\"",
|
||||
"admin.ldap.maxPageSizeHelpText": "The maximum number of users the Mattermost server will request from the AD/LDAP server at one time. 0 is unlimited.",
|
||||
"admin.ldap.maxPageSizeTitle": "Maximum Page Size:",
|
||||
|
|
@ -2629,6 +2631,7 @@
|
|||
"admin.system_users.list.actions.menu.promoteToMember": "Promote to member",
|
||||
"admin.system_users.list.actions.menu.removeMFA": "Remove MFA",
|
||||
"admin.system_users.list.actions.menu.removeSessions": "Remove sessions",
|
||||
"admin.system_users.list.actions.menu.resetAttempts": "Reset login attempts",
|
||||
"admin.system_users.list.actions.menu.resetPassword": "Reset password",
|
||||
"admin.system_users.list.actions.menu.resyncUserViaLdapGroups": "Re-sync user via LDAP groups",
|
||||
"admin.system_users.list.actions.menu.switchToEmailPassword": "Switch to Email/Password",
|
||||
|
|
@ -3484,6 +3487,10 @@
|
|||
"commercial_support.download_support_packet": "Download Support Packet",
|
||||
"commercial_support.title": "Commercial Support",
|
||||
"confirm_modal.cancel": "Cancel",
|
||||
"confirm_reset_failed_attempts_modal.cancel": "No",
|
||||
"confirm_reset_failed_attempts_modal.create": "Yes",
|
||||
"confirm_reset_failed_attempts_modal.desc": "You're about to reset the failed login attempts for {username} and unlock their account. Are you sure you want to continue?",
|
||||
"confirm_reset_failed_attempts_modal.title": "Reset failed login attempts for {username} and unlock account",
|
||||
"confirm_switch_to_yearly_modal.confirm": "Confirm",
|
||||
"confirm_switch_to_yearly_modal.contact_sales": "Contact Sales",
|
||||
"confirm_switch_to_yearly_modal.subtitle": "Changing to the annual plan is irreversible. Are you sure you want to switch from monthly to the annual plan?",
|
||||
|
|
|
|||
|
|
@ -1070,6 +1070,24 @@ export function updateUserPassword(userId: string, currentPassword: string, newP
|
|||
};
|
||||
}
|
||||
|
||||
export function resetFailedAttempts(userId: string): ActionFuncAsync<true> {
|
||||
return async (dispatch, getState) => {
|
||||
try {
|
||||
await Client4.resetFailedAttempts(userId);
|
||||
} catch (error) {
|
||||
dispatch(logError(error));
|
||||
return {error};
|
||||
}
|
||||
|
||||
const profile = getState().entities.users.profiles[userId];
|
||||
if (profile) {
|
||||
dispatch({type: UserTypes.RECEIVED_PROFILE, data: {...profile, failed_attempts: 0}});
|
||||
}
|
||||
|
||||
return {data: true};
|
||||
};
|
||||
}
|
||||
|
||||
export function updateUserActive(userId: string, active: boolean): ActionFuncAsync<true> {
|
||||
return async (dispatch, getState) => {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -467,6 +467,7 @@ export const ModalIdentifiers = {
|
|||
SECURE_CONNECTION_ACCEPT_INVITE: 'secure_connection_accept_invite',
|
||||
SHARED_CHANNEL_REMOTE_INVITE: 'shared_channel_remote_invite',
|
||||
SHARED_CHANNEL_REMOTE_UNINVITE: 'shared_channel_remote_uninvite',
|
||||
CONFIRM_RESET_FAILED_ATTEMPTS_MODAL: 'confirm_reset_failed_attempts_modal',
|
||||
USER_PROPERTY_FIELD_DELETE: 'user_property_field_delete',
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -674,6 +674,13 @@ export default class Client4 {
|
|||
);
|
||||
};
|
||||
|
||||
resetFailedAttempts = (userId: string) => {
|
||||
return this.doFetch<StatusOK>(
|
||||
`${this.getUserRoute(userId)}/reset_failed_attempts`,
|
||||
{method: 'post'},
|
||||
);
|
||||
};
|
||||
|
||||
getKnownUsers = () => {
|
||||
return this.doFetch<Array<UserProfile['id']>>(
|
||||
`${this.getUsersRoute()}/known`,
|
||||
|
|
|
|||
|
|
@ -709,6 +709,7 @@ export type LdapSettings = {
|
|||
LoginButtonColor: string;
|
||||
LoginButtonBorderColor: string;
|
||||
LoginButtonTextColor: string;
|
||||
MaximumLoginAttempts: number;
|
||||
};
|
||||
|
||||
export type ComplianceSettings = {
|
||||
|
|
|
|||
|
|
@ -60,6 +60,7 @@ export type UserProfile = {
|
|||
terms_of_service_create_at: number;
|
||||
remote_id?: string;
|
||||
status?: string;
|
||||
failed_attempts?: number;
|
||||
custom_profile_attributes?: Record<string, string>;
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue