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
-
In a security policy project, create two approval policies that each have several rules matching the target branch, plus
bypass_settingsnaming 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>- Link the security policy project to a target project whose
mainbranch is protected. - As the bypass user, open an MR into
main. - In the merge checks, next to "All policy rules must be satisfied", select Bypass.
- 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
- Frontend (small, low risk): in
result(), deduplicate:uniqBy(get(...).filter(({ allowBypass }) => allowBypass), 'id'). Add a spec that includes duplicatepolicyBypassStatuses. This fixes the modal only; API consumers and the bot comment still see one entry per rule. - Backend (wider fix): have
MergeRequest.policyBypassStatusesreturn 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-631418MR !1 (merged), with security policy projectdjb_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.
Related
- MR !204043 (merged) (!204043 (merged)): added deduplication on the warn-mode path only.