mirror of
https://github.com/grafana/grafana.git
synced 2025-07-31 15:22:26 +08:00
Password policy (#82268)
* add password service interface * add password service implementation * add tests for password service * add password service wiring * add feature toggle * Rework from service interface to static function * Replace previous password validations * Add codeowners to password service * add error logs * update config files --------- Co-authored-by: Karl Persson <kalle.persson@grafana.com>
This commit is contained in:
@ -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]
|
||||
|
@ -745,6 +745,7 @@
|
||||
#################################### Basic Auth ##########################
|
||||
[auth.basic]
|
||||
;enabled = true
|
||||
;password_policy = false
|
||||
|
||||
#################################### Auth Proxy ##########################
|
||||
[auth.proxy]
|
||||
|
@ -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 {
|
||||
|
@ -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"`
|
||||
@ -21,6 +24,6 @@ type CompleteInviteForm struct {
|
||||
Email string `json:"email" binding:"Required"`
|
||||
Name string `json:"name"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
ConfirmPassword string `json:"confirmPassword"`
|
||||
Password user.Password `json:"password"`
|
||||
ConfirmPassword user.Password `json:"confirmPassword"`
|
||||
}
|
||||
|
@ -1,5 +1,7 @@
|
||||
package dtos
|
||||
|
||||
import "github.com/grafana/grafana/pkg/services/user"
|
||||
|
||||
type SignUpForm struct {
|
||||
Email string `json:"email" binding:"Required"`
|
||||
}
|
||||
@ -8,7 +10,7 @@ type SignUpStep2Form struct {
|
||||
Email string `json:"email"`
|
||||
Name string `json:"name"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Password user.Password `json:"password"`
|
||||
Code string `json:"code"`
|
||||
OrgName string `json:"orgName"`
|
||||
}
|
||||
@ -17,12 +19,12 @@ type AdminCreateUserForm struct {
|
||||
Email string `json:"email"`
|
||||
Login string `json:"login"`
|
||||
Name string `json:"name"`
|
||||
Password string `json:"password" binding:"Required"`
|
||||
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 {
|
||||
@ -35,8 +37,8 @@ type SendResetPasswordEmailForm struct {
|
||||
|
||||
type ResetUserPasswordForm struct {
|
||||
Code string `json:"code"`
|
||||
NewPassword string `json:"newPassword"`
|
||||
ConfirmPassword string `json:"confirmPassword"`
|
||||
NewPassword user.Password `json:"newPassword"`
|
||||
ConfirmPassword user.Password `json:"confirmPassword"`
|
||||
}
|
||||
|
||||
type UserLookupDTO struct {
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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 {
|
||||
|
@ -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")
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
|
@ -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")
|
||||
|
@ -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
|
||||
}
|
||||
|
71
pkg/services/user/password.go
Normal file
71
pkg/services/user/password.go
Normal file
@ -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
|
||||
}
|
93
pkg/services/user/password_test.go
Normal file
93
pkg/services/user/password_test.go
Normal file
@ -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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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"])
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -227,6 +227,7 @@ type Cfg struct {
|
||||
AzureAuthEnabled bool
|
||||
AzureSkipOrgRoleSync bool
|
||||
BasicAuthEnabled bool
|
||||
BasicAuthStrongPasswordPolicy bool
|
||||
AdminUser string
|
||||
AdminPassword string
|
||||
DisableLogin 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")
|
||||
|
@ -50,7 +50,7 @@ func NewTestEnv(t *testing.T) TestContext {
|
||||
|
||||
type User struct {
|
||||
User user.User
|
||||
password string
|
||||
password user.Password
|
||||
}
|
||||
|
||||
type GetParams struct {
|
||||
|
@ -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,
|
||||
})
|
||||
|
@ -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": {
|
||||
|
@ -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": {
|
||||
|
@ -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": {
|
||||
|
Reference in New Issue
Block a user