How policy works
Every decision is one Cedar evaluation: may this
principal take this action on this resource, in this context? Policies are
permit and forbid rules you keep in a policy file.
permit(principal, action == Action::"read", resource)
when { context.tier == "gold" };
The request
| You pass | A policy names it | Example |
|---|---|---|
principal — on whose behalf | principal, or principal == User::"alice" | User::"db:4412" |
action — the verb | action, or action == Action::"read" | read |
resource — what it acts on | resource, or resource == Resource::"doc-1" | trip/AX8821 |
context — the runtime facts | context.amount, context.mfa, … | {"amount": 500} |
A bare word — principal, action, resource — matches anything. == pins it.
Default deny
No rule that permits a request means Deny. You open access one permit at a
time, and a governor with no policies at all denies everything.
govern.load("watchlight.policy.json")
assert govern.has_policies, "no policies loaded — every call would be denied"
when and unless
when { … } must be true for a rule to apply; unless { … } must be false.
Both read context, which is where every runtime fact lives.
// spend cap
permit(principal, action == Action::"transfer", resource)
when { context.amount <= 1000 };
// kill switch: normal authority, revoked by a flag
permit(principal == Agent::"payments", action, resource)
unless { context.quarantined == true };
forbid wins
A request is allowed only when at least one permit matches it and no forbid
does. A single matching forbid overrides any number of matching permits.
permit(principal == User::"admin", action, resource);
forbid(principal, action == Action::"delete", resource); // admin still can't delete
That is why a forbid is the right shape for a hard boundary — a compliance
rule, a quarantine — that must hold whatever else is granted.
Name the entity type
The accepted principal types are User, Agent, Group, Role, Tool,
Resource and Workflow. Write the type on both sides:
from watchlight import principals
govern.authorize(action="book", principal=principals.user("db:4412"))
A bare identifier — db:4412, with no Type:: — is accepted but opaque. It
matches a policy naming that id under User, Agent, Group or Role, and
when it matches more than one, an allow beats a forbid — the opposite of
Cedar's usual rule. A forbid naming an agent can be defeated by a permit
naming a user with the same id. Only a typed reference discriminates.
The action is always type Action; a policy giving it another type is rejected.
Full detail is in the identity model.
What the engine resolves
Most policies decide on runtime facts that you pass in at call time. For those, the engine reliably resolves the following:
- Any
context.*field you pass. You can compare it with==,!=,<,<=,>and>=, and combine comparisons with&&(and),||(or) and!(not). - An
==test on the principal, the action or the resource, such asprincipal == User::"alice". is, which tests an entity's type, as inprincipal is User.like, which matches a string against a glob pattern, as incontext.path like "public/*"..contains(),.containsAny()and.containsAll(), which test a set that you put incontext.
Three things do not resolve, in either edition, and deny silently: entity
relationships (principal in Group::"admins"), app-defined entity attributes
(principal.clearance), and in against a list of strings
(context.role in ["admin", "ops"] denies even when the role is admin —
in is the entity-hierarchy operator, not list membership). There is no entity
store to consult. Pass those facts in context and test membership with a set:
context.roles.contains("admins"), context.roles.containsAny(["admin", "ops"]),
context.role == "editor".
like, not .startsWith()Cedar has no .startsWith() / .endsWith() / .contains() on strings, and a
policy using them fails to load. Use a glob: like "analyze_*", like "*_report",
like "*write*". .contains() is for sets only.
Cheat sheet
| You want to… | Write |
|---|---|
| Match a specific user | principal == User::"alice" |
| Match any user | principal is User |
| Match one action | action == Action::"read" |
| Match one resource | resource == Resource::"doc-1" |
| Require a condition | when { context.mfa == true } |
| Exclude a condition | unless { context.quarantined == true } |
| Match a string pattern | when { context.path like "public/*" } |
| Numeric guard | when { context.amount <= 1000 } |
| Hard block, beats any permit | forbid(...) |
| Guard a key that may be absent | when { context has owner && … } |
A rule that reads a key nothing supplied falls through to Deny, so write has
before the comparison.
The full language is documented at docs.cedarpolicy.com.
Next
- Policy files — the JSON shapes a file can take, and what a load refuses.
- Examples — copy-paste recipes for the high-stakes decisions.
- Testing your policies — golden fixtures, and the CLI that fails a CI run.
- Enforcement effects — pausing an action for a human instead of allowing it.