Trellis.Authorization — API Reference
Package: Trellis.Authorization
Namespace: Trellis.Authorization
Purpose: Domain-layer authorization primitives — actor identity / permission / attribute model and the contracts used by the mediator's authorization behavior to perform static (permission) and resource-based authorization. This package contains no ASP.NET Core dependencies; the IActorProvider implementations and DI helpers ship in Trellis.Asp (see trellis-api-asp.md, namespace Trellis.Asp.Authorization).
See also: trellis-api-cookbook.md — recipes using this package.
Use this file when
- You are modeling actors, permissions, forbidden permissions, or actor attributes without ASP.NET dependencies.
- You are implementing static permission authorization through
IAuthorize. - You are implementing resource-based authorization through
IAuthorizeResource<TResource>and want the canonical guard shape.
Patterns Index
| Goal | Canonical API / pattern | See |
|---|---|---|
| Represent the current user/service | Actor |
Actor |
| Check granted permissions with explicit deny override | actor.HasPermission(...), HasAllPermissions(...), HasAnyPermission(...) |
Actor |
| Resolve actor for a request/message | IActorProvider.GetCurrentActorAsync(...) |
IActorProvider |
| Require static permissions on a message | Implement IAuthorize.RequiredPermissions |
IAuthorize |
| Authorize against a loaded resource | Implement IAuthorizeResource<TResource>.Authorize(actor, resource) |
IAuthorizeResource<TResource> |
| Write owner/admin resource guards | Result.Ensure(condition, new Error.Forbidden(...)) |
IAuthorizeResource<TResource>, Core Result.Ensure |
| Identify a resource by id for shared loading | IIdentifyResource<TResource, TId> |
IIdentifyResource<TResource, TId> |
Common traps
Trellis.Authorizationis domain/application-layer only. ASP.NET actor providers are documented in trellis-api-asp.md.- Prefer
Result.Ensurefor boolean authorization guards so generated code uses the same ROP primitive as the rest of Trellis. - Do not mutate
RequiredPermissions; expose the complete permission list as an immutable/read-only collection. - The DI registration extension
AddResourceAuthorization(...)lives inTrellis.Mediator(namespace Trellis.Mediator), not inTrellis.Authorization. Wiring anIAuthorizeResource<TResource>therefore typically requires bothusing Trellis.Authorization;(for the interfaces) andusing Trellis.Mediator;(for the DI extension). The compile error if the second is missing isCS1061: 'IServiceCollection' does not contain a definition for 'AddResourceAuthorization' and no accessible extension method 'AddResourceAuthorization' accepting a first argument of type 'IServiceCollection' could be found— see trellis-api-mediator.md.
Types
Namespace Trellis.Authorization
Actor
Declaration
public sealed record Actor
Constructors
| Signature | Description |
|---|---|
public Actor(string id, IReadOnlySet<string> permissions, IReadOnlySet<string> forbiddenPermissions, IReadOnlyDictionary<string, string> attributes) |
Snapshots the supplied state into frozen collections (ordinal comparison). Throws ArgumentException for null/whitespace id. |
Fields
| Name | Type | Description |
|---|---|---|
PermissionScopeSeparator |
const char |
Separator used between permission name and scope in scoped permission strings. Value: ':'. |
Properties
| Name | Type | Description |
|---|---|---|
Id |
string |
Unique actor identifier (e.g. JWT sub). |
Permissions |
IReadOnlySet<string> |
Granted permissions. Ordinal comparison; setter snapshots into a FrozenSet<string>. |
ForbiddenPermissions |
IReadOnlySet<string> |
Explicit deny-list. Deny always overrides allow. Snapshotted into a FrozenSet<string>. |
Attributes |
IReadOnlyDictionary<string, string> |
ABAC attributes. Snapshotted into a FrozenDictionary<string, string> with ordinal comparer. |
Methods
| Signature | Returns | Description |
|---|---|---|
public static Actor Create(string id, IReadOnlySet<string> permissions) |
Actor |
Creates an actor with empty ForbiddenPermissions and empty Attributes. |
public bool HasPermission(string permission) |
bool |
Returns true only when permission is in Permissions and not in ForbiddenPermissions. |
public bool HasPermission(string permission, string scope) |
bool |
Checks the scoped permission ${permission}:${scope} (deny-aware). |
public bool HasAllPermissions(IEnumerable<string> permissions) |
bool |
true when every entry passes HasPermission. |
public bool HasAnyPermission(IEnumerable<string> permissions) |
bool |
true when at least one entry passes HasPermission. |
public bool IsOwner(string resourceOwnerId) |
bool |
Compares Id and resourceOwnerId with StringComparison.Ordinal. |
public bool HasAttribute(string key) |
bool |
true when Attributes contains key. |
public string? GetAttribute(string key) |
string? |
Returns the attribute value or null when absent. |
ActorAttributes
Declaration
public static class ActorAttributes
Well-known string keys for Actor.Attributes. Claim-derived keys align with Azure Entra ID v2.0 access tokens.
Fields
| Name | Type | Description |
|---|---|---|
TenantId |
const string |
Entra tid claim — issuing tenant GUID. Value: "tid". |
PreferredUsername |
const string |
Entra preferred_username claim. Value: "preferred_username". |
AuthorizedParty |
const string |
Entra azp claim — application ID of the calling client. Value: "azp". |
AuthorizedPartyAcr |
const string |
Entra azpacr claim — client authentication strength (0 public, 1 secret, 2 certificate). Value: "azpacr". |
AuthContextClassReference |
const string |
Entra acrs claim — Conditional Access auth-context references. Value: "acrs". |
IpAddress |
const string |
Request IP address, populated from HttpContext.Connection.RemoteIpAddress. Value: "ip_address". |
MfaAuthenticated |
const string |
Whether the actor authenticated with MFA — derived from the amr claim. Value: "mfa". |
IActorProvider
Declaration
public interface IActorProvider
| Signature | Returns | Description |
|---|---|---|
Task<Actor> GetCurrentActorAsync(CancellationToken cancellationToken = default) |
Task<Actor> |
Returns the current authenticated actor. Implementations should throw InvalidOperationException (or equivalent) when the request is unauthenticated or the actor cannot be resolved. Register as scoped. |
IAuthorize
Declaration
public interface IAuthorize
Marker for commands/queries enforcing static (permission-only) authorization. The mediator's AuthorizationBehavior<TMessage, TResponse> requires the current actor to hold all listed permissions (AND semantics).
| Name | Type | Description |
|---|---|---|
RequiredPermissions |
IReadOnlyList<string> |
Permissions every caller must hold. |
IAuthorizeResource<TResource>
Declaration
public interface IAuthorizeResource<in TResource>
Implemented by a command/query to perform resource-based authorization once the resource has been loaded.
| Signature | Returns | Description |
|---|---|---|
IResult Authorize(Actor actor, TResource resource) |
IResult |
Returns success to proceed or a failure (typically Error.Forbidden) to short-circuit the pipeline. |
IIdentifyResource<TResource, TId>
Declaration
public interface IIdentifyResource<TResource, out TId>
Companion to IAuthorizeResource<TResource> that exposes a typed resource identifier so the pipeline can use a SharedResourceLoaderById<TResource, TId> instead of a per-command loader.
| Signature | Returns | Description |
|---|---|---|
TId GetResourceId() |
TId |
Extracts the typed resource ID from the message. |
IResourceLoader<TMessage, TResource>
Declaration
public interface IResourceLoader<in TMessage, TResource>
Loads the resource required for resource-based authorization. Resolved per request from DI as scoped.
| Signature | Returns | Description |
|---|---|---|
Task<Result<TResource>> LoadAsync(TMessage message, CancellationToken cancellationToken) |
Task<Result<TResource>> |
Returns the loaded resource or a failure (typically Error.NotFound). The pipeline short-circuits on failure before invoking IAuthorizeResource<TResource>.Authorize. |
ResourceLoaderById<TMessage, TResource, TId>
Declaration
public abstract class ResourceLoaderById<TMessage, TResource, TId> : IResourceLoader<TMessage, TResource>
Convenience base for loaders that extract an ID from the message and call a repository.
| Signature | Returns | Description |
|---|---|---|
protected abstract TId GetId(TMessage message) |
TId |
Extract the resource ID from the message. |
protected abstract Task<Result<TResource>> GetByIdAsync(TId id, CancellationToken cancellationToken) |
Task<Result<TResource>> |
Fetch the resource by ID; return Result.Fail with Error.NotFound when missing. |
public Task<Result<TResource>> LoadAsync(TMessage message, CancellationToken cancellationToken) |
Task<Result<TResource>> |
Sealed glue: calls GetId(message) then GetByIdAsync(...). |
SharedResourceLoaderById<TResource, TId>
Declaration
public abstract class SharedResourceLoaderById<TResource, TId>
A single loader shared across every command that authorizes against the same TResource. When a command implements both IAuthorizeResource<TResource> and IIdentifyResource<TResource, TId> the pipeline bridges to this shared loader automatically. Explicit IResourceLoader<TMessage, TResource> registrations win over the shared loader.
| Signature | Returns | Description |
|---|---|---|
public abstract Task<Result<TResource>> GetByIdAsync(TId id, CancellationToken cancellationToken) |
Task<Result<TResource>> |
Load the resource by ID; return Result.Fail with Error.NotFound when missing. |
Behavioral notes
- Deny overrides allow. A permission listed in both
PermissionsandForbiddenPermissionsis denied.HasPermission,HasAllPermissions,HasAnyPermission, andHasPermission(permission, scope)all observe this rule. - Ordinal comparison everywhere. Permission lookups, attribute lookups, and
IsOwneruseStringComparison.Ordinal. Hydrate permissions and attributes with consistent casing. - Permissions snapshot to frozen collections. Mutating a collection passed into
Actorafter construction has no effect; the actor exposes aFrozenSet<string>/FrozenDictionary<string, string>snapshot for O(1) lookups. - Scoped permissions use the
"Permission:Scope"convention (Document.Edit:Tenant_A). Add scoped entries directly toPermissionsand check viaHasPermission(string, string)— no separate scope collection. - Pipeline ordering. When a command implements both
IAuthorize(static) andIAuthorizeResource<TResource>(resource), the mediator behavior runs static checks first; resource loading andAuthorize(actor, resource)only execute if the static check passes. A loader failure short-circuits beforeAuthorizeis called.
Code examples
Static permission authorization
using Trellis;
using Trellis.Authorization;
public sealed partial class OrderId : RequiredGuid<OrderId>;
public sealed record DeleteOrderCommand(OrderId OrderId) : ICommand<Result<Unit>>, IAuthorize
{
public IReadOnlyList<string> RequiredPermissions { get; } = ["orders:delete"];
}
Resource-based authorization with a shared loader
Preferred in generated services. Use
IIdentifyResource<TResource, TId>+SharedResourceLoaderById<TResource, TId>for resource authorization. A per-commandIResourceLoader<TMessage, TResource>is an escape hatch for request-scoped state or command-specific load logic.
using Trellis;
using Trellis.Authorization;
public sealed partial class OrderId : RequiredGuid<OrderId>;
public sealed partial class ActorId : RequiredString<ActorId>;
public sealed record Order(OrderId Id, ActorId OwnerId);
public sealed record CancelOrderCommand(OrderId OrderId)
: ICommand<Result<Unit>>, IAuthorizeResource<Order>, IIdentifyResource<Order, OrderId>
{
public OrderId GetResourceId() => OrderId;
public IResult Authorize(Actor actor, Order order) =>
Result.Ensure(
order.OwnerId.Value == actor.Id || actor.HasPermission("orders:cancel-any"),
new Error.Forbidden("orders.cancel")
{ Detail = "Only the owner can cancel this order." });
}
public interface IOrderRepository
{
Task<Maybe<Order>> GetByIdAsync(OrderId id, CancellationToken ct);
}
public sealed class OrderResourceLoader(IOrderRepository repo)
: SharedResourceLoaderById<Order, OrderId>
{
public override async Task<Result<Order>> GetByIdAsync(OrderId id, CancellationToken ct) =>
(await repo.GetByIdAsync(id, ct)).ToResult(new Error.NotFound(ResourceRef.For<Order>(id)));
}
Constructing an Actor directly (tests, custom providers)
using System.Collections.Generic;
using Trellis.Authorization;
var actor = new Actor(
id: "user-1",
permissions: new HashSet<string>
{
"orders:cancel",
$"orders:view{Actor.PermissionScopeSeparator}tenant-1",
},
forbiddenPermissions: new HashSet<string>(),
attributes: new Dictionary<string, string>
{
[ActorAttributes.TenantId] = "tenant-1",
[ActorAttributes.MfaAuthenticated] = "true",
});
bool canCancel = actor.HasPermission("orders:cancel");
bool canViewTenant = actor.HasPermission("orders:view", "tenant-1");
string? tenant = actor.GetAttribute(ActorAttributes.TenantId);
Cross-references
- trellis-api-asp.md —
Trellis.Asp.Authorizationactor providers (ClaimsActorProvider,EntraActorProvider,DevelopmentActorProvider,CachingActorProvider) and the matchingAddClaimsActorProvider/AddEntraActorProvider/AddDevelopmentActorProvider/AddCachingActorProvider<T>registration helpers. - trellis-api-mediator.md —
AuthorizationBehavior<TMessage, TResponse>pipeline behavior. - trellis-api-core.md —
Result,Error.Forbidden,Error.NotFound. - trellis-api-testing-aspnetcore.md —
WebApplicationFactoryExtensions.CreateClientWithActor(writes theX-Test-Actorheader consumed byDevelopmentActorProvider).