What must an authorization decision record contain so a disputed read can still be explained eighteen months later?
answer
- the answer without the working
- reconstruct, do not merely assert
- who, what, which object, which verdict
- the rule that fired, and its version
- freeze the inputs the rule read
basics
~20 sA 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.
solid answer
~50 sThe difference between a useful record and a useless one is **reconstruct versus assert**. `ALLOW` on its own is the answer with the working thrown away. A record that survives a dispute carries the *subject* (who the request ran as), the *actor* when someone is acting on another principal's behalf, the *resource identity* including the site or tenant it belongs to, the *action*, the *verdict*, the *rule, role or grant that produced it*, the *version of the rule set in force*, the attribute values the verdict turned on, and a *request correlation identifier* tying it to the rest of the request. The version and the inputs are what let you re-run today's rules against that frozen input and see whether the answer has since changed — which separates a rule that was wrong from data that has moved on.
code
json · 12 lines{
"recordedAt": "2026-04-11T09:22:41.118Z",
"requestId": "7f2c1a9e-6d31-4a08-9b77-2f0c5e41d8aa",
"subject": { "id": "u-8831", "roles": ["site-coordinator"], "siteScope": ["SITE-204"] },
"actor": { "id": "u-1042", "roles": ["support-agent"] },
"resource": { "type": "participant", "id": "P-10457", "site": "SITE-311" },
"action": "participant.read",
"verdict": "ALLOW",
"firedRule": "support.cross-site-read-during-open-incident",
"ruleSetVersion": "2026-04-03.17",
"inputs": { "openIncident": true, "subjectSiteMatchesResource": false, "studyPhase": "III" }
}go deeper
Recall the field list and the one-line reason for it: a record has to explain a verdict, not just state it. Subject, resource, action, verdict, the rule that fired, and something that ties it back to the request.
Explain why the rule set version and the inputs are what make the record replayable, and how they separate a wrong rule from changed data. Name the actor field and when it is populated.
Show that the evaluation interface has to return more than a boolean for any of this to be recordable, and that the decision record, the access log and the change history are three artefacts, not one.
Own the consequence: whether a wrong answer is detectable eighteen months later is decided when the evaluation interface and the versioning scheme are designed, and nothing later in the stack can recover what was never returned.
## Assert versus reconstruct Most services that log authorization at all log the outcome: principal, endpoint, `ALLOW`. Eighteen months later somebody asks why a coordinator at one trial site read a participant belonging to another, and that line answers the wrong question. It states that the system allowed it. The dispute is about **why**, and why is a function of inputs the line did not keep. A record is usable when a reader with no access to the running system can recompute the verdict from the record alone, or at minimum can see which rule produced it and what that rule was looking at. Everything below follows from that single test. ## The field set - **subject** — the principal the request ran as, identified by a stable internal identifier, not a display name that can be edited later. Carry the scope that was in force for that subject: the sites, organisations or tenants its grant was confined to at the time. - **actor** — the person performing the action on another principal's behalf, present only when it differs from the subject. Two identities on one record is the difference between *the investigator read it* and *support read it while standing in for the investigator*. - **resource identity** — type plus identifier plus the owning boundary (`participant P-10457`, `SITE-311`). The owning boundary is what makes a cross-site read legible without a second lookup into data that may since have changed. - **action** — the verb the rule was asked about, in the application's own vocabulary, not the transport verb. - **verdict** — allow or deny, as a single field with a closed set of values, so it can be counted without parsing prose. - **the rule, role or grant that produced it** — an identifier, not a sentence. This is the field that turns a record into an explanation. - **the version of the rule set in force** — an immutable identifier for the deployed set of rules and role definitions. Without it the record points at a rule whose text has since been rewritten. - **the inputs the verdict turned on** — the attribute values read while deciding: the subject's site list, the resource's site, a study phase, a consent flag. A rule that reads nothing but a role name has no inputs, and that absence is itself informative. - **a request correlation identifier** — the same value carried by the rest of the request's telemetry, so the record can be joined to what the request actually did afterwards. ## Why the version matters more than it looks Role definitions and rules change. If the record names the rule but not its version, a reviewer reading it today sees the *current* rule and quietly assumes it was the one that fired. That assumption is wrong exactly when it matters — after a widening change. Pin the version at the moment of the decision, keep the versions immutable, and the two questions separate cleanly: 1. **Was the verdict correct under the rules then in force?** Replay the frozen inputs against that version. 2. **Would it still be allowed today?** Replay the same inputs against the current version. A difference is a change in the rules; sameness with a different real-world outcome is a change in the data. ## What this record is not | artefact | question it answers | |---|---| | the authorization decision record | why this request was allowed or refused | | the request access log | that the request happened at all, and what it returned | | the data change history | what the stored values were before and after | They are three different artefacts with three different retention stories, and conflating them produces a record that is bulky and still cannot answer the dispute. The decision record is deliberately narrow: it explains one verdict. ## Where the fields come from The subject, actor, resource and action are known at the enforcement point — the place that refuses or proceeds. The rule identifier, the version and the inputs are known to whatever evaluated the rule, which may be the same code or may be a separate evaluator. If the evaluator returns only a boolean, the record can never carry more than an assertion, so the evaluation result has to be a small structure — verdict plus the rule that fired plus what it read — not a bare yes or no. That is a design constraint on the evaluation interface, and it is much cheaper to impose on day one than to retrofit after the first dispute.
- What does recording the inputs buy you that recording the verdict does not?Replay. With the inputs frozen you can run the same values through the rule set version that fired and confirm the verdict was consistent, then run them through today's version and see whether the answer has changed. Without them you cannot tell a rule that was wrong from data that has since moved, and both explanations fit the evidence equally well.
- The role that allowed the read has since been redefined. How does the record still explain the verdict?Only through the rule set version. The record names the rule and the immutable version identifier in force at the time, and the versions are kept as deployed artefacts, so a reviewer reads the definition as it was rather than as it is. A record naming a rule without a version silently substitutes today's definition for the one that actually fired.
- Should a denied decision carry the same fields as an allowed one?Yes, and the deny case is where the inputs earn their keep fastest: a refusal a user disputes is answered by showing which input failed the rule. The only asymmetry worth having is volume — denials are rare enough to record in full, while routine allows on high-traffic reads may be sampled.
A referee's match report. 'Goal disallowed' settles nothing a year later; 'goal disallowed, offside, called by the assistant, under the wording of the rule in force that season' can be argued with — and that is the whole point of writing it down.
saying these in an interview costs you the question
- Logs the verdict and the endpoint, and nothing about the rule.
- Names the rule but not the version of the rule set in force.
- Stores the subject's display name instead of a stable identifier.
- Assumes today's role definition is the one that fired.
- Treats the request access log as the decision record.
- Has the evaluator return a bare boolean, so no record can explain anything.