Skip to content

ZeroAlloc-Net/ZeroAlloc.Authorization

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

87 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ZeroAlloc.Authorization

NuGet Build License: MIT AOT GitHub Sponsors

Authorization primitives for .NET — ISecurityContext, IAuthorizationPolicy, [Policy], [RequirePolicy], AnonymousSecurityContext, and an AuthorizerFor<TRequest> dispatcher emitted by the bundled source generator.

Used by:

  • AI.Sentinel — tool-call authorization for IChatClient-based agents
  • ZeroAlloc.Mediator.Authorization v5 — request-handler authorization

Install

dotnet add package ZeroAlloc.Authorization

Targets net8.0, net9.0, net10.0. The package bundles a Roslyn source generator — no separate *.Generator install.

The contract

public interface ISecurityContext
{
    string Id { get; }
    IReadOnlySet<string> Roles { get; }
    IReadOnlyDictionary<string, string> Claims { get; }
}

public interface IAuthorizationPolicy
{
    ValueTask<UnitResult<AuthorizationFailure>> EvaluateAsync(
        ISecurityContext ctx, CancellationToken ct = default);
}

Writing a policy

[Policy("AdminOnly")]
public sealed class AdminOnlyPolicy : IAuthorizationPolicy
{
    public ValueTask<UnitResult<AuthorizationFailure>> EvaluateAsync(
        ISecurityContext ctx, CancellationToken ct = default)
        => new(ctx.Roles.Contains("Admin")
            ? UnitResult<AuthorizationFailure>.Success()
            : new AuthorizationFailure(AuthorizationFailure.DefaultDenyCode, "Admin role required"));
}

Bind it on a request type — [RequirePolicy] is class/struct-level only, and stacks:

[RequirePolicy("AdminOnly")]
public sealed record DeleteUserCommand(string UserId);

[RequirePolicy("ActiveTenant")]
[RequirePolicy("AdminOnly")]
public sealed record PurgeTenantCommand(string TenantId);

Host wiring

The bundled generator emits one AuthorizerFor<TRequest> subclass per [RequirePolicy]-decorated type plus an AddZeroAllocAuthorization() extension on IServiceCollection. Hosts call the extension at startup and resolve AuthorizerFor<T> per request:

using ZeroAlloc.Authorization.Generated;

builder.Services.AddZeroAllocAuthorization();
var authorizer = sp.GetService<AuthorizerFor<DeleteUserCommand>>();
if (authorizer is not null)
{
    var result = await authorizer.EvaluateAsync(securityContext, ct);
    if (result.IsFailure)
        return Forbid(result.Error); // host maps Code / Reason to its outcome shape
}

Hosts can extend ISecurityContext

Hosts define their own subinterface for richer payloads. AI.Sentinel adds IToolCallSecurityContext : ISecurityContext with ToolName + Args. ZeroAlloc.Mediator.Authorization v5 adds IRequestSecurityContext<TRequest>. Inside the policy body, downcast:

public ValueTask<UnitResult<AuthorizationFailure>> EvaluateAsync(
    ISecurityContext ctx, CancellationToken ct = default)
    => new(ctx is IToolCallSecurityContext tc && tc.ToolName == "delete_database"
        ? new AuthorizationFailure("tool.destructive", "destructive tool blocked")
        : UnitResult<AuthorizationFailure>.Success());

I/O-bound policies

For checks that need to await something (tenant lookup, external claims validation), mark EvaluateAsync as async:

[Policy("ActiveTenant")]
public sealed class ActiveTenantPolicy(ITenantService tenants) : IAuthorizationPolicy
{
    public async ValueTask<UnitResult<AuthorizationFailure>> EvaluateAsync(
        ISecurityContext ctx, CancellationToken ct = default)
    {
        var active = await tenants.IsActiveAsync(ctx.Id, ct).ConfigureAwait(false);
        return active
            ? UnitResult<AuthorizationFailure>.Success()
            : new AuthorizationFailure("tenant.inactive", "tenant is suspended");
    }
}

Generator diagnostics

The bundled generator emits five compile-time diagnostics:

ID Fires when
ZAUTH001 [RequirePolicy("X")] references a policy name with no matching [Policy("X")].
ZAUTH002 Two [Policy("X")] declarations share the same name.
ZAUTH003 [Policy] class doesn't implement IAuthorizationPolicy.
ZAUTH004 [Policy] class is abstract or static.
ZAUTH005 [RequirePolicy] placed on a non-class/non-struct target.

Performance

BenchmarkDotNet (BDN ShortRun, .NET 10 release build, x64) — happy path on a simple role-check policy:

Method Mean Allocated
EvaluateAsync ~99 ns 0 B

Source: benchmarks/ZeroAlloc.Authorization.Benchmarks/PolicyEvaluationBenchmarks.cs.

The contract is enforced as zero-allocation by:

  1. <IsAotCompatible>true</IsAotCompatible> on the main library (build-time IL2026/IL3050 analyzers fire on any reflection regression).
  2. The samples/ZeroAlloc.Authorization.AotSmoke/ console app, exercised on each CI run with PublishAot=true.
  3. The benchmark project above.

License

MIT.

About

Authorization primitives for .NET — IAuthorizationPolicy, ISecurityContext, [Authorize], [AuthorizationPolicy]. Source-of-truth contract package shared across hosts (AI.Sentinel, ZeroAlloc.Mediator.Authorization).

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Packages

 
 
 

Contributors

Languages