Skip to main content

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 passA policy names itExample
principal — on whose behalfprincipal, or principal == User::"alice"User::"db:4412"
action — the verbaction, or action == Action::"read"read
resource — what it acts onresource, or resource == Resource::"doc-1"trip/AX8821
context — the runtime factscontext.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 as principal == User::"alice".
  • is, which tests an entity's type, as in principal is User.
  • like, which matches a string against a glob pattern, as in context.path like "public/*".
  • .contains(), .containsAny() and .containsAll(), which test a set that you put in context.

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".

String matching uses 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 userprincipal == User::"alice"
Match any userprincipal is User
Match one actionaction == Action::"read"
Match one resourceresource == Resource::"doc-1"
Require a conditionwhen { context.mfa == true }
Exclude a conditionunless { context.quarantined == true }
Match a string patternwhen { context.path like "public/*" }
Numeric guardwhen { context.amount <= 1000 }
Hard block, beats any permitforbid(...)
Guard a key that may be absentwhen { 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​