feature: support password polices: minimal length, repeat check,

password expiration, multi auth failures locks user
This commit is contained in:
Qiu Jian
2019-10-30 02:02:02 +08:00
parent 5d0f9488cd
commit 93adf05547
7 changed files with 158 additions and 17 deletions
+10
View File
@@ -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
}
+36
View File
@@ -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
}
+46 -3
View File
@@ -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")
+42 -4
View File
@@ -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
}
+6
View File
@@ -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 (
+12 -4
View File
@@ -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()
+6 -6
View File
@@ -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 {