Stores
Pragmatic.Authorization uses pluggable stores for role-permission and group-role mappings. This document covers the in-memory implementations, the store interfaces, and the forward-compatible interfaces for temporal and multi-tenant scenarios.
Store Interfaces
Section titled “Store Interfaces”IRolePermissionStore
Section titled “IRolePermissionStore”Resolves permissions assigned to a role:
public interface IRolePermissionStore{ ValueTask<IReadOnlySet<string>> GetPermissionsForRoleAsync( string roleName, CancellationToken ct = default);
ValueTask<IReadOnlyList<string>> GetAllRolesAsync(CancellationToken ct = default);}Used by RoleExpansionProvider (Order 100) and GroupExpansionProvider (Order 200, indirectly).
IGroupRoleStore
Section titled “IGroupRoleStore”Resolves roles assigned to a group:
public interface IGroupRoleStore{ ValueTask<IReadOnlySet<string>> GetRolesForGroupAsync( string groupName, CancellationToken ct = default);
ValueTask<IReadOnlyList<string>> GetAllGroupsAsync(CancellationToken ct = default);}Used by GroupExpansionProvider (Order 200).
In-Memory Stores
Section titled “In-Memory Stores”InMemoryRolePermissionStore
Section titled “InMemoryRolePermissionStore”Default store for development, testing, and configuration-driven setups. Populated by AuthorizationBuilder.MapRole and SeedFromJson().
Thread safety: Uses ConcurrentDictionary<string, HashSet<string>> for the role map. Per-set locking on AddRole and GetPermissionsForRoleAsync (snapshot under lock to avoid concurrent modification during enumeration).
Lifetime: Registered as Singleton when populated via MapRole. The in-memory store is created during UseAuthorization configuration and lives for the application lifetime.
public sealed class InMemoryRolePermissionStore : IRolePermissionStore{ public void AddRole(string roleName, IEnumerable<string> permissions); public ValueTask<IReadOnlySet<string>> GetPermissionsForRoleAsync(string roleName, CancellationToken ct = default); public ValueTask<IReadOnlyList<string>> GetAllRolesAsync(CancellationToken ct = default);}Case-insensitive role name lookup (uses StringComparer.OrdinalIgnoreCase).
InMemoryGroupRoleStore
Section titled “InMemoryGroupRoleStore”Default store for group-role mappings. Populated by AuthorizationBuilder.MapGroup.
public sealed class InMemoryGroupRoleStore : IGroupRoleStore{ public void AddGroup(string groupName, IEnumerable<string> roles); public ValueTask<IReadOnlySet<string>> GetRolesForGroupAsync(string groupName, CancellationToken ct = default); public ValueTask<IReadOnlyList<string>> GetAllGroupsAsync(CancellationToken ct = default);}Case-insensitive group name lookup.
Custom Stores
Section titled “Custom Stores”Override the in-memory stores with database-backed implementations:
authz.UseRolePermissionStore<EfRolePermissionStore>();authz.UseGroupRoleStore<EfGroupRoleStore>();When a custom store is registered:
- The in-memory store (if any roles were mapped) is not registered.
- The
RoleExpansionProviderorGroupExpansionProvideris still registered to use the custom store.
Custom stores are registered as Scoped (one per request) via Services.AddScoped<IRolePermissionStore, T>().
Implementing a Custom Store
Section titled “Implementing a Custom Store”public sealed class EfRolePermissionStore(MyDbContext db) : IRolePermissionStore{ public async ValueTask<IReadOnlySet<string>> GetPermissionsForRoleAsync( string roleName, CancellationToken ct = default) { var permissions = await db.RolePermissions .Where(rp => rp.RoleName == roleName) .Select(rp => rp.PermissionName) .ToListAsync(ct);
return new HashSet<string>(permissions, StringComparer.OrdinalIgnoreCase); }
public async ValueTask<IReadOnlyList<string>> GetAllRolesAsync(CancellationToken ct = default) { return await db.RolePermissions .Select(rp => rp.RoleName) .Distinct() .ToListAsync(ct); }}Register:
authz.UseRolePermissionStore<EfRolePermissionStore>();Store Selection Logic
Section titled “Store Selection Logic”PragmaticBuilderAuthorizationExtensions.AddPragmaticAuthorization determines which stores and providers to register:
Role Store
Section titled “Role Store”| Condition | Store | Provider |
|---|---|---|
MapRole called, no custom store | InMemoryRolePermissionStore (Singleton) | RoleExpansionProvider (Scoped) |
UseRolePermissionStore<T> called | Custom T (Scoped) | RoleExpansionProvider (Scoped) |
| Neither called | None | None |
Group Store
Section titled “Group Store”| Condition | Store | Provider |
|---|---|---|
MapGroup called, no custom store | InMemoryGroupRoleStore (Singleton) | GroupExpansionProvider (Scoped) |
UseGroupRoleStore<T> called | Custom T (Scoped) | GroupExpansionProvider (Scoped) |
| Neither called | None | None |
Group Dependencies
Section titled “Group Dependencies”When groups are mapped (or a custom group store is registered) but no role store exists, the builder automatically registers an empty InMemoryRolePermissionStore. This ensures GroupExpansionProvider (which depends on both IGroupRoleStore and IRolePermissionStore) does not fail.
Forward-Compatible Store Interfaces
Section titled “Forward-Compatible Store Interfaces”These interfaces extend the base stores with additional parameters. They are defined today for forward compatibility but are not consumed by the current runtime. Implement them in your custom store to prepare for future features.
ITemporalRolePermissionStore
Section titled “ITemporalRolePermissionStore”Point-in-time permission resolution for temporal authorization:
public interface ITemporalRolePermissionStore : IRolePermissionStore{ ValueTask<IReadOnlySet<string>> GetPermissionsForRoleAsync( string roleName, DateTimeOffset asOf, CancellationToken ct = default);}Use case: “What permissions did the admin role have on January 15?” Useful for audit trails and compliance.
ITenantRolePermissionStore
Section titled “ITenantRolePermissionStore”Tenant-specific permission resolution for multi-tenant authorization:
public interface ITenantRolePermissionStore : IRolePermissionStore{ ValueTask<IReadOnlySet<string>> GetPermissionsForRoleAsync( string roleName, string tenantId, CancellationToken ct = default);}Use case: “What permissions does the manager role have in Tenant A?” Different tenants may have different permission sets for the same role.
ITemporalGroupRoleStore
Section titled “ITemporalGroupRoleStore”Point-in-time group resolution:
public interface ITemporalGroupRoleStore : IGroupRoleStore{ ValueTask<IReadOnlySet<string>> GetRolesForGroupAsync( string groupName, DateTimeOffset asOf, CancellationToken ct = default);}ITenantGroupRoleStore
Section titled “ITenantGroupRoleStore”Tenant-specific group resolution:
public interface ITenantGroupRoleStore : IGroupRoleStore{ ValueTask<IReadOnlySet<string>> GetRolesForGroupAsync( string groupName, string tenantId, CancellationToken ct = default);}Dynamic Stores
Section titled “Dynamic Stores”These interfaces support runtime RBAC management (creating roles and permissions at runtime via admin UIs). They are consumed by the catalog system and the optional Pragmatic.Authorization.Management package.
IDynamicPermissionStore
Section titled “IDynamicPermissionStore”public interface IDynamicPermissionStore{ ValueTask<IReadOnlyList<PermissionInfo>> GetAllAsync(CancellationToken ct = default); ValueTask<bool> ExistsAsync(string permissionName, CancellationToken ct = default);}Used by DefaultPermissionCatalog to merge runtime-created permissions with SG-generated static permissions.
IDynamicRoleStore
Section titled “IDynamicRoleStore”public interface IDynamicRoleStore{ ValueTask<IReadOnlyList<RoleInfo>> GetAllAsync(CancellationToken ct = default); ValueTask<bool> ExistsAsync(string roleName, CancellationToken ct = default);}Used by DefaultPermissionCatalog to merge runtime-created roles.
IResourcePolicyStore
Section titled “IResourcePolicyStore”public interface IResourcePolicyStore{ ValueTask<IReadOnlyList<ProtectedResource>> GetAllResourcesAsync(CancellationToken ct = default); ValueTask<PolicyExpression?> GetPolicyAsync(string resourceIdentifier, CancellationToken ct = default);}Used by DefaultResourceCatalog for dynamic policy-to-resource assignments.
Testing with In-Memory Stores
Section titled “Testing with In-Memory Stores”For integration tests, use the in-memory stores directly:
var store = new InMemoryRolePermissionStore();store.AddRole("test-role", ["permission.one", "permission.two"]);
services.AddSingleton<IRolePermissionStore>(store);services.AddScoped<IPermissionProvider, RoleExpansionProvider>();Or use the full AuthorizationBuilder:
services.AddPragmaticAuthorization(authz =>{ authz.MapRole("test-admin", r => r.WithAllPermissions()); authz.MapRole("test-viewer", r => r.WithPermissions("entity.read"));});The Showcase integration tests use CreateClientWithRoles and CreateClientWithGroups helpers that set HTTP headers (X-User-Roles, X-User-Groups), which the NoOpAuthenticationHandler maps to claims. The RoleExpansionProvider and GroupExpansionProvider then resolve these claims to permissions via the in-memory stores.
Store Lifecycle Summary
Section titled “Store Lifecycle Summary”| Store | Default Implementation | Lifetime | Populated by |
|---|---|---|---|
IRolePermissionStore | InMemoryRolePermissionStore | Singleton | MapRole, SeedFromJson |
IGroupRoleStore | InMemoryGroupRoleStore | Singleton | MapGroup, SeedFromJson |
IDynamicPermissionStore | None (optional) | Scoped | RBAC Management package |
IDynamicRoleStore | None (optional) | Scoped | RBAC Management package |
IResourcePolicyStore | None (optional) | Scoped | RBAC Management package |
Custom stores registered via UseRolePermissionStore<T>() or UseGroupRoleStore<T>() are Scoped.