Pragmatic.Authorization
Authorization engine for Pragmatic.Design.
The Problem
Section titled “The Problem”Authorization in most .NET applications starts simple and deteriorates. Permission strings are hardcoded and scattered across controllers. Role-to-permission mapping lives in if-statements. Wildcard handling is hand-rolled and inconsistent. Instance-level checks (“can this user act on this specific invoice?”) are buried in business logic. When someone asks “what permissions does the booking-manager role have?”, the answer requires reading every controller in the project.
// Typical authorization: scattered, fragile, invisible[HttpPost("{id}/cancel")]public async Task<IActionResult> Cancel(Guid id, CancellationToken ct){ var permissions = User.Claims .Where(c => c.Type == "permission") .Select(c => c.Value).ToHashSet();
if (!permissions.Contains("booking.reservation.cancel") && !permissions.Any(p => p == "booking.reservation.*") && !permissions.Contains("*")) return Forbid();
await _reservations.CancelAsync(id, ct); return NoContent();}The Solution
Section titled “The Solution”Declare what is protected and how. The framework handles resolution, matching, caching, and enforcement.
// Pragmatic: declarative, compile-safe, observable[RequirePermission(BookingPermissions.Reservation.Cancel)][RequirePolicy<ReservationCancellationPolicy>]public sealed class CancelReservationAction : VoidDomainAction{ // Business logic only -- no authorization code here}// Roles compose from module definitions, declared once in Program.csapp.UseAuthorization(authz =>{ authz.MapRole<BookingManagerRole>(r => r .IncludeDefinition<BookingOperator>() .IncludeDefinition<CatalogReader>()); authz.MapGroup<CustomerCareGroup>(); authz.UsePermissionCache(TimeSpan.FromMinutes(5));});The SG generates permission constants (BookingPermissions.Reservation.Read), a permission registry for admin tooling, and optional role seeding from JSON. At runtime, an ordered provider chain resolves claims to permissions, WildcardMatcher handles * patterns consistently, CachedPermissionResolver caches per request and optionally cross-request, and OTel counters instrument every check.
What It Covers
Section titled “What It Covers”- Permission evaluation with wildcard matching
- Role and group expansion (Claims -> Roles -> Permissions, Groups -> Roles -> Permissions)
- Composable authorization policies (
ResourcePolicy,AsyncResourcePolicy) - Resource-level authorization (ABAC via
IResourceAuthorizer<T>) - Request-scoped and cross-request permission caching
- Static and dynamic catalogs for admin tooling
- Policy serialization for JSON storage
- Built-in observability (OTel metrics and tracing)
Samples
Section titled “Samples”See samples/Pragmatic.Authorization.Samples/ for 4 runnable scenarios: ResourcePolicy composition (operators, factories), WildcardMatcher (hierarchical permission patterns), roles and permissions (provider chain, ABAC), and policy evaluation (runnable checks against AnonymousUser/SystemUser).
Start Here
Section titled “Start Here”If you are new to the stack, read in this order:
- This README for the mental model and the main APIs.
docs/concepts.mdfor the full architecture and core concepts.docs/getting-started.mdfor a zero-to-running walkthrough.docs/permission-resolution.mdfor the provider chain internals.docs/policies.mdfor composable resource policies.docs/roles-and-groups.mdfor the role/group composition model.docs/stores.mdfor in-memory vs EF-backed stores.
When something goes wrong:
docs/common-mistakes.mdfor mistakes to avoid.docs/troubleshooting.mdfor problem/solution guide and diagnostics reference.
For architecture and cross-cutting topics:
../docs/howto/authentication-authorization.mdfor task-oriented examples.../docs/architecture/authentication-authorization.mdfor the end-to-end runtime flow.../docs/architecture/resource-permissions.mdfor permission naming, CRUD generation, roles, and seeding.../docs/architecture/data-level-authorization.mdfor row-level filters.
Code wins over documents. Use this README and the package code as the source of truth for the current public surface.
Mental Model
Section titled “Mental Model”There are two views of the system:
| View | Purpose |
|---|---|
L1-L4 pipeline | Runtime flow: authentication, permission gate, resource authorization, data filtering |
L0-L3 permission scope | Permission taxonomy: resource, CRUD, custom operations, data scope |
Use L1-L4 when you want to understand request execution.
Use L0-L3 when you want to name permissions or compose roles.
Runtime Pipeline
Section titled “Runtime Pipeline”Request | vL1 Authentication Who is the caller? | vL2 Permission Gate Can the caller perform this operation? | (CachedPermissionResolver + WildcardMatcher) vL3 Resource Authorization Can the caller act on THIS resource? | (IResourceAuthorizer<T> + PolicyEvaluationFilter) vL4 Data-Level Filtering Which rows can the caller see? (IQueryFilter<T>, IPermissionBasedFilter<T>)Permission Hierarchy
Section titled “Permission Hierarchy”L0: booking -- boundary (namespace)L1: booking.reservation.read -- CRUD (SG-generated)L2: billing.invoice.refund -- custom operation (IPermission)L3: IQueryFilter<T> -- data-level scopeQuick Start
Section titled “Quick Start”1. Install
Section titled “1. Install”Reference Pragmatic.Authorization in your module’s .csproj. For the host, the SG auto-detects the package via FeatureDetector.
2. Configure in Program.cs
Section titled “2. Configure in Program.cs”await PragmaticApp.RunAsync(args, app =>{ app.UseAuthorization(authz => { // Default policy for endpoints without explicit attributes authz.DefaultPolicy = DefaultEndpointPolicy.RequireAuthenticated;
// Map application roles — compose from module definitions authz.MapRole<AdminRole>(); authz.MapRole<ManagerRole>(r => r .IncludeDefinition<BookingOperator>() .IncludeDefinition<CatalogReader>());
// Map groups (expand to roles at runtime) authz.MapGroup<CustomerCareGroup>();
// Instance-level authorization authz.AddResourceAuthorizer<InvoiceAuthorizer>();
// Cross-request caching authz.UsePermissionCache(TimeSpan.FromMinutes(5)); });});3. Define Permissions on Actions
Section titled “3. Define Permissions on Actions”[RequirePermission("booking.reservation.read")]public sealed class SearchReservationsQuery : IQuery<PagedResult<ReservationDto>>{ // ...}Or with SG-generated constants:
[RequirePermission(BookingPermissions.Reservation.Create)][RequirePolicy<ReservationManagementPolicy>]public sealed class CreateReservationMutation : Mutation<Reservation>{ // ...}4. Define Roles in Modules
Section titled “4. Define Roles in Modules”// In the module — permission template (IRoleDefinition)public sealed class BookingOperator : IRoleDefinition{ public static string Name => "booking-operator"; public static string? Description => "Full CRUD on reservations and guests"; public static IReadOnlyList<string> Permissions => [ BookingPermissions.Reservation.All, BookingPermissions.Guest.All, BookingPermissions.GuestPreferences.All ];}
// In the host — application role (IRole)public sealed class BookingManagerRole : IRole{ public static string Name => "booking-manager"; public static string? Description => "Full booking operations with catalog context"; public static IReadOnlyList<string> DefaultPermissions => []; // Composed via IncludeDefinition in Program.cs}Permission Resolution Chain
Section titled “Permission Resolution Chain”When IUserAuthorization.HasPermission("booking.reservation.read") is called, the system resolves permissions through an ordered provider chain:
ClaimsPermissionProvider (Order 0) | Reads "permission" claims from ICurrentUser vRoleExpansionProvider (Order 100) | For each "role" claim, queries IRolePermissionStore vGroupExpansionProvider (Order 200) | For each "group" claim, queries IGroupRoleStore -> IRolePermissionStore v[Custom providers] (Order N) | Any IPermissionProvider you register vCachedPermissionResolver | Merges all provider results (union) | Caches per request (lazy, resolved on first access) | Optionally caches cross-request via ICacheStack vWildcardMatcher | Checks if any resolved permission matches the required one | Supports: *, booking.*, booking.*.read, *.reservation.read vResult (bool)All providers run asynchronously. Results are merged with union semantics (additive only). The resolved set is cached for the lifetime of the request scope. If UsePermissionCache is configured, the resolved set is also cached cross-request via ICacheStack (from Pragmatic.Caching), keyed by user ID (and tenant ID if multi-tenant).
Wildcard Patterns
Section titled “Wildcard Patterns”| Pattern | Matches | Example |
|---|---|---|
* | All permissions | Super admin |
booking.* | All permissions starting with booking. | Boundary admin |
booking.reservation.* | All reservation operations | Entity admin |
booking.*.read | Read on all entities in booking | Boundary reader |
*.reservation.read | Read Reservation across all boundaries | Cross-boundary reader |
Wildcard matching is case-insensitive. A trailing * matches all remaining segments. A * in the middle matches exactly one segment.
AuthorizationBuilder API
Section titled “AuthorizationBuilder API”AuthorizationBuilder is the fluent builder exposed by UseAuthorization. All methods return the builder for chaining.
DefaultPolicy
Section titled “DefaultPolicy”authz.DefaultPolicy = DefaultEndpointPolicy.RequireAuthenticated;| Value | Behavior |
|---|---|
AllowAnonymous | No authorization required (default) |
RequireAuthenticated | Requires a valid identity |
RequirePermission | Requires the SG-generated CRUD permission based on HTTP verb and entity |
MapRole
Section titled “MapRole”Maps a strongly-typed IRole with its default permissions:
authz.MapRole<AdminRole>();Maps a role with composition via RoleBuilder:
authz.MapRole<ManagerRole>(r => r .IncludeDefinition<BookingOperator>() .WithPermissions("custom.permission") .WithAllPermissions<BookingBoundary>() // "booking.*" .WithOperation<CatalogBoundary>(CrudOperation.Read) // "catalog.*.read" .WithoutPermissions("booking.reservation.delete"));Maps a role by name (string):
authz.MapRole("auditor", r => r .WithOperation<BookingBoundary>(CrudOperation.Read) .WithOperation<BillingBoundary>(CrudOperation.Read));RoleBuilder Methods
Section titled “RoleBuilder Methods”| Method | Description |
|---|---|
WithPermissions(params string[]) | Adds explicit permissions |
IncludeDefinition<T>() | Includes all permissions from an IRoleDefinition |
WithAllPermissions() | Grants * (all permissions) |
WithAllPermissions<TBoundary>() | Grants {boundary}.* |
WithOperation<TBoundary>(CrudOperation) | Grants {boundary}.*.{operation} |
WithoutPermissions(params string[]) | Removes specific permissions. Fails fast if excluded permission is covered by a wildcard |
ClearDefaults() | Clears default permissions from IRole.DefaultPermissions |
MapGroup
Section titled “MapGroup”Maps a strongly-typed IGroup:
authz.MapGroup<CustomerCareGroup>();Maps with additional roles:
authz.MapGroup<CustomerCareGroup>(g => g .WithRoles("extra-role") .WithRole<AnotherRole>());Maps by name:
authz.MapGroup("operations", g => g.WithRoles("front-desk", "catalog-viewer"));GroupBuilder Methods
Section titled “GroupBuilder Methods”| Method | Description |
|---|---|
WithRoles(params string[]) | Adds roles by name |
WithRole<TRole>() | Adds a strongly-typed role |
Custom Stores
Section titled “Custom Stores”authz.UseRolePermissionStore<EfRolePermissionStore>();authz.UseGroupRoleStore<EfGroupRoleStore>();Custom stores override the in-memory stores created by MapRole/MapGroup.
Resource Authorizers
Section titled “Resource Authorizers”authz.AddResourceAuthorizer<InvoiceAuthorizer>();Discovers all IResourceAuthorizer<T> interfaces on the type and registers each in DI.
Permission Caching
Section titled “Permission Caching”// Simple: cache for 5 minutesauthz.UsePermissionCache(TimeSpan.FromMinutes(5));
// Detailed: configure optionsauthz.UsePermissionCache(opts =>{ opts.Expiration = TimeSpan.FromMinutes(10); opts.KeyPrefix = "myapp-permissions";});Custom Permission Providers
Section titled “Custom Permission Providers”authz.AddPermissionProvider<MyExternalPermissionProvider>();JSON Seeding
Section titled “JSON Seeding”If the project includes a roles.pragmatic.json file, the SG generates a SeedFromJson() extension method:
authz.SeedFromJson(); // Reads compile-time JSON, flattens inheritanceExample roles.pragmatic.json:
{ "roles": { "front-desk": { "description": "Front desk staff with basic booking access", "permissions": ["booking.reservation.read", "booking.reservation.create", "booking.guest.read"] }, "revenue-manager": { "description": "Revenue management with full catalog and rate access", "permissions": ["catalog.*", "booking.reservation.read"] } }, "groups": { "operations": { "description": "All operational staff", "roles": ["front-desk", "catalog-viewer"] } }}Role inheritance is expanded at compile-time by the SG. The generated code has flattened permissions.
ResourcePolicy
Section titled “ResourcePolicy”Composable authorization rules that evaluate against ICurrentUser. Analogous to Specification<T> but for authorization decisions.
Factory Methods
Section titled “Factory Methods”| Method | Description |
|---|---|
ResourcePolicy.Allow | Always allows (identity for OR) |
ResourcePolicy.Deny | Always denies (identity for AND) |
ResourcePolicy.RequirePermission(string) | Single permission check |
ResourcePolicy.RequireAnyPermission(params string[]) | Any of the permissions (OR) |
ResourcePolicy.RequireAllPermissions(params string[]) | All of the permissions (AND) |
ResourcePolicy.InRole(string) | Role membership check |
ResourcePolicy.InGroup(string) | Group membership check |
ResourcePolicy.HasClaim(string, string?) | Claim existence / value check |
ResourcePolicy.IsAuthenticated() | Authentication check (singleton) |
ResourcePolicy.HasPrincipalKind(PrincipalKind) | Principal kind check |
ResourcePolicy.Custom(Func<ICurrentUser, bool>) | Delegate-based (not serializable) |
Composition Operators
Section titled “Composition Operators”Policies compose with & (AND), | (OR), and ! (NOT):
var policy = ResourcePolicy.RequirePermission("booking.reservation.create") & ResourcePolicy.IsAuthenticated() | ResourcePolicy.HasPrincipalKind(PrincipalKind.Service);
bool allowed = policy.Evaluate(currentUser);Equivalent fluent API:
var policy = ResourcePolicy.RequirePermission("booking.reservation.create") .And(ResourcePolicy.IsAuthenticated()) .Or(ResourcePolicy.HasPrincipalKind(PrincipalKind.Service));Applying Policies to Actions
Section titled “Applying Policies to Actions”Use the [RequirePolicy<T>] attribute:
[RequirePolicy<ReservationManagementPolicy>]public sealed class CreateReservationMutation : Mutation<Reservation>{ // ...}The policy type must have a parameterless constructor. It is instantiated once and cached. Evaluated by PolicyEvaluationFilter at Order 210 in the action pipeline.
AsyncResourcePolicy
Section titled “AsyncResourcePolicy”For policies that require I/O (database lookups, external services):
var asyncPolicy = AsyncResourcePolicy.RequireExternalPermission( async (user, ct) => { // Call external permission service return await externalService.CheckPermissionAsync(user.Id, ct); });Sync policies are implicitly convertible to async:
ResourcePolicy sync = ResourcePolicy.IsAuthenticated();AsyncResourcePolicy async = sync; // implicit conversion via SyncToAsyncPolicyAsync composition supports short-circuit evaluation (AND returns false immediately if the left side is false, OR returns true immediately if the left side is true).
Policy Serialization
Section titled “Policy Serialization”PolicySerializer converts between ResourcePolicy and PolicyExpression for JSON storage:
var policy = ResourcePolicy.RequirePermission("orders.read") & ResourcePolicy.IsAuthenticated();
PolicyExpression expr = PolicySerializer.Serialize(policy);// expr can be serialized to JSON, stored in database, etc.
ResourcePolicy restored = PolicySerializer.Deserialize(expr);PolicyExpression is a record with a discriminated type (PolicyExpressionType) and child nodes for composite policies. Custom and delegate-based policies are not serializable.
Supported PolicyExpressionType values: Allow, Deny, Permission, AnyPermission, AllPermissions, Role, Group, Claim, Authenticated, PrincipalKind, And, Or, Not.
Interfaces (defined in Pragmatic.Abstractions)
Section titled “Interfaces (defined in Pragmatic.Abstractions)”These interfaces live in the Pragmatic.Abstractions package so modules can depend on them without referencing Pragmatic.Authorization.
Strongly-typed application role with static abstract members:
public interface IRole{ static abstract string Name { get; } static abstract string? Description { get; } static abstract IReadOnlyList<string> DefaultPermissions { get; }}IRoleDefinition
Section titled “IRoleDefinition”Module-defined permission template (not an application role):
public interface IRoleDefinition{ static abstract string Name { get; } static abstract string? Description { get; } static abstract IReadOnlyList<string> Permissions { get; }}Key distinction: IRoleDefinition belongs to modules and provides permission bundles. IRole belongs to the host and represents application roles. The host composes roles from definitions via IncludeDefinition<T>().
IGroup
Section titled “IGroup”Strongly-typed group with default roles:
public interface IGroup{ static abstract string Name { get; } static abstract string? Description { get; } static abstract IReadOnlyList<string> DefaultRoles { get; }}IPermission
Section titled “IPermission”Strongly-typed permission for SG-driven registries:
public interface IPermission{ static abstract string Name { get; } static abstract string? Description { get; } static abstract string? Category { get; }}IPermissionProvider
Section titled “IPermissionProvider”Resolves permissions from an external source. Multiple providers compose in order:
public interface IPermissionProvider{ int Order { get; } ValueTask<IReadOnlySet<string>> ResolvePermissionsAsync( ICurrentUser user, CancellationToken ct = default);}Built-in providers: ClaimsPermissionProvider (Order 0), RoleExpansionProvider (Order 100), GroupExpansionProvider (Order 200).
IUserAuthorization
Section titled “IUserAuthorization”Runtime authorization interface, accessed via ICurrentUser.Authorization:
public interface IUserAuthorization{ IReadOnlyCollection<string> Roles { get; } IReadOnlySet<string> Permissions { get; } IReadOnlyCollection<string> Groups { get; } IReadOnlyCollection<string> Scopes { get; } bool HasPermission(string permission); bool HasAnyPermission(IEnumerable<string> permissions); bool HasAllPermissions(IEnumerable<string> permissions); bool IsInRole(string role); bool IsInGroup(string group); bool HasScope(string scope);}Implemented by CachedPermissionResolver at runtime.
IResourceAuthorizer<T>
Section titled “IResourceAuthorizer<T>”Instance-level authorization (ABAC). Contravariant on TResource:
public interface IResourceAuthorizer<in TResource>{ ValueTask<bool> CanAccessAsync( ICurrentUser user, TResource resource, string action, CancellationToken ct = default);}IPermissionChecker
Section titled “IPermissionChecker”Async permission checker for scenarios requiring I/O:
public interface IPermissionChecker{ ValueTask<bool> HasPermissionAsync(string permission, CancellationToken ct = default); ValueTask<bool> HasAnyPermissionAsync(IEnumerable<string> permissions, CancellationToken ct = default); ValueTask<bool> HasAllPermissionsAsync(IEnumerable<string> permissions, CancellationToken ct = default);}RequirePermissionAttribute
Section titled “RequirePermissionAttribute”Applied to actions, mutations, and queries. Requires ALL specified permissions (AND logic):
[AttributeUsage(AttributeTargets.Class, Inherited = false)]public sealed class RequirePermissionAttribute(params string[] permissions) : Attribute{ public string[] Permissions { get; } public string? Description { get; set; } // Mode 2: SG generates a constant public string? Category { get; set; } // Override for generated permission}Stores
Section titled “Stores”IRolePermissionStore
Section titled “IRolePermissionStore”public interface IRolePermissionStore{ ValueTask<IReadOnlySet<string>> GetPermissionsForRoleAsync(string roleName, CancellationToken ct = default); ValueTask<IReadOnlyList<string>> GetAllRolesAsync(CancellationToken ct = default);}IGroupRoleStore
Section titled “IGroupRoleStore”public interface IGroupRoleStore{ ValueTask<IReadOnlySet<string>> GetRolesForGroupAsync(string groupName, CancellationToken ct = default); ValueTask<IReadOnlyList<string>> GetAllGroupsAsync(CancellationToken ct = default);}In-Memory Implementations
Section titled “In-Memory Implementations”InMemoryRolePermissionStore and InMemoryGroupRoleStore are populated automatically by AuthorizationBuilder.MapRole and MapGroup. Thread-safe via ConcurrentDictionary and per-set locking. Registered as singletons.
Forward-Compatible Store Interfaces
Section titled “Forward-Compatible Store Interfaces”| Interface | Extends | Purpose |
|---|---|---|
ITemporalRolePermissionStore | IRolePermissionStore | Point-in-time permission resolution |
ITenantRolePermissionStore | IRolePermissionStore | Tenant-specific permissions |
ITemporalGroupRoleStore | IGroupRoleStore | Point-in-time group resolution |
ITenantGroupRoleStore | IGroupRoleStore | Tenant-specific groups |
Dynamic Stores (for RBAC Management)
Section titled “Dynamic Stores (for RBAC Management)”| Interface | Purpose |
|---|---|
IDynamicPermissionStore | Runtime-created permissions |
IDynamicRoleStore | Runtime-created roles |
IResourcePolicyStore | Dynamic policy-to-resource assignments |
Catalogs
Section titled “Catalogs”IPermissionCatalog and IResourceCatalog are discovery APIs for admin tooling and inspection. They merge static (SG-generated) and dynamic (database-backed) sources.
IPermissionCatalog
Section titled “IPermissionCatalog”public interface IPermissionCatalog{ ValueTask<IReadOnlyList<PermissionInfo>> GetAllPermissionsAsync(CancellationToken ct = default); ValueTask<IReadOnlyList<RoleInfo>> GetAllRolesAsync(CancellationToken ct = default); ValueTask<IReadOnlySet<string>> GetEffectivePermissionsAsync(string userId, CancellationToken ct = default);}DefaultPermissionCatalog merges IReadOnlyList<PermissionInfo> (static, from SG) with IDynamicPermissionStore (optional). GetEffectivePermissionsAsync returns an empty set in the default implementation (deny-by-default). Full cross-user resolution requires the RBAC Management package.
IResourceCatalog
Section titled “IResourceCatalog”public interface IResourceCatalog{ ValueTask<IReadOnlyList<ProtectedResource>> GetAllResourcesAsync(CancellationToken ct = default); ValueTask<PolicyExpression?> GetResourcePolicyAsync(string resourceIdentifier, CancellationToken ct = default);}DefaultResourceCatalog merges static resources with IResourcePolicyStore (optional).
ProtectedResource
Section titled “ProtectedResource”public sealed record ProtectedResource( string ResourceType, // "Endpoint", "Action", "Mutation" string Identifier, // route pattern or action FQN string DisplayName, string? Category){ public IReadOnlyList<string> StaticPermissions { get; init; } = []; public PolicyExpression? PolicyExpression { get; init; } public string? ResourceGroup { get; init; }}Data Records
Section titled “Data Records”public sealed record PermissionInfo(string Name, string? Description, string? Category);public sealed record RoleInfo(string Name, string? Description, IReadOnlyList<string> DefaultPermissions);Observability
Section titled “Observability”AuthorizationDiagnostics provides OTel-compatible instrumentation:
| Instrument | Name | Description |
|---|---|---|
ActivitySource | Pragmatic.Authorization | Distributed tracing |
Counter<long> | pragmatic.authorization.permission_checks | Total permission checks |
Counter<long> | pragmatic.authorization.permission_denied | Denied permission checks |
Counter<long> | pragmatic.authorization.cache_hits | Cache hits (resolved set already available) |
Counter<long> | pragmatic.authorization.cache_misses | Cache misses (full provider chain invoked) |
DI Registration
Section titled “DI Registration”UseAuthorization / AddPragmaticAuthorization registers:
| Registration | Lifetime | Notes |
|---|---|---|
IPermissionProvider (ClaimsPermissionProvider) | Scoped | Always registered |
IPermissionProvider (RoleExpansionProvider) | Scoped | When roles are mapped or custom store used |
IPermissionProvider (GroupExpansionProvider) | Scoped | When groups are mapped or custom store used |
IRolePermissionStore (InMemoryRolePermissionStore) | Singleton | When MapRole used without custom store |
IGroupRoleStore (InMemoryGroupRoleStore) | Singleton | When MapGroup used without custom store |
IUserAuthorization (CachedPermissionResolver) | Scoped | Always registered |
IPermissionCatalog (DefaultPermissionCatalog) | Singleton | TryAdd (SG-generated takes precedence) |
IResourceCatalog (DefaultResourceCatalog) | Singleton | TryAdd (SG-generated takes precedence) |
AuthorizationOptions | Options | Via IOptions<AuthorizationOptions> |
Source Generator Integration
Section titled “Source Generator Integration”The SG auto-detects Pragmatic.Authorization via FeatureDetector and generates:
- PermissionConstants: Nested static classes per entity (
BookingPermissions.Reservation.Read,.Create,.Update,.Delete,.All). - PermissionRegistry:
IReadOnlyList<PermissionInfo>with all known permissions (static + IPermission). - RoleSeedingExtensions:
SeedFromJson()ifroles.pragmatic.jsonexists. Expands inheritance at compile-time.
Permission Naming Convention
Section titled “Permission Naming Convention”The SG auto-generates permission constants following a hierarchical naming convention:
{boundary}.{entity-kebab}.{operation}| Example | Meaning |
|---|---|
booking.reservation.create | Create a reservation |
booking.reservation.read | Read reservations |
booking.guest.update | Update guest details |
catalog.amenity.delete | Delete an amenity |
billing.invoice.refund | Custom permission (from [RequirePermission]) |
CRUD operations (create, read, update, delete, list) are auto-generated for every entity with [Entity<T>]. Custom permissions are generated from [RequirePermission("...")] attributes on actions/mutations.
Runtime Notes
Section titled “Runtime Notes”WithoutPermissions(...)is safe only with explicit permissions. If you try to exclude something covered by a wildcard, the builder fails fast at startup.- Internal calls skip the permission and policy gates through
ICallContext, but they still preserve user context for downstream resource and data checks. IPermissionCatalogis reliable for listing known permissions and roles. The default catalog does not yet provide full cross-user effective permission resolution; for the current user preferICurrentUser.Authorization, and for admin-side lookup prefer the management actions.
Packages
Section titled “Packages”| Package | Purpose |
|---|---|
Pragmatic.Authorization | Engine: policies, evaluation, caching, stores |
Pragmatic.Authorization.Management | Optional RBAC package: CRUD actions, EF entities, dynamic stores |
All Public Types
Section titled “All Public Types”Pragmatic.Abstractions (Authorization namespace)
Section titled “Pragmatic.Abstractions (Authorization namespace)”| Type | Kind | Purpose |
|---|---|---|
IRole | Interface | Strongly-typed application role |
IRoleDefinition | Interface | Module permission template |
IGroup | Interface | Strongly-typed group |
IPermission | Interface | Strongly-typed permission |
IPermissionProvider | Interface | Permission resolution provider |
IUserAuthorization | Interface | Runtime authorization interface |
IPermissionChecker | Interface | Async permission checker |
IResourceAuthorizer<T> | Interface | Instance-level authorization |
RequirePermissionAttribute | Attribute | Permission enforcement on actions |
PermissionInfo | Record | Permission descriptor |
RoleInfo | Record | Role descriptor |
Pragmatic.Authorization
Section titled “Pragmatic.Authorization”| Type | Kind | Purpose |
|---|---|---|
ResourcePolicy | Abstract class | Composable sync authorization rule |
AsyncResourcePolicy | Abstract class | Composable async authorization rule |
RequirePolicyAttribute<T> | Attribute | Policy enforcement on actions |
AuthorizationBuilder | Class | Fluent configuration builder |
RoleBuilder | Class | Inline role configuration |
GroupBuilder | Class | Inline group configuration |
AuthorizationOptions | Class | Configuration options |
PermissionCacheOptions | Class | Cache configuration |
DefaultEndpointPolicy | Enum | Default endpoint authorization modes |
CrudOperation | Enum | Standard CRUD operations |
CachedPermissionResolver | Class | Request-scoped permission resolver (implements IUserAuthorization) |
WildcardMatcher | Static class | Permission wildcard matching (internal) |
IRolePermissionStore | Interface | Role-to-permissions store |
IGroupRoleStore | Interface | Group-to-roles store |
InMemoryRolePermissionStore | Class | In-memory role store |
InMemoryGroupRoleStore | Class | In-memory group store |
ITemporalRolePermissionStore | Interface | Temporal role store (forward-compat) |
ITenantRolePermissionStore | Interface | Tenant-scoped role store (forward-compat) |
ITemporalGroupRoleStore | Interface | Temporal group store (forward-compat) |
ITenantGroupRoleStore | Interface | Tenant-scoped group store (forward-compat) |
IDynamicPermissionStore | Interface | Runtime permission management |
IDynamicRoleStore | Interface | Runtime role management |
IResourcePolicyStore | Interface | Dynamic policy-resource mapping |
IPermissionCatalog | Interface | Permission/role discovery |
IResourceCatalog | Interface | Resource discovery |
DefaultPermissionCatalog | Class | Default catalog (static + dynamic) |
DefaultResourceCatalog | Class | Default resource catalog |
ProtectedResource | Record | Resource descriptor |
PolicyExpression | Record | Serializable policy tree |
PolicyExpressionType | Enum | Policy node discriminator |
PolicySerializer | Static class | Policy serialization/deserialization |
AuthorizationDiagnostics | Static class | OTel metrics and tracing |
PragmaticBuilderAuthorizationExtensions | Static class | UseAuthorization / AddPragmaticAuthorization |
Test Coverage
Section titled “Test Coverage”58 unit tests covering:
CachedPermissionResolverTests— Provider chain, caching, permissions resolutionCachedPermissionResolverCacheTests— Cross-request ICacheStack integrationCompositePermissionProviderTests— Multi-provider mergingInMemoryRolePermissionStoreTests— Thread-safe role storeInMemoryGroupRoleStoreTests— Group storeGroupExpansionProviderTests— Group -> Role -> Permission chainAuthorizationBuilderTests— Builder configuration, store selectionResourcePolicyTests— All policy factory methodsResourcePolicyCompositionTests— AND, OR, NOT compositionAsyncResourcePolicyTests— Async policies with short-circuit evaluationPolicySerializerTests— Round-trip serializationWildcardMatcherTests— All wildcard patternsDefaultPermissionCatalogTests— Static + dynamic mergingDefaultResourceCatalogTests— Resource catalogRoleBuilderExtensionsTests— WithAllPermissions, WithOperation, WithoutPermissions