Authorization
Model-bound queries support authorization through standard ASP.NET Core authorization attributes as well as the convenient [Roles] attribute provided by the Arc.
Using the Authorize Attribute
You can secure query methods using the standard [Authorize] attribute:
[ReadModel]
public record DebitAccount(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance)
{
[Authorize]
public static IEnumerable<DebitAccount> GetAllAccounts(IMongoCollection<DebitAccount> collection) =>
collection.Find(_ => true).ToList();
[Authorize(Roles = "Admin,Manager")]
public static IEnumerable<DebitAccount> GetSensitiveAccounts(IMongoCollection<DebitAccount> collection) =>
collection.Find(a => a.Balance > 100000).ToList();
}
Using the Roles Attribute
The Arc provides a more convenient [Roles] attribute for cleaner syntax when specifying multiple roles:
[ReadModel]
public record DebitAccount(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance)
{
[Roles("Admin", "Auditor")]
public static IEnumerable<DebitAccount> GetAdminAccounts(IMongoCollection<DebitAccount> collection) =>
collection.Find(_ => true).ToList();
[Roles("Manager")]
public static IEnumerable<DebitAccount> GetManagerAccounts(IMongoCollection<DebitAccount> collection) =>
collection.Find(a => a.Owner != CustomerId.Empty).ToList();
}
The user needs to have at least one of the specified roles to execute the query.
Read Model-Level Authorization
You can apply authorization at the read model level to protect all query methods:
[ReadModel]
[Roles("User")] // All methods require at least "User" role
public record DebitAccount(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance)
{
public static IEnumerable<DebitAccount> GetAllAccounts(IMongoCollection<DebitAccount> collection) =>
collection.Find(_ => true).ToList();
[Roles("Admin")] // Override read model-level authorization
public static IEnumerable<DebitAccount> GetAdminOnlyAccounts(IMongoCollection<DebitAccount> collection) =>
collection.Find(a => a.Balance < 0).ToList();
}
Method-Level Authorization Override
Method-level authorization attributes override class-level ones:
[ReadModel]
[Authorize] // Require authentication for all methods
public record DebitAccount(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance)
{
// Inherits class-level [Authorize] - requires authentication
public static IEnumerable<DebitAccount> GetUserAccounts(IMongoCollection<DebitAccount> collection) =>
collection.Find(_ => true).ToList();
[Roles("Admin", "Manager")] // Overrides class-level, requires specific roles
public static IEnumerable<DebitAccount> GetPrivilegedAccounts(IMongoCollection<DebitAccount> collection) =>
collection.Find(a => a.Balance > 50000).ToList();
[AllowAnonymous] // Completely overrides class-level authorization
public static int GetTotalAccountCount(IMongoCollection<DebitAccount> collection) =>
(int)collection.CountDocuments(_ => true);
}
Policy-Based Authorization
For more complex authorization scenarios, you can use policy-based authorization:
[ReadModel]
public record DebitAccount(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance)
{
[Authorize(Policy = "RequireAccountAccess")]
public static DebitAccount GetAccountById(
AccountId id,
IMongoCollection<DebitAccount> collection) =>
collection.Find(a => a.Id == id).FirstOrDefault();
[Authorize(Policy = "RequireHighValueAccess")]
public static IEnumerable<DebitAccount> GetHighValueAccounts(
IMongoCollection<DebitAccount> collection) =>
collection.Find(a => a.Balance > 1000000).ToList();
}
Context-Dependent Authorization
Access user context within query methods for dynamic authorization:
[ReadModel]
public record DebitAccount(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance)
{
[Authorize]
public static IEnumerable<DebitAccount> GetMyAccounts(
IMongoCollection<DebitAccount> collection,
IHttpContextAccessor httpContextAccessor)
{
var userId = httpContextAccessor.HttpContext?.User?.FindFirst("sub")?.Value;
if (string.IsNullOrEmpty(userId))
return Enumerable.Empty<DebitAccount>();
var customerId = new CustomerId(Guid.Parse(userId));
return collection.Find(a => a.Owner == customerId).ToList();
}
[Authorize]
public static DebitAccount? GetAccountIfOwned(
AccountId accountId,
IMongoCollection<DebitAccount> collection,
IHttpContextAccessor httpContextAccessor)
{
var userId = httpContextAccessor.HttpContext?.User?.FindFirst("sub")?.Value;
if (string.IsNullOrEmpty(userId))
return null;
var customerId = new CustomerId(Guid.Parse(userId));
return collection.Find(a => a.Id == accountId && a.Owner == customerId).FirstOrDefault();
}
}
Role Hierarchies
Implement role hierarchies with custom authorization:
[ReadModel]
public record DebitAccount(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance)
{
[Roles("User")] // Basic users can see their own accounts
public static IEnumerable<DebitAccount> GetBasicAccounts(IMongoCollection<DebitAccount> collection) =>
collection.Find(a => a.Balance >= 0).ToList();
[Roles("Manager", "Admin")] // Managers and admins can see more
public static IEnumerable<DebitAccount> GetManagerAccounts(IMongoCollection<DebitAccount> collection) =>
collection.Find(a => a.Balance > -1000).ToList();
[Roles("Admin")] // Only admins can see all accounts including severely overdrawn
public static IEnumerable<DebitAccount> GetAllAccountsIncludingProblematic(IMongoCollection<DebitAccount> collection) =>
collection.Find(_ => true).ToList();
}
Observable Query Authorization
Authorization also applies to observable queries:
[ReadModel]
public record DebitAccount(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance)
{
[Authorize]
public static ISubject<IEnumerable<DebitAccount>> GetAccountsObservable(
IMongoCollection<DebitAccount> collection) =>
collection.Observe();
[Roles("Admin")]
public static ISubject<IEnumerable<DebitAccount>> GetAdminAccountsObservable(
IMongoCollection<DebitAccount> collection) =>
collection.Observe(a => a.Balance < 0);
}
Authorization with Query Parameters
Combine authorization with parameter-based filtering:
[ReadModel]
public record DebitAccount(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance)
{
[Authorize]
public static IEnumerable<DebitAccount> GetAccountsByOwner(
CustomerId ownerId,
IMongoCollection<DebitAccount> collection,
IHttpContextAccessor httpContextAccessor)
{
var currentUserId = httpContextAccessor.HttpContext?.User?.FindFirst("sub")?.Value;
var isAdmin = httpContextAccessor.HttpContext?.User?.IsInRole("Admin") == true;
// Users can only see their own accounts unless they're admin
if (!isAdmin && currentUserId != ownerId.Value.ToString())
{
return Enumerable.Empty<DebitAccount>();
}
return collection.Find(a => a.Owner == ownerId).ToList();
}
}
Custom Authorization Attributes
Create custom authorization attributes for domain-specific logic:
public class RequireAccountOwnershipAttribute : AuthorizeAttribute
{
public RequireAccountOwnershipAttribute() : base("RequireAccountOwnership") { }
}
[ReadModel]
public record DebitAccount(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance)
{
[RequireAccountOwnership]
public static DebitAccount GetAccountDetails(
AccountId id,
IMongoCollection<DebitAccount> collection) =>
collection.Find(a => a.Id == id).FirstOrDefault();
}
Authorization Results
When authorization fails, the query pipeline automatically returns an unauthorized result. The query method will not be executed:
// In your policy handler or middleware
public class AccountOwnershipHandler : AuthorizationHandler<AccountOwnershipRequirement>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
AccountOwnershipRequirement requirement)
{
var userId = context.User.FindFirst("sub")?.Value;
// Check if user owns the account being accessed
if (/* ownership check logic */index.md)
{
context.Succeed(requirement);
}
return Task.CompletedTask;
}
}
Anonymous Access
Use [AllowAnonymous] to allow public access to specific query methods. This attribute bypasses all authorization requirements, including class-level [Authorize] attributes and role requirements.
How AllowAnonymous Works
The authorization system evaluates attributes in the following order:
- Method-level
[AllowAnonymous]- If present on the method, allows anonymous access immediately - Method-level
[Authorize]or[Roles]- If present on the method, these take precedence over class-level attributes - Class-level
[AllowAnonymous]- If present on the class (and no method-level authorization), allows anonymous access - Class-level
[Authorize]or[Roles]- Applied when no method-level attributes are specified
Method-Level AllowAnonymous
Override class-level authorization for specific query methods:
[ReadModel]
[Authorize] // Require authentication by default
public record DebitAccount(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance)
{
[AllowAnonymous] // Override class-level authorization for public data
public static int GetTotalAccountCount(IMongoCollection<DebitAccount> collection) =>
(int)collection.CountDocuments(_ => true);
[AllowAnonymous]
public static decimal GetAverageBalance(IMongoCollection<DebitAccount> collection)
{
var accounts = collection.Find(_ => true).ToList();
return accounts.Count > 0 ? accounts.Average(a => a.Balance) : 0;
}
// This method requires authentication (inherits from class)
public static IEnumerable<DebitAccount> GetAllAccounts(IMongoCollection<DebitAccount> collection) =>
collection.Find(_ => true).ToList();
}
Class-Level AllowAnonymous
Apply [AllowAnonymous] at the class level to make all query methods publicly accessible by default:
[ReadModel]
[AllowAnonymous] // All methods are publicly accessible by default
public record PublicStatistics(string Category, int Count)
{
public static IEnumerable<PublicStatistics> GetAllStatistics(
IMongoCollection<PublicStatistics> collection) =>
collection.Find(_ => true).ToList();
public static PublicStatistics? GetByCategory(
string category,
IMongoCollection<PublicStatistics> collection) =>
collection.Find(s => s.Category == category).FirstOrDefault();
[Authorize] // Override class-level: this specific method requires authentication
public static IEnumerable<PublicStatistics> GetSensitiveStatistics(
IMongoCollection<PublicStatistics> collection) =>
collection.Find(s => s.Category.StartsWith("Internal")).ToList();
}
Common Use Cases for AllowAnonymous
- Public statistics or counts - Aggregate data that doesn't expose sensitive information
- Product catalogs - Public product listings for e-commerce sites
- Public content - Blog posts, articles, or documentation
- Health checks - System status information for monitoring
- Search endpoints - Public search functionality
Best Practices
- Apply authorization at the appropriate level - Use class-level for broad protection, method-level for specific requirements
- Use the
[Roles]attribute - More convenient than the standard[Authorize(Roles = "...")]syntax - Implement defense in depth - Combine multiple authorization layers when appropriate
- Consider user context - Use
IHttpContextAccessorto access current user information for dynamic authorization - Test authorization - Ensure unauthorized users cannot access protected queries
- Use policies for complex logic - Implement custom authorization policies for domain-specific rules
- Be explicit about public access - Use
[AllowAnonymous]to clearly indicate intentionally public methods - Log authorization failures - Monitor and log unauthorized access attempts
- Keep authorization simple - Complex authorization logic should be in services, not query methods
Note: Authorization is evaluated before the query method is called. If authorization fails, the query will not be executed and the result will indicate the authorization failure. The proxy generator automatically creates TypeScript types that respect your authorization constraints, helping prevent unauthorized client-side calls.