NO WARRANTY. Trax auth is plumbing, not a security product. You are solely responsible. See API Security.

TraxAuthorize

Declares who may run a train, read a query model, or call a GraphQL field. It is Trax's own authorization vocabulary: the same attribute and the same rules apply on a train, an entity, an interface and a resolver method, whatever GraphQL server sits underneath.

Signature

namespace Trax.Effect.Attributes;
 
[AttributeUsage(
    AttributeTargets.Class | AttributeTargets.Interface | AttributeTargets.Method,
    AllowMultiple = true,
    Inherited = true
)]
public class TraxAuthorizeAttribute : Attribute
{
    public TraxAuthorizeAttribute();
    public TraxAuthorizeAttribute(string policy);
 
    public string? Policy { get; init; }
    public string? Roles { get; init; }
}
MemberTypeDescription
TraxAuthorizeAttribute()A bare gate: the caller must be authenticated, nothing more
TraxAuthorizeAttribute(string policy)Sets Policy
Policystring?The name of an ASP.NET Core authorization policy the caller must pass
Rolesstring?A comma-separated list of roles; the caller must hold at least one. Exact, case-sensitive match.

How attributes combine

Attribute(s)The caller must
[TraxAuthorize]Be authenticated
[TraxAuthorize("P")]Pass policy P
[TraxAuthorize(Roles = "A,B")]Hold role A or role B
[TraxAuthorize("P1")] [TraxAuthorize("P2")]Pass P1 and P2: policies AND across attributes
[TraxAuthorize(Roles = "A")] [TraxAuthorize(Roles = "B")]Hold A or B: roles union across attributes
[TraxAuthorize("P", Roles = "A")]Pass P and hold A

On a train, attributes on the class, its base classes and every interface it implements are collected together, so [TraxAuthorize("Admin")] on IDeleteUserTrain gates DeleteUserTrain even when the class carries none.

Where it is enforced

Placed onEnforced by
A train class or its interfaceITrainExecutionService.RunAsync and QueueAsync, before the input is read, through the registered ITrainAuthorizationService (Trax.Api registers one). Failure is TrainAuthorizationException with the public message "Not authorized.", TRAX_AUTHORIZATION over GraphQL.
A [TraxQueryModel] entityHotChocolate's @authorize directive on the generated type and its entry field, so the gate holds through navigations, filters and sorts too
A resolver method, or an [ExtendObjectType] classAn @authorize directive on the field, or on every field the extension contributes. It does not re-gate the type being extended.

A train run through ITrainBus in-process, by the scheduler, or inside an ITrustedExecutionScope is not checked: authorization is enforced once, where a caller submits work.

Startup checks

  • A train with [TraxAuthorize] and no ITrainAuthorizationService registered stops the host at startup, unless the mediator was configured with AllowMissingAuthorizationService(). At run time the same case throws TrainAuthorizationNotConfiguredException outside a trusted scope.
  • An empty or whitespace Policy, or a Roles list with no non-empty entry, fails at startup.
  • A query model naming a policy that AddAuthorization never registered fails at startup.
  • A surface carrying both [TraxAuthorize] and [TraxAllowAnonymous] fails at startup.
  • A surface declaring its posture with HotChocolate's [Authorize] or [AllowAnonymous] instead fails at startup, naming the Trax replacement.

Example

using Trax.Effect.Attributes;
 
[TraxMutation]
[TraxAuthorize("MustBeInternal")]
[TraxAuthorize(Roles = "Admin,Manager")]
public class RefundOrderTrain : ServiceTrain<RefundInput, RefundResult>, IRefundOrderTrain
{
    protected override Task<Either<Exception, RefundResult>> Junctions() =>
        Chain<IssueRefund>().Resolve();
}

The caller must pass MustBeInternal and hold Admin or Manager.

See Authorization for the full model, including the exposure posture every GraphQL surface must declare.

Package

dotnet add package Trax.Effect