fix: log when stale consent access cannot be revoked - #75
Conversation
grantAccessToViews reconciles a consent's shadow user against the views the consent declares: it grants what is missing and revokes what is no longer declared. The grant half collects its results and fails the request when one of them fails. The revoke half discarded its results entirely, so a revoke that failed left the shadow user holding a view the consent had given up, with no trace anywhere. canRevokeOwnerAccess is the realistic way to reach that: it refuses to drop an `owner` row when no other principal holds one on the account, and an OBP-native consent can carry `owner` because createConsentJWT takes its views from whatever the PSU already holds. Narrowing such a consent then silently keeps the wider access. Log rather than fail. Returning a Failure here would make a consent stuck in this state unusable altogether, including the views it still legitimately holds, and leave the operator no way back short of editing rows by hand. The access it should have lost is a smaller problem than the access it should have kept, so the request is served and the discrepancy is recorded at warn with the consent, user, account and view named.
|
VerificationBuilt a jar from No regression — full local suite, 5 flows: 105/105. The branch itself. No-regression says nothing about whether the new path works, so the failure was constructed rather than waited for. The log line: The response body is the part worth reading. That 200 came back with: {"accounts":[{"id":"1","bank_id":"gh.29.uk", …}]}The un-revoked access was actively serving an account the consent never named. That is the concrete harm, and until this change it happened without a single line anywhere. It still happens — deliberately, because failing the request would take the consent's legitimate views down with it — but it is now attributable to a specific consent, user, account and view. The test cleans up in a |



What
grantAccessToViewsreconciles a consent's shadow user against the views the consent declares: grant what is missing, revoke what is no longer declared. The two halves treated failure very differently.The grant half collects its results and turns a failure into
Failure(CouldNotAssignAccountAccess…), so the caller sees it. The revoke half discarded its results:A revoke that failed left the shadow user holding a view the consent had given up, and nothing anywhere recorded it. The only symptom would be data the consent no longer covers still being served.
How it is reached
revokeAccessis not unconditional.canRevokeOwnerAccess(MapperViews.scala:405) refuses to drop anownerrow when no other principal holds one on that account. An OBP-native consent can carryowner, becausecreateConsentJWTbuilds its view list from whatever the PSU already holds. Narrowing such a consent — droppingowner, keeping the rest — is then a silent no-op for that one view.Why log rather than fail
Returning a
Failurewould make the whole request fail, which means a consent stuck in this state becomes completely unusable — including every view it still legitimately holds — and the operator has no way back short of editing rows by hand.The two failure directions are not symmetric. A failed grant denies access the consent asked for, and the caller finds out at once. A failed revoke keeps access the consent gave up, which is narrower and, once logged, actionable. So the request is served and the discrepancy is recorded at
warn, naming the consent, the user, the account and the view:Scope
One file, one loop.
case Full(true) => // gonekeeps the successful path silent; every otherBoxshape logs. No control flow, no signature, no caller changes.Verification
mvn -o -pl obp-api -am compilecleanFound by review of #73, which touched the same function.