PBAC — Policy-Based Access Control
One-Paragraph Summary
PBAC organizes authorization decisions into named policies — composable units that combine role checks, claim checks, attribute checks, and business rules. In .NET, PBAC is the native authorization model: AddAuthorization(options => options.AddPolicy("CanApprove", ...)). A policy is not a new access control theory — it's an organizational pattern that wraps RBAC and ABAC into maintainable, reusable, testable units. PBAC is how you make complex authorization manageable in real systems.
How It Works
Why PBAC Is the Practical Choice in .NET
ASP.NET Core's authorization system IS policy-based. Even [Authorize(Roles = "Admin")] is syntactic sugar for a policy with a role requirement. When you write [Authorize(Policy = "CanApproveDocument")], you're using PBAC.
PBAC is not a replacement for RBAC or ABAC — it's the container that holds them.
PBAC Policy = RBAC Requirements + ABAC Requirements + Business RulesIn This Project
Policy Registration (composing RBAC + ABAC)
// Simple RBAC-only policy
.AddPolicy(AppPolicies.RequireAdmin, policy =>
policy.RequireRole(AppRoles.Admin))
// PBAC: combines claim check (ABAC) with authentication
.AddPolicy(AppPolicies.CanCreateDocument, policy =>
policy.RequireAuthenticatedUser()
.RequireClaim(AppClaims.AccountStatus, "Active"))
// PBAC: composite requirement with custom handler
.AddPolicy(AppPolicies.CanApproveDocument, policy =>
policy.AddRequirements(new DocumentApprovalRequirement()))Composite Handler (PBAC wrapping RBAC + ABAC)
public class DocumentApprovalHandler : AuthorizationHandler<DocumentApprovalRequirement, Document>
{
protected override Task HandleRequirementAsync(...)
{
// Step 1: RBAC — must be Manager or Admin
if (!user.IsInRole("Manager") && !user.IsInRole("Admin"))
return Task.CompletedTask;
// Step 2: Business rule — document must be pending
if (resource.Status != DocumentStatus.PendingApproval)
return Task.CompletedTask;
// Step 3: ABAC — department must match
if (userDepartment != resource.Department)
return Task.CompletedTask;
context.Succeed(requirement);
return Task.CompletedTask;
}
}Policies Defined in This Project
| Policy | Type | Requirements |
|---|---|---|
RequireAdmin | RBAC-only | Role = Admin |
RequireManager | RBAC-only | Role = Manager, RegionalManager, or Admin |
RequireAuditor | RBAC-only | Role = Auditor or Admin |
CanCreateDocument | PBAC | Authenticated + AccountStatus = Active |
CanApproveDocument | PBAC + ABAC | Manager role + same dept + pending status |
CanViewSensitiveDocument | PBAC + ABAC | Same tenant + clearance >= sensitivity |
CanViewAuditTrail | PBAC | Auditor/Admin role + active account |
CanManageTenantUsers | PBAC | Admin/TenantAdmin + active account |
ServiceReadDocuments | Scope-based | Has documents:read scope (machine client) |
Strengths
- Composable: Combine any number of requirements into one policy
- Maintainable: Policy names are self-documenting ("CanApproveDocument")
- Testable: Each handler is independently unit-testable
- Centralized: All policies defined in one place (AddAuthorization)
- Native to .NET: This is how ASP.NET Core authorization works
- Reusable: Same policy used in multiple endpoints
Weaknesses
- Indirection: Developers must look up what "CanApproveDocument" actually checks
- Static registration: Default policies are defined at startup (dynamic policies need custom IAuthorizationPolicyProvider)
- Handler complexity: Composite handlers can grow complex
- Not a standard: Unlike XACML/ABAC, there's no formal PBAC specification
When PBAC Is Enough
- Most .NET applications (it's the default model)
- When you need readable, named access control rules
- When combining RBAC and ABAC checks
- When you want centralized policy management
- When policies can be defined at compile time
When PBAC Becomes Insufficient
- Hundreds of dynamic policies managed by business users → need a policy engine
- Real-time policy updates without redeployment → need IAuthorizationPolicyProvider + database
- Cross-system policy evaluation → need a centralized policy decision point (PDP)
How PBAC Wraps RBAC and ABAC
Declarative vs Imperative Authorization
| Style | When to Use | Example |
|---|---|---|
| Declarative | No resource needed at decision time | [Authorize(Policy = "RequireAdmin")] |
| Imperative | Resource must be loaded first | authService.AuthorizeAsync(user, doc, "CanApprove") |
// Declarative: policy evaluated before endpoint runs
app.MapGet("/admin/policies", ...)
.RequireAuthorization(AppPolicies.RequireAdmin);
// Imperative: resource loaded, then policy evaluated
var doc = await repo.GetByIdAsync(id);
var result = await authService.AuthorizeAsync(User, doc, AppPolicies.CanApproveDocument);
if (!result.Succeeded) return Forbid();Common Mistakes
- Not using policies: Scattering
IsInRoleandHasClaimchecks across controllers instead of centralizing in policies - Monolithic handlers: One handler that checks 15 things. Split into multiple requirements
- Calling context.Fail(): Usually wrong — not calling Succeed() is a soft fail. Fail() blocks all other handlers
- Forgetting resource-based auth: Using only declarative policies when you need to check the resource
- Policy name soup: Poor naming like "Policy1", "Policy2". Names should be verbs: "CanApprove", "CanView"
Memory Hook
PBAC is a recipe book. Each recipe (policy) lists ingredients (requirements). Some ingredients are role-based (need chef certification), some are attribute-based (need fresh ingredients from this supplier). The recipe combines them into one instruction.
Tradeoff Table
| Aspect | Rating | Notes |
|---|---|---|
| Simplicity | ★★★★☆ | Named policies are readable |
| Granularity | ★★★★★ | As granular as the requirements inside |
| Scalability | ★★★★☆ | Scales with policy count |
| Auditability | ★★★★☆ | Policy names are self-documenting |
| Flexibility | ★★★★☆ | Composable, but static by default |
| Performance | ★★★★★ | Minimal overhead over raw checks |
Cheat Sheet
PBAC = Named Policy → [Requirement₁, Requirement₂, ...] → Handlers → Decision
.NET: AddPolicy("Name", policy => policy.RequireRole(...).RequireClaim(...).AddRequirements(...))
Declarative: [Authorize(Policy = "Name")]
Imperative: IAuthorizationService.AuthorizeAsync(user, resource, "Name")
PBAC wraps RBAC and ABAC — it's the organizational layer, not a replacement.
Start with RBAC-only policies → add claim checks → add custom handlers as needed.Staff-Level Interview Questions
- "How is PBAC different from RBAC and ABAC? Is it a separate model or an organizational pattern?"
- "When would you need a custom IAuthorizationPolicyProvider? What problem does it solve?"
- "Walk through the lifecycle of a policy evaluation in ASP.NET Core, from attribute to decision."
- "How would you design policies for a system with 200+ authorization rules? How do you keep them maintainable?"
- "What's the difference between not calling Succeed() and calling Fail() in an authorization handler?"
Related Concepts
- RBAC →
docs/rbac.md - ABAC →
docs/abac.md - Comparison →
docs/rbac-vs-abac-vs-pbac.md - Custom handlers →
docs/custom-authorization-handlers.md - .NET implementation →
docs/dotnet-authorization-implementation.md