Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions pkg/cli/upgradeassistant/cmd/migrate/430.go
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
}

Expand Down
1 change: 1 addition & 0 deletions pkg/microservice/user/core/handler/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
40 changes: 38 additions & 2 deletions pkg/microservice/user/core/handler/user/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) }()
Expand Down Expand Up @@ -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) }()
Expand Down Expand Up @@ -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
Expand All @@ -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) {
Expand Down
3 changes: 2 additions & 1 deletion pkg/microservice/user/core/init/dm_mysql.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
) ;
) ;
3 changes: 2 additions & 1 deletion pkg/microservice/user/core/init/mysql.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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`),
Expand Down Expand Up @@ -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;
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '全局角色/角色绑定信息' ROW_FORMAT = Compact;
15 changes: 8 additions & 7 deletions pkg/microservice/user/core/repository/models/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
10 changes: 10 additions & 0 deletions pkg/microservice/user/core/repository/orm/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
46 changes: 44 additions & 2 deletions pkg/microservice/user/core/service/permission/authn.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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
Expand All @@ -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")
}
Loading