Skip to content

Policies

A policy accepts one quoted or fenced-text description body line, printed first before its implementation. It is report-only authoring metadata (PLAY0270), not a requirement: a description-only policy still needs require, a file or code. Markdown documentation is not supported here.

Policies are named authorization rules. Modules, features, commands, and queries reference them by name with authorize. Policies support role-based, claim-based, and fully custom logic. Authorization gates on enclosing scopes narrow access; they never replace a gate closer to a command or query.

policy <Name>
require <condition>

Write one require line. Its <condition> can be authenticated, role "<role>", claim "<claim>" matches <subject|"value"|expression>, not <condition>, or a combination such as role "<role>" or (role "<role>" and claim "<claim>" matches "<value>"). Continue a condition at deeper indentation rather than starting another require line; a second line reports PLAY0441.

Alternatively, implement the policy in an inline block:

policy <Name>
```csharp
<C# returning bool>
```

Instead of the inline csharp block, a policy can name its implementation with file <path>. A policy may have a require condition or one implementation (file or inline code), never both. Mixing require with an implementation reports PLAY0440.

Declare authorize <policy expression> directly in a module or feature body, or on a command or query. Each gate can use policy names joined by and and or, with parentheses for grouping. Names written next to one another mean and. Every gate along the path to a command or query must pass: its own gate and every enclosing feature’s gate (including nested features) and the module’s gate. This is the same AND rule used for several policies on one construct. An or inside a gate does not bypass any enclosing gate.

policy HasPortalAccess
require authenticated
policy CanManageOrders
require role "OrderManager"
policy IsRegionalManager
require role "RegionalManager"
module Portal
authorize HasPortalAccess
feature Orders
authorize CanManageOrders or IsRegionalManager
feature Returns
slice StateChange ReturnOrder
command RequestReturn
authorize CanManageOrders

RequestReturn requires HasPortalAccess and (CanManageOrders or IsRegionalManager) and CanManageOrders. A feature without its own gate still inherits its ancestors’ gates. When a folder declares the same module or feature gate in different files, distinct gates accumulate with AND; identical repeated gates are reported and kept once. Repeated authorize lines on a command or query combine with AND in authored order; printing writes them as one authorize A and B line. The printer keeps a gate where it was declared rather than copying inherited gates onto each command or query.

The source compiler resolves policy names at all four positions and warns about unknown names. Portable conditions bind to the ESM and run in the reference evaluator. A custom implementation binds as an opaque ESM v3 policy predicate; it requires a target provider to evaluate it.

ConditionMeaning
authenticatedThe caller must be authenticated.
not <condition>The operand must be false; parentheses can group its operand.
role "<role>"The caller must have the role.
claim "<claim>" matches subjectThe claim must match the subject of the current event source.
claim "<claim>" matches "<value>"The claim must equal that literal value.
claim "<claim>" matches <expression>The claim must equal the value the expression resolves to.

A quoted target is the value itself; anything unquoted - a path such as invoice.department, or a $ rooted expression such as $context.tenant - names where the value is read from. Quote a literal, leave an expression unquoted; the two are distinct in the syntax tree, so a tool consuming a policy always knows which one it is looking at.

Conditions combine with or and and, and parentheses group them. A condition may continue on the next line at deeper indentation.

not binds tighter than and, which binds tighter than or; and and or are left associative - the rule a general purpose language follows, so a or b and c means a or (b and c) and a or b or c means (a or b) or c. Parentheses override that. This is the one condition grammar the language has: a produces when condition combines by exactly the same rules, over comparisons instead of roles and claims.

Printing writes the parentheses back wherever the grouping is not the one those rules produce, so a printed policy always compiles to the condition it came from. It also writes them where mixing or and and would otherwise leave a reader to work the precedence out - the text says which grouping it means rather than assuming you know.

To allow signed-in people but exclude service identities, negate their role and claim match:

policy PersonOnly
require authenticated and not role "Service" and not claim "actorKind" matches "service"

A caller with either the Service role or an actorKind value of service is denied. A missing claim has no match, so its negation is true. not role "Service" alone does not require authentication; keep authenticated when you need a signed-in caller. not (role "Service" or claim "actorKind" matches "service") negates the whole group, while not not authenticated restores the original condition. not belongs to policy conditions, not named-policy expressions in authorize, and cannot negate an inline or file implementation.

policy IsAuthenticated
require authenticated
policy IsAccountant
require role "Accountant"
policy CanViewSensitiveFinancials
require role "FinanceDirector"
or role "Auditor"
policy IsCustomerSelf
require claim "customerId" matches subject
policy IsFinanceDepartment
require claim "department" matches "Finance"
policy CanManageInvoice
require role "InvoiceManager"
or (role "Accountant" and claim "department" matches invoice.department)

IsFinanceDepartment compares against the literal text Finance. CanManageInvoice allows an InvoiceManager, or an Accountant whose department matches invoice.department. Its parentheses make the grouping explicit but do not change it: and already binds tighter than or.

Positive declarative policies execute in the portable ESM v1 reference evaluator. Policy-condition negation selects ESM v7 as a byte-preserving extension (decision 0027); models without it keep their existing versions, bytes and revisions. Pre-extension v7 strict readers reject the new not variant. A consumer must explicitly support negation before reading, executing or rendering a model that uses it. Inline csharp and file policy predicates bind to ESM v3 with a stable requirement id, context/result contract version 1, and the required capability pure. The compilation result’s implementation requirement carries the inline body’s exact source span and dedented line map, or a resolved file’s whole-file span; canonical ESM JSON still carries only the requirement id. The reference evaluator cannot execute their bodies. An authorization that depends on one returns SemanticUnsupported with the Authorization capability and the policy name; it never guesses allow or deny. A specification asserting then denied does not pass on this unsupported outcome. A provider must admit and evaluate the implementation before it can decide authorization.

For a mixed gate, evaluation follows authored order: a portable denial on the left of and denies without evaluating the right, and a portable allowance on the left of or allows without evaluating the right. If an opaque operand is reached first, the reference evaluator returns unsupported; a later portable operand cannot decide the result. Enclosing gates combine in module → feature → construct order. This matches Stage’s generated C# &&/|| expression (SemanticPolicyArtifactRenderer.Authorization) and Arc’s ordered, short-circuiting policy loops (ArcAuthorizationPolicyRuntime.IsAuthorized and AspNetAuthorizationPolicyRuntime.IsAuthorized), following the runtime-authority boundary in decision 0001. An absent caller still denies authorization. The execution request must supply a caller explicitly when an authorized command or query runs. A missing caller cannot satisfy authorization. An authenticated condition checks the caller’s authentication flag; a role compares the caller’s roles by ordinal, case-sensitive text. Claim types compare ordinal-ignore-case, while claim values compare ordinal, case-sensitively. If a caller carries several values for the same claim type, any matching value satisfies that condition. A missing caller claim makes a comparison against a known text target false, so its negation is true. A comparison target that is missing, null, an absent subject, or not text instead makes the condition unknown. not unknown remains unknown, including under double negation; a policy whose final condition is unknown denies. Conditions use three-valued Kleene logic: true or unknown is true, false or unknown is unknown, false and unknown is false, and true and unknown is unknown (in either operand order). Thus a satisfied role alternative can still allow access when another target is undecidable. Positive policies keep their existing allow/deny outcomes. Opaque predicates cannot be nested under negation: unsupported execution is never converted to false or permission. and and or retain authored-order short-circuiting when the left operand decides the result.

In the portable ESM v1 profile, an artifact path must resolve from a command property (including declared composite members) or the keyed query argument. An absent or null nested value makes its claim comparison unknown. $-rooted expressions remain valid authoring syntax but do not bind to this portable profile. A subject comes from a command’s identifier property or a keyed query’s argument; if no identifier is available, it cannot match. When binding an authorized command or query, the compiler warns with PLAY0546 if a claim under not (including inside a group) targets an optional path, a command’s subject without an identifier, or a non-string type. The warning does not prevent binding; check that the target is available and text when the policy runs. The TypeScript syntax compiler does not perform this semantic binding check.

A failed effective authorization yields Unauthorized before command validation or query lookup and never changes the world. For the caller fixture and denial assertion, see Specifications.

When the declarative conditions cannot express the rule, drop into C#. The block answers with a bool — the same answer require authenticated gives, so a policy written in code and a policy written in conditions mean the same kind of thing and compose the same way:

policy IsAdultCustomer
```csharp
var dateOfBirth = context.Identity.ClaimValue("dateOfBirth");
return dateOfBirth is not null && DateTime.TryParse(dateOfBirth, System.Globalization.CultureInfo.InvariantCulture,
System.Globalization.DateTimeStyles.AssumeUniversal | System.Globalization.DateTimeStyles.AdjustToUniversal,
out var born) && born <= context.Occurred.UtcDateTime.AddYears(-18);
```

The same custom logic can live in a repository-relative file instead of an inline block:

policy IsAdultCustomer
file Policies/IsAdultCustomer.cs

file is the implemented by alternative: the file provides the same PolicyContext and bool answer as the inline block, not a location for generating a declaration. The compiler does not resolve the path; an absolute path warns with PLAY0264.

There is deliberately no result type carrying a denial reason. The declarative half has no way to say why a require failed either, and adding one only to the code half would split the language into two kinds of policy — one that can explain itself and one that cannot. A policy answers whether the caller may act; the message a user sees is a runtime’s concern, and validate is where the language attaches messages to rejections.

context is the PolicyContext — the caller, and what the decision is about:

MemberValue
context.IdentityThe caller: Id, Name, UserName, IsAuthenticated, Roles, Claims.
context.ArtifactThe command being authorized, or the arguments of the query. What a matches <path> condition resolves against.
context.SubjectThe identifier of the thing being acted on. What matches subject compares the claim to.
context.TenantThe tenant the authorized command or query is executing for.
context.OccurredWhen the authorized command or query was received.

Identity answers the same questions the conditions ask, so the two forms read alike:

context.Identity.IsAuthenticated // require authenticated
context.Identity.HasRole("Accountant") // require role "Accountant"
context.Identity.ClaimValue("department") // require claim "department" matches …

A policy is given an Identity and not a CausedBy: it decides what a caller may do and needs what they can prove, and it records nothing. See Contexts for the full set and for how a rule’s context deliberately differs.

See Commands and Queries for how policies are applied.