Skip to content

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

All factory methods are static members of ResourcePolicy:

Identity elements for composition:

ResourcePolicy.Allow // Always returns true
ResourcePolicy.Deny // Always returns false

Checks a single permission via IUserAuthorization.HasPermission:

ResourcePolicy.RequirePermission("booking.reservation.create")

Checks if the user has at least one of the specified permissions (OR logic):

ResourcePolicy.RequireAnyPermission("booking.reservation.create", "booking.reservation.update")

Checks if the user has all of the specified permissions (AND logic):

ResourcePolicy.RequireAllPermissions("booking.reservation.create", "billing.invoice.read")

Checks role membership via IUserAuthorization.IsInRole:

ResourcePolicy.InRole("admin")

Checks group membership via IUserAuthorization.IsInGroup:

ResourcePolicy.InGroup("customer-care")

Checks if the user has a claim with the specified type. Optionally checks the value:

ResourcePolicy.HasClaim("department") // claim exists
ResourcePolicy.HasClaim("department", "engineering") // claim exists with specific value

The implementation reads from ICurrentUser.Claims:

  • If claimValue is null, any value matches (presence check).
  • If claimValue is specified, it must be in the claim’s value list.

Checks ICurrentUser.IsAuthenticated. Singleton instance:

ResourcePolicy.IsAuthenticated()

Checks ICurrentUser.Kind against the specified PrincipalKind:

ResourcePolicy.HasPrincipalKind(PrincipalKind.Service)
ResourcePolicy.HasPrincipalKind(PrincipalKind.User)

Creates a policy from a delegate. Not serializable:

ResourcePolicy.Custom(user => user.Claims.ContainsKey("premium"))

Policies compose with C# operators & (AND), | (OR), and ! (NOT):

// User must be authenticated AND have the permission
var policy = ResourcePolicy.IsAuthenticated()
& ResourcePolicy.RequirePermission("booking.reservation.create");
// ...OR be a service principal
var fullPolicy = policy | ResourcePolicy.HasPrincipalKind(PrincipalKind.Service);
// Negation
var notAdmin = !ResourcePolicy.InRole("admin");

Equivalent fluent methods are available:

var policy = ResourcePolicy.IsAuthenticated()
.And(ResourcePolicy.RequirePermission("booking.reservation.create"))
.Or(ResourcePolicy.HasPrincipalKind(PrincipalKind.Service));

C# operator precedence applies. & binds tighter than |, so:

A & B | C // means: (A & B) | C
A | B & C // means: A | (B & C)

Use parentheses for clarity.

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

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.

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.

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);
}

Sync policies convert implicitly to async via SyncToAsyncPolicy:

ResourcePolicy sync = ResourcePolicy.IsAuthenticated();
AsyncResourcePolicy asyncPolicy = sync; // implicit operator

Async policies support the same operators (&, |, !):

AsyncResourcePolicy asyncPolicy =
ResourcePolicy.IsAuthenticated() // sync, auto-converted
& AsyncResourcePolicy.RequireExternalPermission(
async (user, ct) => await externalService.CheckAsync(user.Id, ct));

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.

AsyncResourcePolicy has one factory method:

AsyncResourcePolicy.RequireExternalPermission(
Func<ICurrentUser, CancellationToken, ValueTask<bool>> check)

Creates an AsyncDelegatePolicy. Not serializable.

PolicySerializer converts between ResourcePolicy and PolicyExpression for JSON-safe storage.

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" }
]
}
ResourcePolicy restored = PolicySerializer.Deserialize(expr);
bool result = restored.Evaluate(currentUser);
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
}
TypeValueValuesClaimTypeChildren
Allow----
Deny----
Permissionpermission name---
AnyPermission-permission names--
AllPermissions-permission names--
Rolerole name---
Groupgroup name---
Claimclaim value (optional)-claim type-
Authenticated----
PrincipalKindkind name---
And---2+ children
Or---2+ children
Not---1 child
  • CustomPolicy (delegate-based) throws NotSupportedException on serialize.
  • AsyncDelegatePolicy is not serializable.
  • And and Or require at least 2 children.
  • Not requires exactly 1 child.

All concrete policy implementations are internal sealed. They are created through factory methods and composition operators.

ClassCreated byBehavior
AllowPolicyResourcePolicy.AllowReturns true (singleton)
DenyPolicyResourcePolicy.DenyReturns false (singleton)
PermissionPolicyRequirePermission(string)user.Authorization.HasPermission(p)
AnyPermissionPolicyRequireAnyPermission(string[])user.Authorization.HasAnyPermission(ps)
AllPermissionsPolicyRequireAllPermissions(string[])user.Authorization.HasAllPermissions(ps)
RolePolicyInRole(string)user.Authorization.IsInRole(r)
GroupPolicyInGroup(string)user.Authorization.IsInGroup(g)
ClaimPolicyHasClaim(string, string?)Checks user.Claims
AuthenticatedPolicyIsAuthenticated()user.IsAuthenticated (singleton)
PrincipalKindPolicyHasPrincipalKind(PrincipalKind)user.Kind == kind
CustomPolicyCustom(Func<...>)Delegate (not serializable)
AndPolicy& operatorleft.Evaluate(user) && right.Evaluate(user)
OrPolicy`` operator
NotPolicy! operator!inner.Evaluate(user)
SyncToAsyncPolicyImplicit conversionWraps sync in ValueTask.FromResult
AsyncAndPolicyAsync &Short-circuit AND
AsyncOrPolicyAsync ``
AsyncNotPolicyAsync !Negation
AsyncDelegatePolicyRequireExternalPermissionExternal async check
  1. Keep policies declarative: Compose built-in factory methods inside Evaluate rather than writing imperative logic.
  2. Use [RequirePermission] for simple cases: Policies are for complex rules. If a single permission check suffices, use the attribute.
  3. Prefer serializable policies: Avoid Custom() and RequireExternalPermission() when the policy needs to be stored in a database.
  4. Group related checks: Create named policy classes (e.g., ReservationManagementPolicy) rather than inline compositions.
  5. Test policies independently: Policies are pure functions on ICurrentUser — they are easy to unit test.