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:
linoman
2024-02-16 04:58:05 -06:00
committed by GitHub
parent 846eadff63
commit ac84069071
27 changed files with 300 additions and 105 deletions

View File

@ -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]

View File

@ -745,6 +745,7 @@
#################################### Basic Auth ##########################
[auth.basic]
;enabled = true
;password_policy = false
#################################### Auth Proxy ##########################
[auth.proxy]

View File

@ -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 {

View File

@ -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"`
}

View File

@ -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 {

View File

@ -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,

View File

@ -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)

View File

@ -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)

View File

@ -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 {

View File

@ -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")
}

View File

@ -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 {

View File

@ -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

View File

@ -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 {

View File

@ -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)

View File

@ -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")

View File

@ -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
}

View 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
}

View 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)
}
}

View File

@ -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)

View File

@ -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)

View File

@ -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"])
})
}

View File

@ -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")

View File

@ -50,7 +50,7 @@ func NewTestEnv(t *testing.T) TestContext {
type User struct {
User user.User
password string
password user.Password
}
type GetParams struct {

View File

@ -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,
})

View File

@ -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": {

View File

@ -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": {

View File

@ -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": {