mirror of
https://github.com/yunionio/cloudpods.git
synced 2026-09-01 15:07:17 +08:00
feature: support password polices: minimal length, repeat check,
password expiration, multi auth failures locks user
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user