Skip to content

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.

[AuthPolicies]
public static partial class Policies;
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:

[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 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:

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

Dispatch Pipeline Integration

When [Authorize] is applied to a handler, authorization is checked before validation and handler execution:

Authorize → Validate → Handler → AutoCommit → Audit

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:

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.

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

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