API Function-Level Authorization Review Guide
Privilege matrix and denied invocation proof
List operations by capability: approve refund, suspend account, export audit data, rotate integration secret, impersonate support user, and replay a queue command. Include internal RPC and scheduled handlers because a hidden admin screen is not a security boundary. For each role or machine credential, declare required permission, tenant scope, approval prerequisite, and expected audit event.
Exercise direct calls with a lower role. A support session calling POST /refunds/42/approve should produce neither approval state nor payment-provider request. Repeat with a valid finance role above its approval ceiling, an ingestion key calling delete, and a user attempting a protected action through another HTTP method. Record exact policy decision and target state. A denial that still writes audit data is fine; a denial that queues work is not.
Close a BFLA finding by placing permission evaluation before handler side effects and removing fallback “admin” branches. Emergency access may be an exception only when named approvers, constrained duration, target scope, and separate audit review exist. Regression tests must cover each privileged verb and method variant; release evidence names policy owner who accepted the matrix.
Permissions need a stable vocabulary. Map route actions to verbs such as refund.approve or audit.read rather than infer privilege from controller name. Machine credentials require narrower scopes than human administrators and should be denied interactive actions. Test policy cache invalidation after a role removal: an old token or long-lived worker must not retain a function after permission changes. Store policy version with audit event so a later incident can reconstruct which rule permitted action.
Additional mechanism checks
Function tests need state-machine awareness. A caller permitted to create a refund request may not approve it; an approver may need a different person’s request and a monetary threshold. Send requests at every transition, including cancel-after-approve and repeat-approve. Verify idempotency does not turn a denied first attempt into allowed replay after role changes. Administrative APIs often have alternate serializations such as GraphQL mutation, RPC, or batch endpoint; permission must converge on same policy action. Audit log should distinguish attempted denial from completed action so review does not infer success from presence of event.
Topic-specific validation
A permission change has two clocks: policy publication and enforcement cache expiry. Revoke a role, wait through documented propagation, then call each privileged endpoint with old token and fresh token. Inspect scheduled commands too. If policy uses approval limits, test exact boundary and currency conversion branch. A safe denial preserves draft data only where design says it may; it never completes payment, export, deletion, or impersonation. Keep a decision trace containing principal, action, target, policy version, and result. Emergency access must expire automatically and receive a later independent review.
Approval-state test case
Use one draft refund and four synthetic principals: customer, support agent, finance approver below a configured limit, and finance approver above that limit. The customer may cancel its own draft if policy permits, but must not set approved. Support may read a scoped ticket without reaching payment-provider code. The below-limit approver must receive a denial with unchanged balance and no queue message. The authorized approver should create exactly one immutable approval event and exactly one provider request.
Repeat the same capability through a wrong HTTP method, an internal RPC method, and a batch representation. Remove the approver permission, then retry with an old token and a fresh token after policy-cache propagation. Expected outcome is a policy denial before handler side effect for every representation. Evidence includes policy action name, policy version, principal, target refund, state before and after, provider-call count, and audit correlation ID.
Emergency access is not a broad administrator fallback. If business process requires it, grant a narrowly scoped action for one target and a short approved interval, log use, and require a second reviewer to inspect completed work. Closure reruns the denied request after expiry and confirms access disappears. This article describes test mechanics; it does not assert a service-level outcome or compliance status.
Permission mapping evidence
Use stable policy actions such as refund.approve, account.suspend, and audit.export; route names are too easy to change or duplicate. Map every HTTP, RPC, queue, and GraphQL entry point to one action. Service credentials should hold only actions needed for their integration. A key that ingests records must fail attempts to delete accounts, rotate secrets, or impersonate users.
Test cached authorization deliberately. Revoke a permission, record policy publication time, then call privileged action with a token issued before revocation and another after it. Any cache grace period needs documented maximum and must not exceed risk owner decision. Inspect scheduled workers because they may retain claims longer than request handlers. Audit evidence should separate denied attempt from completed action.
For remediation, remove implicit administrator branches and evaluate policy before provider call, write, or enqueue. A valid approval must retain target, approver identity, approval limit, and decision time in immutable audit event. Retest a neighboring privileged operation so a shared middleware change cannot silently create a second bypass.
Privileged-action closure register
| Policy action and principal | Owner | Evidence | Expected result | Observed result / status | Exception | Closure |
|---|---|---|---|---|---|---|
refund.approve; support fixture calls POST /refunds/42/approve | Authorization policy owner | Decision trace BFLA-042, policy v18; balance and provider-call diff | Deny before write or provider call | 403; balance unchanged; provider calls 0 — closed | None | Regression case authz-refund-07 passed after cache expiry; owner signed 2026-06-11 |