Policies
Pragmatic.Authorization provides a composable policy system for authorization rules that go beyond simple permission checks. Policies are analogous to Specification<T> but operate on ICurrentUser instead of entity predicates.
ResourcePolicy
Section titled “ResourcePolicy”ResourcePolicy is an abstract class with a single abstract method:
public abstract bool Evaluate(ICurrentUser user);Policies evaluate synchronously against the current user and return true (allow) or false (deny).
Factory Methods
Section titled “Factory Methods”All factory methods are static members of ResourcePolicy:
Allow and Deny
Section titled “Allow and Deny”Identity elements for composition:
ResourcePolicy.Allow // Always returns trueResourcePolicy.Deny // Always returns falseRequirePermission
Section titled “RequirePermission”Checks a single permission via IUserAuthorization.HasPermission:
ResourcePolicy.RequirePermission("booking.reservation.create")RequireAnyPermission
Section titled “RequireAnyPermission”Checks if the user has at least one of the specified permissions (OR logic):
ResourcePolicy.RequireAnyPermission("booking.reservation.create", "booking.reservation.update")RequireAllPermissions
Section titled “RequireAllPermissions”Checks if the user has all of the specified permissions (AND logic):
ResourcePolicy.RequireAllPermissions("booking.reservation.create", "billing.invoice.read")InRole
Section titled “InRole”Checks role membership via IUserAuthorization.IsInRole:
ResourcePolicy.InRole("admin")InGroup
Section titled “InGroup”Checks group membership via IUserAuthorization.IsInGroup:
ResourcePolicy.InGroup("customer-care")HasClaim
Section titled “HasClaim”Checks if the user has a claim with the specified type. Optionally checks the value:
ResourcePolicy.HasClaim("department") // claim existsResourcePolicy.HasClaim("department", "engineering") // claim exists with specific valueThe implementation reads from ICurrentUser.Claims:
- If
claimValueis null, any value matches (presence check). - If
claimValueis specified, it must be in the claim’s value list.
IsAuthenticated
Section titled “IsAuthenticated”Checks ICurrentUser.IsAuthenticated. Singleton instance:
ResourcePolicy.IsAuthenticated()HasPrincipalKind
Section titled “HasPrincipalKind”Checks ICurrentUser.Kind against the specified PrincipalKind:
ResourcePolicy.HasPrincipalKind(PrincipalKind.Service)ResourcePolicy.HasPrincipalKind(PrincipalKind.User)Custom
Section titled “Custom”Creates a policy from a delegate. Not serializable:
ResourcePolicy.Custom(user => user.Claims.ContainsKey("premium"))Composition Operators
Section titled “Composition Operators”Policies compose with C# operators & (AND), | (OR), and ! (NOT):
// User must be authenticated AND have the permissionvar policy = ResourcePolicy.IsAuthenticated() & ResourcePolicy.RequirePermission("booking.reservation.create");
// ...OR be a service principalvar fullPolicy = policy | ResourcePolicy.HasPrincipalKind(PrincipalKind.Service);
// Negationvar notAdmin = !ResourcePolicy.InRole("admin");Equivalent fluent methods are available:
var policy = ResourcePolicy.IsAuthenticated() .And(ResourcePolicy.RequirePermission("booking.reservation.create")) .Or(ResourcePolicy.HasPrincipalKind(PrincipalKind.Service));Operator Precedence
Section titled “Operator Precedence”C# operator precedence applies. & binds tighter than |, so:
A & B | C // means: (A & B) | CA | B & C // means: A | (B & C)Use parentheses for clarity.
Evaluation
Section titled “Evaluation”AND short-circuits on false (left-to-right). OR short-circuits on true (sync evaluation does not short-circuit at the ResourcePolicy level since both Evaluate calls are synchronous; async does).
Defining Custom Policies
Section titled “Defining Custom Policies”Create a class that extends ResourcePolicy:
public sealed class ReservationManagementPolicy : ResourcePolicy{ public override bool Evaluate(ICurrentUser user) { var isService = HasPrincipalKind(PrincipalKind.Service); var hasPermission = IsAuthenticated() & RequirePermission(BookingPermissions.Reservation.Create);
return (isService | hasPermission).Evaluate(user); }}This pattern composes built-in policies within the Evaluate method, keeping the logic declarative.
Applying Policies to Actions
Section titled “Applying Policies to Actions”Use the [RequirePolicy<T>] attribute on actions, mutations, or queries:
[RequirePolicy<ReservationManagementPolicy>]public sealed class CreateReservationMutation : Mutation<Reservation>{ // ...}Requirements:
- The policy type must have a parameterless constructor.
- The instance is created once and cached by
PolicyEvaluationFilter. - Evaluated at Order 210 in the action pipeline.
- When the policy evaluates to false, the filter returns a 403 Forbidden.
AsyncResourcePolicy
Section titled “AsyncResourcePolicy”For policies that require I/O (external service checks, database lookups), use AsyncResourcePolicy:
public abstract class AsyncResourcePolicy{ public abstract ValueTask<bool> EvaluateAsync(ICurrentUser user, CancellationToken ct = default);}Implicit Conversion
Section titled “Implicit Conversion”Sync policies convert implicitly to async via SyncToAsyncPolicy:
ResourcePolicy sync = ResourcePolicy.IsAuthenticated();AsyncResourcePolicy asyncPolicy = sync; // implicit operatorComposition
Section titled “Composition”Async policies support the same operators (&, |, !):
AsyncResourcePolicy asyncPolicy = ResourcePolicy.IsAuthenticated() // sync, auto-converted & AsyncResourcePolicy.RequireExternalPermission( async (user, ct) => await externalService.CheckAsync(user.Id, ct));Short-Circuit Evaluation
Section titled “Short-Circuit Evaluation”Async AND and OR short-circuit:
AsyncAndPolicy: If left evaluates to false, skips right entirely.AsyncOrPolicy: If left evaluates to true, skips right entirely.
This avoids unnecessary I/O calls.
Factory Methods
Section titled “Factory Methods”AsyncResourcePolicy has one factory method:
AsyncResourcePolicy.RequireExternalPermission( Func<ICurrentUser, CancellationToken, ValueTask<bool>> check)Creates an AsyncDelegatePolicy. Not serializable.
Policy Serialization
Section titled “Policy Serialization”PolicySerializer converts between ResourcePolicy and PolicyExpression for JSON-safe storage.
Serialize
Section titled “Serialize”var policy = ResourcePolicy.RequirePermission("orders.read") & ResourcePolicy.IsAuthenticated();
PolicyExpression expr = PolicySerializer.Serialize(policy);The resulting PolicyExpression is a record tree:
{ "type": "And", "children": [ { "type": "Permission", "value": "orders.read" }, { "type": "Authenticated" } ]}Deserialize
Section titled “Deserialize”ResourcePolicy restored = PolicySerializer.Deserialize(expr);bool result = restored.Evaluate(currentUser);PolicyExpression Structure
Section titled “PolicyExpression Structure”public sealed record PolicyExpression{ public required PolicyExpressionType Type { get; init; } public string? Value { get; init; } // Permission, Role, Group, PrincipalKind, Claim value public string[]? Values { get; init; } // AnyPermission, AllPermissions public string? ClaimType { get; init; } // Claim public PolicyExpression[]? Children { get; init; } // And, Or, Not}PolicyExpressionType
Section titled “PolicyExpressionType”| Type | Value | Values | ClaimType | Children |
|---|---|---|---|---|
Allow | - | - | - | - |
Deny | - | - | - | - |
Permission | permission name | - | - | - |
AnyPermission | - | permission names | - | - |
AllPermissions | - | permission names | - | - |
Role | role name | - | - | - |
Group | group name | - | - | - |
Claim | claim value (optional) | - | claim type | - |
Authenticated | - | - | - | - |
PrincipalKind | kind name | - | - | - |
And | - | - | - | 2+ children |
Or | - | - | - | 2+ children |
Not | - | - | - | 1 child |
Limitations
Section titled “Limitations”CustomPolicy(delegate-based) throwsNotSupportedExceptionon serialize.AsyncDelegatePolicyis not serializable.AndandOrrequire at least 2 children.Notrequires exactly 1 child.
Internal Policy Classes
Section titled “Internal Policy Classes”All concrete policy implementations are internal sealed. They are created through factory methods and composition operators.
| Class | Created by | Behavior |
|---|---|---|
AllowPolicy | ResourcePolicy.Allow | Returns true (singleton) |
DenyPolicy | ResourcePolicy.Deny | Returns false (singleton) |
PermissionPolicy | RequirePermission(string) | user.Authorization.HasPermission(p) |
AnyPermissionPolicy | RequireAnyPermission(string[]) | user.Authorization.HasAnyPermission(ps) |
AllPermissionsPolicy | RequireAllPermissions(string[]) | user.Authorization.HasAllPermissions(ps) |
RolePolicy | InRole(string) | user.Authorization.IsInRole(r) |
GroupPolicy | InGroup(string) | user.Authorization.IsInGroup(g) |
ClaimPolicy | HasClaim(string, string?) | Checks user.Claims |
AuthenticatedPolicy | IsAuthenticated() | user.IsAuthenticated (singleton) |
PrincipalKindPolicy | HasPrincipalKind(PrincipalKind) | user.Kind == kind |
CustomPolicy | Custom(Func<...>) | Delegate (not serializable) |
AndPolicy | & operator | left.Evaluate(user) && right.Evaluate(user) |
OrPolicy | ` | ` operator |
NotPolicy | ! operator | !inner.Evaluate(user) |
SyncToAsyncPolicy | Implicit conversion | Wraps sync in ValueTask.FromResult |
AsyncAndPolicy | Async & | Short-circuit AND |
AsyncOrPolicy | Async ` | ` |
AsyncNotPolicy | Async ! | Negation |
AsyncDelegatePolicy | RequireExternalPermission | External async check |
Best Practices
Section titled “Best Practices”- Keep policies declarative: Compose built-in factory methods inside
Evaluaterather than writing imperative logic. - Use
[RequirePermission]for simple cases: Policies are for complex rules. If a single permission check suffices, use the attribute. - Prefer serializable policies: Avoid
Custom()andRequireExternalPermission()when the policy needs to be stored in a database. - Group related checks: Create named policy classes (e.g.,
ReservationManagementPolicy) rather than inline compositions. - Test policies independently: Policies are pure functions on
ICurrentUser— they are easy to unit test.