Duplicate policies still appear in the MR bypass-policy dropdown

Everyone can contribute. Help move this issue forward while earning points, leveling up and collecting rewards.

Summary

The merge request Bypass Policy Violation modal shows the same policy more than once in the Select policies to bypass dropdown: one entry for each rule in the policy. MR !204043 (merged) added deduplication to security_policy_violations.vue, but only on the warn-mode data path. The exception-mode path, which feeds this modal, is still not deduplicated.

Reproduced on GitLab.com 19.5.0-pre (4ceeb1e8524) on 2026-09-30.

Steps to reproduce

  1. In a security policy project, create two approval policies that each have several rules matching the target branch, plus bypass_settings naming an eligible user:

    policy.yml used
   approval_policy:
   - name: Dup Repro Policy A
     enabled: true
     rules:
     - { type: any_merge_request, branch_type: protected, commits: any }
     - { type: any_merge_request, branch_type: default, commits: any }
     - { type: any_merge_request, branches: [main], commits: any }
     - { type: any_merge_request, branches: ["ma*"], commits: any }
     actions:
     - { type: require_approval, approvals_required: 1, role_approvers: [owner] }
     bypass_settings:
       users:
       - id: <USER_ID>
   - name: Dup Repro Policy B
     enabled: true
     rules:
     - { type: any_merge_request, branch_type: protected, commits: any }
     - { type: any_merge_request, branches: [main], commits: any }
     actions:
     - { type: require_approval, approvals_required: 1, role_approvers: [owner] }
     bypass_settings:
       users:
       - id: <USER_ID>
  1. Link the security policy project to a target project whose main branch is protected.
  2. As the bypass user, open an MR into main.
  3. In the merge checks, next to "All policy rules must be satisfied", select Bypass.
  4. Open Select policies to bypass.

Actual behavior

The dropdown lists Policy A four times and Policy B twice, one entry per rule. The GitLab Security Bot comment on the MR also lists each policy once per rule.

Duplicates are identical at the data layer: same name (byte-identical, no whitespace or case differences) and same id.

getPolicyViolations response (excerpt):

"policyBypassStatuses": [
  { "allowBypass": true, "bypassed": false, "id": "gid://gitlab/Security::Policy/2492732", "name": "Dup Repro Policy B" },
  { "allowBypass": true, "bypassed": false, "id": "gid://gitlab/Security::Policy/2492732", "name": "Dup Repro Policy B" },
  { "allowBypass": true, "bypassed": false, "id": "gid://gitlab/Security::Policy/2492731", "name": "Dup Repro Policy A" },
  { "allowBypass": true, "bypassed": false, "id": "gid://gitlab/Security::Policy/2492731", "name": "Dup Repro Policy A" },
  { "allowBypass": true, "bypassed": false, "id": "gid://gitlab/Security::Policy/2492731", "name": "Dup Repro Policy A" },
  { "allowBypass": true, "bypassed": false, "id": "gid://gitlab/Security::Policy/2492731", "name": "Dup Repro Policy A" }
]

policyViolations.policies and policyViolations.anyMergeRequest also return 6 entries each.

Expected behavior

Each policy appears once in the dropdown, however many of its rules are violated. Selecting a policy still covers all of that policy's rules.

Root cause

In ee/app/assets/javascripts/vue_merge_request_widget/components/checks/security_policy_violations.vue, one Apollo query (getPolicyViolations) fills two separate lists:

Data property Populated by Deduplicated? Consumed by
policies update(): uniqBy(policies, 'securityPolicyId') Yes (2 entries in repro) activeWarnPolicies → SecurityPolicyViolationsModal (warn mode)
bypassStatuses result(): policyBypassStatuses.filter(allowBypass) No (6 entries in repro) activeBypassStatuses → SecurityPolicyBypassStatusesModal (exception mode)

The Select policies to bypass dropdown is in SecurityPolicyBypassStatusesModal, so it gets the list that isn't deduplicated. This matches the running app: policies.length === 2 while bypassStatuses.length === 6 and SecurityPolicyBypassStatusesModal.policyItems.length === 6.

Note: master now deduplicates by securityPolicyId, not 'name' as originally merged in !204043 (merged). Neither version covers bypassStatuses.

Proposed fix options

  1. Frontend (small, low risk): in result(), deduplicate: uniqBy(get(...).filter(({ allowBypass }) => allowBypass), 'id'). Add a spec that includes duplicate policyBypassStatuses. This fixes the modal only; API consumers and the bot comment still see one entry per rule.
  2. Backend (wider fix): have MergeRequest.policyBypassStatuses return one entry per policy instead of one per violated rule. This fixes every consumer, but first check whether anything depends on per-rule entries, such as bypass submission payloads or audit events.

Both can be done: the frontend guard now, the backend change as a follow-up.

Customer evidence

  • Support ticket: #754754
  • Internal reproduction (private): djb_ultimate_group/bypass-dup-repro-631418 MR !1 (merged), with security policy project djb_ultimate_group/bypass-dup-spp-631418.

Impact

Duplicate options make it hard to pick the right policy and reduce confidence that the intended policies will be bypassed. The duplicates grow with rule count, so policies with many rules make the dropdown noisy quickly.

Edited by 🤖 GitLab Bot 🤖