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:

  1. Role-to-permission mapping leaks into policy code. Permissions should be granted by role YAML configuration (config/authz/roles/*.yml), not declared inline as rule { developer }.enable :foo. The latter scatters role definitions across files and prevents a single source of truth.
  2. Custom roles cannot satisfy compound rules. A custom role with :foo permission doesn't pass rule { developer & condition_x }.enable :foo because it isn't 'developer.' Role conditions short-circuit custom-role grants.
  3. Permission audits become impossible. When the answer to 'what does developer get on a project?' lives partly in YAML and partly in policy rule blocks, 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.rb
  • app/policies/group_policy.rb
  • app/policies/project_snippet_policy.rb
  • app/policies/ci/pipeline_policy.rb
  • app/policies/deploy_token_policy.rb (separate handling - see child issue)

EE:

  • ee/app/policies/ee/project_policy.rb
  • ee/app/policies/ee/group_policy.rb
  • ee/app/policies/dependency_proxy/packages/setting_policy.rb
  • ee/app/policies/virtual_registries/policies/group_policy.rb
  • ee/app/policies/ai/conversation/thread_policy.rb
  • ee/app/policies/concerns/work_items/group_policy.rb
  • ee/app/policies/concerns/work_items/shared_policy.rb

Out of scope

  • condition(:owner) in app/policies/namespaces/user_namespace_policy.rb. This is @subject.owner == @user (does this user own the personal namespace), not role-membership.
  • *_access aliases (: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::RolePermissions concern.
  • define_role_permissions(:project) in ProjectPolicy and define_role_permissions() in GroupPolicy.
  • 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

Edited by 🤖 GitLab Bot 🤖