Authorization
Dynamic RBAC permission system with module-scoped permission definitions, a policy provider backed by distributed cache, and an EF Core grant store for persistent role-permission assignments.
Package structure
Section titled “Package structure”DirectoryGranit.Authorization/ Dynamic RBAC, permission definitions, policy provider
- Granit.Authorization.EntityFrameworkCore EF Core permission grant store
- Granit.Authorization.Endpoints Permission management HTTP endpoints
| Package | Role | Depends on |
|---|---|---|
Granit.Authorization | RBAC permissions, dynamic policy provider | Granit.Security, Granit.Caching |
Granit.Authorization.EntityFrameworkCore | EF Core permission grant store | Granit.Authorization, Granit.Persistence |
Dependency graph
Section titled “Dependency graph”graph TD
AZ[Granit.Authorization] --> S[Granit.Security]
AZ --> CA[Granit.Caching]
AZEF[Granit.Authorization.EntityFrameworkCore] --> AZ
AZEF --> P[Granit.Persistence]
[DependsOn(typeof(GranitAuthorizationModule))]public class AppModule : GranitModule { }With EF Core persistence:
[DependsOn(typeof(GranitAuthorizationEntityFrameworkCoreModule))]public class AppModule : GranitModule{ public override void ConfigureServices(ServiceConfigurationContext context) { context.Services.AddGranitAuthorizationEntityFrameworkCore<AppDbContext>(); }}Permission definitions
Section titled “Permission definitions”Modules declare their permissions via IPermissionDefinitionProvider:
public class InvoicePermissionDefinitionProvider : IPermissionDefinitionProvider{ public void DefinePermissions(IPermissionDefinitionContext context) { var group = context.AddGroup("Invoices", "Invoice management"); group.AddPermission("Invoices.Invoices.Read", "View invoices"); group.AddPermission("Invoices.Invoices.Create", "Create invoices"); group.AddPermission("Invoices.Invoices.Update", "Edit invoices"); group.AddPermission("Invoices.Invoices.Delete", "Delete invoices"); }}Naming convention: [Module].[Resource].[Action]
Standard actions: Read, Create, Update, Delete, Manage (all CRUD), Execute (non-CRUD).
Permission checking pipeline
Section titled “Permission checking pipeline”flowchart LR
A[Request] --> B{AlwaysAllow?}
B -->|yes| C[Granted]
B -->|no| D{AdminRole?}
D -->|yes| C
D -->|no| E{Cache hit?}
E -->|yes| F{Granted?}
E -->|no| G[IPermissionGrantStore]
G --> H[Cache result]
H --> F
F -->|yes| C
F -->|no| I[Denied]
The PermissionChecker evaluates in order:
AlwaysAllowoption (development only)- Admin role bypass — users with any role in
AdminRolesget all permissions - Per-role cache lookup (key:
perm:{tenantId}:{roleName}:{permissionName}) IPermissionGrantStorefallback (default:NullPermissionGrantStore— always denied)
Using permissions
Section titled “Using permissions”// Option 1: Attribute on endpointapp.MapGet("/invoices", [Permission("Invoices.Invoices.Read")] async ( AppDbContext db, CancellationToken cancellationToken) =>{ return await db.Invoices .ToListAsync(cancellationToken) .ConfigureAwait(false);});
// Option 2: Imperative checkpublic class InvoiceService(IPermissionChecker permissionChecker){ public async Task DeleteAsync(Guid id, CancellationToken cancellationToken) { if (!await permissionChecker.IsGrantedAsync( "Invoices.Invoices.Delete", cancellationToken) .ConfigureAwait(false)) { throw new ForbiddenException("Not authorized to delete invoices"); } // ... }}Configuration
Section titled “Configuration”{ "Authorization": { "AdminRoles": ["admin"], "CacheDuration": "00:05:00", "AlwaysAllow": false }}| Property | Default | Description |
|---|---|---|
AdminRoles | ["admin"] | Roles that bypass all permission checks |
CacheDuration | 00:05:00 | Per-role permission cache TTL |
AlwaysAllow | false | Skip all checks (development only) |
EF Core permission store
Section titled “EF Core permission store”Granit.Authorization.EntityFrameworkCore replaces NullPermissionGrantStore with a
real store backed by EF Core.
Entity:
public class PermissionGrant : AuditedEntity, IMultiTenant{ public string Name { get; set; } = string.Empty; // Permission name public string RoleName { get; set; } = string.Empty; // Role granted to public Guid? TenantId { get; set; } // Tenant scope (null = global)}// Unique index: (TenantId, Name, RoleName)DbContext integration:
public class AppDbContext : DbContext, IPermissionGrantDbContext{ public DbSet<PermissionGrant> PermissionGrants => Set<PermissionGrant>();}Managing grants:
public class PermissionAdminService( IPermissionManagerWriter writer, IPermissionManagerReader reader){ public async Task GrantAsync( string roleName, string permissionName, CancellationToken cancellationToken) { await writer.SetAsync(permissionName, roleName, tenantId: null, isGranted: true, cancellationToken) .ConfigureAwait(false); }
public async Task<IReadOnlyList<string>> GetGrantedAsync( string roleName, CancellationToken cancellationToken) { return await reader.GetGrantedPermissionsAsync(roleName, tenantId: null, cancellationToken) .ConfigureAwait(false); }}All grant changes are audit-logged via AuditedEntity (ISO 27001 — 3-year retention).
Public API summary
Section titled “Public API summary”| Category | Key types | Package |
|---|---|---|
| Modules | GranitAuthorizationModule, GranitAuthorizationEntityFrameworkCoreModule | — |
| Abstractions | IPermissionDefinitionProvider, IPermissionDefinitionManager, IPermissionChecker, IPermissionGrantStore, PermissionAttribute | Granit.Authorization |
| Options | GranitAuthorizationOptions | — |
| EF Core | PermissionGrant, IPermissionGrantDbContext, IPermissionManagerReader, IPermissionManagerWriter | Granit.Authorization.EntityFrameworkCore |
| Extensions | AddGranitAuthorization(), AddGranitAuthorizationEntityFrameworkCore<T>() | — |
See also
Section titled “See also”- Authentication — JWT Bearer, claims transformation
- Security — core abstractions (ICurrentUserService, ActorKind)
- Persistence —
AuditedEntitybase class, interceptors