diff --git a/conf/defaults.ini b/conf/defaults.ini index ad7b944a02d..93158d3b6cb 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -812,6 +812,14 @@ use_refresh_token = false #################################### Basic Auth ########################## [auth.basic] enabled = true +# This setting will enable a stronger password policy for user's password under basic auth. +# The password will need to comply with the following password policy +# 1. Have a minimum of 12 characters +# 2. Composed by at least 1 uppercase character +# 3. Composed by at least 1 lowercase character +# 4. Composed by at least 1 digit character +# 5. Composed by at least 1 symbol character +password_policy = false #################################### Auth Proxy ########################## [auth.proxy] diff --git a/conf/sample.ini b/conf/sample.ini index 259c110d8ed..ad4f8569fd6 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -745,6 +745,7 @@ #################################### Basic Auth ########################## [auth.basic] ;enabled = true +;password_policy = false #################################### Auth Proxy ########################## [auth.proxy] diff --git a/pkg/api/admin_users.go b/pkg/api/admin_users.go index b20fa140953..85617774261 100644 --- a/pkg/api/admin_users.go +++ b/pkg/api/admin_users.go @@ -115,8 +115,8 @@ func (hs *HTTPServer) AdminUpdateUserPassword(c *contextmodel.ReqContext) respon return response.Error(http.StatusBadRequest, "id is invalid", err) } - if len(form.Password) < 4 { - return response.Error(http.StatusBadRequest, "New password too short", nil) + if err := form.Password.Validate(hs.Cfg); err != nil { + return response.Err(err) } userQuery := user.GetUserByIDQuery{ID: userID} @@ -134,14 +134,14 @@ func (hs *HTTPServer) AdminUpdateUserPassword(c *contextmodel.ReqContext) respon } } - passwordHashed, err := util.EncodePassword(form.Password, usr.Salt) + passwordHashed, err := util.EncodePassword(string(form.Password), usr.Salt) if err != nil { return response.Error(http.StatusInternalServerError, "Could not encode password", err) } cmd := user.ChangeUserPasswordCommand{ UserID: userID, - NewPassword: passwordHashed, + NewPassword: user.Password(passwordHashed), } if err := hs.userService.ChangePassword(c.Req.Context(), &cmd); err != nil { diff --git a/pkg/api/dtos/invite.go b/pkg/api/dtos/invite.go index 9586ba95cbd..a0fe16472eb 100644 --- a/pkg/api/dtos/invite.go +++ b/pkg/api/dtos/invite.go @@ -1,6 +1,9 @@ package dtos -import "github.com/grafana/grafana/pkg/services/org" +import ( + "github.com/grafana/grafana/pkg/services/org" + "github.com/grafana/grafana/pkg/services/user" +) type AddInviteForm struct { LoginOrEmail string `json:"loginOrEmail" binding:"Required"` @@ -17,10 +20,10 @@ type InviteInfo struct { } type CompleteInviteForm struct { - InviteCode string `json:"inviteCode"` - Email string `json:"email" binding:"Required"` - Name string `json:"name"` - Username string `json:"username"` - Password string `json:"password"` - ConfirmPassword string `json:"confirmPassword"` + InviteCode string `json:"inviteCode"` + Email string `json:"email" binding:"Required"` + Name string `json:"name"` + Username string `json:"username"` + Password user.Password `json:"password"` + ConfirmPassword user.Password `json:"confirmPassword"` } diff --git a/pkg/api/dtos/user.go b/pkg/api/dtos/user.go index 2cd16df1c02..35cdb3c78b0 100644 --- a/pkg/api/dtos/user.go +++ b/pkg/api/dtos/user.go @@ -1,28 +1,30 @@ package dtos +import "github.com/grafana/grafana/pkg/services/user" + type SignUpForm struct { Email string `json:"email" binding:"Required"` } type SignUpStep2Form struct { - Email string `json:"email"` - Name string `json:"name"` - Username string `json:"username"` - Password string `json:"password"` - Code string `json:"code"` - OrgName string `json:"orgName"` + Email string `json:"email"` + Name string `json:"name"` + Username string `json:"username"` + Password user.Password `json:"password"` + Code string `json:"code"` + OrgName string `json:"orgName"` } type AdminCreateUserForm struct { - Email string `json:"email"` - Login string `json:"login"` - Name string `json:"name"` - Password string `json:"password" binding:"Required"` - OrgId int64 `json:"orgId"` + Email string `json:"email"` + Login string `json:"login"` + Name string `json:"name"` + Password user.Password `json:"password" binding:"Required"` + OrgId int64 `json:"orgId"` } type AdminUpdateUserPasswordForm struct { - Password string `json:"password" binding:"Required"` + Password user.Password `json:"password" binding:"Required"` } type AdminUpdateUserPermissionsForm struct { @@ -34,9 +36,9 @@ type SendResetPasswordEmailForm struct { } type ResetUserPasswordForm struct { - Code string `json:"code"` - NewPassword string `json:"newPassword"` - ConfirmPassword string `json:"confirmPassword"` + Code string `json:"code"` + NewPassword user.Password `json:"newPassword"` + ConfirmPassword user.Password `json:"confirmPassword"` } type UserLookupDTO struct { diff --git a/pkg/api/org_invite.go b/pkg/api/org_invite.go index ab4f1b944b9..af21963d7f7 100644 --- a/pkg/api/org_invite.go +++ b/pkg/api/org_invite.go @@ -270,6 +270,10 @@ func (hs *HTTPServer) CompleteInvite(c *contextmodel.ReqContext) response.Respon } } + if err := completeInvite.Password.Validate(hs.Cfg); err != nil { + return response.Err(err) + } + cmd := user.CreateUserCommand{ Email: completeInvite.Email, Name: completeInvite.Name, diff --git a/pkg/api/password.go b/pkg/api/password.go index c04e6712b8b..f3b265029e4 100644 --- a/pkg/api/password.go +++ b/pkg/api/password.go @@ -97,17 +97,18 @@ func (hs *HTTPServer) ResetPassword(c *contextmodel.ReqContext) response.Respons return response.Error(http.StatusBadRequest, "Passwords do not match", nil) } - password := user.Password(form.NewPassword) - if password.IsWeak() { - return response.Error(http.StatusBadRequest, "New password is too short", nil) + if err := form.NewPassword.Validate(hs.Cfg); err != nil { + c.Logger.Warn("the new password doesn't meet the password policy criteria", "err", err) + return response.Err(err) } cmd := user.ChangeUserPasswordCommand{} cmd.UserID = userResult.ID - cmd.NewPassword, err = util.EncodePassword(form.NewPassword, userResult.Salt) + encodedPassword, err := util.EncodePassword(string(form.NewPassword), userResult.Salt) if err != nil { return response.Error(http.StatusInternalServerError, "Failed to encode password", err) } + cmd.NewPassword = user.Password(encodedPassword) if err := hs.userService.ChangePassword(c.Req.Context(), &cmd); err != nil { return response.Error(http.StatusInternalServerError, "Failed to change user password", err) diff --git a/pkg/api/user.go b/pkg/api/user.go index 1a08e1b636c..dd71c70a14d 100644 --- a/pkg/api/user.go +++ b/pkg/api/user.go @@ -490,24 +490,25 @@ func (hs *HTTPServer) ChangeUserPassword(c *contextmodel.ReqContext) response.Re } } - passwordHashed, err := util.EncodePassword(cmd.OldPassword, usr.Salt) + passwordHashed, err := util.EncodePassword(string(cmd.OldPassword), usr.Salt) if err != nil { return response.Error(http.StatusInternalServerError, "Failed to encode password", err) } - if passwordHashed != usr.Password { + if user.Password(passwordHashed) != usr.Password { return response.Error(http.StatusUnauthorized, "Invalid old password", nil) } - password := user.Password(cmd.NewPassword) - if password.IsWeak() { - return response.Error(http.StatusBadRequest, "New password is too short", nil) + if err := cmd.NewPassword.Validate(hs.Cfg); err != nil { + c.Logger.Warn("the new password doesn't meet the password policy criteria", "err", err) + return response.Err(err) } cmd.UserID = userID - cmd.NewPassword, err = util.EncodePassword(cmd.NewPassword, usr.Salt) + encodedPassword, err := util.EncodePassword(string(cmd.NewPassword), usr.Salt) if err != nil { return response.Error(http.StatusInternalServerError, "Failed to encode password", err) } + cmd.NewPassword = user.Password(encodedPassword) if err := hs.userService.ChangePassword(c.Req.Context(), &cmd); err != nil { return response.Error(http.StatusInternalServerError, "Failed to change user password", err) diff --git a/pkg/cmd/grafana-cli/commands/reset_password_command.go b/pkg/cmd/grafana-cli/commands/reset_password_command.go index c63ef80d814..08c8b7edb57 100644 --- a/pkg/cmd/grafana-cli/commands/reset_password_command.go +++ b/pkg/cmd/grafana-cli/commands/reset_password_command.go @@ -18,7 +18,7 @@ import ( const DefaultAdminUserId = 1 func resetPasswordCommand(c utils.CommandLine, runner server.Runner) error { - newPassword := "" + var newPassword user.Password adminId := int64(c.Int("user-id")) if c.Bool("password-from-stdin") { @@ -31,9 +31,13 @@ func resetPasswordCommand(c utils.CommandLine, runner server.Runner) error { } return fmt.Errorf("can't read password from stdin") } - newPassword = scanner.Text() + newPassword = user.Password(scanner.Text()) } else { - newPassword = c.Args().First() + newPassword = user.Password(c.Args().First()) + } + + if err := newPassword.Validate(runner.Cfg); err != nil { + return fmt.Errorf("the new password doesn't meet the password policy criteria") } err := resetPassword(adminId, newPassword, runner.UserService) @@ -44,12 +48,7 @@ func resetPasswordCommand(c utils.CommandLine, runner server.Runner) error { return err } -func resetPassword(adminId int64, newPassword string, userSvc user.Service) error { - password := user.Password(newPassword) - if password.IsWeak() { - return fmt.Errorf("new password is too short") - } - +func resetPassword(adminId int64, newPassword user.Password, userSvc user.Service) error { userQuery := user.GetUserByIDQuery{ID: adminId} usr, err := userSvc.GetByID(context.Background(), &userQuery) if err != nil { @@ -59,14 +58,14 @@ func resetPassword(adminId int64, newPassword string, userSvc user.Service) erro return ErrMustBeAdmin } - passwordHashed, err := util.EncodePassword(newPassword, usr.Salt) + passwordHashed, err := util.EncodePassword(string(newPassword), usr.Salt) if err != nil { return err } cmd := user.ChangeUserPasswordCommand{ UserID: adminId, - NewPassword: passwordHashed, + NewPassword: user.Password(passwordHashed), } if err := userSvc.ChangePassword(context.Background(), &cmd); err != nil { diff --git a/pkg/services/authn/clients/grafana.go b/pkg/services/authn/clients/grafana.go index 31a94f27205..f84a88d2ec6 100644 --- a/pkg/services/authn/clients/grafana.go +++ b/pkg/services/authn/clients/grafana.go @@ -100,7 +100,7 @@ func (c *Grafana) AuthenticatePassword(ctx context.Context, r *authn.Request, us // user was found so set auth module in req metadata r.SetMeta(authn.MetaKeyAuthModule, "grafana") - if ok := comparePassword(password, usr.Salt, usr.Password); !ok { + if ok := comparePassword(password, usr.Salt, string(usr.Password)); !ok { return nil, errInvalidPassword.Errorf("invalid password") } diff --git a/pkg/services/authn/clients/grafana_test.go b/pkg/services/authn/clients/grafana_test.go index 53dab4486e4..02d31b95572 100644 --- a/pkg/services/authn/clients/grafana_test.go +++ b/pkg/services/authn/clients/grafana_test.go @@ -171,7 +171,7 @@ func TestGrafana_AuthenticatePassword(t *testing.T) { hashed, _ := util.EncodePassword("password", "salt") userService := &usertest.FakeUserService{ ExpectedSignedInUser: tt.expectedSignedInUser, - ExpectedUser: &user.User{Password: hashed, Salt: "salt"}, + ExpectedUser: &user.User{Password: user.Password(hashed), Salt: "salt"}, } if !tt.findUser { diff --git a/pkg/services/notifications/codes.go b/pkg/services/notifications/codes.go index de3c7bfaa79..1b2df44c974 100644 --- a/pkg/services/notifications/codes.go +++ b/pkg/services/notifications/codes.go @@ -70,7 +70,7 @@ func validateUserEmailCode(cfg *setting.Cfg, user *user.User, code string) (bool } // right active code - payload := strconv.FormatInt(user.ID, 10) + user.Email + user.Login + user.Password + user.Rands + payload := strconv.FormatInt(user.ID, 10) + user.Email + user.Login + string(user.Password) + user.Rands expectedCode, err := createTimeLimitCode(cfg.SecretKey, payload, minutes, startStr) if err != nil { return false, err @@ -103,7 +103,7 @@ func getLoginForEmailCode(code string) string { func createUserEmailCode(cfg *setting.Cfg, user *user.User, startStr string) (string, error) { minutes := cfg.EmailCodeValidMinutes - payload := strconv.FormatInt(user.ID, 10) + user.Email + user.Login + user.Password + user.Rands + payload := strconv.FormatInt(user.ID, 10) + user.Email + user.Login + string(user.Password) + user.Rands code, err := createTimeLimitCode(cfg.SecretKey, payload, minutes, startStr) if err != nil { return "", err diff --git a/pkg/services/notifications/codes_test.go b/pkg/services/notifications/codes_test.go index 5ea255e0c64..36105c7545f 100644 --- a/pkg/services/notifications/codes_test.go +++ b/pkg/services/notifications/codes_test.go @@ -18,7 +18,7 @@ func TestTimeLimitCodes(t *testing.T) { user := &user.User{ID: 10, Email: "t@a.com", Login: "asd", Password: "1", Rands: "2"} format := "200601021504" - mailPayload := strconv.FormatInt(user.ID, 10) + user.Email + user.Login + user.Password + user.Rands + mailPayload := strconv.FormatInt(user.ID, 10) + user.Email + user.Login + string(user.Password) + user.Rands tenMinutesAgo := time.Now().Add(-time.Minute * 10) tests := []struct { diff --git a/pkg/services/sqlstore/sqlstore.go b/pkg/services/sqlstore/sqlstore.go index 9cc3c1b51b8..b24615fb6b8 100644 --- a/pkg/services/sqlstore/sqlstore.go +++ b/pkg/services/sqlstore/sqlstore.go @@ -226,7 +226,7 @@ func (ss *SQLStore) ensureMainOrgAndAdminUser(test bool) error { if _, err := ss.createUser(ctx, sess, user.CreateUserCommand{ Login: ss.Cfg.AdminUser, Email: ss.Cfg.AdminEmail, - Password: ss.Cfg.AdminPassword, + Password: user.Password(ss.Cfg.AdminPassword), IsAdmin: true, }); err != nil { return fmt.Errorf("failed to create admin user: %s", err) diff --git a/pkg/services/sqlstore/user.go b/pkg/services/sqlstore/user.go index 591b485482e..169bbe7ee3f 100644 --- a/pkg/services/sqlstore/user.go +++ b/pkg/services/sqlstore/user.go @@ -88,11 +88,11 @@ func (ss *SQLStore) createUser(ctx context.Context, sess *DBSession, args user.C usr.Rands = rands if len(args.Password) > 0 { - encodedPassword, err := util.EncodePassword(args.Password, usr.Salt) + encodedPassword, err := util.EncodePassword(string(args.Password), usr.Salt) if err != nil { return usr, err } - usr.Password = encodedPassword + usr.Password = user.Password(encodedPassword) } sess.UseBool("is_admin") diff --git a/pkg/services/user/model.go b/pkg/services/user/model.go index 430f0ddccf3..c771aa0e993 100644 --- a/pkg/services/user/model.go +++ b/pkg/services/user/model.go @@ -26,7 +26,7 @@ type User struct { Email string Name string Login string - Password string + Password Password Salt string Rands string Company string @@ -52,7 +52,7 @@ type CreateUserCommand struct { Company string OrgID int64 OrgName string - Password string + Password Password EmailVerified bool IsAdmin bool IsDisabled bool @@ -79,8 +79,8 @@ type UpdateUserCommand struct { } type ChangeUserPasswordCommand struct { - OldPassword string `json:"oldPassword"` - NewPassword string `json:"newPassword"` + OldPassword Password `json:"oldPassword"` + NewPassword Password `json:"newPassword"` UserID int64 `json:"-"` } @@ -280,9 +280,3 @@ type AdminCreateUserResponse struct { ID int64 `json:"id"` Message string `json:"message"` } - -type Password string - -func (p Password) IsWeak() bool { - return len(p) <= 4 -} diff --git a/pkg/services/user/password.go b/pkg/services/user/password.go new file mode 100644 index 00000000000..915d42733a5 --- /dev/null +++ b/pkg/services/user/password.go @@ -0,0 +1,71 @@ +package user + +import ( + "unicode" + + "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/util/errutil" +) + +var ( + ErrPasswordTooShort = errutil.NewBase(errutil.StatusBadRequest, "password-policy-too-short", errutil.WithPublicMessage("New password is too short")) + ErrPasswordPolicyInfringe = errutil.NewBase(errutil.StatusBadRequest, "password-policy-infringe", errutil.WithPublicMessage("New password doesn't comply with the password policy")) + MinPasswordLength = 12 +) + +type Password string + +func NewPassword(newPassword string, config *setting.Cfg) (Password, error) { + if err := ValidatePassword(newPassword, config); err != nil { + return "", err + } + return Password(newPassword), nil +} + +func (p Password) Validate(config *setting.Cfg) error { + return ValidatePassword(string(p), config) +} + +// ValidatePassword checks if a new password meets the required criteria based on the given configuration. +// If BasicAuthStrongPasswordPolicy is disabled, it only checks for password length. +// Otherwise, it ensures the password meets the minimum length requirement and contains at least one uppercase letter, +// one lowercase letter, one number, and one symbol. +func ValidatePassword(newPassword string, config *setting.Cfg) error { + if !config.BasicAuthStrongPasswordPolicy { + if len(newPassword) <= 4 { + return ErrPasswordTooShort + } + return nil + } + if len(newPassword) < MinPasswordLength { + return ErrPasswordTooShort + } + + hasUpperCase := false + hasLowerCase := false + hasNumber := false + hasSymbol := false + + for _, r := range newPassword { + if !hasLowerCase && unicode.IsLower(r) { + hasLowerCase = true + } + + if !hasUpperCase && unicode.IsUpper(r) { + hasUpperCase = true + } + + if !hasNumber && unicode.IsNumber(r) { + hasNumber = true + } + + if !hasSymbol && !unicode.IsLetter(r) && !unicode.IsNumber(r) { + hasSymbol = true + } + + if hasUpperCase && hasLowerCase && hasNumber && hasSymbol { + return nil + } + } + return ErrPasswordPolicyInfringe +} diff --git a/pkg/services/user/password_test.go b/pkg/services/user/password_test.go new file mode 100644 index 00000000000..8f8235fae13 --- /dev/null +++ b/pkg/services/user/password_test.go @@ -0,0 +1,93 @@ +package user + +import ( + "testing" + + "github.com/grafana/grafana/pkg/setting" + "github.com/stretchr/testify/assert" +) + +func TestPasswowrdService_ValidatePasswordHardcodePolicy(t *testing.T) { + LOWERCASE := "lowercase" + UPPERCASE := "UPPERCASE" + NUMBER := "123" + SYMBOLS := "!@#$%" + testCases := []struct { + expectedError error + name string + passwordTest string + strongPasswordPolicyEnabled bool + }{ + { + name: "should return error when the password has less than 4 characters and strong password policy is disabled", + passwordTest: NUMBER, + expectedError: ErrPasswordTooShort, + strongPasswordPolicyEnabled: false, + }, + {name: "should not return error when the password has 4 characters and strong password policy is disabled", + passwordTest: LOWERCASE, + expectedError: nil, + strongPasswordPolicyEnabled: false, + }, + { + name: "should return error when the password has less than 12 characters and strong password policy is enabled", + passwordTest: NUMBER, + expectedError: ErrPasswordTooShort, + strongPasswordPolicyEnabled: true, + }, + { + name: "should return error when the password is missing an uppercase character and strong password policy is enabled", + passwordTest: LOWERCASE + NUMBER + SYMBOLS, + expectedError: ErrPasswordPolicyInfringe, + strongPasswordPolicyEnabled: true, + }, + { + name: "should return error when the password is missing a lowercase character and strong password policy is enabled", + passwordTest: UPPERCASE + NUMBER + SYMBOLS, + expectedError: ErrPasswordPolicyInfringe, + strongPasswordPolicyEnabled: true, + }, + { + name: "should return error when the password is missing a number character and strong password policy is enabled", + passwordTest: LOWERCASE + UPPERCASE + SYMBOLS, + expectedError: ErrPasswordPolicyInfringe, + strongPasswordPolicyEnabled: true, + }, + { + name: "should return error when the password is missing a symbol characters and strong password policy is enabled", + passwordTest: LOWERCASE + UPPERCASE + NUMBER, + expectedError: ErrPasswordPolicyInfringe, + strongPasswordPolicyEnabled: true, + }, + { + name: "should not return error when the password has lowercase, uppercase, number and symbol and strong password policy is enabled", + passwordTest: LOWERCASE + UPPERCASE + NUMBER + SYMBOLS, + expectedError: nil, + strongPasswordPolicyEnabled: true, + }, + { + name: "should not return error when the password has uppercase, number, symbol and lowercase and strong password policy is enabled", + passwordTest: UPPERCASE + NUMBER + SYMBOLS + LOWERCASE, + expectedError: nil, + strongPasswordPolicyEnabled: true, + }, + { + name: "should not return error when the password has number, symbol, lowercase and uppercase and strong password policy is enabled", + passwordTest: NUMBER + SYMBOLS + LOWERCASE + UPPERCASE, + expectedError: nil, + strongPasswordPolicyEnabled: true, + }, + { + name: "should not return error when the password has symbol, lowercase, uppercase and number and strong password policy is enabled", + passwordTest: SYMBOLS + LOWERCASE + UPPERCASE + NUMBER, + expectedError: nil, + strongPasswordPolicyEnabled: true, + }, + } + for _, tc := range testCases { + cfg := setting.NewCfg() + cfg.BasicAuthStrongPasswordPolicy = tc.strongPasswordPolicyEnabled + err := ValidatePassword(tc.passwordTest, cfg) + assert.Equal(t, tc.expectedError, err) + } +} diff --git a/pkg/services/user/userimpl/store_test.go b/pkg/services/user/userimpl/store_test.go index 33ffd7ded73..9b6cd8391a4 100644 --- a/pkg/services/user/userimpl/store_test.go +++ b/pkg/services/user/userimpl/store_test.go @@ -209,7 +209,7 @@ func TestIntegrationUserDataAccess(t *testing.T) { require.Nil(t, err) require.Equal(t, result.Email, "usertest@test.com") - require.Equal(t, result.Password, "") + require.Equal(t, string(result.Password), "") require.Len(t, result.Rands, 10) require.Len(t, result.Salt, 10) require.False(t, result.IsDisabled) @@ -218,7 +218,7 @@ func TestIntegrationUserDataAccess(t *testing.T) { require.Nil(t, err) require.Equal(t, result.Email, "usertest@test.com") - require.Equal(t, result.Password, "") + require.Equal(t, string(result.Password), "") require.Len(t, result.Rands, 10) require.Len(t, result.Salt, 10) require.False(t, result.IsDisabled) @@ -230,7 +230,7 @@ func TestIntegrationUserDataAccess(t *testing.T) { require.Nil(t, err) require.Equal(t, result.Email, "usertest@test.com") - require.Equal(t, result.Password, "") + require.Equal(t, string(result.Password), "") require.Len(t, result.Rands, 10) require.Len(t, result.Salt, 10) require.False(t, result.IsDisabled) @@ -243,7 +243,7 @@ func TestIntegrationUserDataAccess(t *testing.T) { require.Nil(t, err) require.Equal(t, result.Email, "usertest@test.com") - require.Equal(t, result.Password, "") + require.Equal(t, string(result.Password), "") require.Len(t, result.Rands, 10) require.Len(t, result.Salt, 10) require.False(t, result.IsDisabled) @@ -252,7 +252,7 @@ func TestIntegrationUserDataAccess(t *testing.T) { require.Nil(t, err) require.Equal(t, result.Email, "usertest@test.com") - require.Equal(t, result.Password, "") + require.Equal(t, string(result.Password), "") require.Len(t, result.Rands, 10) require.Len(t, result.Salt, 10) require.False(t, result.IsDisabled) diff --git a/pkg/services/user/userimpl/user.go b/pkg/services/user/userimpl/user.go index e62d76e03eb..cdcd7be9a26 100644 --- a/pkg/services/user/userimpl/user.go +++ b/pkg/services/user/userimpl/user.go @@ -72,11 +72,16 @@ func ProvideService( func (s *Service) GetUsageStats(ctx context.Context) map[string]any { stats := map[string]any{} caseInsensitiveLoginVal := 0 + basicAuthStrongPasswordPolicyVal := 0 if s.cfg.CaseInsensitiveLogin { caseInsensitiveLoginVal = 1 } + if s.cfg.BasicAuthStrongPasswordPolicy { + basicAuthStrongPasswordPolicyVal = 1 + } stats["stats.case_insensitive_login.count"] = caseInsensitiveLoginVal + stats["stats.password_policy.count"] = basicAuthStrongPasswordPolicyVal count, err := s.store.CountUserAccountsWithEmptyRole(ctx) if err != nil { @@ -161,11 +166,11 @@ func (s *Service) Create(ctx context.Context, cmd *user.CreateUserCommand) (*use usr.Rands = rands if len(cmd.Password) > 0 { - encodedPassword, err := util.EncodePassword(cmd.Password, usr.Salt) + encodedPassword, err := util.EncodePassword(string(cmd.Password), usr.Salt) if err != nil { return nil, err } - usr.Password = encodedPassword + usr.Password = user.Password(encodedPassword) } _, err = s.store.Insert(ctx, usr) diff --git a/pkg/services/user/userimpl/user_test.go b/pkg/services/user/userimpl/user_test.go index 549515d458f..b33aa2eb4f7 100644 --- a/pkg/services/user/userimpl/user_test.go +++ b/pkg/services/user/userimpl/user_test.go @@ -219,13 +219,15 @@ func TestMetrics(t *testing.T) { userService.cfg = setting.NewCfg() userService.cfg.CaseInsensitiveLogin = true + userService.cfg.BasicAuthStrongPasswordPolicy = true stats := userService.GetUsageStats(context.Background()) assert.NotEmpty(t, stats) - assert.Len(t, stats, 2, stats) + assert.Len(t, stats, 3, stats) assert.Equal(t, 1, stats["stats.case_insensitive_login.count"]) assert.Equal(t, int64(1), stats["stats.user.role_none.count"]) + assert.Equal(t, 1, stats["stats.password_policy.count"]) }) } diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index 5d6ed89392d..05e89eed226 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -218,24 +218,25 @@ type Cfg struct { DefaultHomeDashboardPath string // Auth - LoginCookieName string - LoginMaxInactiveLifetime time.Duration - LoginMaxLifetime time.Duration - TokenRotationIntervalMinutes int - SigV4AuthEnabled bool - SigV4VerboseLogging bool - AzureAuthEnabled bool - AzureSkipOrgRoleSync bool - BasicAuthEnabled bool - AdminUser string - AdminPassword string - DisableLogin bool - AdminEmail string - DisableLoginForm bool - SignoutRedirectUrl string - IDResponseHeaderEnabled bool - IDResponseHeaderPrefix string - IDResponseHeaderNamespaces map[string]struct{} + LoginCookieName string + LoginMaxInactiveLifetime time.Duration + LoginMaxLifetime time.Duration + TokenRotationIntervalMinutes int + SigV4AuthEnabled bool + SigV4VerboseLogging bool + AzureAuthEnabled bool + AzureSkipOrgRoleSync bool + BasicAuthEnabled bool + BasicAuthStrongPasswordPolicy bool + AdminUser string + AdminPassword string + DisableLogin bool + AdminEmail string + DisableLoginForm bool + SignoutRedirectUrl string + IDResponseHeaderEnabled bool + IDResponseHeaderPrefix string + IDResponseHeaderNamespaces map[string]struct{} // Not documented & not supported // stand in until a more complete solution is implemented AuthConfigUIAdminAccess bool @@ -1591,6 +1592,7 @@ func readAuthSettings(iniFile *ini.File, cfg *Cfg) (err error) { // basic auth authBasic := iniFile.Section("auth.basic") cfg.BasicAuthEnabled = authBasic.Key("enabled").MustBool(true) + cfg.BasicAuthStrongPasswordPolicy = authBasic.Key("password_policy").MustBool(false) // Extended JWT auth authExtendedJWT := cfg.SectionWithEnvOverrides("auth.extended_jwt") diff --git a/pkg/tests/api/correlations/common_test.go b/pkg/tests/api/correlations/common_test.go index 35696356f03..9dd112fff8b 100644 --- a/pkg/tests/api/correlations/common_test.go +++ b/pkg/tests/api/correlations/common_test.go @@ -50,7 +50,7 @@ func NewTestEnv(t *testing.T) TestContext { type User struct { User user.User - password string + password user.Password } type GetParams struct { diff --git a/pkg/tests/apis/helper.go b/pkg/tests/apis/helper.go index fbf2f304e73..d0fca227bc4 100644 --- a/pkg/tests/apis/helper.go +++ b/pkg/tests/apis/helper.go @@ -390,7 +390,7 @@ func (c K8sTestHelper) createTestUsers(orgName string) OrgUsers { createUser := func(key string, role org.RoleType) User { u, err := userSvc.Create(context.Background(), &user.CreateUserCommand{ DefaultOrgRole: string(role), - Password: key, + Password: user.Password(key), Login: fmt.Sprintf("%s-%d", key, orgId), OrgID: orgId, }) diff --git a/public/api-enterprise-spec.json b/public/api-enterprise-spec.json index 5fbeb8a8096..097d24e5705 100644 --- a/public/api-enterprise-spec.json +++ b/public/api-enterprise-spec.json @@ -2147,7 +2147,7 @@ "format": "int64" }, "password": { - "type": "string" + "$ref": "#/definitions/Password" } } }, @@ -2268,7 +2268,7 @@ "type": "object", "properties": { "password": { - "type": "string" + "$ref": "#/definitions/Password" } } }, @@ -3020,10 +3020,10 @@ "type": "object", "properties": { "newPassword": { - "type": "string" + "$ref": "#/definitions/Password" }, "oldPassword": { - "type": "string" + "$ref": "#/definitions/Password" } } }, @@ -5440,6 +5440,9 @@ } } }, + "Password": { + "type": "string" + }, "PatchAnnotationsCmd": { "type": "object", "properties": { diff --git a/public/api-merged.json b/public/api-merged.json index 42a22d19b30..4d724641091 100644 --- a/public/api-merged.json +++ b/public/api-merged.json @@ -11773,7 +11773,7 @@ "format": "int64" }, "password": { - "type": "string" + "$ref": "#/definitions/Password" } } }, @@ -11894,7 +11894,7 @@ "type": "object", "properties": { "password": { - "type": "string" + "$ref": "#/definitions/Password" } } }, @@ -13196,10 +13196,10 @@ "type": "object", "properties": { "newPassword": { - "type": "string" + "$ref": "#/definitions/Password" }, "oldPassword": { - "type": "string" + "$ref": "#/definitions/Password" } } }, @@ -17176,6 +17176,9 @@ } } }, + "Password": { + "type": "string" + }, "PatchAnnotationsCmd": { "type": "object", "properties": { diff --git a/public/openapi3.json b/public/openapi3.json index dbe2e8efc6c..14ae4182c7c 100644 --- a/public/openapi3.json +++ b/public/openapi3.json @@ -2341,7 +2341,7 @@ "type": "integer" }, "password": { - "type": "string" + "$ref": "#/components/schemas/Password" } }, "type": "object" @@ -2462,7 +2462,7 @@ "AdminUpdateUserPasswordForm": { "properties": { "password": { - "type": "string" + "$ref": "#/components/schemas/Password" } }, "type": "object" @@ -3764,10 +3764,10 @@ "ChangeUserPasswordCommand": { "properties": { "newPassword": { - "type": "string" + "$ref": "#/components/schemas/Password" }, "oldPassword": { - "type": "string" + "$ref": "#/components/schemas/Password" } }, "type": "object" @@ -7745,6 +7745,9 @@ }, "type": "object" }, + "Password": { + "type": "string" + }, "PatchAnnotationsCmd": { "properties": { "data": {