API reference — Integration

Nuvora.Nexus.Sentinel.Relay

Relay bridge for Sentinel: feeds Relay's AuthContext from the Sentinel principal, evaluates Relay [RequirePermission] attributes against Sentinel's authorization engine (grants, wildcards, deny-overrides), and maps the org claim onto Relay's TenantContext.

dotnet add package Nuvora.Nexus.Sentinel.Relay

Nuvora.Nexus.Sentinel.Relay.Authorization

SentinelAuthorizationPolicy

public sealed class SentinelAuthorizationPolicy : IAuthorizationPolicy

Relay IAuthorizationPolicy that evaluates a message’s [RequirePermission("service:scope:action")] against Sentinel’s authorization engine. Attach it with [RequirePolicy(SentinelAuthorizationPolicy.PolicyName)] next to the [RequirePermission] attribute.

How this hooks into Relay: AuthorizationService only consults IAuthorizationPolicy for [RequirePolicy]-decorated messages — its [RequirePermission] check is a claim lookup, which the bridge middleware already answers with engine-projected claims (see SentinelRelayPermissionCatalog). This policy is the belt to that suspender: a per-dispatch, straight-to-the-engine evaluation that (a) works even when the claim projection is absent, and (b) re-verifies that the projected AuthContext identity still matches the snapshot it claims to speak for. Relay’s behaviors invoke exactly one method per message kind — SentinelAuthorizationPolicy.EvaluateCommandAsync from AuthorizationBehavior, SentinelAuthorizationPolicy.EvaluateVoidCommandAsync from AuthorizationBehaviorVoid, SentinelAuthorizationPolicy.EvaluateQueryAsync from QueryAuthorizationBehavior — and all three funnel into the same evaluation here, so commands and queries get identical semantics.

Constructors

SentinelAuthorizationPolicy(IHttpContextAccessor httpContextAccessor)

Properties

string Name { get; }

The unique name this policy is referenced by from [RequirePolicy("...")] attributes

Methods

Task<AuthorizationResult> EvaluateCommandAsync<TCommand, TResponse>(AuthContext authContext, TCommand command, CancellationToken cancellationToken = default(CancellationToken))

Evaluates the authorization policy for a command with response

Task<AuthorizationResult> EvaluateQueryAsync<TQuery, TResponse>(AuthContext authContext, TQuery query, CancellationToken cancellationToken = default(CancellationToken))

Evaluates the authorization policy for a query

Task<AuthorizationResult> EvaluateVoidCommandAsync<TCommand>(AuthContext authContext, TCommand command, CancellationToken cancellationToken = default(CancellationToken))

Evaluates the authorization policy for a void command

Fields

const string PolicyName = "sentinel"

The name used in [RequirePolicy("sentinel")].

SentinelRelayPermissionCatalog

public sealed class SentinelRelayPermissionCatalog

The distinct set of permissions the application’s Relay messages demand via [RequirePermission("service:scope:action")], parsed as Sentinel PermissionIds.

Why this exists: Relay’s AuthorizationService enforces [RequirePermission] by exact string match against the permission claims on its AuthContext — it never calls out to an engine. Sentinel grants, however, are patterns (wildcards, deny-overrides, org/team/self scoping, ABAC conditions), so no static claim list can represent them. The bridge closes the gap by evaluating exactly this catalog against the subject’s snapshot on each request (SentinelRelayAuthContextMiddleware) and projecting the ALLOWED subset as permission claims. Relay’s exact-match check then agrees with Sentinel’s engine by construction. The catalog is enumerated from Relay’s own command/query registries — the same types AddRelay() scanned — so it can never drift from what the bus actually dispatches.

Contract consequence: when Sentinel is the authorization authority, every Relay permission string MUST use the Sentinel service:scope:action grammar. A non-conforming string is a configuration error and fails fast here, listing the offending message type, rather than silently never matching any grant.

Constructors

SentinelRelayPermissionCatalog(IServiceProvider services)

Properties

IReadOnlyList<PermissionId> RequiredPermissions { get; }

Every distinct permission demanded by a registered command or query.

Nuvora.Nexus.Sentinel.Relay.DependencyInjection

SentinelRelayServiceCollectionExtensions

public static class SentinelRelayServiceCollectionExtensions

Registration for the Sentinel → Relay bridge.

Methods

static IServiceCollection AddSentinelRelayAuthorization(this IServiceCollection services)

Wires Sentinel as Relay’s authorization authority: registers Relay’s auth services (accessor, policy provider, pipeline behaviors — the same set AddRelayAuth() installs), the permission catalog the bridge middleware projects from, and SentinelAuthorizationPolicy so [RequirePolicy("sentinel")] messages are evaluated per-dispatch against the Sentinel engine. Pair with app.UseSentinelRelayAuthContext() in the pipeline (which replaces UseRelayAuthContext() — see that extension’s remarks).

static IServiceCollection AddSentinelRelayTenancy(this IServiceCollection services, Action<RelayTenancyOptions>? configure = null)

Wires Sentinel’s org claim as Relay’s tenant: registers SentinelTenantResolver and Relay tenancy, with the claim source enabled. Add app.UseRelayTenantContext() after UseSentinelRelayAuthContext() so the resolver sees the authenticated principal.

Nuvora.Nexus.Sentinel.Relay.Middleware

SentinelRelayApplicationBuilderExtensions

public static class SentinelRelayApplicationBuilderExtensions

Pipeline wiring for the Sentinel → Relay auth-context bridge.

Methods

static IApplicationBuilder UseSentinelRelayAuthContext(this IApplicationBuilder app)

Adds SentinelRelayAuthContextMiddleware, projecting the Sentinel principal and its permission snapshot onto Relay’s AuthContext.

SentinelRelayAuthContextMiddleware

public sealed class SentinelRelayAuthContextMiddleware

Projects the request’s SentinelPrincipal (and its subject snapshot) onto Relay’s AuthContext via IAuthContextAccessor. This is the Sentinel-flavored replacement for Relay’s own RelayAuthorizationMiddleware: same pipeline slot, but the projection source is Sentinel’s typed principal and permission engine instead of generic RelayAuthOptions claim probes.

The mapping:

  • UserIdSentinelPrincipal.SubjectId. Sentinel subject ids are Guid v7 precisely so they satisfy Relay’s Guid-keyed AuthContext.UserId — no translation table, the ids line up by design.
  • Username ← the principal’s email/name claims when the token carries them; falls back to the subject id so logs always have a stable handle.
  • Roles ← always empty. Sentinel deliberately does not surface role NAMES on principals: roles, groups, and policies are compiled into grants inside the snapshot, so there is nothing role-shaped to project. Use [RequirePermission], not [RequireRole], when Sentinel is the authority.
  • ClaimValuesrealm, org, sid, mfa from the typed principal, plus one permission claim per catalog entry the Sentinel engine ALLOWS for this subject (see SentinelRelayPermissionCatalog for why projection — not interception — is how [RequirePermission] gets Sentinel semantics).

Constructors

SentinelRelayAuthContextMiddleware(RequestDelegate next, ILogger<SentinelRelayAuthContextMiddleware> logger)

Methods

Task InvokeAsync(HttpContext context)

Nuvora.Nexus.Sentinel.Relay.Tenancy

SentinelTenantResolver

public sealed class SentinelTenantResolver : ITenantResolver

Relay ITenantResolver that maps Sentinel’s org claim onto Relay’s TenantContext: the organization context minted into the access token IS the tenant. Registered for the TenantSource.Claim source ahead of Relay’s generic ClaimTenantResolver, and reads the typed SentinelPrincipal first — the validated token, not a spoofable header — falling back to the org claim on the mirrored HttpContext.User for hosts that authenticate Sentinel-minted JWTs through a different handler.

The org id is emitted as a Guid string; Relay’s TenantResolutionMiddleware turns it into TenantContext.For(orgId) via the registry or the Guid-identifier fallback (on by default), so no static tenant catalog is required. Org-less (realm-level) tokens yield null — “no tenant in scope” — and Relay’s [TenantScoped] enforcement then fails closed downstream, which is exactly the semantics of a token minted without an org context.

Properties

TenantSource Source { get; }

The source this resolver reads from.

Methods

string? Resolve(HttpContext context)

The raw identifier, or null when this source carries none.