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_rulevia their authz role — can read all branch rule types admin_protected_branchcustom role: grantedread_branch_ruleand_read_protected_branch_ruleat project level via the custom ability YAML, but prevented onAllBranchesRuleandAllProtectedBranchesRulebecause those pseudo-rules have no associated protected branchmanage_merge_request_settingscustom role: grantedread_branch_rule,_read_custom_branch_rule, and_read_protected_branch_rulevia 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
branchRulesGraphQL 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.
Validate via specs (recommended)
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.rbValidate via Rails console
Prerequisites
This assumes GDK is running with an Ultimate licence and
gitlab-org/gitlab-testis available locally. Pick any two guest-level members of the project to act as test users.
-
Create two custom roles on the
gitlab-orggroup — 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.
-
Assign the roles to two guest members of
gitlab-org/gitlab-test:- User A → APB Test Role
- User B → MMS Test Role
-
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) # => trueValidate 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.