Your authorization rule reads a researcher's affiliation, an animal's embargo date and the wall clock — where must each value come from?
answer
- trust first, freshness second
- frozen at issue, read live, or refused
- the caller names the resource, not itself
- credential lifetime bounds the staleness
- decide on the row you serve
basics
~20 sThe affiliation from the verified credential or a live store, the embargo date from the record the response is built from, the instant from the evaluating server's clock. Nothing the rule believes about the caller may come from the caller's own request.
solid answer
~50 sEvery attribute has a provenance, and provenance decides both trust and freshness. A value **frozen into the verified credential** at sign-in — `sub`, `roles`, `groups`, `entitlements`, an affiliation list — costs no extra round trip but can be wrong for as long as that credential lives; withdrawing an affiliation does not reach a credential already issued. A value **read live** from the store that owns it is current but adds a lookup to every guarded request, and needs its own failure path. A value the **caller sent** is not an input at all: the request may name which animal and which action it wants, but it may not supply the values the rule uses to judge the caller, because the caller would then be choosing its own answer. One more rule: read the record once and decide on the same object you serve, or the decision and the response can disagree.
code
pseudocode · 23 linesfunction decideReadPreciseFix(request, credential):
# subject: verified claims only - never request fields
subject = {
sub: credential.sub, # frozen at sign-in
affiliations: credential.affiliations, # frozen at sign-in
suspended: accountStore.isSuspended(credential.sub) # read live
}
# resource: one read, and the SAME object is served later
animal = animalStore.load(request.pathParam("animalId"))
if animal is null:
return NotFound
input = {
subject: subject,
action: "read-precise-fix",
resource: animal,
environment: { now: serverClock.now() }
}
# request.body.clearance is deliberately absent from `input`
decision = evaluator.evaluate(input)
return { decision: decision, servedFrom: animal }go deeper
Remember the hard line: the request says which animal and which action, and nothing else the rule reads. A clearance level sent by the caller is not an input, no matter how carefully it is validated.
Sort each attribute into frozen-at-issue, read-live, or not-an-input, and justify each placement. Explain why a frozen affiliation is wrong for as long as the credential lives, and why the record must be read once and reused for the response.
Show the operational side: a live read is a hot-path dependency with its own timeout and its own fail-closed path, and an undecided request must be distinguishable from a refused one in whatever the operator is looking at.
Own the staleness budget across the whole chain — credential lifetime plus any stored answer — and say which attributes may be frozen at all for data this sensitive. That number is a product decision about how fast a withdrawal must bite, not an implementation detail.
## Three provenances, and only two of them are inputs A rule over attributes is exactly as sound as the values handed to it. In the wildlife-tracking service, one rule reads three values with three different origins: the researcher's study-area affiliation, the animal's `embargoUntil` date, and the current instant. Each arrives by a different route, ages at a different rate, and fails differently. | value | origin | how stale it can be | what getting it wrong costs | |---|---|---|---| | study-area affiliation | frozen into the verified credential at sign-in | up to that credential's remaining lifetime | a withdrawn affiliation still reads precise fixes | | `embargoUntil`, critical flag | read live from the record that owns it | as stale as the row you read | a lifted embargo is not honoured, or a new one is ignored | | current instant | the evaluating server's own clock | not applicable; clock skew between hosts is the risk | a boundary fires early or late on one replica | | anything in the request body | the caller | irrelevant — it is not trustworthy at any age | the caller picks its own answer | ## What the credential froze Attributes carried as claims were decided by the issuer at sign-in and are true as of that instant and no later. They are cheap: verifying the credential costs no round trip and the values are already in memory when the request arrives. What that cheapness buys is a **lag**. If an affiliation is withdrawn ten minutes after sign-in, every request made with that credential until it expires still presents the old affiliation, and the evaluator has no way to notice. The lag is bounded by the credential's lifetime and by nothing else. That is a design choice made **per attribute**, not once for the system. A one-hour lag on a study-area affiliation is often fine. A one-hour lag on "this account was suspended for leaking coordinates" is usually not, which is why the second is read live even in a service that happily freezes the first. If you cannot say out loud how long a frozen attribute may be wrong, you have not made the choice — you have inherited it from whoever set the credential lifetime. ## What must be read live Resource attributes belong to the record, so the service reads them from the system of record at decision time. Two disciplines apply. First, a live read is a dependency on the hot path and needs its own failure path. If the store that holds the embargo date is unreachable, the rule cannot be evaluated; the request is **undecided**, which is not the same as denied by rule, and the two must be distinguishable to whoever is paged. Second, and less obvious: decide on the snapshot you serve. If the service loads the animal, decides, and then loads it again to build the response, the two reads can disagree — an embargo lifted in between, a flag set in between — and the bytes returned are not the bytes the decision was made about. Read once, decide on that object, serve that object. Where the rule needs a value that genuinely cannot be loaded alongside the record, keep both reads inside one transaction or accept the window and write it down. ## What the caller sent The request legitimately designates **which** animal, **which** action, which page size, which format. It may not carry the caller's clearance, affiliation or role, because a value the caller controls is a value the caller can choose to make the rule say yes. This is a question of provenance, not of validation: a `clearance` field that passes every schema check and every length limit is still a number the caller typed. The same applies to a header a client can set, and to a value the client obtained honestly and then edited. The line is easy to draw in practice: everything the rule believes **about the caller** comes from the verified credential or from a store the caller cannot write. Everything the rule believes **about the resource** comes from the record. The request supplies identifiers and intent, and nothing else the rule reads. ## A checklist you can run over any rule 1. For each attribute the rule references, name its origin: verified credential, live store, or server clock. 2. For each credential-carried attribute, state how long it may be wrong, and say whether that is acceptable **for this rule**. 3. For each live-read attribute, say what happens when the read fails — that is a fail-closed path like any other, and it must be reported as an availability failure rather than a refusal. 4. Confirm that nothing the rule believes about the caller arrives in the request the caller sent. 5. Confirm the resource attributes come from the same object the response body is built from. An engineer who can run that list over a real rule is doing the part of attribute-based authorization that actually goes wrong in production. The rule expression itself is rarely the defect; the value handed to it usually is.
- A live attribute read times out. What does the rule return?Nothing — it cannot be evaluated, so there is no rule answer. Treat the request as undecided and fail closed, but report it as an availability failure rather than a refusal, so the researcher is told to retry and the operator sees a store outage instead of a spike of permission denials that reads like an attack.
- Which attributes are worth freezing into the credential rather than reading live?Ones that change rarely, whose staleness you can state and accept, and that most guarded requests need — a study-area affiliation is the model case. Anything whose whole purpose is to stop access quickly, such as a suspension flag, belongs in a live read, because freezing it means the stop does not take effect until the credential expires.
- If a decision is later cached, do the two staleness windows add up?Yes. A credential that lives another 20 minutes plus a stored answer that lives another 60 seconds means a withdrawn affiliation can still be honoured for roughly 21 minutes in the worst case. Budget the sum, not each half, and choose which of the two to shorten when the total is more than the data can tolerate.
A site visitor's badge. The name printed this morning is frozen — right when it was printed, possibly wrong by four o'clock. The host's phone number is looked up live at the desk. And a clearance the visitor has written on the badge in biro is simply not read: the desk refuses it, however neat the handwriting.
saying these in an interview costs you the question
- Reads the caller's clearance from the request body after validating its shape.
- Assumes a value on a signed credential is current rather than as of issue.
- Treats an unreachable attribute store as a plain denial.
- Loads the record twice: once to decide, once to respond.
- Says freezing attributes is fine because the credential is signed.
- Sets one freshness policy for every attribute in the vocabulary.