diff --git a/pkg/keystone/driver/sql/sql.go b/pkg/keystone/driver/sql/sql.go index 97b9ae6d77..8d8739038b 100644 --- a/pkg/keystone/driver/sql/sql.go +++ b/pkg/keystone/driver/sql/sql.go @@ -22,6 +22,7 @@ import ( api "yunion.io/x/onecloud/pkg/apis/identity" "yunion.io/x/onecloud/pkg/keystone/driver" "yunion.io/x/onecloud/pkg/keystone/models" + o "yunion.io/x/onecloud/pkg/keystone/options" "yunion.io/x/onecloud/pkg/mcclient" ) @@ -49,10 +50,19 @@ func (sql *SSQLDriver) Authenticate(ctx context.Context, ident mcclient.SAuthent if err != nil { return nil, errors.Wrap(err, "UserManager.FetchUserExtended") } + localUser, err := models.LocalUserManager.FetchLocalUser(usrExt) + if err != nil { + return nil, errors.Wrap(err, "LocalUserManager.FetchLocalUser") + } err = models.VerifyPassword(usrExt, ident.Password.User.Password) if err != nil { + localUser.SaveFailedAuth() + if localUser.FailedAuthCount > o.Options.PasswordErrorLockCount { + models.UserManager.LockUser(usrExt.Id) + } return nil, errors.Wrap(err, "usrExt.VerifyPassword") } + localUser.ClearFailedAuth() return usrExt, nil } diff --git a/pkg/keystone/models/localusers.go b/pkg/keystone/models/localusers.go index 67640438b3..3b176960ab 100644 --- a/pkg/keystone/models/localusers.go +++ b/pkg/keystone/models/localusers.go @@ -21,6 +21,7 @@ import ( "github.com/pkg/errors" + api "yunion.io/x/onecloud/pkg/apis/identity" "yunion.io/x/onecloud/pkg/cloudcommon/db" ) @@ -74,6 +75,17 @@ func (user *SLocalUser) GetName() string { return user.Name } +func (manager *SLocalUserManager) FetchLocalUser(usrExt *api.SUserExtended) (*SLocalUser, error) { + localUser := SLocalUser{} + localUser.SetModelManager(manager, &localUser) + q := manager.Query().Equals("id", usrExt.LocalId) + err := q.First(&localUser) + if err != nil { + return nil, errors.Wrap(err, "Query") + } + return &localUser, nil +} + func (manager *SLocalUserManager) register(userId string, domainId string, name string) (*SLocalUser, error) { localUser := SLocalUser{} localUser.SetModelManager(manager, &localUser) @@ -121,3 +133,27 @@ func (manager *SLocalUserManager) delete(userId string, domainId string) (*SLoca return &localUser, nil } + +func (usr *SLocalUser) SaveFailedAuth() error { + _, err := db.Update(usr, func() error { + usr.FailedAuthCount += 1 + usr.FailedAuthAt = time.Now() + return nil + }) + if err != nil { + return errors.Wrap(err, "Update") + } + return nil +} + +func (usr *SLocalUser) ClearFailedAuth() error { + _, err := db.Update(usr, func() error { + usr.FailedAuthCount = 0 + usr.FailedAuthAt = time.Time{} + return nil + }) + if err != nil { + return errors.Wrap(err, "Update") + } + return nil +} diff --git a/pkg/keystone/models/passwords.go b/pkg/keystone/models/passwords.go index be18c44048..273d9687dd 100644 --- a/pkg/keystone/models/passwords.go +++ b/pkg/keystone/models/passwords.go @@ -15,14 +15,16 @@ package models import ( + "crypto/sha256" "database/sql" + "encoding/hex" "time" - "github.com/pkg/errors" - + "yunion.io/x/pkg/errors" "yunion.io/x/sqlchemy" "yunion.io/x/onecloud/pkg/cloudcommon/db" + o "yunion.io/x/onecloud/pkg/keystone/options" "yunion.io/x/onecloud/pkg/util/seclib2" ) @@ -73,6 +75,22 @@ type SPassword struct { ExpiresAtInt int64 `nullable:"true"` } +func shaPassword(passwd string) string { + shaOut := sha256.Sum224([]byte(passwd)) + return hex.EncodeToString(shaOut[:]) +} + +func (manager *SPasswordManager) FetchLastPassword(localUserId int) (*SPassword, error) { + passes, err := manager.fetchByLocaluserId(localUserId) + if err != nil { + return nil, err + } + if len(passes) == 0 { + return nil, nil + } + return &passes[0], nil +} + func (manager *SPasswordManager) fetchByLocaluserId(localUserId int) ([]SPassword, error) { passes := make([]SPassword, 0) passwords := manager.Query().SubQuery() @@ -92,6 +110,25 @@ func (manager *SPasswordManager) fetchByLocaluserId(localUserId int) ([]SPasswor return passes, nil } +func (manager *SPasswordManager) verifyPassword(localUserId int, password string) error { + if o.Options.PasswordMinimalLength > 0 && len(password) < o.Options.PasswordMinimalLength { + return errors.Error("too simple password") + } + if o.Options.PasswordUniqueHistoryCheck > 0 { + shaPass := shaPassword(password) + histPasses, err := manager.fetchByLocaluserId(localUserId) + if err != nil { + return errors.Wrap(err, "manager.fetchByLocaluserId") + } + for i := 0; i < len(histPasses) && i < o.Options.PasswordUniqueHistoryCheck; i += 1 { + if histPasses[i].Password == shaPass { + return errors.Error("repeated password") + } + } + } + return nil +} + func (manager *SPasswordManager) savePassword(localUserId int, password string) error { hash, err := seclib2.BcryptPassword(password) if err != nil { @@ -100,7 +137,13 @@ func (manager *SPasswordManager) savePassword(localUserId int, password string) rec := SPassword{} rec.LocalUserId = localUserId rec.PasswordHash = hash - rec.CreatedAtInt = time.Now().UnixNano() / 1000 + rec.Password = shaPassword(password) + now := time.Now() + rec.CreatedAtInt = now.UnixNano() / 1000 + if o.Options.PasswordExpirationDays > 0 { + rec.ExpiresAt = now.Add(24 * time.Hour * time.Duration(o.Options.PasswordExpirationDays)) + rec.ExpiresAtInt = rec.ExpiresAt.UnixNano() / 1000 + } err = manager.TableSpec().Insert(&rec) if err != nil { return errors.Wrap(err, "Insert") diff --git a/pkg/keystone/models/users.go b/pkg/keystone/models/users.go index bdf1eabb34..a03a7d3f2c 100644 --- a/pkg/keystone/models/users.go +++ b/pkg/keystone/models/users.go @@ -31,7 +31,7 @@ import ( "yunion.io/x/onecloud/pkg/cloudcommon/db" "yunion.io/x/onecloud/pkg/cloudcommon/policy" "yunion.io/x/onecloud/pkg/httperrors" - "yunion.io/x/onecloud/pkg/keystone/options" + o "yunion.io/x/onecloud/pkg/keystone/options" "yunion.io/x/onecloud/pkg/mcclient" "yunion.io/x/onecloud/pkg/util/logclient" "yunion.io/x/onecloud/pkg/util/rbacutils" @@ -179,14 +179,14 @@ func (manager *SUserManager) initSysUser() error { } if cnt == 1 { // if ResetAdminUserPassword is true, reset sysadmin password - if options.Options.ResetAdminUserPassword { + if o.Options.ResetAdminUserPassword { usr := SUser{} usr.SetModelManager(manager, &usr) err = q.First(&usr) if err != nil { return errors.Wrap(err, "ResetAdminUserPassword Query user") } - err = usr.initLocalData(options.Options.BootstrapAdminUserPassword) + err = usr.initLocalData(o.Options.BootstrapAdminUserPassword) if err != nil { return errors.Wrap(err, "initLocalData") } @@ -212,7 +212,7 @@ func (manager *SUserManager) initSysUser() error { if err != nil { return errors.Wrap(err, "insert") } - err = usr.initLocalData(options.Options.BootstrapAdminUserPassword) + err = usr.initLocalData(o.Options.BootstrapAdminUserPassword) if err != nil { return errors.Wrap(err, "initLocalData") } @@ -385,6 +385,16 @@ func (manager *SUserManager) FilterByHiddenSystemAttributes(q *sqlchemy.SQuery, return q } +func (manager *SUserManager) ValidateCreateData(ctx context.Context, userCred mcclient.TokenCredential, ownerId mcclient.IIdentityProvider, query jsonutils.JSONObject, data *jsonutils.JSONDict) (*jsonutils.JSONDict, error) { + passwd, _ := data.GetString("password") + if len(passwd) > 0 { + if o.Options.PasswordMinimalLength > 0 && len(passwd) < o.Options.PasswordMinimalLength { + return nil, errors.Error("too simple password") + } + } + return manager.SEnabledIdentityBaseResourceManager.ValidateCreateData(ctx, userCred, ownerId, query, data) +} + func (user *SUser) ValidateUpdateData(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data *jsonutils.JSONDict) (*jsonutils.JSONDict, error) { if data.Contains("name") { if user.IsAdminUser() { @@ -405,6 +415,17 @@ func (user *SUser) ValidateUpdateData(ctx context.Context, userCred mcclient.Tok } } } + passwd, _ := data.GetString("password") + if len(passwd) > 0 { + usrExt, err := UserManager.FetchUserExtended(user.Id, "", "", "") + if err != nil { + return nil, errors.Wrap(err, "UserManager.FetchUserExtended") + } + err = PasswordManager.verifyPassword(usrExt.LocalId, passwd) + if err != nil { + return nil, httperrors.NewInputParameterError("invalid password: %s", err) + } + } return user.SEnabledIdentityBaseResource.ValidateUpdateData(ctx, userCred, query, data) } @@ -816,3 +837,20 @@ func leaveProjects(ident db.IModel, isUser bool, ctx context.Context, userCred m } return nil } + +func (manager *SUserManager) LockUser(uid string) error { + usrObj, err := manager.FetchById(uid) + if err != nil { + return errors.Wrapf(err, "manager.FetchById %s", uid) + } + usr := usrObj.(*SUser) + diff, err := db.Update(usr, func() error { + usr.Enabled = tristate.False + return nil + }) + if err != nil { + return errors.Wrap(err, "Update") + } + db.OpsLog.LogEvent(usr, db.ACT_UPDATE, diff, GetDefaultAdminCred()) + return nil +} diff --git a/pkg/keystone/options/options.go b/pkg/keystone/options/options.go index 55e5f0492d..7fb405a4c2 100644 --- a/pkg/keystone/options/options.go +++ b/pkg/keystone/options/options.go @@ -37,6 +37,12 @@ type SKeystoneOptions struct { DefaultSyncIntervalSeconds int `help:"frequency to do auto sync tasks" default:"900"` FetchProjectResourceCountIntervalSeconds int `help:"frequency tp fetch project resource counts" default:"900"` + + PasswordExpirationDays int `help:"password expires after the duration"` + PasswordMinimalLength int `help:"password minimal length"` + PasswordUniqueHistoryCheck int `help:"password must be unique in last N passwords"` + + PasswordErrorLockCount int `help:"lock user account if given number of failed auth"` } var ( diff --git a/pkg/keystone/tokens/token.go b/pkg/keystone/tokens/token.go index 33402d5a68..7983e4ce6c 100644 --- a/pkg/keystone/tokens/token.go +++ b/pkg/keystone/tokens/token.go @@ -19,8 +19,7 @@ import ( "strings" "time" - "github.com/pkg/errors" - + "yunion.io/x/pkg/errors" "yunion.io/x/pkg/utils" api "yunion.io/x/onecloud/pkg/apis/identity" @@ -255,11 +254,20 @@ func (t *SAuthToken) getTokenV3( token.Token.Methods = []string{t.Method} token.Token.User.Id = user.Id token.Token.User.Name = user.Name + token.Token.User.Domain.Id = user.DomainId + token.Token.User.Domain.Name = user.DomainName + if user.IsLocal { + lastPass, err := models.PasswordManager.FetchLastPassword(user.LocalId) + if err != nil { + return nil, errors.Wrap(err, "FetchLastPassword") + } + if lastPass != nil && !lastPass.ExpiresAt.IsZero() { + token.Token.User.PasswordExpiresAt = lastPass.ExpiresAt + } + } token.Token.User.Displayname = user.Displayname token.Token.User.Email = user.Email token.Token.User.Mobile = user.Mobile - token.Token.User.Domain.Id = user.DomainId - token.Token.User.Domain.Name = user.DomainName token.Token.Context = t.Context tk, err := t.EncodeFernetToken() diff --git a/pkg/mcclient/token3.go b/pkg/mcclient/token3.go index bd0a863d73..a04c73cd54 100644 --- a/pkg/mcclient/token3.go +++ b/pkg/mcclient/token3.go @@ -56,10 +56,10 @@ type KeystoneProjectV3 struct { } type KeystoneUserV3 struct { - Id string - Name string - Domain KeystoneDomainV3 - Password_expires_at time.Time + Id string + Name string + Domain KeystoneDomainV3 + PasswordExpiresAt time.Time Displayname string Email string @@ -95,7 +95,7 @@ type KeystoneTokenV3 struct { type TokenCredentialV3 struct { Token KeystoneTokenV3 `json:"token"` - Id string `json:"-"` + Id string `json:"id"` } func (token *TokenCredentialV3) GetTokenString() string { @@ -155,7 +155,7 @@ func (this *TokenCredentialV3) GetExpires() time.Time { } func (this *TokenCredentialV3) IsValid() bool { - return this.ValidDuration() > 0 + return len(this.Id) > 0 && this.ValidDuration() > 0 } func (this *TokenCredentialV3) ValidDuration() time.Duration {