From a3ae83a37a0ca4b5f4a3364be76e773158cee5bc Mon Sep 17 00:00:00 2001 From: mohammad-hamid Date: Tue, 16 Dec 2025 03:26:42 -0500 Subject: [PATCH] IAM: Add disabled field to role permissions This change adds support for disabling permissions without removing them from roles. This allows users to revoke permissions while signaling to apps that the permission should not be re-granted. Key changes: - Add disabled boolean field to RolespecPermission in CUE schema - Update generated Go types for Role, CoreRole, and GlobalRole - Add disabled column to permission table via migration - Filter disabled permissions in Zanzana tuple conversion - Filter disabled permissions in legacy RBAC queries - Update SQL queries to persist and retrieve disabled field When a permission has disabled: true: - It remains visible in the role definition - It is NOT written to Zanzana (authorization engine) - It is NOT returned in RBAC permission queries This prevents apps from re-granting permissions that users have explicitly revoked, while maintaining visibility of the permission in the role. --- apps/iam/kinds/v0alpha1/rolespec.cue | 2 ++ apps/iam/pkg/apis/iam/v0alpha1/corerole_spec_gen.go | 2 ++ apps/iam/pkg/apis/iam/v0alpha1/globalrole_spec_gen.go | 2 ++ apps/iam/pkg/apis/iam/v0alpha1/role_spec_gen.go | 2 ++ pkg/registry/apis/iam/role_hooks.go | 8 +++++++- pkg/services/accesscontrol/database/database.go | 5 ++++- pkg/services/accesscontrol/models.go | 9 +++++---- .../sqlstore/migrations/accesscontrol/migrations.go | 5 +++++ 8 files changed, 29 insertions(+), 6 deletions(-) diff --git a/apps/iam/kinds/v0alpha1/rolespec.cue b/apps/iam/kinds/v0alpha1/rolespec.cue index b7934f0174e..d0aa9361a77 100644 --- a/apps/iam/kinds/v0alpha1/rolespec.cue +++ b/apps/iam/kinds/v0alpha1/rolespec.cue @@ -6,6 +6,8 @@ RoleSpec: { action: string // RBAC scope (e.g: "dashboards:uid:dash1") scope: string + // When true, the permission is disabled and not granted to the role + disabled: bool | *false } // Display name of the role diff --git a/apps/iam/pkg/apis/iam/v0alpha1/corerole_spec_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/corerole_spec_gen.go index ac0b41e1972..21913c8ff10 100644 --- a/apps/iam/pkg/apis/iam/v0alpha1/corerole_spec_gen.go +++ b/apps/iam/pkg/apis/iam/v0alpha1/corerole_spec_gen.go @@ -8,6 +8,8 @@ type CoreRolespecPermission struct { Action string `json:"action"` // RBAC scope (e.g: "dashboards:uid:dash1") Scope string `json:"scope"` + // When true, the permission is disabled and not granted to the role + Disabled bool `json:"disabled"` } // NewCoreRolespecPermission creates a new CoreRolespecPermission object. diff --git a/apps/iam/pkg/apis/iam/v0alpha1/globalrole_spec_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/globalrole_spec_gen.go index 1ce8d65646f..1d0a8b9e715 100644 --- a/apps/iam/pkg/apis/iam/v0alpha1/globalrole_spec_gen.go +++ b/apps/iam/pkg/apis/iam/v0alpha1/globalrole_spec_gen.go @@ -8,6 +8,8 @@ type GlobalRolespecPermission struct { Action string `json:"action"` // RBAC scope (e.g: "dashboards:uid:dash1") Scope string `json:"scope"` + // When true, the permission is disabled and not granted to the role + Disabled bool `json:"disabled"` } // NewGlobalRolespecPermission creates a new GlobalRolespecPermission object. diff --git a/apps/iam/pkg/apis/iam/v0alpha1/role_spec_gen.go b/apps/iam/pkg/apis/iam/v0alpha1/role_spec_gen.go index 5a8185cd25c..e5cf9df6f7f 100644 --- a/apps/iam/pkg/apis/iam/v0alpha1/role_spec_gen.go +++ b/apps/iam/pkg/apis/iam/v0alpha1/role_spec_gen.go @@ -8,6 +8,8 @@ type RolespecPermission struct { Action string `json:"action"` // RBAC scope (e.g: "dashboards:uid:dash1") Scope string `json:"scope"` + // When true, the permission is disabled and not granted to the role + Disabled bool `json:"disabled"` } // NewRolespecPermission creates a new RolespecPermission object. diff --git a/pkg/registry/apis/iam/role_hooks.go b/pkg/registry/apis/iam/role_hooks.go index 831a2993fe5..f52559a732e 100644 --- a/pkg/registry/apis/iam/role_hooks.go +++ b/pkg/registry/apis/iam/role_hooks.go @@ -17,10 +17,16 @@ import ( // convertRolePermissionsToTuples converts role permissions (action/scope) to v1 TupleKey format // using the shared zanzana.ConvertRolePermissionsToTuples utility and common.ToAuthzExtTupleKeys +// Disabled permissions are filtered out and not converted to tuples func convertRolePermissionsToTuples(roleUID string, permissions []iamv0.CoreRolespecPermission) ([]*v1.TupleKey, error) { - // Convert IAM permissions to zanzana.RolePermission format + // Convert IAM permissions to zanzana.RolePermission format, filtering out disabled permissions rolePerms := make([]zanzana.RolePermission, 0, len(permissions)) for _, perm := range permissions { + // Skip disabled permissions - they should not be added to Zanzana + if perm.Disabled { + continue + } + // Split the scope to get kind, attribute, identifier kind, _, identifier := accesscontrol.SplitScope(perm.Scope) rolePerms = append(rolePerms, zanzana.RolePermission{ diff --git a/pkg/services/accesscontrol/database/database.go b/pkg/services/accesscontrol/database/database.go index ce6dd393f77..78f4418a258 100644 --- a/pkg/services/accesscontrol/database/database.go +++ b/pkg/services/accesscontrol/database/database.go @@ -67,7 +67,9 @@ func (s *AccessControlStore) GetUserPermissions(ctx context.Context, query acces permission.scope FROM permission INNER JOIN role ON role.id = permission.role_id - ` + filter + ` + filter + ` + AND (permission.disabled = 0 OR permission.disabled IS NULL) + ` if len(query.RolePrefixes) > 0 { rolePrefixesFilter, filterParams := accesscontrol.RolePrefixesFilter(query.RolePrefixes) @@ -132,6 +134,7 @@ func (s *AccessControlStore) GetTeamsPermissions(ctx context.Context, query acce WHERE tr.team_id IN(?` + strings.Repeat(", ?", len(teams)-1) + `) AND tr.org_id = ? ) as all_role ON role.id = all_role.role_id + WHERE (permission.disabled = 0 OR permission.disabled IS NULL) ` params := make([]any, 0) diff --git a/pkg/services/accesscontrol/models.go b/pkg/services/accesscontrol/models.go index ec67cd03c64..3d9a5581a71 100644 --- a/pkg/services/accesscontrol/models.go +++ b/pkg/services/accesscontrol/models.go @@ -203,10 +203,11 @@ type BuiltinRole struct { // Permission is the model for access control permissions type Permission struct { - ID int64 `json:"-" xorm:"pk autoincr 'id'"` - RoleID int64 `json:"-" xorm:"role_id"` - Action string `json:"action"` - Scope string `json:"scope"` + ID int64 `json:"-" xorm:"pk autoincr 'id'"` + RoleID int64 `json:"-" xorm:"role_id"` + Action string `json:"action"` + Scope string `json:"scope"` + Disabled bool `json:"disabled" xorm:"disabled"` Kind string `json:"-"` Attribute string `json:"-"` diff --git a/pkg/services/sqlstore/migrations/accesscontrol/migrations.go b/pkg/services/sqlstore/migrations/accesscontrol/migrations.go index 2502caf51dc..2bd0ba9b6fd 100644 --- a/pkg/services/sqlstore/migrations/accesscontrol/migrations.go +++ b/pkg/services/sqlstore/migrations/accesscontrol/migrations.go @@ -218,4 +218,9 @@ func AddMigration(mg *migrator.Migrator) { mg.AddMigration("Remove permission role_id index", migrator.NewDropIndexMigration(permissionV1, &migrator.Index{ Cols: []string{"role_id"}, })) + + // Add disabled column to permission table + mg.AddMigration("add column disabled to permission table", migrator.NewAddColumnMigration(permissionV1, &migrator.Column{ + Name: "disabled", Type: migrator.DB_Bool, Nullable: false, Default: "0", + })) }