Evaluating a policy
All names, domains, and identifiers in these examples are fictional. When Omar tried to download photo 9002 at 13:30 UTC, in Decision and enforcement points, the decision service took a question, looked up the facts it needed, and returned a denial with the reason embargoed. Between the facts and the denial, it worked through the Gazette's policy one rule at a time, and the way it combined those rules decided what Omar got.
Rules, conditions, and effects
A policy is a set of rules. Here is part of the Gazette's policy version 14, with each rule's name on the line above it:
# editor-published
permit photo.download
when "picture-editor" in subject.roles
and resource.status is "published"
# editor-unpublished
permit photo.download
when "picture-editor" in subject.roles
and resource.status in ["draft", "approved"]
and environment.device.managed is true
# embargo
forbid photo.download
when environment.time is before resource.embargo_until
# legal-hold
forbid photo.delete
when resource.legal_hold is true
# own-approval
forbid photo.approve
when resource.uploaded_by is subject.id
Every rule has three parts. Its target says which requests it is about: here, one action on a photo. The embargo rule's target is every photo.download, and a request to delete a photo falls outside it. Its conditions, the when lines, are tests on attributes, and all of them must be true for the rule to match. A condition can compare two attributes with each other, as own-approval does to keep photographers from approving their own work. Its effect says what a match means: permit grants and forbid refuses.
Targets do more than organize the rules. They let the decision point skip rules that cannot apply, which keeps evaluation fast as a policy grows to hundreds of rules, and they keep each rule's reach visible. A forbid written for every action on a photo, when only deletion was meant, would quietly refuse requests nobody intended to refuse, and the mistake would sit in the first line of the rule, where a reviewer can see it.
When rules disagree
Omar's 13:30 download matches two rules with opposite effects: editor-unpublished permits it and embargo forbids it. A policy needs a combining approach to settle that, and the common ones settle it differently:
- Deny overrides: if any matching rule forbids, the decision is deny.
- Permit overrides: if any matching rule permits, the decision is permit.
- First applicable: the rules are checked in order, and the first one that matches decides.
All three also need an answer for when nothing matches, and the safe answer is the one from Designing permissions: deny by default. Here are the rules above applied to three requests for photo 9002, which is approved but unpublished and embargoed until 14:00 UTC:
| Request | Matching rules | Deny overrides | Permit overrides | First applicable |
|---|---|---|---|---|
| Omar downloads at 13:30 on a managed laptop | editor-unpublished (permit) and embargo (forbid) | Deny | Permit | Permit, because editor-unpublished comes first |
| Omar downloads at 14:05 on a managed laptop | editor-unpublished (permit) | Permit | Permit | Permit |
| Maya downloads at 14:05 | None | Deny | Deny | Deny |
The Gazette uses deny overrides, and the first row shows why it is the common safe default. The embargo and the legal hold are exceptions that must hold whatever else is true. Under deny overrides, a forbid stays in force no matter what anyone adds later, so it can be read and trusted on its own. Under permit overrides, the embargo would protect nothing. The first row shows editor-unpublished already outweighing it, and wherever no permit matches, default deny refuses the request anyway, so a forbid could never change an answer. Every new permit rule, perhaps written for a different purpose, would widen the gap. First applicable makes the order of the rules part of their meaning: moving a rule, or inserting one near the top, changes decisions in a way that a reviewer reading the new rule alone will not notice. Ordered lists suit short rule sets that are meant to be read from top to bottom, and permit overrides can make sense for a small group of rules that only describe exceptions.
Following an evaluation
A trace records what happened to each rule during one evaluation. Here is the trace for Omar's request at 13:30 UTC, from the managed laptop, for photo 9002:
| Rule | Effect | In its target? | Conditions | Result |
|---|---|---|---|---|
editor-published | Permit | Yes | Picture editor in the Sports library: yes. Published: no, the photo is approved but not yet published. | Not matched |
editor-unpublished | Permit | Yes | Picture editor in the Sports library: yes. Draft or approved: yes. Managed device: yes. | Matched |
embargo | Forbid | Yes | 13:30 is before 14:00: yes. | Matched |
legal-hold | Forbid | No, it applies to photo.delete | Not evaluated | Not applicable |
own-approval | Forbid | No, it applies to photo.approve | Not evaluated | Not applicable |
Two rules matched and one of them was a forbid, so under deny overrides the decision is deny. The reason comes from the rule that decided it: embargo produces embargoed. The trace also shows what did not cause the refusal. editor-unpublished matched, so Omar may download unpublished photos in general, and anyone looking into the refusal later can see that the problem was the clock, not Omar's role or device.
At 14:05 the same request changes in one row. The embargo condition is now false, because 14:05 is not before 14:00, so the rule does not match. Only editor-unpublished matches, and the decision is permit. Had the laptop also been unmanaged, editor-unpublished would not have matched either. Nothing would have permitted the download, and default deny would have refused it for a different reason.
One kind of row needs care. Suppose the rights database did not answer, so there was no embargo time for photo 9002. The embargo condition cannot be evaluated, and treating it as false would let the permit win and release a photo that may still be embargoed. A rule that cannot be evaluated is not the same as a rule that does not match. Where attributes come from showed how to make that gap fail safely, and a good trace records the embargo rule as an error instead of leaving it out, so that whoever reads it later looks at the rights database rather than the rule.
Policies as code
Version 14 has a number because the Gazette treats its policy as code. The rules live in version control, next to their tests. A change, such as a new rule for downloads in risky sessions, arrives as a proposed edit with its reason written down, and someone other than its author reviews it. The reviewer reads it the way the earlier sections suggest: is the target as narrow as intended, does a new permit run into an existing forbid, and do the tests include requests that should now be refused? Automated tests run before the change can be merged, and the merged policy is published from the administration point as version 15. Version 14 stays available, so the newsroom can return to it. The last lesson in this series covers those tests and how to try a change before it takes effect.
Writing policy this way is common enough that dedicated policy languages exist for it. They differ in syntax and in what they can express, but most keep the policy outside the application, evaluate it the same way wherever it runs, and come with tools for testing it. Some can analyze a policy without running it, and answer questions such as whether any combination of attributes could ever permit deleting a photo under legal hold. The format in these lessons is not one of those languages. It is only a way to show the ideas.
Decisions that come with instructions
At 14:05 Omar may download photo 9002, but the photo is still unpublished, and the Gazette does not want unpublished files leaving the newsroom unmarked or unrecorded. A decision can carry instructions like these back to the enforcement point. The rules that attach them do not grant anything. They add instructions to a permit that other rules have already given:
# record-unpublished
on permit photo.download
when resource.status in ["draft", "approved"]
require record_access
suggest notice "unpublished_photo"
# offsite-watermark
on permit photo.download
when resource.status in ["draft", "approved"]
and environment.network is not "newsroom"
require watermark "Tidewell Gazette: not for publication"
If Omar is working away from the newsroom network, the decision comes back like this:
{
"decision": "permit",
"policy_version": 14,
"rules": ["editor-unpublished", "record-unpublished", "offsite-watermark"],
"obligations": [
{ "type": "record_access" },
{ "type": "watermark", "text": "Tidewell Gazette: not for publication" }
],
"advice": [
{ "type": "notice", "notice": "unpublished_photo" }
]
}
An obligation is an instruction the enforcement point must carry out for the permit to count. If the photo API cannot add the watermark, because the image service that does it is down, it must refuse the download rather than send the original. The same applies to record_access: a download of an unpublished photo that cannot be recorded does not happen. An enforcement point that receives an obligation it does not recognize has to refuse as well, since it cannot carry out an instruction it does not understand.
Advice is a suggestion that the enforcement point may follow or ignore without changing the decision. Here it suggests reminding Omar that the photo is unpublished. The web app shows a banner, and a command-line tool with nowhere to show one simply carries on.
The rules field, like the reason on a denial, serves a later reader. When the sports desk asks why a downloaded file carries a watermark, or Omar asks why the 13:30 attempt failed, the answer is in the decision itself rather than reconstructed from code after the fact.