Skip to content
Merged
Show file tree
Hide file tree
Changes from 30 commits
Commits
Show all changes
37 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
58be7c7
fix: delete empty comment
leebrouse Apr 28, 2026
4e49105
Merge remote-tracking branch 'upstream/main' into feature/global-read…
leebrouse Apr 28, 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
217 changes: 213 additions & 4 deletions pkg/cli/upgradeassistant/cmd/migrate/430.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,33 @@ import (
"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"
"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"
pkgtypes "github.com/koderover/zadig/v2/pkg/types"
)

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},
}

func init() {
upgradepath.RegisterHandler("4.2.0", "4.3.0", V420ToV430)
upgradepath.RegisterHandler("4.3.0", "4.2.0", V430ToV420)
upgradepath.RegisterHandler("4.2.1", "4.3.0", V421ToV430)
upgradepath.RegisterHandler("4.3.0", "4.2.1", V430ToV421)
}

func V420ToV430() error {
func V421ToV430() error {
ctx := internalhandler.NewBackgroupContext()

migrationInfo, err := getMigrationInfo()
Expand All @@ -49,6 +67,11 @@ func V420ToV430() error {
return err
}

err = migrateGlobalReadOnlyRole(ctx, migrationInfo)
if err != nil {
return err
}

return nil
}

Expand All @@ -68,6 +91,192 @@ func migrateUserAPITokenEnabledColumn(_ *internalhandler.Context, migrationInfo
return nil
}

func V430ToV420() error {
// 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)
}
}
}

// 写入 globalreadonly role
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
// - otherwise append the full business-directory action set.
Comment thread
leebrouse marked this conversation as resolved.
Outdated
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.
// 2) If a role does not have get_business_directory, append the full set.
Comment thread
leebrouse marked this conversation as resolved.
Outdated
// global-read-only role is skipped to keep readonly semantics.
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 V430ToV421() error {
return nil
}
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"`
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
Loading
Loading