Authorization Policies¶
The auth policy builder generates ASP.NET authorization policies from plain C# methods. Define policies as static bool methods, reference them with nameof() for compile-time safety, and the generator wires everything into AuthorizationOptions.
Quick Start¶
using System.Security.Claims;
using Deepstaging.Dispatch;
[AuthPolicies]
public static partial class Policies
{
[AuthPolicy]
public static bool CanPublishArticle(ClaimsPrincipal user) =>
user.HasClaim("role", "editor") || user.HasClaim("role", "admin");
[AuthPolicy]
public static bool CanUploadMedia(ClaimsPrincipal user) =>
user.HasClaim("role", "editor");
[AuthPolicy]
public static bool IsAdmin(ClaimsPrincipal user) =>
user.HasClaim("role", "admin");
}
The generator produces a Register method that wires each policy into ASP.NET:
// Generated
public static partial class Policies
{
public static void Register(AuthorizationOptions options)
{
options.AddPolicy("CanPublishArticle", policy =>
policy.RequireAssertion(ctx => CanPublishArticle(ctx.User)));
options.AddPolicy("CanUploadMedia", policy =>
policy.RequireAssertion(ctx => CanUploadMedia(ctx.User)));
options.AddPolicy("IsAdmin", policy =>
policy.RequireAssertion(ctx => IsAdmin(ctx.User)));
}
}
Attributes¶
[AuthPolicies]¶
Marks a static partial class as the authorization policy container. One per assembly is typical, though multiple are allowed.
| Requirement | Diagnostic |
|---|---|
Must be static partial |
DSDSP08 (Error) |
[AuthPolicy]¶
Marks a method inside an [AuthPolicies] class as a named policy. The method name becomes the policy name.
Required signature:
| Requirement | Diagnostic |
|---|---|
Must be static |
DSDSP09 (Error) |
Must return bool |
DSDSP09 (Error) |
Must accept single ClaimsPrincipal parameter |
DSDSP09 (Error) |
[Authorize]¶
Applied to command or query handler methods to require a policy. Multiple [Authorize] attributes are allowed per handler — all policies must pass.
[CommandHandler]
[Authorize(nameof(Policies.CanPublishArticle))]
public static Eff<AppRuntime, ArticlePublished> Handle(PublishArticle cmd) => ...
[Authorize] is repeatable:
[CommandHandler]
[Authorize(nameof(Policies.CanUploadMedia))]
[Authorize(nameof(Policies.IsAdmin))]
public static Eff<AppRuntime, MediaUploaded> Handle(UploadMedia cmd) => ...
Registration¶
Call Register during startup to wire policies into ASP.NET authorization:
Or within a broader configuration:
builder.Services.AddAuthorization(options =>
{
Policies.Register(options);
options.FallbackPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
});
Dispatch Pipeline Integration¶
When [Authorize] is applied to a handler, authorization is checked before validation and handler execution:
The generated dispatch method includes the authorization check automatically. In web endpoints, the policy name is also forwarded to .RequireAuthorization() on the generated endpoint.
Compile-Time Safety¶
Use nameof() to reference policies — the compiler catches typos and renames propagate automatically:
// ✅ Compile-time safe
[Authorize(nameof(Policies.CanPublishArticle))]
// ❌ Magic string — no compiler help
[Authorize("CanPublishArticle")]
Non-Web Usage¶
Policy methods are plain C# — call them directly in message queue handlers, CLI tools, or background jobs:
Testing Policies¶
Policy methods are simple static bool functions — test them directly:
[Test]
public async Task CanPublishArticle_RequiresEditorRole()
{
var editor = new ClaimsPrincipal(new ClaimsIdentity(
[
new Claim("role", "editor")
], "test"));
var viewer = new ClaimsPrincipal(new ClaimsIdentity(
[
new Claim("role", "viewer")
], "test"));
await Assert.That(Policies.CanPublishArticle(editor)).IsTrue();
await Assert.That(Policies.CanPublishArticle(viewer)).IsFalse();
}
RBAC — Permission-Based Access Control¶
For applications that need fine-grained, role-based permissions instead of (or alongside) ad-hoc policies, Deepstaging provides a full RBAC system: define permissions as an enum, map them to roles, and enforce them on handlers with [Require].
Define Permissions¶
Mark an enum with [Permissions] — each value is a unique permission:
using Deepstaging.Dispatch;
[Permissions]
public enum Permission
{
Contacts_Read,
Contacts_Write,
Contacts_Delete,
Campaigns_Read,
Campaigns_Write,
Reports_Read,
}
One [Permissions] enum per assembly.
Define Roles¶
Create a role catalog with [Roles] and [Role]. Each role grants a set of permissions:
using Deepstaging.Dispatch;
[Roles]
public static partial class AppRoles
{
[Role("Admin",
(int)Permission.Contacts_Read,
(int)Permission.Contacts_Write,
(int)Permission.Contacts_Delete,
(int)Permission.Reports_Read)]
public static partial void Admin();
[Role("Editor",
(int)Permission.Contacts_Read,
(int)Permission.Contacts_Write)]
public static partial void Editor();
[Role("Viewer",
(int)Permission.Contacts_Read,
(int)Permission.Reports_Read)]
public static partial void Viewer();
}
Permission values are specified as int casts because C# attributes don't support generic enum constraints.
Require Permissions on Handlers¶
Use [Require] to enforce permissions on command and query handlers:
public static class ContactCommands
{
[CommandHandler]
[Require((int)Permission.Contacts_Write)]
public static Eff<AppRuntime, ContactCreated> Handle(CreateContact cmd) => ...
[QueryHandler]
[Require((int)Permission.Contacts_Read)]
[Authorize("OwnsOrganization")]
public static Eff<AppRuntime, QueryResult<Contact>> Handle(GetContacts query) => ...
}
[Require] and [Authorize] can coexist on the same handler — both the RBAC permission AND the named policy must pass.
What the Generator Produces¶
The PermissionsGenerator emits three files:
| Generated File | Description |
|---|---|
PermissionResolver |
IPermissionResolver implementation mapping role names → permission sets |
PermissionPolicies |
Authorization policy registration — one policy per permission value ("Permission:Contacts_Read", etc.) |
RoleSeedData |
Role seed data for identity store initialization (role names, permission grants) |
Registration¶
// Permissions + roles are registered automatically by the runtime bootstrapper.
// For manual registration, call the generated policy registrar:
builder.Services.AddAuthorization(options =>
{
Policies.Register(options); // ad-hoc policies
PermissionPolicies.Register(options); // RBAC permission policies
});
Diagnostics¶
| ID | Severity | Description | Code Fix |
|---|---|---|---|
| DSDSP14 | Warning | Handler has no [Require], [Authorize], or [Public] attribute |
Add [Require] |
Diagnostics¶
| ID | Severity | Description | Code Fix |
|---|---|---|---|
| DSDSP08 | Error | [AuthPolicies] class must be static partial |
Add modifiers |
| DSDSP09 | Error | [AuthPolicy] method must be static bool(ClaimsPrincipal) |
— |
| DSDSP14 | Warning | Handler method has no access control attribute | Add [Require] |
Complete Example¶
using System.Security.Claims;
using Deepstaging.Dispatch;
// Define policies
[AuthPolicies]
public static partial class Policies
{
[AuthPolicy]
public static bool CanCreateArticle(ClaimsPrincipal user) =>
user.HasClaim("role", "author") || user.HasClaim("role", "admin");
[AuthPolicy]
public static bool CanPublishArticle(ClaimsPrincipal user) =>
user.HasClaim("role", "editor") || user.HasClaim("role", "admin");
[AuthPolicy]
public static bool CanDeleteArticle(ClaimsPrincipal user) =>
user.HasClaim("role", "admin");
}
// Apply to handlers
public static class ArticleCommands
{
[CommandHandler]
[Authorize(nameof(Policies.CanCreateArticle))]
public static Eff<ScribeRuntime, ArticleCreated> Handle(CreateArticle cmd) => ...
[CommandHandler]
[Authorize(nameof(Policies.CanPublishArticle))]
public static Eff<ScribeRuntime, ArticlePublished> Handle(PublishArticle cmd) => ...
[CommandHandler]
[Authorize(nameof(Policies.CanDeleteArticle))]
public static Eff<ScribeRuntime, ArticleDeleted> Handle(DeleteArticle cmd) => ...
}
// Register at startup
builder.Services.AddAuthorization(Policies.Register);