feat(auth): tell staff why an elevated endpoint is refused - #4566
Conversation
A staff member who holds the role but is not KYC-cleared got the generic "Forbidden resource", which is indistinguishable from a removed role. Neither the person nor any tooling in front of the API could tell that the fix is to complete an identification. RoleGuard now throws StaffKycRequiredException: HTTP 403 with a machine-readable code STAFF_KYC_REQUIRED and a message naming the actual requirement. This follows the existing TFA_REQUIRED pattern, so clients branch on the code instead of matching prose. A wrong ROLE still yields the generic 403 - the two cases need different actions and must stay distinguishable. The protected KYC file route checked both conditions through one predicate and reported "Requires admin or compliance role" even when the role was fine; it now reports each case separately. JwtUserActiveGuard calls the guard programmatically, but only with UserRole.USER, which is never elevated - so it still receives a boolean.
|
CI is green on the current head (12/12), including the dedicated 100% coverage gate, which now also Two independent reviews were run. The conformity review returned two minor points. Both concern
A second review pass (logic/correctness) is still in progress. Its findings will go into the same The matching frontend change is DFXswiss/services#1233. It is intentionally still a draft: a review |
Why
A staff member who holds the role but is not KYC-cleared received the generic
{"statusCode":403,"message":"Forbidden resource"}— indistinguishable from a removed role or amissing endpoint. Neither the person nor any tooling in front of the API could tell that the fix is
to complete an identification, which makes the rollout of the staff KYC gate needlessly opaque.
What
RoleGuardnow throwsStaffKycRequiredExceptioninstead of returning a barefalse:{ "code": "STAFF_KYC_REQUIRED", "message": "Staff access requires a completed identification: KYC level 50 and a verified name on your account" }Still HTTP 403. The
codefollows the existingTFA_REQUIREDpattern, so clients branch on a stableidentifier instead of matching prose.
A wrong role still yields the generic 403. The two situations need different actions — obtain a
role vs. complete an identification — and collapsing them into one answer is what caused the problem
in the first place.
The protected KYC file route (
KycService.getFileByUid) checked role and clearance through a singlepredicate and reported
Requires admin or compliance roleeven when the role was fine. It nowreports each case separately.
JwtUserActiveGuardcalls the guard programmatically, but only withUserRole.USER, which is neverelevated — so it still receives a boolean.
Tests
The guard suite now pins the thrown status, the code and the message, and asserts that a wrong role
still returns
falsewithout consulting the clearance. The new exception is added to the dedicated100% coverage gate (
test:staff-gate:cov) alongside the other files deciding elevated access.Follow-up
The matching frontend change (showing the reason and offering to start KYC) is a separate PR in the
services repository.