Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
a9e1983
feat(permission): support global read-only role visibility across all…
leebrouse Apr 8, 2026
f16b2c6
feat: add explicit global_read_only role flag and enforce read-only a…
leebrouse Apr 8, 2026
2cbe5d6
feat: update ua for the global readonly role
leebrouse Apr 8, 2026
e0b14bd
feat: update ua migrate
leebrouse Apr 8, 2026
8d24b02
feat: change ua in 430.go
leebrouse Apr 8, 2026
5d81116
feat: update new ua for global readonly role
leebrouse Apr 8, 2026
dfdaf4a
feat: update ua
leebrouse Apr 8, 2026
290ccf4
feat: updata ua 430.go
leebrouse Apr 8, 2026
4f5bd86
feat: update ua 430.go
leebrouse Apr 8, 2026
d5560e2
fix(permission): fix global read-only role permission validation and …
leebrouse Apr 9, 2026
b9ee5dd
fix(permission): fix global read-only role permission filtering
leebrouse Apr 9, 2026
2d502c1
fix(permission): remove validation logic for creating and updating gl…
leebrouse Apr 9, 2026
c0a7e66
fix(permission): fix permission validation and synchronization logic …
leebrouse Apr 9, 2026
2de49af
fix(permission): fix global read-only role not taking effect in proje…
leebrouse Apr 9, 2026
e5ae6ca
feat: add VerbGetRegistryManagement
leebrouse Apr 9, 2026
5ee78a9
feat(permission): add S3 storage management permission to global read…
leebrouse Apr 9, 2026
5390446
feat: add some comments
leebrouse Apr 9, 2026
1135d90
refactor: deprecate GlobalReadOnly role backfilling and optimize auth…
leebrouse Apr 13, 2026
dc0bd57
refactor: deprecate global read-only role validation and logic
leebrouse Apr 13, 2026
c22d259
refactor: remove global read-only role logic and associated field ini…
leebrouse Apr 13, 2026
ad9c222
feat: implement global-read-only role backfill in migration 430 and r…
leebrouse Apr 13, 2026
2ad071a
feat: add idempotent global-read-only role migration and define post-…
leebrouse Apr 13, 2026
87711c6
refactor: optimize group role permission logic and remove obsolete up…
leebrouse Apr 13, 2026
5d39305
refactor: remove global read-only role validation logic and simplify …
leebrouse Apr 13, 2026
a49e93f
refactor(permission): extract permission handling logic into helper f…
leebrouse Apr 13, 2026
5ebd96f
fix: rollback last commit
leebrouse Apr 16, 2026
018f47e
fix: add BusinessDirectory action for the global read only role
leebrouse Apr 17, 2026
46378e9
feat: add permission control for business directory
leebrouse Apr 17, 2026
b2667db
fix: add permission checks to business directory APIs
leebrouse Apr 20, 2026
3280cbf
fix: improve ua 430.go
leebrouse Apr 20, 2026
7fcd5b8
perf(authz): cache project list to improve authorization performance
leebrouse Apr 20, 2026
0a3e343
fix: add env view permission in the get env init
leebrouse Apr 20, 2026
63ec4da
fix: improve comment in ua 430.go
leebrouse Apr 20, 2026
82b096b
Merge remote-tracking branch 'upstream/main' into feature/global-read…
leebrouse Apr 23, 2026
b02f147
fix: change permission setting in the field definition creating opera…
leebrouse Apr 24, 2026
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
194 changes: 194 additions & 0 deletions pkg/cli/upgradeassistant/cmd/migrate/430.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,10 @@ import (
collaborationmongodb "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/collaboration/repository/mongodb"
"github.com/koderover/zadig/v2/pkg/microservice/user/core/repository"
usermodels "github.com/koderover/zadig/v2/pkg/microservice/user/core/repository/models"
"github.com/koderover/zadig/v2/pkg/microservice/user/core/repository/orm"
userorm "github.com/koderover/zadig/v2/pkg/microservice/user/core/repository/orm"
permissionservice "github.com/koderover/zadig/v2/pkg/microservice/user/core/service/permission"
"github.com/koderover/zadig/v2/pkg/setting"
internalhandler "github.com/koderover/zadig/v2/pkg/shared/handler"
"github.com/koderover/zadig/v2/pkg/tool/log"
pkgtypes "github.com/koderover/zadig/v2/pkg/types"
Expand All @@ -39,6 +41,14 @@ type permissionActionSeed430 struct {
Name string
Action string
Resource string
Scope int
}

var businessDirectoryActionSeeds430 = []permissionActionSeed430{
{Name: "查看", Action: permissionservice.VerbGetBusinessDirectory, Resource: "BusinessDirectory", Scope: pkgtypes.DBSystemScope},
{Name: "新建", Action: permissionservice.VerbCreateBusinessDirectory, Resource: "BusinessDirectory", Scope: pkgtypes.DBSystemScope},
{Name: "编辑", Action: permissionservice.VerbEditBusinessDirectory, Resource: "BusinessDirectory", Scope: pkgtypes.DBSystemScope},
{Name: "删除", Action: permissionservice.VerbDeleteBusinessDirectory, Resource: "BusinessDirectory", Scope: pkgtypes.DBSystemScope},
}

type permissionBackfillRule430 struct {
Expand Down Expand Up @@ -116,6 +126,7 @@ func V421ToV430() error {
return err
}

err = migrateGlobalReadOnlyRole(ctx, migrationInfo)
err = migrateScalePermissions(migrationInfo)
if err != nil {
return err
Expand Down Expand Up @@ -146,6 +157,189 @@ func migrateUserAPITokenEnabledColumn(_ *internalhandler.Context, migrationInfo
return nil
}

// check global read only role column
func migrateGlobalReadOnlyRole(_ *internalhandler.Context, migrationInfo *internalmodels.Migration) error {
if !migrationInfo.Migration430GlobalReadOnlyRole {
if !repository.DB.Migrator().HasColumn(&usermodels.NewRole{}, "GlobalReadOnly") {
if err := repository.DB.Migrator().AddColumn(&usermodels.NewRole{}, "GlobalReadOnly"); err != nil {
return fmt.Errorf("failed to add global_read_only column for role table, err: %s", err)
}
}
}

// write globalreadonly role into system roles
err := backfillGlobalReadOnlyRole()
if err != nil {
return err
}
// Ensure business-directory actions exist for upgraded instances.
if err := ensureBusinessDirectoryActions430(); err != nil {
return err
}
// Fallback backfill:
// - if a role already has get_business_directory, append create/edit/delete
if err := backfillBusinessDirectoryRolePermissions430(); err != nil {
return err
}

_ = internalmongodb.NewMigrationColl().UpdateMigrationStatus(migrationInfo.ID, map[string]interface{}{
getMigrationFieldBsonTag(migrationInfo, &migrationInfo.Migration430GlobalReadOnlyRole): true,
})

return nil
}

func ensureBusinessDirectoryActions430() error {
tx := repository.DB.Begin()
if tx.Error != nil {
return fmt.Errorf("failed to begin tx for business directory action migration, err: %s", tx.Error)
}

for _, seed := range businessDirectoryActionSeeds430 {
action, err := orm.GetActionByVerb(seed.Action, tx)
if err != nil {
tx.Rollback()
return fmt.Errorf("failed to query action %s, err: %s", seed.Action, err)
}
if action != nil && action.ID != 0 {
continue
}

action = &usermodels.Action{
Name: seed.Name,
Action: seed.Action,
Resource: seed.Resource,
Scope: seed.Scope,
}
if err := orm.CreateAction(action, tx); err != nil {
tx.Rollback()
return fmt.Errorf("failed to create action %s, err: %s", seed.Action, err)
}
}

if err := tx.Commit().Error; err != nil {
return fmt.Errorf("failed to commit business directory action migration tx, err: %s", err)
}
return nil
}

// backfillBusinessDirectoryRolePermissions430 provides a migration fallback for
// historical system roles:
// 1) If a role already has get_business_directory, only append write verbs.
func backfillBusinessDirectoryRolePermissions430() error {
tx := repository.DB.Begin()
if tx.Error != nil {
return fmt.Errorf("failed to begin tx for business directory permission backfill, err: %s", tx.Error)
}

actionIDMap := make(map[string]uint, len(businessDirectoryActionSeeds430))
for _, seed := range businessDirectoryActionSeeds430 {
action, err := orm.GetActionByVerb(seed.Action, tx)
if err != nil {
tx.Rollback()
return fmt.Errorf("failed to query action %s for backfill, err: %s", seed.Action, err)
}
if action == nil || action.ID == 0 {
tx.Rollback()
return fmt.Errorf("action %s is missing while backfilling business directory permissions", seed.Action)
}
actionIDMap[seed.Action] = action.ID
}

roles, err := orm.ListRoleByNamespace(permissionservice.GeneralNamespace, tx)
if err != nil {
tx.Rollback()
return fmt.Errorf("failed to list system roles for business directory backfill, err: %s", err)
}

for _, role := range roles {
if role == nil || role.ID == 0 {
continue
}
// Keep global-read-only role as readonly.
if role.GlobalReadOnly {
continue
}

actions, err := orm.ListActionByRole(role.ID, tx)
if err != nil {
tx.Rollback()
return fmt.Errorf("failed to list actions for role %d during business directory backfill, err: %s", role.ID, err)
}

existingVerbs := map[string]struct{}{}
for _, action := range actions {
if action == nil {
continue
}
existingVerbs[action.Action] = struct{}{}
}

// Only backfill write verbs for roles that already have get_business_directory.
if _, hasGet := existingVerbs[permissionservice.VerbGetBusinessDirectory]; !hasGet {
continue
}
targetVerbs := []string{
permissionservice.VerbCreateBusinessDirectory,
permissionservice.VerbEditBusinessDirectory,
permissionservice.VerbDeleteBusinessDirectory,
}

missingActionIDs := make([]uint, 0)
for _, verb := range targetVerbs {
if _, ok := existingVerbs[verb]; ok {
continue
}
if actionID, ok := actionIDMap[verb]; ok {
missingActionIDs = append(missingActionIDs, actionID)
}
}

if len(missingActionIDs) == 0 {
continue
}
if err := orm.BulkCreateRoleActionBindings(role.ID, missingActionIDs, tx); err != nil {
tx.Rollback()
return fmt.Errorf("failed to backfill business directory permissions for role %d, err: %s", role.ID, err)
}
}

if err := tx.Commit().Error; err != nil {
return fmt.Errorf("failed to commit business directory permission backfill tx, err: %s", err)
}
return nil
}

// backfill global read only role
func backfillGlobalReadOnlyRole() error {
tx := repository.DB.Begin()
role := &usermodels.NewRole{
Name: "global-read-only",
Description: "拥有系统全局只读的权限",
Type: int64(setting.RoleTypeSystem),
Namespace: "*",
GlobalReadOnly: true,
}

// Check if role already exists
existingRole, err := orm.GetRole("global-read-only", "*", tx)
if err == nil && existingRole != nil && existingRole.ID != 0 {
tx.Commit()
return nil
}

if err := orm.CreateRole(role, tx); err != nil {
tx.Rollback()
return fmt.Errorf("failed to create global-read-only role in backfill, error: %s", err)
}

if err := tx.Commit().Error; err != nil {
return fmt.Errorf("failed to commit tx, err: %s", err)
}

return nil
}

func migrateScalePermissions(migrationInfo *internalmodels.Migration) error {
alreadyMigrated := migrationInfo.Migration430ScalePermission

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ type Migration struct {
Migration421CollaborationRollbackPermission bool `bson:"migration_421_collaboration_rollback_permission"`
Migration421WorkflowDeploySpec bool `bson:"migration_421_workflow_deploy_spec"`
Migration430UserAPITokenEnabled bool `bson:"migration_430_user_api_token_enabled"`
Migration430GlobalReadOnlyRole bool `bson:"migration_430_global_read_only_role"`
Migration430ScalePermission bool `bson:"migration_430_scale_permission"`
Migration430CollaborationScalePermission bool `bson:"migration_430_collaboration_scale_permission"`
Error string `bson:"error"`
Expand Down
50 changes: 47 additions & 3 deletions pkg/microservice/aslan/core/application/handler/application.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ func CreateApplication(c *gin.Context) {
ctx.UnAuthorized = true
return
}
if !ctx.Resources.IsSystemAdmin && !ctx.Resources.SystemActions.BusinessDirectory.Create {
ctx.UnAuthorized = true
return
}
args := new(commonmodels.Application)
data, _ := c.GetRawData()
c.Request.Body = io.NopCloser(bytes.NewBuffer(data))
Expand All @@ -58,6 +62,11 @@ func BulkCreateApplications(c *gin.Context) {
return
}

if !ctx.Resources.IsSystemAdmin && !ctx.Resources.SystemActions.BusinessDirectory.Create {
ctx.UnAuthorized = true
return
}

var args []*commonmodels.Application
data, _ := c.GetRawData()
c.Request.Body = io.NopCloser(bytes.NewBuffer(data))
Expand All @@ -74,8 +83,17 @@ func BulkCreateApplications(c *gin.Context) {
}

func GetApplication(c *gin.Context) {
ctx := internalhandler.NewContext(c)
ctx, err := internalhandler.NewContextWithAuthorization(c)
defer func() { internalhandler.JSONResponse(c, ctx) }()
if err != nil {
ctx.RespErr = fmt.Errorf("authorization Info Generation failed: err %s", err)
ctx.UnAuthorized = true
return
}
if !ctx.Resources.IsSystemAdmin && !ctx.Resources.SystemActions.BusinessDirectory.View {
ctx.UnAuthorized = true
return
}
ctx.Resp, ctx.RespErr = service.GetApplication(c.Param("id"), ctx.Logger)
}

Expand All @@ -87,6 +105,10 @@ func UpdateApplication(c *gin.Context) {
ctx.UnAuthorized = true
return
}
if !ctx.Resources.IsSystemAdmin && !ctx.Resources.SystemActions.BusinessDirectory.Edit {
ctx.UnAuthorized = true
return
}
args := new(commonmodels.Application)
data, _ := c.GetRawData()
if err := json.Unmarshal(data, args); err != nil {
Expand All @@ -104,12 +126,25 @@ func DeleteApplication(c *gin.Context) {
ctx.UnAuthorized = true
return
}
if !ctx.Resources.IsSystemAdmin && !ctx.Resources.SystemActions.BusinessDirectory.Delete {
ctx.UnAuthorized = true
return
}
ctx.RespErr = service.DeleteApplication(c.Param("id"), ctx.Logger)
}

func SearchApplications(c *gin.Context) {
ctx := internalhandler.NewContext(c)
ctx, err := internalhandler.NewContextWithAuthorization(c)
defer func() { internalhandler.JSONResponse(c, ctx) }()
if err != nil {
ctx.RespErr = fmt.Errorf("authorization Info Generation failed: err %s", err)
ctx.UnAuthorized = true
return
}
if !ctx.Resources.IsSystemAdmin && !ctx.Resources.SystemActions.BusinessDirectory.View {
ctx.UnAuthorized = true
return
}
var req service.SearchApplicationsRequest
if err := c.ShouldBindJSON(&req); err != nil {
ctx.RespErr = e.ErrInvalidParam.AddErr(err)
Expand All @@ -124,8 +159,17 @@ func SearchApplications(c *gin.Context) {
}

func ListApplicationEnvs(c *gin.Context) {
ctx := internalhandler.NewContext(c)
ctx, err := internalhandler.NewContextWithAuthorization(c)
defer func() { internalhandler.JSONResponse(c, ctx) }()
if err != nil {
ctx.RespErr = fmt.Errorf("authorization Info Generation failed: err %s", err)
ctx.UnAuthorized = true
return
}
if !ctx.Resources.IsSystemAdmin && !ctx.Resources.SystemActions.BusinessDirectory.View {
ctx.UnAuthorized = true
return
}
resp, err := service.ListApplicationEnvs(c.Param("id"), ctx.Logger)
if err != nil {
ctx.RespErr = err
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ func CreateFieldDefinition(c *gin.Context) {
ctx.UnAuthorized = true
return
}
if !ctx.Resources.IsSystemAdmin && !ctx.Resources.SystemActions.BusinessDirectory.Edit {
ctx.UnAuthorized = true
return
}
args := new(commonmodels.ApplicationFieldDefinition)
data, _ := c.GetRawData()
c.Request.Body = io.NopCloser(bytes.NewBuffer(data))
Expand All @@ -50,8 +54,17 @@ func CreateFieldDefinition(c *gin.Context) {
}

func ListFieldDefinitions(c *gin.Context) {
ctx := internalhandler.NewContext(c)
ctx, err := internalhandler.NewContextWithAuthorization(c)
defer func() { internalhandler.JSONResponse(c, ctx) }()
if err != nil {
ctx.RespErr = fmt.Errorf("authorization Info Generation failed: err %s", err)
ctx.UnAuthorized = true
return
}
if !ctx.Resources.IsSystemAdmin && !ctx.Resources.SystemActions.BusinessDirectory.View {
ctx.UnAuthorized = true
return
}
ctx.Resp, ctx.RespErr = service.ListFieldDefinitions(ctx.Logger)
}

Expand All @@ -63,6 +76,10 @@ func UpdateFieldDefinition(c *gin.Context) {
ctx.UnAuthorized = true
return
}
if !ctx.Resources.IsSystemAdmin && !ctx.Resources.SystemActions.BusinessDirectory.Edit {
ctx.UnAuthorized = true
return
}
args := new(commonmodels.ApplicationFieldDefinition)
data, _ := c.GetRawData()
if err := json.Unmarshal(data, args); err != nil {
Expand All @@ -80,5 +97,9 @@ func DeleteFieldDefinition(c *gin.Context) {
ctx.UnAuthorized = true
return
}
if !ctx.Resources.IsSystemAdmin && !ctx.Resources.SystemActions.BusinessDirectory.Delete {
ctx.UnAuthorized = true
return
}
ctx.RespErr = service.DeleteFieldDefinition(c.Param("id"), ctx.Logger)
}
1 change: 1 addition & 0 deletions pkg/microservice/aslan/core/environment/handler/product.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ func GetInitProduct(c *gin.Context) {
if !ctx.Resources.SystemActions.Project.Create &&
// this api is also used in creating testing env for some reason
!(ctx.Resources.ProjectAuthInfo[projectKey].Env.Create ||
ctx.Resources.ProjectAuthInfo[projectKey].Env.View ||
ctx.Resources.ProjectAuthInfo[projectKey].IsProjectAdmin) {
ctx.UnAuthorized = true
return
Expand Down
3 changes: 3 additions & 0 deletions pkg/microservice/user/core/init/action_initialization.sql
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ VALUES
("删除", "delete_release_plan", "ReleasePlan", 2),
("配置", "edit_config_release_plan", "ReleasePlan", 2),
("查看", "get_business_directory", "BusinessDirectory", 2),
("新建", "create_business_directory", "BusinessDirectory", 2),
("编辑", "edit_business_directory", "BusinessDirectory", 2),
("删除", "delete_business_directory", "BusinessDirectory", 2),
("查看", "get_cluster_management", "ClusterManagement", 2),
("新建", "create_cluster_management", "ClusterManagement", 2),
("编辑", "edit_cluster_management", "ClusterManagement", 2),
Expand Down
Loading
Loading