mirror of
https://github.com/yunionio/cloudpods.git
synced 2026-09-19 02:37:24 +08:00
feature: user expired_at attribute support (#23727)
Co-authored-by: Qiu Jian <qiujian@yunionyun.com>
This commit is contained in:
@@ -191,6 +191,8 @@ func init() {
|
||||
IdpEntityId string `help:"Entity id of identity provider to link with"`
|
||||
|
||||
Lang string `help:"user default language"`
|
||||
|
||||
Expire string `help:"user expired at"`
|
||||
}
|
||||
R(&UserCreateOptions{}, "user-create", "Create a user", func(s *mcclient.ClientSession, args *UserCreateOptions) error {
|
||||
params := jsonutils.NewDict()
|
||||
@@ -245,6 +247,10 @@ func init() {
|
||||
params.Add(jsonutils.NewString(args.Lang), "lang")
|
||||
}
|
||||
|
||||
if len(args.Expire) > 0 {
|
||||
params.Add(jsonutils.NewString(args.Expire), "expired_at")
|
||||
}
|
||||
|
||||
/*if len(args.DefaultProject) > 0 {
|
||||
projId, err := modules.Projects.GetId(s, args.DefaultProject, nil)
|
||||
if err != nil {
|
||||
@@ -288,6 +294,10 @@ func init() {
|
||||
SkipPasswordComplexityCheck bool `help:"skip_password_complexity_check"`
|
||||
|
||||
Lang string `help:"update user language"`
|
||||
|
||||
Expire string `help:"user expired at"`
|
||||
|
||||
ClearExpire bool `help:"clear user expired at"`
|
||||
}
|
||||
R(&UserUpdateOptions{}, "user-update", "Update a user", func(s *mcclient.ClientSession, args *UserUpdateOptions) error {
|
||||
query := jsonutils.NewDict()
|
||||
@@ -347,6 +357,11 @@ func init() {
|
||||
if len(args.Lang) > 0 {
|
||||
params.Add(jsonutils.NewString(args.Lang), "lang")
|
||||
}
|
||||
if args.ClearExpire {
|
||||
params.Add(jsonutils.JSONTrue, "clear_expire")
|
||||
} else if len(args.Expire) > 0 {
|
||||
params.Add(jsonutils.NewString(args.Expire), "expired_at")
|
||||
}
|
||||
// if len(args.DefaultProject) > 0 {
|
||||
// projId, err := modules.Projects.GetId(s, args.DefaultProject, nil)
|
||||
// if err != nil {
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
package identity
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"yunion.io/x/jsonutils"
|
||||
"yunion.io/x/pkg/errors"
|
||||
"yunion.io/x/pkg/util/rbacscope"
|
||||
@@ -501,6 +503,12 @@ type UserUpdateInput struct {
|
||||
SkipPasswordComplexityCheck *bool `json:"skip_password_complexity_check"`
|
||||
|
||||
Lang string `json:"lang"`
|
||||
|
||||
// 过期时间
|
||||
ExpiredAt *time.Time `json:"expired_at"`
|
||||
|
||||
// 清除过期时间
|
||||
ClearExpire *bool `json:"clear_expire"`
|
||||
}
|
||||
|
||||
type UserCreateInput struct {
|
||||
@@ -527,6 +535,8 @@ type UserCreateInput struct {
|
||||
IdpEntityId string `json:"idp_entity_id"`
|
||||
|
||||
Lang string `json:"lang"`
|
||||
|
||||
ExpiredAt *time.Time `json:"expired_at"`
|
||||
}
|
||||
|
||||
type ProjectCreateInput struct {
|
||||
|
||||
@@ -20,6 +20,7 @@ type SUserExtended struct {
|
||||
Id string
|
||||
Name string
|
||||
Enabled bool
|
||||
ExpiredAt time.Time
|
||||
DefaultProjectId string
|
||||
CreatedAt time.Time
|
||||
LastActiveAt time.Time
|
||||
|
||||
@@ -91,6 +91,7 @@ const (
|
||||
ErrUserNotFound = errors.Error("UserNotFound")
|
||||
ErrUserLocked = errors.Error("UserLocked")
|
||||
ErrUserDisabled = errors.Error("UserDisabled")
|
||||
ErrUserExpired = errors.Error("UserExpired")
|
||||
ErrWrongPassword = errors.Error("WrongPassword")
|
||||
ErrIncorrectUsernameOrPassword = errors.Error("IncorrectUsernameOrPassword")
|
||||
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
// Copyright 2019 Yunion
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package models
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"time"
|
||||
|
||||
"yunion.io/x/pkg/errors"
|
||||
"yunion.io/x/pkg/utils"
|
||||
|
||||
"yunion.io/x/onecloud/pkg/cloudcommon/db"
|
||||
"yunion.io/x/onecloud/pkg/mcclient"
|
||||
)
|
||||
|
||||
// +onecloud:swagger-gen-ignore
|
||||
type SUserLoginManager struct {
|
||||
db.SModelBaseManager
|
||||
}
|
||||
|
||||
var UserLoginManager *SUserLoginManager
|
||||
|
||||
func init() {
|
||||
UserLoginManager = &SUserLoginManager{
|
||||
SModelBaseManager: db.NewModelBaseManager(
|
||||
SUserLogin{},
|
||||
"user_login",
|
||||
"user_login",
|
||||
"user_logins",
|
||||
),
|
||||
}
|
||||
UserLoginManager.SetVirtualObject(UserLoginManager)
|
||||
}
|
||||
|
||||
// +onecloud:swagger-gen-ignore
|
||||
type SUserLogin struct {
|
||||
db.SModelBase
|
||||
|
||||
UserId string `width:"64" charset:"ascii" nullable:"false" primary:"true"`
|
||||
|
||||
// 上次登录时间
|
||||
LastActiveAt time.Time `nullable:"true" list:"domain"`
|
||||
// 上次用户登录IP
|
||||
LastLoginIp string `nullable:"true" list:"domain"`
|
||||
// 上次用户登录方式,可能值有:web(web控制台),cli(命令行climc),API(api)
|
||||
LastLoginSource string `nullable:"true" list:"domain"`
|
||||
}
|
||||
|
||||
func (manager *SUserLoginManager) fetchUserLogin(userId string) (*SUserLogin, error) {
|
||||
userLogin := &SUserLogin{}
|
||||
userLogin.SetModelManager(manager, userLogin)
|
||||
err := manager.Query().Equals("user_id", userId).First(userLogin)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Query")
|
||||
}
|
||||
return userLogin, nil
|
||||
}
|
||||
|
||||
func (manager *SUserLoginManager) traceLoginEvent(ctx context.Context, userId string, authCtx mcclient.SAuthContext) error {
|
||||
userLogin, err := manager.fetchUserLogin(userId)
|
||||
if err != nil {
|
||||
if errors.Cause(err) == sql.ErrNoRows {
|
||||
// do insert
|
||||
userLogin := &SUserLogin{
|
||||
UserId: userId,
|
||||
LastActiveAt: time.Now().UTC(),
|
||||
LastLoginIp: authCtx.Ip,
|
||||
LastLoginSource: authCtx.Source,
|
||||
}
|
||||
err := manager.TableSpec().Insert(ctx, userLogin)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Insert")
|
||||
}
|
||||
return nil
|
||||
} else {
|
||||
return errors.Wrap(err, "fetchUserLogin")
|
||||
}
|
||||
}
|
||||
// only save web console login record
|
||||
if userLogin.LastActiveAt.IsZero() || utils.IsInArray(authCtx.Source, []string{mcclient.AuthSourceWeb}) {
|
||||
_, err := db.Update(userLogin, func() error {
|
||||
userLogin.LastActiveAt = time.Now().UTC()
|
||||
userLogin.LastLoginIp = authCtx.Ip
|
||||
userLogin.LastLoginSource = authCtx.Source
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Update")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -17,6 +17,7 @@ package models
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"yunion.io/x/jsonutils"
|
||||
@@ -24,7 +25,6 @@ import (
|
||||
"yunion.io/x/pkg/errors"
|
||||
"yunion.io/x/pkg/tristate"
|
||||
"yunion.io/x/pkg/util/rbacscope"
|
||||
"yunion.io/x/pkg/utils"
|
||||
"yunion.io/x/sqlchemy"
|
||||
|
||||
"yunion.io/x/onecloud/pkg/apis"
|
||||
@@ -94,10 +94,16 @@ type SUser struct {
|
||||
Displayname string `with:"128" charset:"utf8" nullable:"true" list:"domain" update:"domain" create:"domain_optional"`
|
||||
|
||||
// 上次登录时间
|
||||
// deprecated
|
||||
// swagger:ignore
|
||||
LastActiveAt time.Time `nullable:"true" list:"domain"`
|
||||
// 上次用户登录IP
|
||||
// deprecated
|
||||
// swagger:ignore
|
||||
LastLoginIp string `nullable:"true" list:"domain"`
|
||||
// 上次用户登录方式,可能值有:web(web控制台),cli(命令行climc),API(api)
|
||||
// deprecated
|
||||
// swagger:ignore
|
||||
LastLoginSource string `nullable:"true" list:"domain"`
|
||||
|
||||
// 是否为系统账号,系统账号不会检查密码复杂度,默认不在列表显示
|
||||
@@ -113,6 +119,9 @@ type SUser struct {
|
||||
|
||||
// 用户的默认语言设置,默认是zh_CN
|
||||
Lang string `width:"8" charset:"ascii" nullable:"false" list:"domain" update:"domain" create:"domain_optional"`
|
||||
|
||||
// 过期时间
|
||||
ExpiredAt time.Time `nullable:"true" list:"domain" update:"domain" create:"domain_optional"`
|
||||
}
|
||||
|
||||
func (manager *SUserManager) GetContextManagers() [][]db.IModelManager {
|
||||
@@ -178,6 +187,49 @@ func (manager *SUserManager) InitializeData() error {
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
err := manager.migrateUserLogin()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "migrateUserLogin")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (manager *SUserManager) migrateUserLogin() error {
|
||||
userLoginQ := UserLoginManager.Query("user_id").SubQuery()
|
||||
q := manager.Query().NotIn("id", userLoginQ)
|
||||
|
||||
rows, err := q.Rows()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "query.Rows")
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
type SUserLoginExt struct {
|
||||
SUserLogin
|
||||
Id string
|
||||
}
|
||||
for rows.Next() {
|
||||
userLogin := SUserLoginExt{}
|
||||
err := q.Row2Struct(rows, &userLogin)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "row2struct")
|
||||
}
|
||||
userLogin.UserId = userLogin.Id
|
||||
userLogin.SUserLogin.SetModelManager(UserLoginManager, &userLogin.SUserLogin)
|
||||
err = UserLoginManager.TableSpec().Insert(context.Background(), &userLogin.SUserLogin)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "insert")
|
||||
}
|
||||
}
|
||||
|
||||
sql := fmt.Sprintf("UPDATE `%s` SET last_active_at = NULL, last_login_ip = NULL, last_login_source = NULL WHERE last_active_at IS NOT NULL", manager.TableSpec().Name())
|
||||
_, err = manager.TableSpec().GetTableSpec().Database().Exec(sql)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "exec batch update")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -300,6 +352,7 @@ func (manager *SUserManager) FetchUserExtended(userId, userName, domainId, domai
|
||||
users.Field("last_active_at"),
|
||||
users.Field("domain_id"),
|
||||
users.Field("is_system_account"),
|
||||
users.Field("expired_at"),
|
||||
localUsers.Field("id", "local_id"),
|
||||
localUsers.Field("name", "local_name"),
|
||||
localUsers.Field("failed_auth_count", "local_failed_auth_count"),
|
||||
@@ -632,6 +685,10 @@ func (user *SUser) ValidateUpdateData(ctx context.Context, userCred mcclient.Tok
|
||||
boolTrue := true
|
||||
input.EnableMfa = &boolTrue
|
||||
}
|
||||
if input.ClearExpire != nil && *input.ClearExpire {
|
||||
tmZero := time.Time{}
|
||||
input.ExpiredAt = &tmZero
|
||||
}
|
||||
var err error
|
||||
input.EnabledIdentityBaseUpdateInput, err = user.SEnabledIdentityBaseResource.ValidateUpdateData(ctx, userCred, query, input.EnabledIdentityBaseUpdateInput)
|
||||
if err != nil {
|
||||
@@ -799,7 +856,7 @@ func (manager *SUserManager) FetchCustomizeColumns(
|
||||
if !ok {
|
||||
projectMap[p.UserId] = []api.SFetchDomainObjectWithMetadata{}
|
||||
}
|
||||
p.SFetchDomainObjectWithMetadata.Metadata, _ = metaMap[p.Id]
|
||||
p.SFetchDomainObjectWithMetadata.Metadata = metaMap[p.Id]
|
||||
projectMap[p.UserId] = append(projectMap[p.UserId], p.SFetchDomainObjectWithMetadata)
|
||||
}
|
||||
|
||||
@@ -836,11 +893,27 @@ func (manager *SUserManager) FetchCustomizeColumns(
|
||||
groupMap[ug.UserId] = append(groupMap[ug.UserId], ug.SUserGroup)
|
||||
}
|
||||
|
||||
userLogins := make(map[string]*SUserLogin)
|
||||
userLoginRows := []SUserLogin{}
|
||||
err = UserLoginManager.Query().In("user_id", userIds).All(&userLoginRows)
|
||||
if err != nil {
|
||||
log.Errorf("query user logins error: %v", err)
|
||||
return rows
|
||||
}
|
||||
for _, userLogin := range userLoginRows {
|
||||
userLogins[userLogin.UserId] = &userLogin
|
||||
}
|
||||
|
||||
for i := range rows {
|
||||
rows[i].ExternalResourceInfo, _ = scopeResources[userIds[i]]
|
||||
rows[i].UserUsage, _ = usage[userIds[i]]
|
||||
rows[i].Projects, _ = projectMap[userIds[i]]
|
||||
rows[i].Groups, _ = groupMap[userIds[i]]
|
||||
rows[i].ExternalResourceInfo = scopeResources[userIds[i]]
|
||||
rows[i].UserUsage = usage[userIds[i]]
|
||||
rows[i].Projects = projectMap[userIds[i]]
|
||||
rows[i].Groups = groupMap[userIds[i]]
|
||||
if userLogin, ok := userLogins[userIds[i]]; ok {
|
||||
rows[i].LastActiveAt = userLogin.LastActiveAt
|
||||
rows[i].LastLoginIp = userLogin.LastLoginIp
|
||||
rows[i].LastLoginSource = userLogin.LastLoginSource
|
||||
}
|
||||
}
|
||||
|
||||
return rows
|
||||
@@ -1202,14 +1275,11 @@ func (manager *SUserManager) traceLoginEvent(ctx context.Context, token mcclient
|
||||
return
|
||||
}
|
||||
|
||||
// only save web console login record
|
||||
if usr.LastActiveAt.IsZero() || utils.IsInArray(authCtx.Source, []string{mcclient.AuthSourceWeb}) {
|
||||
db.Update(usr, func() error {
|
||||
usr.LastActiveAt = time.Now().UTC()
|
||||
usr.LastLoginIp = authCtx.Ip
|
||||
usr.LastLoginSource = authCtx.Source
|
||||
return nil
|
||||
})
|
||||
{
|
||||
err = UserLoginManager.traceLoginEvent(ctx, usr.Id, authCtx)
|
||||
if err != nil {
|
||||
log.Errorf("UserLoginManager.traceLoginEvent fail %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
db.OpsLog.LogEvent(usr, "auth", &s, token)
|
||||
|
||||
@@ -62,6 +62,7 @@ func InitHandlers(app *appsrv.Application) {
|
||||
models.NonlocalUserManager,
|
||||
models.PasswordManager,
|
||||
models.UsergroupManager,
|
||||
models.UserLoginManager,
|
||||
|
||||
models.FederatedUserManager,
|
||||
models.FederationProtocolManager,
|
||||
|
||||
@@ -151,7 +151,11 @@ func authUserByIdentityInternal(ctx context.Context, ident *mcclient.SAuthentica
|
||||
return nil, httperrors.ErrUserLocked
|
||||
}
|
||||
// user disabled
|
||||
return nil, httperrors.ErrUserLocked
|
||||
return nil, httperrors.ErrUserDisabled
|
||||
}
|
||||
// user is enabled, check expired time
|
||||
if !usrExt.ExpiredAt.IsZero() && usrExt.ExpiredAt.Before(time.Now()) {
|
||||
return nil, httperrors.ErrUserExpired
|
||||
}
|
||||
// user exists, query user's idp
|
||||
idps, err := models.IdentityProviderManager.FetchIdentityProvidersByUserId(usrExt.Id, api.PASSWORD_PROTECTED_IDPS)
|
||||
@@ -497,6 +501,10 @@ func AuthenticateV3(ctx context.Context, input mcclient.SAuthenticationInputV3)
|
||||
if !user.Enabled {
|
||||
return nil, ErrUserDisabled
|
||||
}
|
||||
// user is expired
|
||||
if !user.ExpiredAt.IsZero() && user.ExpiredAt.Before(time.Now()) {
|
||||
return nil, httperrors.ErrUserExpired
|
||||
}
|
||||
|
||||
if !user.DomainEnabled {
|
||||
return nil, ErrDomainDisabled
|
||||
@@ -508,6 +516,9 @@ func AuthenticateV3(ctx context.Context, input mcclient.SAuthenticationInputV3)
|
||||
token.AuditIds = user.AuditIds
|
||||
now := time.Now().UTC()
|
||||
token.ExpiresAt = now.Add(time.Duration(options.Options.TokenExpirationSeconds) * time.Second)
|
||||
if !user.ExpiredAt.IsZero() && user.ExpiredAt.Before(token.ExpiresAt) {
|
||||
token.ExpiresAt = user.ExpiredAt
|
||||
}
|
||||
token.Context = input.Auth.Context
|
||||
|
||||
if len(input.Auth.Scope.Project.Id) == 0 && len(input.Auth.Scope.Project.Name) == 0 && len(input.Auth.Scope.Domain.Id) == 0 && len(input.Auth.Scope.Domain.Name) == 0 {
|
||||
|
||||
Reference in New Issue
Block a user