Allow MR approval on group-level protected branches

What does this MR do and why?

Fixes the "Enable approval permissions for additional users" feature when the target branch is protected only at the group level (inherited).

The feature grants :approve_merge_request through the merge_request_group_approver policy condition in ee/app/policies/ee/merge_request_policy.rb. That condition matched protected branches via project.protected_branches, which returns project-level records only. When the target branch was protected only at the group level, no record matched, the ability was never enabled, and the Approve button and /approve quick action silently disappeared for the users who had been granted approval permission — even though the approval rule and approver avatars remained visible.

Every other part of the protected-branch permission flow (ProtectedBranch.protected?, Gitlab::UserAccess#can_merge_to_branch? — the "allowed to merge" half of the same feature) already uses project.all_protected_branches (the union of project- and root-group protected branches). This MR aligns the approval-permission check with them.

It checks every matching protected-branch record, not just the first, via a single existence query (the new ApprovalProjectRule.with_protected_branches scope). This preserves the pre-existing project-level approval behavior when a group-level protected branch also matches the same branch name — both protections can coexist and the approval rule may be attached to either record — without introducing an N+1.

The change is gated behind the approve_mr_on_group_protected_branches feature flag (default off); when disabled, the original project-level-only behavior is preserved unchanged.

Query plans

Took 10 protected branches from production clone and used their IDs. Using:

SELECT b.protected_branch_id, pb.project_id, pb.name, g.group_id AS approver_group_id
FROM approval_project_rules_protected_branches b
JOIN protected_branches pb ON pb.id = b.protected_branch_id
JOIN approval_project_rules_groups g ON g.approval_project_rule_id = b.approval_project_rule_id
WHERE pb.project_id IS NOT NULL
LIMIT 10;

Then took the top 1 user from one of the approver_group_id:

SELECT user_id FROM members
WHERE source_type = 'Namespace' AND type = 'GroupMember'
  AND source_id = <approver_group_id> AND access_level >= 15
LIMIT 1;

New with_protected_branches scope

https://console.postgres.ai/gitlab/projects/gitlab-production-main/sessions/53617/commands/155795

Updated group_access?

https://console.postgres.ai/gitlab/projects/gitlab-production-main/sessions/53617/commands/155796

Feature flag

Behind approve_mr_on_group_protected_branches (gitlab_com_derisk, default disabled, actor: project).

  • Rollout issue: #605444
  • When disabled (default): approval permission is evaluated against project-level protected branches only — identical to current behavior.
  • When enabled: group-level (inherited) protected branches are also considered.

The Changelog: trailer is intentionally omitted while the flag is default-off; it will be added when the flag is enabled/removed.

References

Closes #605091

Screenshots or screen recordings

Permission logic change with no UI code; validated through automated specs (see below).

Before After
Approve button / /approve absent for granted users when the branch is protected only at group level Granted users can approve regardless of whether protection is project- or group-level

How to set up and validate locally

  1. Enable the flag: Feature.enable(:approve_mr_on_group_protected_branches) (or scope it to a group/project in the rails console).
  2. Create a top-level group and a project inside it.
  3. Add a Reporter as an approver via the Enable approval permissions for additional users flow, scoped to a specific branch.
  4. Protect that branch at the group level (inherited), with no project-level protected branch for it.
  5. Open a merge request targeting the branch.
  6. Before this change (or with the flag disabled): the Approve button is absent and /approve does nothing. After (flag enabled): the Reporter can approve.

Automated coverage:

bin/rspec ee/spec/policies/merge_request_policy_spec.rb
bin/rspec ee/spec/requests/api/merge_request_approvals_spec.rb
bin/rspec ee/spec/models/approval_project_rule_spec.rb
  • Policy spec: branch protected at the group level (member allowed, non-member disallowed, and disallowed when the flag is off), and a branch protected at both levels with the rule on the project-level record (allowed with the flag on or off).
  • Request spec: end-to-end POST .../approve as a Reporter (group member approves; non-member is forbidden).
  • Model spec: the with_protected_branches scope (array and relation arguments).

MR acceptance checklist

Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.

Edited by Patrick Bajao

Merge request reports

Loading