Remove RBAC role conditions from policy files
Why this matters
Many policy files define RBAC-tier conditions (condition(:guest), condition(:planner), condition(:reporter), condition(:security_manager), condition(:developer), condition(:maintainer), condition(:owner)) and reference them directly in rule { ... } blocks to gate permissions. This is the in-policy counterpart to the *_access enforcement-point anti-pattern tracked in #593225 (closed).
Using role-tier conditions inside policy rules is an anti-pattern because:
- Role-to-permission mapping leaks into policy code. Permissions should be granted by role YAML configuration (
config/authz/roles/*.yml), not declared inline asrule { developer }.enable :foo. The latter scatters role definitions across files and prevents a single source of truth. - Custom roles cannot satisfy compound rules. A custom role with
:foopermission doesn't passrule { developer & condition_x }.enable :foobecause it isn't 'developer.' Role conditions short-circuit custom-role grants. - Permission audits become impossible. When the answer to 'what does developer get on a project?' lives partly in YAML and partly in policy
ruleblocks, programmatic auditing has to walk the policy AST.
The Authz::RolePermissions concern and define_role_permissions(boundary) are now in place in both ProjectPolicy and GroupPolicy. They generate per-permission role_enables_<perm> conditions and rules from the role YAMLs. Once every rule that references a role-tier condition is migrated to that mechanism, the conditions can be removed.
What needs to change
For each rule { ... } referencing a role-tier condition, apply one of these patterns:
Bulk-enable (rule { developer }.policy do enable :a; enable :b end): move :a and :b into developer.yml. define_role_permissions regenerates per-permission rules automatically. Drop the rule.
Single-enable (rule { developer }.enable :foo): move :foo into developer.yml. Drop the rule.
OR-compound (rule { reporter | other_condition }.enable :foo): split into two rules. Move the role half into the role YAML, keep the other half as rule { other_condition }.enable :foo.
AND-compound (rule { developer & condition_x }.enable :foo): move :foo into developer.yml AND add rule { ~condition_x }.prevent :foo to preserve the conjunctive guard.
Negation (rule { ~admin & owner & ... }.prevent :foo): case-by-case. May not be a candidate for this refactor; document if so.
In scope
FOSS:
app/policies/project_policy.rbapp/policies/group_policy.rbapp/policies/project_snippet_policy.rbapp/policies/ci/pipeline_policy.rbapp/policies/deploy_token_policy.rb(separate handling - see child issue)
EE:
ee/app/policies/ee/project_policy.rbee/app/policies/ee/group_policy.rbee/app/policies/dependency_proxy/packages/setting_policy.rbee/app/policies/virtual_registries/policies/group_policy.rbee/app/policies/ai/conversation/thread_policy.rbee/app/policies/concerns/work_items/group_policy.rbee/app/policies/concerns/work_items/shared_policy.rb
Out of scope
condition(:owner)inapp/policies/namespaces/user_namespace_policy.rb. This is@subject.owner == @user(does this user own the personal namespace), not role-membership.*_accessaliases (:developer_access,:maintainer_access,:owner_access). Separate cleanup; tracked separately.- Custom-role overlay (
custom_role_enables_*in EE). Different mechanism.
Prerequisites
Already in place:
Authz::RolePermissionsconcern.define_role_permissions(:project)inProjectPolicyanddefine_role_permissions()inGroupPolicy.- The role YAMLs already cover most direct grants.
The infrastructure is ready. The work is per-file migration.
Approach
Mirrors #593225 (closed): each in-scope policy file gets a child Task work item; each Task is a small refactor MR. A final Task removes the seven condition(:role) definitions once nothing references them.
Parent: gitlab-org/gitlab&21288