Where attributes come from
All names, domains, and identifiers in these examples are fictional. The Gazette's download rules read subject.roles, resource.status, resource.embargo_until and environment.device.managed, and they believe every one of them. If a value is wrong, the rule makes the wrong decision while looking exactly right. The condition is correct; the input is not.
Who says so?
For each attribute, the question to ask is which system is responsible for knowing its value. That system is the attribute's authoritative source, and the decision should take the value from it, or from a copy that faithfully follows it.
At the Gazette, the answers are mostly obvious once the question is asked. Omar's desk, and whether someone is staff or freelance, come from the HR system, which changes them when people are hired, move or leave. License regions and embargo times come from the rights database, where the rights desk records the terms each photo arrived with. A photo's status, its uploader and any legal hold come from the photo library itself, because the library is where Maya uploads, where Omar approves, and where Raj places a hold. So does subject.roles, which the library works out from the role assignments Theo keeps there. Whether a device is managed comes from the Gazette's device management system. The action, the time and the network come from the request, as the photo service receives it.
The diagram also shows what is left out. The photo service has a profile page where each person can fill in their desk, so colleagues know whom to ask about a picture. It is tempting to read subject.desk from there, since the value already sits in the service's own database. But a value the user can edit is only the user's claim about themselves. If the News library's rules read that field, any contributor could move to the news desk by typing a word, and their access would follow. A value sent with the request, such as a form field saying desk=news, is self-asserted in exactly the same way.
Attributes carried in tokens
The diagram has one more input: claims in the access token. When Omar signs in, the authorization server at auth.photos.example can look up attributes and copy them into the token as claims. The photo API then reads them without asking anyone. Decoded, part of Omar's token might look like this:
{
"iss": "https://auth.photos.example",
"sub": "user-1187",
"aud": "https://api.photos.example",
"desk": "sports",
"employment": "staff",
"iat": 1781523600,
"exp": 1781527200
}
iat and exp are times in seconds. This token was issued at 11:40 UTC on 15 June 2026 and expires an hour later, at 12:40. Everything in it was true at 11:40.
Suppose that at noon Omar moves from the sports desk to the news desk, and HR records the change. The token does not notice. Until 12:40, every request Omar sends still says "desk": "sports", and every rule that tests the desk keeps deciding as though Omar still worked in sports. A claim is a snapshot taken at sign-in, and it stays unchanged until the token expires. Even a new token helps only if the authorization server looks the desk up again when it issues one, rather than copying the old claims forward.
Snapshots have real advantages. The decision needs no lookup, so it is fast, and it keeps working when the HR system is slow or down. For attributes that rarely change and can safely be an hour old, that is a good trade. A shorter token lifetime narrows the gap at the cost of more frequent token requests. Administering roles safely met the same delay when access is taken away.
Two other costs are easier to overlook. Every claim makes the token larger, and a token that lists every group and library a person belongs to can outgrow the request headers that carry it. And everything in a token is visible to whatever handles it. A JWT access token is usually signed but not encrypted, so a claim saying someone is a freelancer, or on the legal team, can be read by every API the token is sent to. An attribute belongs in a token only if every recipient may see it.
How an application asks an identity provider for particular claims was covered earlier, in Scopes and the claims parameter.
Looking attributes up
The alternative is to ask the source when the decision is made. In the same example, when Omar opens a News library photo at 12:05, the photo service asks HR for Omar's desk and gets news, five minutes after the change.
Freshness has a price. Each lookup adds time to the request, and a download that consults HR, the rights database and device management waits for all three. Each source is also a new dependency. If the rights database is unavailable, the decision has no embargo time, and the request has to be refused, for reasons the next section explains. The sources also take load they were rarely designed for, since few HR systems expect a question for every photo download.
Caching sits between the two. The photo service can keep Omar's desk for ten minutes after looking it up, so most decisions skip the lookup, and the value is never more than ten minutes old. The cache lifetime is a deliberate trade between freshness and speed, and it is best made attribute by attribute rather than once for everything:
| Attribute | Source | How current it must be | Approach |
|---|---|---|---|
legal_hold | Photo library | Current at the moment of the decision | Read from the library for every decision, never cached |
embargo_until | Rights database | Within a minute or two | Look up, and cache briefly |
device.managed | Device management | Within a few minutes | Look up, and cache briefly |
desk | HR system | Within the hour | Token claim or cached lookup |
| Display name | Directory | A day old is fine | Token claim or long cache |
Legal hold is the strict one. If Raj places a hold at 14:02 because a court has asked about a photo, a deletion at 14:03 must see it. A hold that takes ten minutes to arrive leaves ten minutes in which the photo can be deleted. Fortunately it is also the cheapest attribute to keep current, because it lives in the same library that holds the photo. A display name sits at the other end. It appears beside a caption and plays no part in any decision, so a day-old value harms nothing.
When an attribute is missing
Sooner or later a decision arrives without one of its inputs. The rights database times out. A new freelancer's HR record has no desk yet. A wire photo arrives with no rights record at all. The rule has to do something with a value it does not have.
The safe answer is to refuse, to fail closed, and the detail that matters is that "unknown" is never the same as "false". Here is the legal hold requirement as a newsroom might first write it, next to the permit that lets picture editors delete photos:
permit photo.delete
when "picture-editor" in subject.roles
forbid photo.delete
when resource.legal_hold is true
Suppose the lookup for photo 9001's legal hold fails, and the value is missing. resource.legal_hold is true is not true, so the forbid does not match. The permit does, and Omar's deletion goes through, even though Raj placed a hold an hour earlier. Nothing in the rules is wrong as written. The forbid simply protects nothing when its input fails to arrive.
Written the other way round, the same requirement fails safely:
permit photo.delete
when "picture-editor" in subject.roles
and resource.legal_hold is false
Now the permit needs a positive answer. With the value missing, the permit does not match, nothing else grants the request, and default deny refuses it. The forbid can stay as a plain statement of intent, but the permit is what makes a missing value safe.
The embargo forbid from Writing rules with attributes has the same weakness. If embargo_until is missing, it never matches, and the download permits let the photo out. The fix has the same shape: the rights database records "no embargo" as an explicit value instead of an empty field, and the download permits require a rights record to be present, so a wire photo with no rights record cannot be downloaded at all.
Types and formats cause the same failure more quietly. The value arrives, but not in the form the rule expects: a legal hold sent as the text "false" instead of the boolean false, an embargo written as 2026-06-14 10:00 with no time zone, a time in milliseconds where seconds were expected, or a desk spelled Sports where the rule tests sports. Depending on the evaluator, the comparison may silently fail or, worse, succeed. Each attribute's type and format should be checked where it enters the decision, and a value that does not parse is treated as missing, never as a best guess.
A denial for missing data should still be useful. Omar needs to know only that the photo cannot be deleted right now. The people who run the service need to know why, so that they fix the source rather than the rule:
decision: deny
action: photo.delete
resource: photo 9001
subject: user-1187
reason: attribute_missing
attribute: resource.legal_hold
source: photo library, no answer within 300 ms
policy_version: 14
request_id: req-4e18
From that record, an operator can see at once that the library was slow, not that Omar lacks a role.
A missing desk for a new freelancer is a different kind of gap: the source itself is behind. Keeping directory attributes such as desk and employment current as people join, move and leave is the work of provisioning. In the governance lessons, Sources of truth covers how an organization decides which system owns each attribute, and Joining an organization and the lifecycle lessons after it cover keeping those attributes current.