Model-bound command authorization
A valid command is not necessarily an allowed command. Arc evaluates authorization before ordinary command filters and does not invoke Provide() or Handle() when authorization is denied.
Using the Authorize attribute
Section titled “Using the Authorize attribute”Use the attributes in Cratis.Arc.Authorization for model-bound commands. This complete example requires authentication and returns an identifier without persisting a business record:
using System;using Cratis.Arc.Commands.ModelBound;
[Command][Cratis.Arc.Authorization.Authorize]public record AllocatePrivateIdentifier(){ public Guid Handle() => Guid.NewGuid();}Configure authentication in your host to establish the request principal. The attribute checks that principal; it does not authenticate a request by itself.
Using the Roles attribute
Section titled “Using the Roles attribute”This complete command requires an authenticated principal in at least one of the listed roles:
using System;using Cratis.Arc.Commands.ModelBound;
public enum ApplicationRole{ Admin, Manager}
[Command][Cratis.Arc.Authorization.Roles(nameof(ApplicationRole.Admin), nameof(ApplicationRole.Manager))]public record AllocateManagedIdentifier(){ public Guid Handle() => Guid.NewGuid();}[Cratis.Arc.Authorization.Authorize(Roles = $"{nameof(ApplicationRole.Admin)},{nameof(ApplicationRole.Manager)}")] expresses the same role requirement. These application-owned names still emit the role strings Admin and Manager; preserve exact names when roles come from an external identity provider. A comma-separated list means any one of those roles. Several authorization attributes on one command all apply, so [Roles(nameof(ApplicationRole.Admin))] together with [Authorize(Roles = nameof(ApplicationRole.Auditor))] requires both.
Anonymous access with AllowAnonymous
Section titled “Anonymous access with AllowAnonymous”[Cratis.Arc.Authorization.AllowAnonymous] bypasses Arc’s built-in attribute authorization check for that type. With no authorization requirement, the built-in evaluator also allows access. Mark intentionally public commands explicitly, and review their abuse controls separately.
This does not promise to bypass custom authorization filters or external middleware. Likewise, adding an attribute to an unrelated controller does not establish a global authorization requirement for model-bound command types.
Policy-based authorization
Section titled “Policy-based authorization”Register a named, scoped IAuthorizationPolicy as shown in Core named policies, then protect a command using [Cratis.Arc.Authorization.Authorize(Policy = "ActiveSubscription")]. Arc awaits the policy before running Provide() or Handle(). Stacked policy attributes all apply. An unknown or duplicate policy fails host startup rather than admitting callers. AuthorizationPolicyContext.Resource is the executing CommandContext; the policy sees the selected caller through Principal. Both contexts carry the same ReceivedAt (Arc receipt time). It does not change when policy evaluation is delayed or repeated. See command context for the capture boundary and validator access.
On an ASP.NET Core host, you can also use a named ASP.NET policy and AuthenticationSchemes (including schemes contributed by a policy). Arc authenticates the requested schemes, not just the default principal. A changed identity executes in a fresh Arc-owned service scope, so command values, execution-scope Begin, and handler dependencies bind to the selected tenant. An arbitrary explicit IServiceProvider may already hold dependencies for the default identity and is rejected for a scheme-selected command; use mapped HTTP or the command pipeline’s scope-owning Execute(command) overload from a live request. Ordinary nested server-side commands with the same no-scheme actor remain supported; a nested scheme that changes identity after an outer command has started its unit of work is rejected before it can join that work. Every named scheme must be registered; a request without an authenticated selected scheme is denied. The standalone Core host rejects AuthenticationSchemes at startup (see ARC0021).
With Cratis.Arc, Microsoft.AspNetCore.Authorization.AuthorizeAttribute and AllowAnonymousAttribute on a model-bound command are enforced like Arc’s own - authentication, roles, policies, and schemes. Arc’s attributes are still the portable choice, because Arc Core’s own HTTP host does not read the Microsoft ones; analyzer ARC0020 reports a Microsoft attribute in a project without Cratis.Arc. Standard Microsoft authorization on MVC controllers/actions, or explicitly configured external middleware, is a separate enforcement path.
For domain-specific access control in the model-bound pipeline, implement an authorization command filter using IAuthorizationCommandFilter and an actual unauthorized verdict. That example uses real authentication and role checks, not a placeholder policy or an overridable validator. Record ownership needs its own real resource lookup and owner comparison before changes occur.
Test protection through every entry point your application exposes.
Authorization results
Section titled “Authorization results”Caller fragment with ICommandPipeline pipeline injected:
var result = await pipeline.Execute<Guid>(new AllocateManagedIdentifier());if (!result.IsAuthorized){ Console.WriteLine("Access denied");}A denied result has IsSuccess == false and the handler has not executed. Check IsSuccess, not IsValid alone, before using a response. Validation severity does not override an unauthorized verdict.
Executing commands from server-side code
Section titled “Executing commands from server-side code”Arc’s ICurrentPrincipalAccessor uses the HTTP request principal when a request exists. Otherwise it uses a principal established through ISystemExecution, or null if none exists. A protected background command therefore needs an explicitly trusted server-side actor.
This complete caller reuses AllocateManagedIdentifier above and requires ISystemExecution and ICommandPipeline from Arc DI:
using System;using System.Threading.Tasks;using Cratis.Arc.Authorization;using Cratis.Arc.Commands;
public class ManagedIdentifierJob(ISystemExecution systemExecution, ICommandPipeline pipeline){ public async Task<CommandResult<Guid>> Run() { using (systemExecution.AsSystem(nameof(ApplicationRole.Manager))) { return await pipeline.Execute<Guid>(new AllocateManagedIdentifier()); } }}AsSystem(params string[] roles) creates an authenticated system actor carrying exactly those roles. No roles satisfies authentication alone, not a role requirement. As(ClaimsPrincipal) supports an explicitly supplied principal. Dispose the scope to restore the previous actor, and await execution inside it.
Never derive the system roles from request input. An HTTP request principal remains authoritative; a server-side scope cannot elevate that request. For the separate, optional event-sourced use case, see Chronicle reactor command side effects and Chronicle command integration.