skip to content

Testing & Auditing

Authorization bugs stay invisible until exploited, so you bring a permission matrix generated from the route table and a decision record you can replay. Interviewers ask what an allow logs.

on this pageshow

questions

4

Why can an endpoint test suite pass every case and still miss that one user reads another user's records?

level: juniorimportance: must knowfreq 62%

answer

  1. green suite, one principal only
  2. every test logs in as the owner
  3. the rule is defined by refusals
  4. second principal, same role, other site
  5. assert the refusal, not the body

basics

~20 s

A suite that authenticates as the record's owner only ever exercises the allow path. Catching a cross-user read needs a second principal in the fixtures, holding the same role in a different scope, and an explicit assertion that their request is refused.

solid answer

~40 s

Almost every endpoint test seeds a record and then reads it back as the principal that seeded it, so every assertion is about the **allow** path. You could delete the authorization check from the handler and the file would still pass. The rule is defined by what it refuses, so the fixture needs a second principal — the *same role*, a different scope, say a coordinator at another trial site — and a test that calls the route as that principal and asserts the refusal. Two details decide whether the negative case means anything: the object must really exist (otherwise the refusal may come from a missing row), and the peer must hold the same role (otherwise you have only proved that a roleless user is refused).

code

pseudocode · 12 lines
pseudocode
# the negative case, written beside the positive one
owner = principal(role = "site-coordinator", site = "SITE-204")
peer  = principal(role = "site-coordinator", site = "SITE-311")   # same role, other site
p     = seedParticipant(site = "SITE-204", id = "P-10457")

# 1. prove the object exists, or the refusal below means nothing
assertStatus(get("/participants/" + p.id, as = owner), 200)

# 2. the assertion that actually exercises the rule
r = get("/participants/" + p.id, as = peer)
assertStatus(r, DENIAL_STATUS)                 # 403, or 404 if the contract hides existence
assertBodyContainsNone(r, [p.screeningNumber, p.initials])

go deeper

for a junior

Recall the shape: two principals in the fixture, the same role and different scope, a real object, and an assertion that the second one is refused. Say out loud that a passing positive test does not exercise the rule.

for a middle

Explain why the object must exist before the peer calls the route, and why a roleless peer weakens the test. Name what you assert beyond the status: no leaked fields, and an unchanged object after a refused write.

for a senior

Show how the negative case stops rotting — added in the same change as the route, visible in review, and covering the object relationships that actually exist in your model rather than just mine-versus-not-mine.

for a principal

Argue where this floor stops paying: per-route negative cases cover the route table and nothing else, and the same records are reachable from scheduled work and consumers that no request-level test ever calls.

## The suite only ever logs in as the owner A typical endpoint test does four things: seed a record, authenticate as somebody, call the route, assert the response body. The principal that seeds the record is almost always the principal that reads it back, because that is the shortest path to a passing test. Every assertion in the file is therefore an assertion about the **allow** path. Delete the authorization check from the handler and the file still passes — which is the precise sense in which a green suite says nothing about the *refusal* path. An authorization rule is defined by what it refuses; a test that never asks it to refuse has not exercised it. This is how cross-account reads ship out of teams with high coverage numbers. Line coverage counts the handler as covered, because the request went through it. What was never covered is the second principal. ## The fixture needs two principals and a real object Take a clinical-trial platform where each investigator site sees only its own participants. The minimum fixture for a route that takes a participant identifier is: - **an owner** — a coordinator at `SITE-204`, with a participant record that genuinely belongs to `SITE-204`; - **a peer** — a coordinator at `SITE-311`, holding the *same role*, differing only in the site their grant is confined to. This is the principal the negative case runs as; - **an unauthenticated caller**, whose refusal is a different statement: it is about identity (`401` — I do not know who you are), not entitlement (`403` — I know, and no). That case passes even on a route with no authorization rule at all, so it is not a substitute; - optionally a principal whose cross-site access *is* expected, so the deliberate exception is written down as a test rather than left undeclared. Two fixture details decide whether the negative case means anything: 1. **The peer must hold the same role as the owner.** If the peer is seeded with no roles at all, a refusal proves only that a roleless user is refused — the coarse check, which is rarely the broken one. The bug you are hunting lives in the scope comparison, not the role comparison. 2. **The object must exist.** If the fixture never created participant `P-10457`, the route can answer with a refusal simply because the row is missing, and the test stays green while the authorization check is absent. Read the object as the owner first, in the same test, and only then read it as the peer. ## Assert the refusal, not the absence of data | what the test asserts | what that proves | |---|---| | the status matches the denial status your contract promises | the check ran and produced a verdict | | no participant field appears in the body or the error text | the refusal did not leak the record it refused | | a refused write left the object unchanged | the check runs before the effect, not after it | | the owner's own read still succeeds in the same test | the fixture is real and the route is not merely broken | Pin whichever status the contract promises. Some routes answer `403`. Some deliberately answer `404` so that a refusal does not confirm the record exists. Either is defensible; a suite that asserts neither lets a later change flip between them silently. ## Add the negative case in the same change as the route The discipline that holds over time is simple to state and visible in review: a route that takes an object identifier is not finished until it has a test that calls it as a principal who must not reach that object. A change that adds a route with only positive tests is an incomplete change, and a reviewer can see that from the diff alone without reading the handler. ## What this still does not cover - **A second object relationship.** The peer test covers *not mine*. It does not cover *shared with me*, *delegated to me*, or *mine last month and transferred since*. - **Entry points that are not routes.** Scheduled work and message consumers reach the same records without passing through the route, and no request-level test touches them. - **Every object shape.** The test pins one participant at one site; a rule that is right for that shape can still be wrong for a participant enrolled at two sites. That is why the per-route negative case is a floor rather than a strategy. It is the cheapest test that turns the most common access bug from invisible into red.

  • The route answers 404 for a refused read so that a denial does not confirm the record exists. What should the negative test assert then?
    Assert `404` — whichever status the contract promises, asserted uniformly across every object-bearing route. The point of the assertion is not that one status is correct; it is that a later change which flips a refusal from `404` to `403`, or from a refusal to a success, turns the suite red instead of shipping quietly.
  • What makes a cross-user negative test falsely green?
    Three things, all fixture problems. The object was never created, so the refusal came from a missing row. The peer principal was seeded with no roles, so the coarse role check refused it and the scope comparison was never reached. Or the two principals were seeded into the same site, so the request was legitimately allowed and the assertion was rewritten to match.
  • Does an unauthenticated request to the route count as the negative case?
    No. An anonymous refusal is a statement about identity — `401`, I do not know who you are — and it passes on a route that has an authentication requirement and no authorization rule whatsoever. The case that exercises the rule is a known, authenticated principal who is refused: `403`, I know, and no.

Testing a lock only with the key cut for it. The key turns, the test passes, and nobody has yet tried the neighbour's key — which is the only trial that tells you whether the lock is a lock.

saying these in an interview costs you the question

  • Thinks a success for the record's owner proves the check exists.
  • Treats any refusal as proof the check ran, even when the fixture never created the object.
  • Uses one fixture user for the whole suite and adds roles to it.
  • Tests the denial with an anonymous request instead of a second logged-in principal.
  • Asserts only that the record is missing from the body, never the status.
  • Adds one denied-access test for the service rather than one per route.
open as a page

What must an authorization decision record contain so a disputed read can still be explained eighteen months later?

level: middleimportance: should knowfreq 48%

basics

~20 s

A usable record lets a reader recompute the verdict rather than merely read it: subject, the acting principal when it differs, resource identity, action, verdict, the rule or grant that fired, that rule set's version, the inputs it turned on, and the request correlation identifier.

open as a page

Your authorization log records only denials to keep volume down — what does that cost you when an access is later disputed?

level: seniorimportance: should knowfreq 40%

basics

~20 s

Denials are the system working; the event a dispute is about is an access that was wrongly allowed, and a deny-only log has no record of it. The fix is not logging every allow, but recording every deny plus the allows that are not routine.

open as a page

How would you build a roles-by-endpoints permission matrix that a newly added route cannot quietly slip past?

level: principalimportance: should knowfreq 34%

basics

~20 s

Generate the matrix from the server's own route table at test time and cross it with fixture principals. Any route without a declared expectation fails the suite, so a new route is red until a human classifies it, and the declaration file becomes the reviewable statement of policy.

open as a page