Skip to content

Model-bound query authorization

For model-bound queries, use attributes from Cratis.Arc.Authorization. The default evaluator checks whether the current principal is authenticated and, when roles are specified, belongs to at least one of those roles.

Treat policy support as a current implementation limitation. For a policy-based HTTP API, use an explicitly protected MVC action and test its middleware configuration. For rules needed across model-bound HTTP, direct pipeline execution, and hub subscriptions, implement a real authorization query filter. Do not substitute input validation for an authorization verdict.

Use the shared AccountId and AccountName concepts with an authenticated host and the configured MongoDB provider. This replaces the earlier DebitAccount declaration:

using System.Collections.Generic;
using Cratis.Arc.Authorization;
using Cratis.Arc.Queries.ModelBound;
using MongoDB.Driver;
namespace Banking.Accounts;
[ReadModel]
[Roles("AccountReader")]
public record DebitAccount(AccountId Id, AccountName Name, decimal Balance)
{
[Path("/api/accounts")]
public static IEnumerable<DebitAccount> AllAccounts(IMongoCollection<DebitAccount> collection) =>
collection.Find(_ => true).ToList();
[Roles("Admin", "Auditor")]
[Path("/api/accounts/overdrawn")]
public static IEnumerable<DebitAccount> OverdrawnAccounts(IMongoCollection<DebitAccount> collection) =>
collection.Find(account => account.Balance < 0).ToList();
}

AllAccounts requires AccountReader. OverdrawnAccounts requires Admin or Auditor, replacing the type-level requirement rather than adding to it. These roles intentionally grant access to all matching accounts; this example does not claim owner-only access.

For Arc’s built-in attributes:

  1. Method-level [AllowAnonymous] permits anonymous access.
  2. Method-level [Authorize] or [Roles] takes precedence over the declaring type’s requirements.
  3. Otherwise type-level anonymous/authorization requirements apply.
  4. Without a requirement, the default evaluator permits access.

Do not combine [AllowAnonymous] with [Authorize]/[Roles] on the same target: the built-in anonymous evaluator treats that combination as ambiguous.

Use [Authorize] for authentication alone and [Roles("Admin", "Auditor")] for authentication plus any listed role. A denied model-bound query does not invoke its method and produces isAuthorized: false. Direct HTTP normally maps that verdict to 403; hub denial is an Unauthorized message.

A role check does not establish record ownership. Neither a balance predicate nor a non-null user ID is an ownership check. If a read is owner-scoped, derive the owner from the authenticated principal’s trusted identity mapping and constrain the database predicate to that owner and the requested record ID. Handle absent/unmapped identities by denying access, never by dropping the owner predicate.

There is no universal claim-to-owner mapping Arc can supply for your application. This page therefore does not provide a success-shaped ownership-policy stub. Test at least two owners, a missing identity, and an unauthorized caller through every exposed route/transport before using such a query for private data. Generated proxies provide type safety, not client-side security enforcement.

Authorization gates each new subscription. It does not automatically revoke a running stream when roles change or credentials expire. Use an emission guard backed by your current session/permission state when revocation must affect ongoing delivery.

Do not use interceptor masking as your only data-access control: observable HTTP snapshots currently bypass that interception path. Ensure the producer itself never yields fields or rows the caller must not receive.