Skip to content

ENG-3418: Block approval of unverified duplicate privacy requests#7972

Merged
JadeCara merged 9 commits intomainfrom
ENG-3418/block-approve-unverified-duplicate
Apr 21, 2026
Merged

ENG-3418: Block approval of unverified duplicate privacy requests#7972
JadeCara merged 9 commits intomainfrom
ENG-3418/block-approve-unverified-duplicate

Conversation

@JadeCara
Copy link
Copy Markdown
Contributor

@JadeCara JadeCara commented Apr 20, 2026

Ticket ENG-3418

Description Of Changes

A duplicate privacy request whose identity was never verified could be approved and processed by an admin, bypassing identity verification entirely. This happened because duplicate was an allowed status for approval (intentional admin override for false-positive duplicates), but there was no secondary check for whether the identity had actually been verified.

The fix adds a guard: duplicate requests can only be approved if identity_verified_at is set. The admin UI also hides the approve button for unverified duplicates. Deny is still allowed for cleanup.

How this happens:

  1. Request created from privacy center → identity_unverified
  2. Duplicate detection marks it as duplicate before user can verify
  3. Admin sees the duplicate in the request list and clicks Approve
  4. Request gets processed without identity ever being proven

Code Changes

  • src/fides/service/privacy_request/privacy_request_service.py — Added guard in approve_privacy_requests: rejects duplicate + identity_verified_at is None; logs error for blocked attempts
  • clients/admin-ui/.../useApproveDenyPrivacyRequest.ts — Hides approve button for unverified duplicates (deny still shown)
  • clients/admin-ui/.../types.ts — Fixed identity_verified_at type to string | null to match API response
  • tests/.../test_privacy_request_endpoints.py — Added test_approve_unverified_duplicate_rejected and test_bulk_approve_mixed_verified_unverified_duplicates; updated existing duplicate approve tests to set identity_verified_at

Steps to Confirm

  1. Enable duplicate request detection, identity verification, and manual approval in settings
  2. Configure Mailgun messaging service
  3. Submit a DSR from the privacy center → status: identity_unverified
  4. Submit a second DSR with the same identity → status: duplicate
  5. Log in to the admin UI and navigate to the privacy requests list
  6. Confirm the Approve button is not shown for the unverified duplicate request (Deny should still be visible)
  7. Via API, attempt to approve the unverified duplicate → blocked with "Cannot approve unverified duplicate request"
  8. Verified duplicates CAN still be approved (the admin override for false positives)

E2E results:

Request 1 (pri_c0a2d...): identity_unverified (canonical)
Request 2 (pri_7e71e...): duplicate, identity_verified_at=None
Approve Request 2 → Failed: "Cannot approve unverified duplicate request"  ✓

Pre-Merge Checklist

  • Issue requirements met
  • All CI pipelines succeeded
  • CHANGELOG.md updated
    • Add a db-migration This indicates that a change includes a database migration label to the entry if your change includes a DB migration
    • Add a high-risk This issue suggests changes that have a high-probability of breaking existing code label to the entry if your change includes a high-risk change
    • Updates unreleased work already in Changelog, no new entry necessary
  • UX feedback:
    • All UX related changes have been reviewed by a designer
    • No UX review needed
  • Followup issues:
    • Followup issues created
    • No followup issues
  • Database migrations:
    • Ensure that your downrev is up to date with the latest revision on main
    • Ensure that your downgrade() migration is correct and works
      • If a downgrade migration is not possible for this change, please call this out in the PR description!
    • No migrations
  • Documentation:
    • Documentation complete, PR opened in fidesdocs
    • Documentation issue created in fidesdocs
    • If there are any new client scopes created as part of the pull request, remember to update public-facing documentation that references our scope registry
    • No documentation updates required

JadeCara and others added 2 commits April 20, 2026 14:54
A duplicate request whose identity was never verified could be approved
and processed by an admin, bypassing identity verification. This adds a
guard in approve_privacy_requests that rejects duplicates with
identity_verified_at=None, and hides the approve button in the admin UI
for unverified duplicates (deny is still allowed for cleanup).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented Apr 20, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

2 Skipped Deployments
Project Deployment Actions Updated (UTC)
fides-plus-nightly Ignored Ignored Preview Apr 21, 2026 5:02pm
fides-privacy-center Ignored Ignored Apr 21, 2026 5:02pm

Request Review

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 20, 2026

Title Lines Statements Branches Functions
admin-ui Coverage: 8%
6.34% (2799/44080) 5.59% (1402/25046) 4.44% (579/13039)
fides-js Coverage: 78%
78.98% (1962/2484) 65.55% (1214/1852) 72.57% (336/463)
privacy-center Coverage: 88%
85.97% (331/385) 81.36% (179/220) 78.87% (56/71)

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 20, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 84.94%. Comparing base (b5b98e6) to head (57072af).
⚠️ Report is 10 commits behind head on main.

❌ Your project status has failed because the head coverage (84.94%) is below the target coverage (85.00%). You can increase the head coverage or adjust the target coverage.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #7972      +/-   ##
==========================================
- Coverage   85.04%   84.94%   -0.11%     
==========================================
  Files         631      630       -1     
  Lines       41213    41086     -127     
  Branches     4807     4769      -38     
==========================================
- Hits        35049    34899     -150     
- Misses       5070     5103      +33     
+ Partials     1094     1084      -10     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@JadeCara
Copy link
Copy Markdown
Contributor Author

/code-review

Copy link
Copy Markdown
Contributor

@claude claude Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review: Block approval of unverified duplicate privacy requests

This is a well-scoped security fix. The core guard is placed at exactly the right point in approve_privacy_requests — after the status allowlist check but before the DB write — so it cannot be bypassed by manipulating the request body. Backend and frontend defenses are independent of each other (defense in depth), which is the right pattern.

What's done well

  • The if block in the service is placed correctly: after the existing PrivacyRequestStatus allowlist check, before the try block that performs the state transition. A caller cannot bypass it.
  • The frontend guard in useApproveDenyPrivacyRequest is independent of the backend — the Approve button is simply not rendered for unverified duplicates, not just disabled.
  • Two new tests cover both the single-request rejection and the mixed bulk-approval scenario with specific assertions on succeeded/failed counts and the error message.
  • The fix to test_approve_privacy_request (setting identity_verified_at for the duplicate parametrize case) is the correct minimal change to keep the existing parametrized test passing under the new invariant.
  • Changelog entry is present.

Findings

Suggestion — Missing audit log for blocked approval attempt (see inline comment on privacy_request_service.py): Every successful approval and terminal failure (e.g. wrong status) records an AuditLog entry. The unverified-duplicate case silently continues with no audit trail. Since this is a security-relevant event (an admin attempted to approve a request that bypassed identity verification), it would be valuable to log it for compliance and anomaly-detection purposes.

Nitidentity_verified_at type mismatch in PrivacyRequestEntity (see inline comment on useApproveDenyPrivacyRequest.ts): The field is typed as string | undefined in the entity type, but API responses return string | null. Runtime behaviour is correct since !value handles both falsy values, but updating the type to string | null | undefined would prevent subtle bugs if stricter checks are added later.

Nit — Missing privacy_request.delete(db) at the end of test_approve_unverified_duplicate_rejected for consistency with sibling tests (see inline comment on the test file).

Overall this is a solid, well-tested fix. The audit log suggestion is the only thing worth considering before merge.

🔬 Codegraph: connected (47107 nodes)


💡 Write /code-review in a comment to re-run this review.

data=PrivacyRequestResponse.model_validate(
privacy_request
).model_dump(mode="json"),
)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

src/fides/service/privacy_request/privacy_request_service.py:665

Suggestion: Consider adding an audit log entry for blocked approval attempts

Every successful approval records an AuditLog entry. The unverified-duplicate case is a security-relevant event (an admin attempted to approve a request that skipped identity verification), but it silently continues with no audit trail. Compared to a normal validation failure (e.g. wrong status), this case is more useful to surface for compliance audits or to detect repeated attempts.

Consider adding an AuditLog.create(...) before the continue, using an appropriate existing AuditLogAction (e.g. rejected):

AuditLog.create(
    db=db,
    data={
        "user_id": requester_id,
        "privacy_request_id": privacy_request.id,
        "action": AuditLogAction.rejected,
        "message": "Cannot approve unverified duplicate request",
    },
)

This keeps it consistent with the rest of the approve flow where all terminal outcomes are logged.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done — added a logger.error() call since this path only fires if someone bypasses the UI and hits the API directly. AuditLog wasn't the right fit since denied means an admin denied the request, whereas this is an invalid API call. The error log captures the request ID and user for investigation.

@JadeCara JadeCara marked this pull request as ready for review April 20, 2026 22:18
@JadeCara JadeCara requested review from a team as code owners April 20, 2026 22:18
@JadeCara JadeCara requested review from erosselli, lucanovera, nreyes-dev and speaker-ender and removed request for a team April 20, 2026 22:18
- Add logger.error() for blocked approval of unverified duplicate requests
- Fix identity_verified_at type to include null in PrivacyRequestEntity

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@gilluminate gilluminate left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FE looks good!

@daveqnet daveqnet added the do not merge Please don't merge yet, bad things will happen if you do label Apr 20, 2026
@daveqnet daveqnet marked this pull request as draft April 20, 2026 22:59
@daveqnet
Copy link
Copy Markdown
Contributor

Adding do not merge label and converting to draft for now

The guard added in the previous commit blocked approval of ALL
unverified duplicates, but when subject_identity_verification_required
is false, identity_verified_at is always None — breaking the admin
override for false-positive duplicates in non-verification deployments.

Now the guard only fires when identity verification is actually
required by the system. Also applies the same logic to both UI paths
(detail dropdown and table row actions) for consistency, and adds a
test confirming unverified duplicates are approvable when verification
is disabled.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Change type from Fixed to Security and link to GHSA-qx5f-ghc2-7g5c.
@daveqnet daveqnet removed the do not merge Please don't merge yet, bad things will happen if you do label Apr 21, 2026
@daveqnet daveqnet marked this pull request as ready for review April 21, 2026 17:27
@JadeCara JadeCara added this pull request to the merge queue Apr 21, 2026
Merged via the queue into main with commit e7a6527 Apr 21, 2026
103 of 106 checks passed
@JadeCara JadeCara deleted the ENG-3418/block-approve-unverified-duplicate branch April 21, 2026 17:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants