Entity Permissions¶
Enforce fine-grained, entity-level authorization with fluent configuration and runtime evaluation.
Overview¶
The Application.Identity feature manages permissions for entity types and individual entity instances. It supports predefined values such as Permission.Read, application-defined values such as "Review", user and role grants, optional hierarchy inheritance, and default permission providers. Entity Framework Core stores grants, while application and presentation components expose evaluation and authorization APIs.
Challenges¶
- Granular Access Control: Traditional role-based access control is often too coarse when access must be restricted to specific entity instances.
- Configuration Complexity: Setting up permissions across multiple entities and endpoints becomes hard to maintain without a central model.
- Hierarchical Permissions: Parent-child structures often need inherited access, such as a manager's access flowing to subordinate entities.
- Runtime Management: Applications need programmatic and operational ways to grant, revoke, inspect, and validate permissions.
- Cross-Layer Consistency: The same permission logic should be reusable in endpoints, application services, and rules without duplicating checks.
Solution¶
The Application.Identity feature centralizes entity-permission configuration within AddEntityAuthorization, supports predefined Permission values and custom permission strings, and integrates with ASP.NET Core authorization.
Permissions can be applied at two levels:
- Type-wide permissions: wildcard permissions that apply to all instances of an entity type
- Entity-specific permissions: permissions granted for a single entity instance identified by an id
Key components include:
- Entity Permissions: persisted or defaulted rights for a specific entity type or entity instance
- Fluent Configuration: centralized setup through
AddEntityAuthorization(...)andWithEntityPermissions<TContext>(...) - Permission Evaluation:
IEntityPermissionEvaluator<TEntity>for application-layer permission checks - Management Tools:
IEntityPermissionProviderplusEntityPermissionProviderBuilderfor granting and revoking permissions - Hierarchy Support: optional parent inheritance for entities configured through
AddHierarchicalEntity(...) - Rules Integration:
HasPermissionRule<TEntity>andHasNotPermissionRule<TEntity>for rule-based authorization checks
Key Features¶
- User and role grants for an entity type or a specific entity id.
- Built-in and application-defined permission names.
- Entity Framework Core persistence through
IEntityPermissionContext. - Optional default providers, hierarchy inheritance, and successful-result caching.
- Evaluator overloads for current-user, explicit-user, entity, entity-id, and type-wide checks.
- ASP.NET Core controller, Minimal API, and optional runtime endpoint integration.
- Rules that return structured authorization failures.
Architecture¶
Application.Identity defines the permission model, evaluator, providers, and rules. Infrastructure.EntityFramework implements persisted grants and hierarchy lookup. Presentation.Web connects the evaluator to ASP.NET Core authorization and optional management and evaluation endpoints. ICurrentUserAccessor supplies the current user id and roles when a caller does not pass them explicitly.
Permission evaluation flow diagram¶
This diagram illustrates the effective permission evaluation process:
graph TD
A[Request or Application Check] --> B[IEntityPermissionEvaluator<TEntity>]
B --> C{Cache Enabled?}
C -->|Hit| D[Cached Permission Result]
C -->|Miss| E[IEntityPermissionProvider]
E -->|Direct Grants| F[EntityPermissions Store]
E -->|Role Grants| F
E -->|Hierarchy Path| G[Parent Entity Chain]
B --> H[Default Permission Providers]
F --> B
G --> B
H --> B
D --> I[Allow or Deny]
B --> I
- Evaluation Flow Explanation:
- Application code or ASP.NET Core authorization triggers a permission check.
IEntityPermissionEvaluator<TEntity>optionally checks the cache first.- If needed, it asks
IEntityPermissionProviderfor direct user and role grants. - If the entity type is configured as hierarchical, parent ids are resolved and checked as inherited permission sources.
- Configured default permission providers are evaluated.
- The evaluator returns an allow/deny result and may cache successful resolutions.
Permission granting flow diagram¶
This diagram depicts how permissions are granted:
graph TD
L[Grant Request] --> M[IEntityPermissionProvider or Builder]
M -->|GrantUserPermissionAsync / GrantRolePermissionAsync| N[EntityPermissions Store]
N --> O[Persist Grant]
O --> P[Subsequent Evaluation Sees Grant]
- Granting Flow Explanation:
- A grant request is initiated programmatically through
IEntityPermissionProvideror the fluentEntityPermissionProviderBuilder. - User- or role-based grants are persisted for an entity type or a concrete entity id.
- Later permission checks resolve those grants directly or via inherited or cached results.
Use Cases¶
- Grant a user read access to one employee record.
- Grant a role a type-wide list or administration permission.
- Inherit access from a parent department or organizational node.
- Provide public or module-wide baseline access through a default provider.
- Protect controller actions, Minimal API routes, or application operations with the same evaluator.
- Inspect or administer grants through optional protected runtime endpoints.
Basic Usage¶
The following setup gives authenticated users a default read permission for Employee and exposes an endpoint that checks a concrete employee id. A successful request returns a visible result; a denied request returns HTTP 403.
services.AddDbContext<CoreDbContext>(options =>
options.UseSqlServer(connectionString));
services.AddHttpContextAccessor();
services.AddScoped<ICurrentUserAccessor, HttpCurrentUserAccessor>();
services.AddEntityAuthorization(identity =>
{
identity.WithEntityPermissions<CoreDbContext>(permissions =>
{
permissions
.AddEntity<Employee>(Permission.Read)
.AddDefaultPermissions<Employee>(Permission.Read)
.UseDefaultPermissionProvider<Employee>();
});
});
app.MapGet("/employees/{employeeId}", async Task<Microsoft.AspNetCore.Http.IResult> (
string employeeId,
IEntityPermissionEvaluator<Employee> evaluator,
ICurrentUserAccessor currentUser,
CancellationToken cancellationToken) =>
{
var allowed = await evaluator.HasPermissionAsync(
currentUser,
employeeId,
Permission.Read,
cancellationToken: cancellationToken);
return allowed
? Results.Ok(new { employeeId, permission = "Read", allowed = true })
: Results.Forbid();
})
.RequireAuthorization();
For employeeId = "emp1", the default provider produces:
{
"employeeId": "emp1",
"permission": "Read",
"allowed": true
}
Getting started¶
Prerequisites¶
- An ASP.NET Core application with dependency injection configured
- Entity Framework Core with a database context implementing
IEntityPermissionContext - An
ICurrentUserAccessorimplementation for user-aware evaluation in web requests - A database migration that creates the mapped
__Identity_EntityPermissionstable after the context adds itsDbSet
Basic setup¶
Configure entity permissions in Program.cs:
using BridgingIT.DevKit.Application.Identity;
using BridgingIT.DevKit.Common;
services.AddEntityAuthorization(identity =>
{
identity.WithEntityPermissions<CoreDbContext>(permissions =>
{
permissions.AddEntity<Employee>(
Permission.Read,
Permission.Write,
Permission.Delete,
Permission.List);
});
});
services.AddHttpContextAccessor();
services.AddScoped<ICurrentUserAccessor, HttpCurrentUserAccessor>();
services.AddDbContext<CoreDbContext>(options =>
options.UseSqlServer("Server=.;Database=YourDb;Trusted_Connection=True;"));
First secured endpoint¶
Secure a Minimal API endpoint:
app.MapGet("/employees", () => Results.Ok())
.RequireEntityPermission<Employee>(Permission.List);
This keeps the example focused on authorization itself. Application-specific request handling can be plugged into the endpoint however the host prefers.
Setup and configuration¶
Fluent configuration¶
Define permissions using the fluent syntax:
using BridgingIT.DevKit.Application.Identity;
using BridgingIT.DevKit.Common;
services.AddEntityAuthorization(identity =>
{
identity.WithEntityPermissions<CoreDbContext>(permissions =>
{
permissions
.AddEntity<Employee>(
Permission.Read,
Permission.Write,
Permission.Delete,
Permission.List,
Permission.For("Review"))
.AddDefaultPermissions<Employee>(Permission.Read)
.UseDefaultPermissionProvider<Employee>()
.EnableCaching()
.WithCacheLifetime(TimeSpan.FromMinutes(5));
})
.EnableEvaluationEndpoints()
.EnableManagementEndpoints(options =>
{
options.RequireAuthorization = true;
});
});
AddEntityregisters a regular entity type and the permission names that should be available for it.AddHierarchicalEntityregisters a parent-link expression for inherited permissions.AddDefaultPermissionsdefines baseline permissions for an entity type.UseDefaultPermissionProvideractivates either the built-in or a custom default provider.EnableCachingandWithCacheLifetimecontrol evaluator caching.
Hierarchical entities¶
For entities with parent-child relationships, use AddHierarchicalEntity(...):
permissions.AddHierarchicalEntity<Department>(
d => d.ParentId,
Permission.Read,
Permission.Write,
Permission.List);
This tells the evaluator how to walk the hierarchy when direct grants are absent.
Database context¶
Define the persistence context:
public class CoreDbContext : DbContext, IEntityPermissionContext
{
public CoreDbContext(DbContextOptions<CoreDbContext> options)
: base(options)
{
}
public DbSet<EntityPermission> EntityPermissions { get; set; }
}
Securing controllers¶
For controller-based scenarios, use EntityPermissionRequirementAttribute from the presentation layer:
[Authorize]
[Route("api/employees")]
[ApiController]
public class EmployeeController : ControllerBase
{
[EntityPermissionRequirement(typeof(Employee), nameof(Permission.List))]
[HttpGet]
public IActionResult GetAll() => this.Ok();
}
Securing Minimal APIs¶
Use RequireEntityPermission(...):
app.MapGet("/employees", () => Results.Ok())
.RequireEntityPermission<Employee>(Permission.List);
For route groups:
app.MapGroup("/employees")
.RequireEntityPermission<Employee>(Permission.Read);
Important boundary note¶
The feature spans multiple packages:
Application.Identitydefines the permission model, evaluator, provider abstractions, and rulesInfrastructure.EntityFrameworkprovidesWithEntityPermissions<TContext>(...)and the EF-backed provider wiringPresentation.Webadds endpoint helpers and the optional evaluation/management endpoints
That split matters because some APIs that feel like part of the feature actually live outside the core application package.
Managing permissions¶
Permissions can be managed programmatically using IEntityPermissionProvider or EntityPermissionProviderBuilder.
Using IEntityPermissionProvider directly¶
var provider = services.GetRequiredService<IEntityPermissionProvider>();
await provider.GrantUserPermissionAsync(
"user123",
typeof(Employee).FullName,
"emp1",
Permission.Write);
await provider.GrantRolePermissionAsync(
"Admins",
typeof(Employee).FullName,
null,
"Review");
The provider also supports:
- revoking single user or role permissions
- revoking all permissions for one user or role
- listing permissions for users, roles, or a concrete entity
- retrieving the hierarchy path for configured hierarchical entities
Using EntityPermissionProviderBuilder¶
var provider = new EntityPermissionProviderBuilder(
services.GetRequiredService<IEntityPermissionProvider>())
.ForUser("user123")
.WithPermission<Employee>("emp1", Permission.Write)
.WithPermission<Employee>("emp1", "Review")
.ForRole("Admins")
.WithPermission<Employee>(Permission.List)
.Build();
The builder performs each asynchronous grant synchronously through GetAwaiter().GetResult(). Use it in controlled seeding or setup code; use the asynchronous provider methods in request handling and other asynchronous workflows.
Default permission providers¶
Default providers supply permissions even when no explicit persisted grant exists.
Use cases include:
- public read access
- baseline permissions for specific modules
- environment- or tenant-dependent defaults
The core contract is IDefaultEntityPermissionProvider<TEntity>, which exposes GetDefaultPermissions().
Cache invalidation¶
The Entity Framework provider invalidates affected cache entries after its grant and revoke methods. Broader invalidation is useful when permissions or default-provider behavior change outside those methods. The cache extension helpers support patterns such as:
- invalidating all permissions for a specific user
- invalidating all permissions for an entity type
See also Common Caching.
Checking permissions¶
Permissions can be verified using IEntityPermissionEvaluator<TEntity> or ASP.NET Core authorization.
Using IEntityPermissionEvaluator<TEntity>¶
var evaluator = services.GetRequiredService<IEntityPermissionEvaluator<Employee>>();
var canWrite = await evaluator.HasPermissionAsync(
"user123",
["Admins"],
"emp1",
Permission.Write);
var canReview = await evaluator.HasPermissionAsync(
"user123",
[],
"emp1",
"Review");
The evaluator supports several shapes:
- checks against a concrete entity instance
- checks against an entity id
- wildcard checks against the entity type
- checks for a single permission or any permission in a set
- permission inspection through
GetPermissionsAsync(...)
Using ICurrentUserAccessor¶
For application services or handlers already running in a user-aware context:
var canRead = await evaluator.HasPermissionAsync(
currentUserAccessor,
employeeId,
Permission.Read,
cancellationToken: cancellationToken);
Using ASP.NET Core authorization¶
ASP.NET Core authorization flows into the same permission system through authorization handlers. For Minimal APIs, RequireEntityPermission(...) adds an EntityPermissionAuthorizationRequirement. For controller-based scenarios, the feature can also participate through policy-based authorization and the attribute helper shown earlier.
Via runtime evaluation endpoints¶
If enabled, the evaluation endpoints expose the evaluator over HTTP for operational inspection and debugging.
Rules integration¶
Application.Identity integrates with the Rules feature through:
HasPermissionRule<TEntity>HasNotPermissionRule<TEntity>
Example:
var result = await Rule.CheckAsync(
new HasPermissionRule<Employee>(
currentUserAccessor,
permissionEvaluator,
employeeId,
Permission.Write),
cancellationToken: cancellationToken);
This is useful when permission failures should become structured Result failures instead of ad hoc branching.
For the broader rule style, see Rules.
API reference¶
The runtime API surface is optional and lives in Presentation.Web.
Management endpoints¶
Default group path:
/_bdk/api/identity/management/entities/permissions
| Endpoint | Method | Purpose |
|---|---|---|
/users/{userId}/grant |
POST |
Grant a permission to a user for an entity type or entity id |
/users/{userId}/revoke |
POST |
Revoke one permission from a user |
/users/{userId}/revoke/all |
POST |
Revoke all permissions from a user |
/users/{userId}?entityType={type}&entityId={id} |
GET |
Get granted permissions for one user and entity target |
/users?entityType={type}&entityId={id} |
GET |
Get granted permissions for all users for an entity target |
/roles/{role}/grant |
POST |
Grant a permission to a role |
/roles/{role}/revoke |
POST |
Revoke one permission from a role |
/roles/{role}/revoke/all |
POST |
Revoke all permissions from a role |
/roles/{role}?entityType={type}&entityId={id} |
GET |
Get granted permissions for one role and entity target |
/roles?entityType={type}&entityId={id} |
GET |
Get granted permissions for all roles for an entity target |
Request body for grant/revoke:
{
"entityType": "MyApp.Domain.Model.Employee",
"entityId": "emp1",
"permission": "Write"
}
The management GET endpoints return persisted grants. They do not add permissions supplied by default providers.
Evaluation endpoints¶
Default group path:
/_bdk/api/identity/evaluate/entities/permissions
| Endpoint | Method | Purpose |
|---|---|---|
/{permission}?entityType={type}&entityId={id} |
GET |
Check whether the current user has a specific permission |
?entityType={type}&entityId={id} |
GET |
Get the current user's effective permissions |
Example response:
{
"entityType": "MyApp.Domain.Model.Employee",
"entityId": "emp1",
"permission": "Read",
"source": null,
"hasAccess": true
}
Notes:
entityTypecan be the configured short CLR name or full CLR type name; persisted grants use the full nameentityIdis optional for type-wide checks- evaluation endpoints can be configured to bypass the cache through
IdentityEntityPermissionEvaluationEndpointsOptions - the single-permission evaluation response does not identify the grant source; the effective-permissions endpoint returns source values
Best practices¶
- Define the smallest permission set that reflects real business needs.
- Use wildcard permissions sparingly for administrative or cross-cutting access.
- Prefer
IEntityPermissionEvaluator<TEntity>in application code and reserve raw provider access for administration and seeding. - Use hierarchical entities only when inheritance is a true domain rule.
- Enable caching for read-heavy systems, but understand when freshly granted or revoked permissions should bypass cached results.
- Protect management endpoints with strong authorization and operational access controls.
- Keep permission names consistent across endpoint protection, evaluator checks, and seeded grants.
Troubleshooting¶
- 403 Forbidden: Verify the current user is available through
ICurrentUserAccessor, and check that the permission was granted for the correct full entity type name. - Permission Missing After Grant: Check whether the evaluation path is hitting a cached result and whether the check should bypass the cache.
- Hierarchy Not Applied: Confirm the entity type was registered with
AddHierarchicalEntity(...)and that the parent-id expression matches the entity id type. - Endpoints Not Available: Confirm
EnableEvaluationEndpoints(...)orEnableManagementEndpoints(...)was configured in the identity setup. - Entity Type Not Valid: The runtime endpoints expect the full entity type name, not a short display name.
Appendix: Working with hierarchical entities¶
Example: Employee hierarchy¶
Structure¶
CEO (ceo1) <- "Read"
Manager (mgr1) <- "Write", "Delete" (Admins role)
Employee (emp1)
Step 1: Configure¶
services.AddEntityAuthorization(identity =>
{
identity.WithEntityPermissions<CoreDbContext>(permissions =>
{
permissions.AddHierarchicalEntity<Employee>(
e => e.ManagerId,
Permission.Read,
Permission.Write,
Permission.Delete);
});
});
Step 2: Grant permissions¶
var provider = services.GetRequiredService<IEntityPermissionProvider>();
await provider.GrantUserPermissionAsync(
"user123",
typeof(Employee).FullName,
"ceo1",
Permission.Read);
await provider.GrantUserPermissionAsync(
"user123",
typeof(Employee).FullName,
"mgr1",
Permission.Write);
await provider.GrantRolePermissionAsync(
"Admins",
typeof(Employee).FullName,
"mgr1",
Permission.Delete);
Step 3: Check effective permissions¶
var evaluator = services.GetRequiredService<IEntityPermissionEvaluator<Employee>>();
var permissions = await evaluator.GetPermissionsAsync(
"user123",
["Admins"],
"emp1");
foreach (var permission in permissions)
{
Console.WriteLine($"{permission.Permission} from {permission.Source}");
}
Possible output:
Read from Parent:Entity:ceo1Write from Parent:Entity:mgr1Delete from Parent:Role:Admins