Authorization¶
Deepstaging provides two authorization systems that can be used independently or together: ad-hoc policies for custom claims-based checks, and RBAC for structured permission-based access control. Both integrate with ASP.NET's authorization middleware and are enforced before handler execution.
Ad-Hoc Policies¶
Define Policies¶
Mark a static partial class with [AuthPolicies] and individual methods with [AuthPolicy]. Each method becomes a named policy — the method name is the policy name.
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)));
}
}
Apply Policies to Handlers¶
Use [Authorize] on command or query handler methods. Multiple [Authorize] attributes are allowed — all policies must pass.
[CommandHandler]
[Authorize(nameof(Policies.CanPublishArticle))]
public static Eff<AppRuntime, ArticlePublished> Handle(PublishArticle cmd) => ...
[CommandHandler]
[Authorize(nameof(Policies.CanUploadMedia))]
[Authorize(nameof(Policies.IsAdmin))]
public static Eff<AppRuntime, MediaUploaded> Handle(UploadMedia cmd) => ...
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")]
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.
Required signature:
| Requirement | Diagnostic |
|---|---|
Must be static |
DSDSP09 (Error) |
Must return bool |
DSDSP09 (Error) |
Must accept single ClaimsPrincipal parameter |
DSDSP09 (Error) |
[Authorize]¶
Applied to [CommandHandler] or [QueryHandler] methods. Repeatable — all policies must pass.
[CommandHandler]
[Authorize(nameof(Policies.CanCreateArticle))]
public static Eff<ScribeRuntime, ArticleCreated> Handle(CreateArticle cmd) => ...
In the web layer, the policy name is forwarded to .RequireAuthorization("PolicyName") on the generated endpoint.
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();
});
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. The generator also produces a typed RoleAttribute in your namespace that accepts the Permission enum directly — see Generated Code below.
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.
Typed [Require] attribute
The generator produces a typed RequireAttribute in your namespace that accepts the Permission enum directly, eliminating the (int) cast:
// With the generated typed attribute (no cast needed)
[Require(Permission.Contacts_Write)]
// With the base framework attribute (requires cast)
[Require((int)Permission.Contacts_Write)]
Both forms work identically. The generated attribute inherits from Deepstaging.Dispatch.RequireAttribute and is picked up automatically via C# namespace resolution.
Public Handlers¶
When your app has authentication enabled, all endpoints require authorization by default. Use [Public] to opt out for specific handlers:
using Deepstaging.Web;
[QueryHandler, HttpGet("/recipes")]
[Public]
public static Eff<AppRuntime, QueryResult<Recipe>> Handle(GetRecipes query) => ...
[Public] can also be applied at the class level to make all handlers in a class publicly accessible. It lives in the Deepstaging.Web namespace.
What the Generator Produces¶
The PermissionsGenerator emits several files from [Permissions] and [Roles]:
| Generated File | Description |
|---|---|
PermissionResolver |
IPermissionResolver implementation mapping role names to permission sets |
PermissionPolicies |
Authorization policy registration — one policy per permission value ("Permission:Contacts_Read", etc.) |
RoleSeedData |
GetRoleSeedData() method on the [Roles] class for identity store initialization |
RequireAttribute |
Typed version accepting Permission enum directly (eliminates (int) casts) |
RoleAttribute |
Typed version accepting Permission[] (eliminates (int) casts in role definitions) |
The PermissionPolicies class generates one ASP.NET authorization policy per permission value. Each policy checks ICurrentUser.Permissions at runtime:
// Generated
public static class PermissionPolicies
{
public static void RegisterPermissionPolicies(AuthorizationOptions options)
{
options.AddPolicy("Permission:Contacts_Read", policy =>
policy.RequireAssertion(ctx =>
ctx.Resource is HttpContext http &&
http.RequestServices.GetService<ICurrentUser>() is { } user &&
user.Permissions.Contains(0)));
// ... for each permission value
}
}
Registration¶
// Permissions + roles are registered automatically by the runtime bootstrapper.
// For manual registration, call the generated registrars:
builder.Services.AddAuthorization(options =>
{
Policies.Register(options); // ad-hoc policies
PermissionPolicies.RegisterPermissionPolicies(options); // RBAC permission policies
});
How [Require] Maps to Policies¶
When [Require(Permission.Contacts_Write)] is applied to a handler:
- The projection layer resolves the permission to policy name
"Permission:Contacts_Write" - The generated web endpoint includes
.RequireAuthorization("Permission:Contacts_Write") - At runtime, ASP.NET calls the matching policy registered by
PermissionPolicies.RegisterPermissionPolicies - The policy checks
ICurrentUser.Permissions.Contains(permissionIntValue)
The ICurrentUser is built per-request by IdentityMiddleware — see Current User for how roles are resolved into permissions.
Dispatch Pipeline Integration¶
When [Authorize] or [Require] 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.
Missing Access Control (DSDSP14)¶
The MissingRequireAnalyzer warns when a handler has no access control attribute. Every [CommandHandler] and [QueryHandler] must have at least one of:
[Require(Permission.X)]— RBAC permission check[Authorize("PolicyName")]— named policy check[Public]— explicitly marks the handler as publicly accessible
This prevents accidentally exposing handlers without authorization.
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;
// --- Ad-hoc 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");
}
// --- RBAC ---
[Permissions]
public enum Permission
{
Articles_Read,
Articles_Write,
Articles_Publish,
Articles_Delete,
}
[Roles]
public static partial class AppRoles
{
[Role("Admin",
(int)Permission.Articles_Read,
(int)Permission.Articles_Write,
(int)Permission.Articles_Publish,
(int)Permission.Articles_Delete)]
public static partial void Admin();
[Role("Editor",
(int)Permission.Articles_Read,
(int)Permission.Articles_Write,
(int)Permission.Articles_Publish)]
public static partial void Editor();
[Role("Viewer",
(int)Permission.Articles_Read)]
public static partial void Viewer();
}
// --- Handlers combining both ---
public static class ArticleCommands
{
[CommandHandler]
[Authorize(nameof(Policies.CanCreateArticle))]
[Require(Permission.Articles_Write)]
public static Eff<ScribeRuntime, ArticleCreated> Handle(CreateArticle cmd) => ...
[CommandHandler]
[Authorize(nameof(Policies.CanPublishArticle))]
[Require(Permission.Articles_Publish)]
public static Eff<ScribeRuntime, ArticlePublished> Handle(PublishArticle cmd) => ...
[CommandHandler]
[Authorize(nameof(Policies.CanDeleteArticle))]
[Require(Permission.Articles_Delete)]
public static Eff<ScribeRuntime, ArticleDeleted> Handle(DeleteArticle cmd) => ...
}
// --- Registration ---
builder.Services.AddAuthorization(options =>
{
Policies.Register(options);
PermissionPolicies.RegisterPermissionPolicies(options);
});