Refactor branch rule policies and grant custom role access

What does this MR do and why?

Refactors branch rule policies to move read_branch_rule out of BranchRulePolicy and into the authz role definitions directly. AllBranchesRulePolicy and AllProtectedBranchesRulePolicy now inherit from BasePolicy instead of BranchRulePolicy, replacing the previous overrides approach with a prevent rule that preserves all delegated prevent_all steps from ProjectPolicy (job-token scope, namespace bans, IP restrictions).

Permission grants:

  • Maintainer, owner: granted read_branch_rule via their authz role — can read all branch rule types
  • admin_protected_branch custom role: granted read_branch_rule and _read_protected_branch_rule at project level via the custom ability YAML, but prevented on AllBranchesRule and AllProtectedBranchesRule because those pseudo-rules have no associated protected branch
  • manage_merge_request_settings custom role: granted read_branch_rule, _read_custom_branch_rule, and _read_protected_branch_rule via the custom ability YAML — can read all branch rule types (pseudo-rules and real per-branch rules) because all include approval rules or external status checks

Also restricts the approvalRules field on BranchRuleType to users with manage_merge_request_settings and externalStatusChecks to users with read_external_status_check, preventing privilege escalation where admin_protected_branch users could previously read approval rules.

The show_approvers flag in branch rules helper data is updated to check manage_merge_request_settings rather than read_approval_rule.

Related to #584313

How to set up and validate locally

Note: The branchRules GraphQL field is not yet accessible to custom role users — that gate will be lifted in a follow-up MR once this refactor is merged. Until then, the GraphQL query below only works for maintainers/owners. The primary validation path for custom role logic is the Rails console and the spec suite.

Run the new and updated specs directly:

bundle exec rspec ee/spec/policies/ee/projects/all_branches_rule_policy_spec.rb
bundle exec rspec ee/spec/policies/projects/all_protected_branches_rule_policy_spec.rb
bundle exec rspec ee/spec/requests/api/graphql/project/branch_rules_spec.rb
bundle exec rspec ee/spec/helpers/ee/projects/settings/branch_rules_helper_spec.rb
bundle exec rspec ee/spec/policies/project_policy_spec.rb

Validate via Rails console

Prerequisites

This assumes GDK is running with an Ultimate licence and gitlab-org/gitlab-test is available locally. Pick any two guest-level members of the project to act as test users.

  1. Create two custom roles on the gitlab-org group — one for each scenario:

    # admin_protected_branch role
    POST /api/v4/groups/9970/member_roles
    { "name": "APB Test Role", "base_access_level": 10, "admin_protected_branch": true }
    
    # manage_merge_request_settings role
    POST /api/v4/groups/9970/member_roles
    { "name": "MMS Test Role", "base_access_level": 10, "manage_merge_request_settings": true }

    Or via the UI: gitlab-org group → Settings → Roles and Permissions → Create new role.

  2. Assign the roles to two guest members of gitlab-org/gitlab-test:

    • User A → APB Test Role
    • User B → MMS Test Role
  3. Create an approval rule on gitlab-org/gitlab-test (any name, 0 approvers required is fine):

    POST /api/v4/projects/gitlab-org%2Fgitlab-test/approval_rules
    { "name": "Test Approval Rule", "approvals_required": 0 }

Testing

After setting up the prerequisite users and roles above, open a Rails console (gdk rails c) and check the policy decisions directly:

user_apb = User.find_by(username: 'user_a_username')
user_mms = User.find_by(username: 'user_b_username')
project   = Project.find_by_full_path('gitlab-org/gitlab-test')

all_branches      = Projects::AllBranchesRule.new(project)
all_protected     = Projects::AllProtectedBranchesRule.new(project)
protected_branch  = project.protected_branches.first
branch_rule       = Projects::BranchRule.new(project, protected_branch)

# User A (admin_protected_branch): can read individual protected branch rules only
Ability.allowed?(user_apb, :read_branch_rule, branch_rule)   # => true
Ability.allowed?(user_apb, :read_branch_rule, all_branches)  # => false
Ability.allowed?(user_apb, :read_branch_rule, all_protected) # => false

# User B (manage_merge_request_settings): can read all branch rule types
Ability.allowed?(user_mms, :read_branch_rule, branch_rule)   # => true
Ability.allowed?(user_mms, :read_branch_rule, all_branches)  # => true
Ability.allowed?(user_mms, :read_branch_rule, all_protected) # => true

Validate approvalRules / externalStatusChecks field guards (GraphQL, maintainer only)

The approvalRules and externalStatusChecks fields now carry authorize: guards. Use the GraphQL explorer at http://gdk.test:3000/-/graphql-explorer signed in as a maintainer or owner to confirm those fields still return data:

query {
  project(fullPath: "gitlab-org/gitlab-test") {
    branchRules(buildMissing: true) {
      nodes {
        id
        name
        approvalRules {
          nodes { id name }
        }
        externalStatusChecks {
          nodes { id name }
        }
      }
    }
  }
}

Expected: approvalRules is populated with "Test Approval Rule" on the All branches node; externalStatusChecks is present. Both fields will be null for a user without manage_merge_request_settings / read_external_status_check respectively.

Edited by Joe Woodward

Merge request reports

Loading
Loading