Skip to content

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:

[AuthPolicy]
public static bool MethodName(ClaimsPrincipal user) => ...;
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:

Program.cs
builder.Services.AddAuthorization(Policies.Register);

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:

if (!Policies.CanPublishArticle(currentUser))
    return FailEff<Unit>(Error.New("Unauthorized"));

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

Program.cs
// 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:

  1. The projection layer resolves the permission to policy name "Permission:Contacts_Write"
  2. The generated web endpoint includes .RequireAuthorization("Permission:Contacts_Write")
  3. At runtime, ASP.NET calls the matching policy registered by PermissionPolicies.RegisterPermissionPolicies
  4. 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:

Authorize → Validate → Handler → Audit → Auto-commit

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);
});