Entity Permissions Feature Documentation¶
Enforce fine-grained, entity-level authorization with fluent configuration and runtime evaluation.
Overview¶
The Application.Identity feature within the bITDevKit provides a robust framework for managing entity-level permissions in ASP.NET Core applications. Designed to enforce precise access control on entities such as Employee, it supports a range of permissions, including predefined constants like Permission.Read and custom strings such as "Review". Configured through a fluent AddEntityAuthorization syntax in Program.cs, this feature integrates with application code, ASP.NET Core authorization, Minimal APIs, and optional runtime management endpoints. Leveraging Entity Framework Core for persistence, it also supports hierarchical permission inheritance, making it well-suited for organizational or tree-shaped data models.
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 addresses these challenges by delivering a unified and developer-friendly system for entity-level permissions. It centralizes configuration within AddEntityAuthorization, supports predefined Permission constants and custom permission strings, and integrates with ASP.NET Core's authorization pipeline.
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
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.
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
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();
This builder is useful for seeding or setup code where a fluent style reads better than calling the provider directly.
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¶
If caching is enabled, permission cache entries may need invalidation after administrative changes. The cache extension helpers support broad invalidation 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:
/api/_system/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} |
GET |
Get granted permissions for one user and entity target |
/users |
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} |
GET |
Get granted permissions for one role and entity target |
/roles |
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"
}
Evaluation Endpoints¶
Default group path:
/api/_system/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": "Direct",
"hasAccess": true
}
Notes:
entityTypemust be the full CLR type nameentityIdis optional for type-wide checks- evaluation endpoints can be configured to bypass the cache through
IdentityEntityPermissionEvaluationEndpointsOptions
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:ceo1Write from Parent:mgr1Delete from Parent:Role:Admins