From a7d68d180a0663a703bf3f11f75859f5463044d9 Mon Sep 17 00:00:00 2001 From: Patrick Zhao Date: Thu, 5 Mar 2026 14:12:37 +0800 Subject: [PATCH 1/2] harden user api key security Signed-off-by: Patrick Zhao --- pkg/cli/upgradeassistant/cmd/migrate/430.go | 73 +++++++ .../internal/repository/models/migration.go | 1 + pkg/microservice/user/core/handler/router.go | 1 + .../user/core/handler/user/user.go | 40 +++- pkg/microservice/user/core/init/dm_mysql.sql | 3 +- pkg/microservice/user/core/init/mysql.sql | 3 +- .../user/core/repository/models/user.go | 15 +- .../user/core/repository/orm/user.go | 10 + .../user/core/service/permission/authn.go | 46 +++- .../user/core/service/permission/user.go | 204 ++++++++++++------ pkg/microservice/user/server/grpc/server.go | 51 +---- pkg/shared/handler/repository/models/user.go | 19 +- pkg/types/user.go | 1 + 13 files changed, 335 insertions(+), 132 deletions(-) create mode 100644 pkg/cli/upgradeassistant/cmd/migrate/430.go diff --git a/pkg/cli/upgradeassistant/cmd/migrate/430.go b/pkg/cli/upgradeassistant/cmd/migrate/430.go new file mode 100644 index 0000000000..3bc95cb8d7 --- /dev/null +++ b/pkg/cli/upgradeassistant/cmd/migrate/430.go @@ -0,0 +1,73 @@ +/* +Copyright 2025 The KodeRover Authors. + +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 migrate + +import ( + "fmt" + + internalmodels "github.com/koderover/zadig/v2/pkg/cli/upgradeassistant/internal/repository/models" + internalmongodb "github.com/koderover/zadig/v2/pkg/cli/upgradeassistant/internal/repository/mongodb" + "github.com/koderover/zadig/v2/pkg/cli/upgradeassistant/internal/upgradepath" + "github.com/koderover/zadig/v2/pkg/microservice/user/core/repository" + usermodels "github.com/koderover/zadig/v2/pkg/microservice/user/core/repository/models" + internalhandler "github.com/koderover/zadig/v2/pkg/shared/handler" +) + +func init() { + upgradepath.RegisterHandler("4.2.0", "4.3.0", V420ToV430) + upgradepath.RegisterHandler("4.3.0", "4.2.0", V430ToV420) +} + +func V420ToV430() error { + ctx := internalhandler.NewBackgroupContext() + + migrationInfo, err := getMigrationInfo() + if err != nil { + return fmt.Errorf("failed to get migration info from db, err: %s", err) + } + + defer func() { + updateMigrationError(migrationInfo.ID, err) + }() + + err = migrateUserAPITokenEnabledColumn(ctx, migrationInfo) + if err != nil { + return err + } + + return nil +} + +func migrateUserAPITokenEnabledColumn(_ *internalhandler.Context, migrationInfo *internalmodels.Migration) error { + if !migrationInfo.Migration430UserAPITokenEnabled { + if !repository.DB.Migrator().HasColumn(&usermodels.User{}, "APITokenEnabled") { + if err := repository.DB.Migrator().AddColumn(&usermodels.User{}, "APITokenEnabled"); err != nil { + return fmt.Errorf("failed to add api_token_enabled column for user table, err: %s", err) + } + } + } + + _ = internalmongodb.NewMigrationColl().UpdateMigrationStatus(migrationInfo.ID, map[string]interface{}{ + getMigrationFieldBsonTag(migrationInfo, &migrationInfo.Migration430UserAPITokenEnabled): true, + }) + + return nil +} + +func V430ToV420() error { + return nil +} diff --git a/pkg/cli/upgradeassistant/internal/repository/models/migration.go b/pkg/cli/upgradeassistant/internal/repository/models/migration.go index 9aa46335d3..6560e80642 100644 --- a/pkg/cli/upgradeassistant/internal/repository/models/migration.go +++ b/pkg/cli/upgradeassistant/internal/repository/models/migration.go @@ -37,6 +37,7 @@ type Migration struct { Migration420VMDeployEnvSource bool `bson:"migration_420_vm_deploy_env_source"` Migration420EditReleasePlanAction bool `bson:"migration_420_edit_release_plan_action"` Migration420SAE bool `bson:"migration_420_sae"` + Migration430UserAPITokenEnabled bool `bson:"migration_430_user_api_token_enabled"` Error string `bson:"error"` } diff --git a/pkg/microservice/user/core/handler/router.go b/pkg/microservice/user/core/handler/router.go index 5897947891..1c95b6d705 100644 --- a/pkg/microservice/user/core/handler/router.go +++ b/pkg/microservice/user/core/handler/router.go @@ -38,6 +38,7 @@ func (*Router) Inject(router *gin.RouterGroup) { users.DELETE("/:uid", user.DeleteUser) users.GET("/:uid/personal", user.GetPersonalUser) users.GET("/:uid/setting", user.GetUserSetting) + users.POST("/:uid/token", user.GenerateAPIToken) users.POST("/brief", user.ListUsersBrief) users.POST("/search", user.ListUsers) users.GET("/count", user.CountSystemUsers) diff --git a/pkg/microservice/user/core/handler/user/user.go b/pkg/microservice/user/core/handler/user/user.go index f4652ac9cf..948c047872 100644 --- a/pkg/microservice/user/core/handler/user/user.go +++ b/pkg/microservice/user/core/handler/user/user.go @@ -105,6 +105,10 @@ type OpenAPIGetUserResponse struct { Account string `json:"account"` } +type APITokenResponse struct { + Token string `json:"token"` +} + func OpenAPIGetUser(c *gin.Context) { ctx := internalhandler.NewContext(c) defer func() { internalhandler.JSONResponse(c, ctx) }() @@ -190,6 +194,27 @@ func GetPersonalUser(c *gin.Context) { ctx.Resp, ctx.RespErr = permission.GetUser(uid, ctx.Logger) } +func GenerateAPIToken(c *gin.Context) { + ctx := internalhandler.NewContext(c) + defer func() { internalhandler.JSONResponse(c, ctx) }() + + uid := c.Param("uid") + if ctx.UserID != uid { + ctx.RespErr = e.ErrForbidden + return + } + + token, err := permission.GenerateAPIToken(uid, ctx.Logger) + if err != nil { + ctx.RespErr = err + return + } + + ctx.Resp = &APITokenResponse{ + Token: token, + } +} + func GetUserSetting(c *gin.Context) { ctx := internalhandler.NewContext(c) defer func() { internalhandler.JSONResponse(c, ctx) }() @@ -437,7 +462,14 @@ func UpdateUser(c *gin.Context) { func UpdatePersonalUser(c *gin.Context) { ctx := internalhandler.NewContext(c) defer func() { internalhandler.JSONResponse(c, ctx) }() - args := &permission.UpdateUserInfo{} + + type updatePersonalUserReq struct { + Name string `json:"name,omitempty"` + Email string `json:"email,omitempty"` + Phone string `json:"phone,omitempty"` + } + + args := &updatePersonalUserReq{} if err := c.ShouldBindJSON(args); err != nil { ctx.RespErr = err return @@ -447,7 +479,11 @@ func UpdatePersonalUser(c *gin.Context) { ctx.RespErr = e.ErrForbidden return } - ctx.RespErr = permission.UpdateUser(uid, args, ctx.Logger) + ctx.RespErr = permission.UpdateUser(uid, &permission.UpdateUserInfo{ + Name: args.Name, + Email: args.Email, + Phone: args.Phone, + }, ctx.Logger) } func UpdateUserSetting(c *gin.Context) { diff --git a/pkg/microservice/user/core/init/dm_mysql.sql b/pkg/microservice/user/core/init/dm_mysql.sql index fb8cf682de..ac43947a0e 100644 --- a/pkg/microservice/user/core/init/dm_mysql.sql +++ b/pkg/microservice/user/core/init/dm_mysql.sql @@ -22,6 +22,7 @@ CREATE TABLE IF NOT EXISTS "user"( phone varchar(16) NOT NULL DEFAULT '' COMMENT '手机号码', email varchar(100) NOT NULL DEFAULT '' COMMENT '邮箱', api_token varchar(1024) NOT NULL DEFAULT '' COMMENT 'openAPIToken', + api_token_enabled int NOT NULL DEFAULT '0' COMMENT 'api token authorization enabled', created_at int NOT NULL COMMENT '创建时间', updated_at int NOT NULL COMMENT '修改时间', PRIMARY KEY (uid) @@ -123,4 +124,4 @@ CREATE TABLE IF NOT EXISTS role_template_binding ( PRIMARY KEY (id), FOREIGN KEY (role_id) REFERENCES role(id) ON DELETE CASCADE, FOREIGN KEY (role_template_id) REFERENCES role_template(id) ON DELETE CASCADE -) ; \ No newline at end of file +) ; diff --git a/pkg/microservice/user/core/init/mysql.sql b/pkg/microservice/user/core/init/mysql.sql index 022d9f5afc..704089bb0d 100644 --- a/pkg/microservice/user/core/init/mysql.sql +++ b/pkg/microservice/user/core/init/mysql.sql @@ -22,6 +22,7 @@ CREATE TABLE IF NOT EXISTS `user`( `phone` varchar(16) NOT NULL DEFAULT '' COMMENT '手机号码', `email` varchar(100) NOT NULL DEFAULT '' COMMENT '邮箱', `api_token` varchar(1024) NOT NULL DEFAULT '' COMMENT 'openAPIToken', + `api_token_enabled` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'api token authorization enabled', `created_at` int(11) unsigned NOT NULL COMMENT '创建时间', `updated_at` int(11) unsigned NOT NULL COMMENT '修改时间', UNIQUE KEY `account` (`account`,`identity_type`), @@ -119,4 +120,4 @@ CREATE TABLE IF NOT EXISTS `role_template_binding` ( PRIMARY KEY (`id`), FOREIGN KEY (`role_id`) REFERENCES role(`id`) ON DELETE CASCADE, FOREIGN KEY (`role_template_id`) REFERENCES role_template(`id`) ON DELETE CASCADE -) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '全局角色/角色绑定信息' ROW_FORMAT = Compact; \ No newline at end of file +) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '全局角色/角色绑定信息' ROW_FORMAT = Compact; diff --git a/pkg/microservice/user/core/repository/models/user.go b/pkg/microservice/user/core/repository/models/user.go index 27b2f6d58b..24f87967ec 100644 --- a/pkg/microservice/user/core/repository/models/user.go +++ b/pkg/microservice/user/core/repository/models/user.go @@ -18,13 +18,14 @@ package models type User struct { Model - UID string `gorm:"primary" json:"uid"` - Name string `json:"name"` - IdentityType string `gorm:"default:'unknown'" json:"identity_type"` - Email string `json:"email"` - Phone string `json:"phone"` - Account string `json:"account"` - APIToken string `gorm:"api_token" json:"api_token"` + UID string `gorm:"primary" json:"uid"` + Name string `json:"name"` + IdentityType string `gorm:"default:'unknown'" json:"identity_type"` + Email string `json:"email"` + Phone string `json:"phone"` + Account string `json:"account"` + APIToken string `gorm:"api_token" json:"api_token"` + APITokenEnabled bool `gorm:"column:api_token_enabled;default:0" json:"api_token_enabled"` // used to mention the foreign key relationship between user and groupBinding // and specify the onDelete action. diff --git a/pkg/microservice/user/core/repository/orm/user.go b/pkg/microservice/user/core/repository/orm/user.go index 633607f1d6..b765bdcee4 100644 --- a/pkg/microservice/user/core/repository/orm/user.go +++ b/pkg/microservice/user/core/repository/orm/user.go @@ -254,6 +254,16 @@ func UpdateUser(uid string, user *models.User, db *gorm.DB) error { return nil } +func UpdateUserValues(uid string, values map[string]interface{}, db *gorm.DB) error { + if len(values) == 0 { + return nil + } + if err := db.Model(&models.User{}).Where("uid = ?", uid).Updates(values).Error; err != nil { + return err + } + return nil +} + func CountUserByType(db *gorm.DB) ([]*types.UserCountByType, error) { var resp []*types.UserCountByType err := db.Model(&models.User{}).Select("count(*) as count, identity_type").Group("identity_type").Find(&resp).Error diff --git a/pkg/microservice/user/core/service/permission/authn.go b/pkg/microservice/user/core/service/permission/authn.go index f531597dc9..74abe1dc01 100644 --- a/pkg/microservice/user/core/service/permission/authn.go +++ b/pkg/microservice/user/core/service/permission/authn.go @@ -21,10 +21,13 @@ import ( "net/http" "regexp" "strings" + "time" "github.com/golang-jwt/jwt" - "github.com/koderover/zadig/v2/pkg/config" + globalConfig "github.com/koderover/zadig/v2/pkg/config" + userConfig "github.com/koderover/zadig/v2/pkg/microservice/user/config" "github.com/koderover/zadig/v2/pkg/microservice/user/core/service/login" + "github.com/koderover/zadig/v2/pkg/tool/cache" "github.com/koderover/zadig/v2/pkg/tool/log" ) @@ -247,7 +250,7 @@ func IsPublicURL(reqPath, method string) bool { // ValidateToken validates if the token is valid and returns the claims that belongs to this token if the token is valid func ValidateToken(tokenString string) (*login.Claims, bool, error) { - secretKey := config.SecretKey() + secretKey := globalConfig.SecretKey() token, err := jwt.ParseWithClaims(tokenString, &login.Claims{}, func(token *jwt.Token) (interface{}, error) { return []byte(secretKey), nil @@ -259,9 +262,48 @@ func ValidateToken(tokenString string) (*login.Claims, bool, error) { } if claims, ok := token.Claims.(*login.Claims); ok && token.Valid { + // internal tokens bypass runtime revocation checks + if isInternalTokenClaims(claims) { + return claims, true, nil + } + + // short-lived login token: validate against redis cache + if claims.ExpiresAt-time.Now().Unix() < 8760*60*60 { + cachedToken, err := cache.NewRedisCache(userConfig.RedisUserTokenDB()).GetString(claims.UID) + if err != nil { + log.Errorf("Failed to validate token against redis cache, uid: %s, err: %s", claims.UID, err) + return nil, false, err + } + if cachedToken != tokenString { + log.Errorf("token mismatch for uid: %s", claims.UID) + return nil, false, fmt.Errorf("token mismatch") + } + return claims, true, nil + } + + // long-lived api token: validate against current user token and authorization switch + matched, err := ValidateAPIToken(claims.UID, tokenString) + if err != nil { + log.Errorf("Failed to validate api token, uid: %s, err: %s", claims.UID, err) + return nil, false, err + } + if !matched { + log.Errorf("api token mismatch or unauthorized for uid: %s", claims.UID) + return nil, false, fmt.Errorf("token mismatch") + } + return claims, true, nil } else { log.Errorf("invalid token detected") return nil, false, fmt.Errorf("invalid token") } } + +func isInternalTokenClaims(claims *login.Claims) bool { + return claims.ExpiresAt == 0 && + (claims.Name == "aslan" && claims.PreferredUsername == "aslan" && claims.FederatedClaims.UserId == "aslan" && claims.Email == "aslan@koderover.com" || + claims.Name == "user" && claims.PreferredUsername == "user" && claims.FederatedClaims.UserId == "user" && claims.Email == "user@koderover.com" || + claims.Name == "cron" && claims.PreferredUsername == "cron" && claims.FederatedClaims.UserId == "cron" && claims.Email == "cron@koderover.com" || + claims.Name == "hub-agent" && claims.PreferredUsername == "hub-agent" && claims.FederatedClaims.UserId == "hub-agent" && claims.Email == "hub-agent@koderover.com" || + claims.Name == "hub-server" && claims.PreferredUsername == "hub-server" && claims.FederatedClaims.UserId == "hub-server" && claims.Email == "hub-server@koderover.com") +} diff --git a/pkg/microservice/user/core/service/permission/user.go b/pkg/microservice/user/core/service/permission/user.go index 7c38e179af..ea513a2b27 100644 --- a/pkg/microservice/user/core/service/permission/user.go +++ b/pkg/microservice/user/core/service/permission/user.go @@ -59,9 +59,10 @@ type User struct { } type UpdateUserInfo struct { - Name string `json:"name,omitempty"` - Email string `json:"email,omitempty"` - Phone string `json:"phone,omitempty"` + Name string `json:"name,omitempty"` + Email string `json:"email,omitempty"` + Phone string `json:"phone,omitempty"` + APITokenEnabled *bool `json:"api_token_enabled,omitempty"` } type OpenAPIQueryArgs struct { @@ -194,7 +195,8 @@ func GetUser(uid string, logger *zap.SugaredLogger) (*types.UserInfo, error) { } userInfo := mergeUserLogin([]models.User{*user}, []models.UserLogin{*userLogin}, logger) userInfoRes := userInfo[0] - userInfoRes.APIToken = user.APIToken + userInfoRes.APIToken = "" + userInfoRes.APITokenEnabled = user.APITokenEnabled userGroups, err := orm.ListUserGroupByUID(uid, repository.DB) if err != nil { @@ -228,41 +230,106 @@ func GetUser(uid string, logger *zap.SugaredLogger) (*types.UserInfo, error) { } userInfoRes.UserGroups = userGroupList - //TODO Create a permanent OpenAPI token - if user.APIToken == "" { - token, err := login.CreateToken(&login.Claims{ - Name: user.Name, - UID: user.UID, - Email: user.Email, - PreferredUsername: user.Account, - StandardClaims: jwt.StandardClaims{ - Audience: setting.ProductName, - //24*365*100=876000 - ExpiresAt: time.Now().Add(876000 * time.Hour).Unix(), - }, - FederatedClaims: login.FederatedClaims{ - ConnectorId: user.IdentityType, - UserId: user.Account, - }, - }) - if err != nil { - logger.Errorf("LocalLogin user:%s create token error, error msg:%s", user.Account, err.Error()) - return nil, err - } - userInfoRes.APIToken = token - userWithToken := &models.User{ - APIToken: token, - } - err = orm.UpdateUser(uid, userWithToken, repository.DB) - if err != nil { - logger.Errorf("UpdateUser user:%s save token error:%s", user.Account, err.Error()) - return nil, err - } + isSystemAdmin, err := checkUserIsSystemAdmin(uid, repository.DB) + if err != nil { + logger.Errorf("GetUser checkUserIsSystemAdmin uid:%s error, error msg:%s", uid, err.Error()) + return nil, err + } + if isSystemAdmin { + userInfoRes.Admin = true + userInfoRes.APITokenEnabled = true } return userInfoRes, nil } +func GenerateAPIToken(uid string, logger *zap.SugaredLogger) (string, error) { + user, err := orm.GetUserByUid(uid, repository.DB) + if err != nil { + logger.Errorf("GenerateAPIToken getUserByUid:%s error, error msg:%s", uid, err.Error()) + return "", err + } + if user == nil { + return "", fmt.Errorf("user not exist") + } + + tokenEnabled, err := userCanUseAPIToken(user) + if err != nil { + logger.Errorf("GenerateAPIToken check user token authorization uid:%s error, error msg:%s", uid, err.Error()) + return "", err + } + if !tokenEnabled { + return "", e.ErrForbidden + } + + token, err := generatePermanentAPIToken(user) + if err != nil { + logger.Errorf("GenerateAPIToken create token for user:%s error, error msg:%s", user.Account, err.Error()) + return "", err + } + + err = orm.UpdateUser(uid, &models.User{ + APIToken: token, + }, repository.DB) + if err != nil { + logger.Errorf("GenerateAPIToken save token for user:%s error, error msg:%s", user.Account, err.Error()) + return "", err + } + + return token, nil +} + +func ValidateAPIToken(uid, token string) (bool, error) { + if uid == "" || token == "" { + return false, nil + } + + user, err := orm.GetUserByUid(uid, repository.DB) + if err != nil { + return false, err + } + if user == nil { + return false, nil + } + + tokenEnabled, err := userCanUseAPIToken(user) + if err != nil { + return false, err + } + if !tokenEnabled || user.APIToken == "" { + return false, nil + } + + return token == user.APIToken, nil +} + +func userCanUseAPIToken(user *models.User) (bool, error) { + isSystemAdmin, err := checkUserIsSystemAdmin(user.UID, repository.DB) + if err != nil { + return false, err + } + return isSystemAdmin || user.APITokenEnabled, nil +} + +func generatePermanentAPIToken(user *models.User) (string, error) { + return login.CreateToken(&login.Claims{ + Name: user.Name, + UID: user.UID, + Email: user.Email, + PreferredUsername: user.Account, + StandardClaims: jwt.StandardClaims{ + Audience: setting.ProductName, + // 24*365*100=876000 + ExpiresAt: time.Now().Add(876000 * time.Hour).Unix(), + Id: uuid.NewString(), + }, + FederatedClaims: login.FederatedClaims{ + ConnectorId: user.IdentityType, + UserId: user.Account, + }, + }) +} + func GetUserSetting(uid string, logger *zap.SugaredLogger) (*types.UserSetting, error) { user, err := orm.GetUserByUid(uid, repository.DB) if err != nil { @@ -321,6 +388,7 @@ func SearchUserByAccount(args *QueryArgs, logger *zap.SugaredLogger) (*types.Use }) if role.Name == string(setting.SystemAdmin) { uInfo.Admin = true + uInfo.APITokenEnabled = true } } uInfo.SystemRoleBindings = rolebindings @@ -386,13 +454,14 @@ func SearchUsers(args *QueryArgs, logger *zap.SugaredLogger) (*types.UsersResp, if args.OrderBy == setting.ListUserOrderByLoginTime { for _, user := range users { usersInfo = append(usersInfo, &types.UserInfo{ - LastLoginTime: user.LastLoginTime, - Uid: user.UID, - Phone: user.Phone, - Name: user.Name, - Email: user.Email, - IdentityType: user.IdentityType, - Account: user.Account, + LastLoginTime: user.LastLoginTime, + Uid: user.UID, + Phone: user.Phone, + Name: user.Name, + Email: user.Email, + IdentityType: user.IdentityType, + Account: user.Account, + APITokenEnabled: user.APITokenEnabled, }) } } else { @@ -418,6 +487,7 @@ func SearchUsers(args *QueryArgs, logger *zap.SugaredLogger) (*types.UsersResp, }) if role.Name == string(setting.SystemAdmin) { uInfo.Admin = true + uInfo.APITokenEnabled = true } } uInfo.SystemRoleBindings = rolebindings @@ -438,13 +508,14 @@ func mergeUserLoginWithLoginTime(users []models.UserWithLoginTime, userLogins [] for _, user := range users { if userLogin, ok := userLoginMap[user.UID]; ok { usersInfo = append(usersInfo, &types.UserInfo{ - LastLoginTime: userLogin.LastLoginTime, - Uid: user.UID, - Phone: user.Phone, - Name: user.Name, - Email: user.Email, - IdentityType: user.IdentityType, - Account: user.Account, + LastLoginTime: userLogin.LastLoginTime, + Uid: user.UID, + Phone: user.Phone, + Name: user.Name, + Email: user.Email, + IdentityType: user.IdentityType, + Account: user.Account, + APITokenEnabled: user.APITokenEnabled, }) } else { logger.Error("user:%s login info not exist") @@ -462,13 +533,14 @@ func mergeUserLogin(users []models.User, userLogins []models.UserLogin, logger * for _, user := range users { if userLogin, ok := userLoginMap[user.UID]; ok { usersInfo = append(usersInfo, &types.UserInfo{ - LastLoginTime: userLogin.LastLoginTime, - Uid: user.UID, - Phone: user.Phone, - Name: user.Name, - Email: user.Email, - IdentityType: user.IdentityType, - Account: user.Account, + LastLoginTime: userLogin.LastLoginTime, + Uid: user.UID, + Phone: user.Phone, + Name: user.Name, + Email: user.Email, + IdentityType: user.IdentityType, + Account: user.Account, + APITokenEnabled: user.APITokenEnabled, }) } else { logger.Error("user:%s login info not exist") @@ -504,6 +576,7 @@ func SearchUsersByUIDs(uids []string, logger *zap.SugaredLogger) (*types.UsersRe }) if role.Name == string(setting.SystemAdmin) { uInfo.Admin = true + uInfo.APITokenEnabled = true } } uInfo.SystemRoleBindings = rolebindings @@ -709,12 +782,23 @@ func CreateUser(args *User, logger *zap.SugaredLogger) (*models.User, error) { } func UpdateUser(uid string, args *UpdateUserInfo, _ *zap.SugaredLogger) error { - user := &models.User{ - Name: args.Name, - Email: args.Email, - Phone: args.Phone, + updates := make(map[string]interface{}) + if args.Name != "" { + updates["name"] = args.Name + } + if args.Email != "" { + updates["email"] = args.Email + } + if args.Phone != "" { + updates["phone"] = args.Phone + } + if args.APITokenEnabled != nil { + updates["api_token_enabled"] = *args.APITokenEnabled + if !*args.APITokenEnabled { + updates["api_token"] = "" + } } - return orm.UpdateUser(uid, user, repository.DB) + return orm.UpdateUserValues(uid, updates, repository.DB) } func UpdateUserSetting(uid string, args *UserSetting) error { diff --git a/pkg/microservice/user/server/grpc/server.go b/pkg/microservice/user/server/grpc/server.go index b2e5a07b42..c8b998448c 100644 --- a/pkg/microservice/user/server/grpc/server.go +++ b/pkg/microservice/user/server/grpc/server.go @@ -18,11 +18,9 @@ package grpc import ( "context" - "fmt" "net/http" "net/url" "strings" - "time" ext_authz_v3 "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3" typev3 "github.com/envoyproxy/go-control-plane/envoy/type/v3" @@ -32,7 +30,6 @@ import ( "github.com/koderover/zadig/v2/pkg/microservice/user/config" "github.com/koderover/zadig/v2/pkg/microservice/user/core/service/permission" - "github.com/koderover/zadig/v2/pkg/tool/cache" "github.com/koderover/zadig/v2/pkg/tool/log" ) @@ -106,7 +103,7 @@ func (s *AuthServer) Check(ctx context.Context, request *ext_authz_v3.CheckReque return resp, nil } else { // validate the token. - claims, isValid, err := permission.ValidateToken(userToken) + _, isValid, err := permission.ValidateToken(userToken) if err != nil || !isValid { resp.Status = &rpc_status.Status{Code: int32(code.Code_UNAUTHENTICATED)} resp.HttpResponse = &ext_authz_v3.CheckResponse_DeniedResponse{DeniedResponse: &ext_authz_v3.DeniedHttpResponse{ @@ -121,54 +118,8 @@ func (s *AuthServer) Check(ctx context.Context, request *ext_authz_v3.CheckReque ) return resp, nil } - - // validate if internal token - if claims.ExpiresAt == 0 && - (claims.Name == "aslan" && claims.PreferredUsername == "aslan" && claims.FederatedClaims.UserId == "aslan" && claims.Email == "aslan@koderover.com" || - claims.Name == "user" && claims.PreferredUsername == "user" && claims.FederatedClaims.UserId == "user" && claims.Email == "user@koderover.com" || - claims.Name == "cron" && claims.PreferredUsername == "cron" && claims.FederatedClaims.UserId == "cron" && claims.Email == "cron@koderover.com" || - claims.Name == "hub-agent" && claims.PreferredUsername == "hub-agent" && claims.FederatedClaims.UserId == "hub-agent" && claims.Email == "hub-agent@koderover.com" || - claims.Name == "hub-server" && claims.PreferredUsername == "hub-server" && claims.FederatedClaims.UserId == "hub-server" && claims.Email == "hub-server@koderover.com") { - goto allowed - } - - // if the expiration time is so huge that it is not possible, it is a constant api token, we don't check for the redis. - if claims.ExpiresAt-time.Now().Unix() < 8760*60*60 { - // check if the given token is removed from the cache - token, err := cache.NewRedisCache(config.RedisUserTokenDB()).GetString(claims.UID) - if err != nil { - resp.Status = &rpc_status.Status{Code: int32(code.Code_UNAUTHENTICATED)} - resp.HttpResponse = &ext_authz_v3.CheckResponse_DeniedResponse{DeniedResponse: &ext_authz_v3.DeniedHttpResponse{ - Status: &typev3.HttpStatus{Code: http.StatusUnauthorized}, - }} - errReason := fmt.Sprintf("cache check failed, error: %s", err) - logger.Info("Request Denied", - zap.String("path", requestPath), - zap.String("method", method), - zap.String("body", body), - zap.String("reason", errReason), - ) - return resp, nil - } - - if token != userToken { - resp.Status = &rpc_status.Status{Code: int32(code.Code_UNAUTHENTICATED)} - resp.HttpResponse = &ext_authz_v3.CheckResponse_DeniedResponse{DeniedResponse: &ext_authz_v3.DeniedHttpResponse{ - Status: &typev3.HttpStatus{Code: http.StatusUnauthorized}, - }} - logger.Info("Request Denied", - zap.String("path", requestPath), - zap.String("method", method), - zap.String("body", body), - zap.String("reason", "token mismatch"), - ) - return resp, nil - } - } } } - -allowed: resp.Status = &rpc_status.Status{Code: int32(code.Code_OK)} resp.HttpResponse = &ext_authz_v3.CheckResponse_OkResponse{OkResponse: &ext_authz_v3.OkHttpResponse{}} logger.Info("Request Allowed", diff --git a/pkg/shared/handler/repository/models/user.go b/pkg/shared/handler/repository/models/user.go index 1890e077b7..094e37e6a9 100644 --- a/pkg/shared/handler/repository/models/user.go +++ b/pkg/shared/handler/repository/models/user.go @@ -17,15 +17,16 @@ limitations under the License. package models type User struct { - UID string `json:"uid"` - Name string `json:"name"` - IdentityType string `gorm:"default:'unknown'" json:"identity_type"` - Email string `json:"email"` - Phone string `json:"phone"` - Account string `json:"account"` - APIToken string `gorm:"api_token" json:"api_token"` - CreatedAt int64 `json:"created_at"` - UpdatedAt int64 `json:"updated_at"` + UID string `json:"uid"` + Name string `json:"name"` + IdentityType string `gorm:"default:'unknown'" json:"identity_type"` + Email string `json:"email"` + Phone string `json:"phone"` + Account string `json:"account"` + APIToken string `gorm:"api_token" json:"api_token"` + APITokenEnabled bool `gorm:"column:api_token_enabled;default:0" json:"api_token_enabled"` + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` } // TableName sets the insert table name for this struct type diff --git a/pkg/types/user.go b/pkg/types/user.go index 86c47b7927..bb54c464aa 100644 --- a/pkg/types/user.go +++ b/pkg/types/user.go @@ -27,6 +27,7 @@ type UserInfo struct { Phone string `json:"phone"` Account string `json:"account"` APIToken string `json:"token"` + APITokenEnabled bool `json:"api_token_enabled"` UserGroups []*UserGroup `json:"user_groups"` SystemRoleBindings []*RoleBinding `json:"system_role_bindings"` Admin bool `json:"admin"` From bced01fdb8f5fd224325a3f188e5bcfac2db9540 Mon Sep 17 00:00:00 2001 From: Patrick Zhao Date: Fri, 6 Mar 2026 10:56:08 +0800 Subject: [PATCH 2/2] Add API token status to user model Signed-off-by: Patrick Zhao --- pkg/shared/client/user/user.go | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/pkg/shared/client/user/user.go b/pkg/shared/client/user/user.go index 02968d120e..167d00cfd7 100644 --- a/pkg/shared/client/user/user.go +++ b/pkg/shared/client/user/user.go @@ -26,12 +26,13 @@ import ( ) type User struct { - UID string `json:"uid"` - Name string `json:"name"` - Email string `json:"email"` - Phone string `json:"phone"` - IdentityType string `json:"identity_type"` - Account string `json:"account"` + UID string `json:"uid"` + Name string `json:"name"` + Email string `json:"email"` + Phone string `json:"phone"` + IdentityType string `json:"identity_type"` + Account string `json:"account"` + APITokenEnabled bool `json:"api_token_enabled"` } type usersResp struct {