Skip to content

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.

  • DirectoryGranit.Authorization/ Dynamic RBAC, permission definitions, policy provider
    • Granit.Authorization.EntityFrameworkCore EF Core permission grant store
    • Granit.Authorization.Endpoints Permission management HTTP endpoints
PackageRoleDepends on
Granit.AuthorizationRBAC permissions, dynamic policy providerGranit.Security, Granit.Caching
Granit.Authorization.EntityFrameworkCoreEF Core permission grant storeGranit.Authorization, Granit.Persistence
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>();
}
}

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

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:

  1. AlwaysAllow option (development only)
  2. Admin role bypass — users with any role in AdminRoles get all permissions
  3. Per-role cache lookup (key: perm:{tenantId}:{roleName}:{permissionName})
  4. IPermissionGrantStore fallback (default: NullPermissionGrantStore — always denied)
// Option 1: Attribute on endpoint
app.MapGet("/invoices", [Permission("Invoices.Invoices.Read")] async (
AppDbContext db,
CancellationToken cancellationToken) =>
{
return await db.Invoices
.ToListAsync(cancellationToken)
.ConfigureAwait(false);
});
// Option 2: Imperative check
public 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");
}
// ...
}
}
{
"Authorization": {
"AdminRoles": ["admin"],
"CacheDuration": "00:05:00",
"AlwaysAllow": false
}
}
PropertyDefaultDescription
AdminRoles["admin"]Roles that bypass all permission checks
CacheDuration00:05:00Per-role permission cache TTL
AlwaysAllowfalseSkip all checks (development only)

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

CategoryKey typesPackage
ModulesGranitAuthorizationModule, GranitAuthorizationEntityFrameworkCoreModule
AbstractionsIPermissionDefinitionProvider, IPermissionDefinitionManager, IPermissionChecker, IPermissionGrantStore, PermissionAttributeGranit.Authorization
OptionsGranitAuthorizationOptions
EF CorePermissionGrant, IPermissionGrantDbContext, IPermissionManagerReader, IPermissionManagerWriterGranit.Authorization.EntityFrameworkCore
ExtensionsAddGranitAuthorization(), AddGranitAuthorizationEntityFrameworkCore<T>()
  • Authentication — JWT Bearer, claims transformation
  • Security — core abstractions (ICurrentUserService, ActorKind)
  • PersistenceAuditedEntity base class, interceptors