Skip to content

fix: report the revokes the consent revocation sweep could not perform, and document both cases - #76

Merged
hongwei1 merged 2 commits into
develop-obpfrom
docs/consent-stale-access-runbook
Aug 9, 2026
Merged

fix: report the revokes the consent revocation sweep could not perform, and document both cases#76
hongwei1 merged 2 commits into
develop-obpfrom
docs/consent-stale-access-runbook

Conversation

@hongwei1

@hongwei1 hongwei1 commented Aug 9, 2026

Copy link
Copy Markdown
Owner

Follow-up to #75. Writing the runbook for #75's warning turned up its sibling one function away, so this carries the fix and the runbook together.

The fix

revokeConsentAccountAccess (ConsentUtil.scala:920) counted attempts, not successes:

.map { access => revokeAccessToViewForUserAndConsumer(access, shadowUser, ALL_CONSUMERS) }.size

so dropped N account access rows named rows that were still in the table. revokeAccessToViewForUserAndConsumer calls the same canRevokeOwnerAccessAsBox as revokeAccess, so it refuses in exactly the cases #75 describes.

This is the worse of the two. When grantAccessToViews cannot revoke a stale view, the consent is live and revoking it would still clean up. Here the consent is already revoked and the access it created outlives it — nothing left in the system will ever come back for that row — while the log asserted it had been removed. The one signal an operator had was actively wrong.

Now: count only successes, and warn once per row that stayed.

WARN code.api.util.Consent$ -- revokeConsentAccountAccess: could not revoke owner on gh.29.uk/1
for revoked consent 74f4433e-…. The access outlives the consent: Failure(access cannot be revoked,Empty,Empty)

Introduced in 01826cede — my own, from the UK consent principal work.

The runbook

docs/operations/CONSENT_STALE_ACCESS.md, covering both warnings: what they mean, why the request is still served, how to tell the two cases apart, diagnosis SQL, resolution, verification, alerting.

The point worth surfacing from it — for a shadow user, a refusal can only mean no other principal holds owner on that account, not even the PSU. That is a finding about the account, not just the consent. It also gives the best resolution: restore the PSU's own access and the count exceeds 1, at which point the next use of the consent revokes the stale row by itself. That is the only fix that does not involve deleting rows by hand.

Verification

Built and ran locally; javap confirms the new string is in code/api/util/Consent$.class.

Extended the #75 reproduction to drive the sweep as well — same injected row, then revoke the consent through the real UK endpoint:

[PASS] the revoke case's consent works before anything is injected     got 200
[PASS] the revoke case has a shadow user to sweep                      resourceuser.id=773
[PASS] the consent was revoked                                         got 204
[PASS] the consent is recorded as revoked
[PASS] the sweep still removes the access it can remove                (0 legitimate rows left)
[PASS] the refused row outlives the consent                            (1 row still there)
[PASS] the sweep logged what it could not revoke
[PASS] the sweep's warning names the consent
22 checks, 0 failed   (both cases)

The fifth and sixth lines are the pair that matters: one row dropped, one refused. The old code would have logged dropped 2.

Full local suite across all five flows: 105/105, unchanged.

Every line number cited in the runbook was checked against develop-obp after the code edit rather than carried over from drafting.

revokeConsentAccountAccess counted its revoke attempts, not its successes:

    .map { access => revokeAccessToViewForUserAndConsumer(...) }.size

so "dropped N account access rows" named rows that were still in the table.
revokeAccessToViewForUserAndConsumer applies the same canRevokeOwnerAccess rule
as revokeAccess, and will not drop the last `owner` row on an account, so the
refusal is reachable here too.

This is the worse place for it to happen. When grantAccessToViews cannot revoke
a stale view, the consent is live and revoking it would still clean up. Here the
consent is already revoked and the access it created outlives it, with nothing
left in the system that will ever come back for that row -- while the log
asserted it had been removed.

Count only the successes, and warn once per row that stayed, naming the account,
the view and the consent so it can be cleared by hand.

Introduced in 01826ce.
Both warnings mean access is still held that a consent no longer covers, and
neither self-heals. The runbook says what the line means, why the request is
still served, how to tell the two cases apart, and what to do.

The diagnosis leans on one point that is easy to miss: for a shadow user, a
refusal means no other principal holds `owner` on that account -- not even the
PSU. That is a finding about the account, not just the consent, and restoring
the PSU's own access lets the next use of the consent revoke the stale row by
itself, which is the only resolution that does not involve editing rows by hand.
@sonarqubecloud

sonarqubecloud Bot commented Aug 9, 2026

Copy link
Copy Markdown

@hongwei1
hongwei1 merged commit 49723b4 into develop-obp Aug 9, 2026
25 checks passed
@hongwei1
hongwei1 deleted the docs/consent-stale-access-runbook branch August 9, 2026 18:14
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.

1 participant