Centralized Authorization System¶
This document describes the centralized role-based authorization system implemented for commands and queries in the LBS Foundry platform.
Overview¶
The system implements a decorator pattern to enforce role-based authorization at the executor level for both commands and queries. This provides defense-in-depth security and ensures consistent authorization enforcement across the entire CQRS system.
Security Rules and Requirements¶
IMPORTANT: These rules are enforced by unit tests that will fail the build if violated.
Command Security Rules¶
All commands that modify system state MUST follow these rules:
- REQUIRED: [RequiresRoles] Attribute
- ALL
DomainCommand<T>implementations MUST have the[RequiresRoles]attribute - Commands without this attribute will FAIL security validation tests
-
Example:
[RequiresRoles(RoleDefinition.Admin)] -
FORBIDDEN: Public Role on Commands
- Commands CANNOT use
RoleDefinition.Public - Commands modify state and must ALWAYS require authentication
-
Use
IPublicQueryfor publicly accessible read operations instead -
REQUIRED: Valid Roles Only
- All roles must exist in
RoleDefinition.All - Invalid roles will FAIL security validation tests
Enforced by: CommandSecurityAttributeTests.cs
Query Security Rules¶
All queries that retrieve data MUST follow these rules:
- REQUIRED: Security Marker Interface
- ALL queries MUST implement either
ISecureQueryORIPublicQuery - Queries CANNOT implement
ISearchQuerydirectly (ambiguous security) -
This makes security requirements explicit and prevents accidents
-
ISecureQuery Requirements (for authenticated queries):
- MUST have
[RequiresRoles]attribute with specific roles - CANNOT use
RoleDefinition.Public(useIPublicQueryinstead) -
Example:
[RequiresRoles(RoleDefinition.Member)] -
IPublicQuery Requirements (for public queries):
- Should NOT have
[RequiresRoles]attribute - If
[RequiresRoles]is present, can only containRoleDefinition.Public -
No authentication required, but can optionally enhance data for authenticated users
-
REQUIRED: Valid Roles Only
- All roles must exist in
RoleDefinition.All - Invalid roles will FAIL security validation tests
Enforced by: QuerySecurityAttributeTests.cs
Choosing Between ISecureQuery and IPublicQuery¶
Use ISecureQuery when: - Query returns sensitive data requiring authentication - Query results should be filtered based on user roles - Query accesses user-specific or private information - Example: User profiles, private competitions, financial data
Use IPublicQuery when: - Query returns publicly available information - No authentication required to access the data - Data is the same for all users (though may be enhanced for authenticated users) - Example: Public leaderboards, public competition listings, general statistics
Contract (Read Model) Security Rules¶
Read model contracts also have security requirements:
- REQUIRED: [RequiresRoles] Attribute
- ALL
IContractimplementations MUST have the[RequiresRoles]attribute -
Can use
RoleDefinition.Publicfor publicly accessible contracts -
REQUIRED: Valid Roles Only
- All roles must exist in
RoleDefinition.All
Enforced by: ContractSecurityAttributeTests.cs
Key Components¶
1. RequiresRoles Attribute (LBS.Anchor.Security)¶
Declarative attribute to specify role requirements:
[AttributeUsage(AttributeTargets.Class)]
public sealed class RequiresRolesAttribute : Attribute
{
public string[] Roles { get; }
public bool AllowSystemBypass { get; set; } = true;
public UnauthorizedBehavior UnauthorizedBehavior { get; set; } = UnauthorizedBehavior.ThrowException;
public RequiresRolesAttribute(params string[] roles);
}
2. Security Mapping (LBS.Augment.Security)¶
Central service that discovers and manages authorization rules:
public interface ISecurityMapping
{
string[]? GetRequiredRoles(Type type);
bool IsAuthorized(Type type, ClaimsPrincipal? user);
void DiscoverSecurityRequirements(params Assembly[] assemblies);
}
3. Hierarchical Role Authorization (LBS.Domain.Infrastructure.Authentication.Core)¶
Implements role inheritance for HTTP endpoint authorization:
// HierarchicalRoleRequirement defines a requirement with automatic role inheritance
public sealed class HierarchicalRoleRequirement : IAuthorizationRequirement
{
public string[] RequiredRoles { get; }
}
// HierarchicalRoleHandler evaluates requirements using the role hierarchy
public sealed class HierarchicalRoleHandler : AuthorizationHandler<HierarchicalRoleRequirement>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
HierarchicalRoleRequirement requirement);
}
This system ensures consistent role inheritance across both CQRS and HTTP endpoint layers.
4. Security Executors¶
- SecurityCommandExecutor: Wraps command execution with authorization (uses role hierarchy)
- SecurityQueryService: Wraps query execution with authorization (uses role hierarchy)
- HierarchicalRoleHandler: Handles HTTP endpoint authorization (uses role hierarchy)
Usage¶
Command Authorization¶
[DataContract(Name = "CreateUser")]
[RequiresRoles(RoleDefinition.UserManager, RoleDefinition.Admin)]
public sealed record CreateUserCommand : DomainCommand<UserId>
{
// ... command properties
}
Query Authorization¶
Secure Query (ISecureQuery)¶
For queries requiring authentication and specific roles:
using System.Runtime.Serialization;
using System.Security.Claims;
using LBS.Anchor;
using LBS.Anchor.Security;
using LBS.Augment.Authentication;
using LBS.Augment.Query;
[RequiresRoles(RoleDefinition.Member)]
[DataContract(Name = "PlayerStats")]
public class PlayerStatsQuery : ISecureQuery, IRequiresUserContext
{
public string QueryType => this.GetType().GetQueryTypeName();
public ClaimsPrincipal? User => UserContext.Current;
// ... query properties
}
Public Query (IPublicQuery)¶
For queries accessible without authentication:
using System.Runtime.Serialization;
using System.Security.Claims;
using LBS.Anchor;
using LBS.Augment.Authentication;
using LBS.Augment.Query;
[DataContract(Name = "PublicLeaderboard")]
public class PublicLeaderboardQuery : IPublicQuery, IRequiresUserContext
{
public string QueryType => this.GetType().GetQueryTypeName();
public ClaimsPrincipal? User => UserContext.Current;
// Optional: Enhance results for authenticated users
// Handler can check if User is authenticated to provide additional data
// ... query properties
}
Important: Do NOT implement ISearchQuery directly - always use ISecureQuery or IPublicQuery to make security requirements explicit.
System Operations¶
For internal system operations, there are several approaches:
1. Manual System Context (for one-off operations)¶
2. SystemHostedService Base Class (for startup/shutdown tasks)¶
Use when you need system privileges during application startup or shutdown:
public class MyStartupService : SystemHostedService
{
public MyStartupService(ILogger<MyStartupService> logger) : base(logger) { }
protected override async Task StartAsSystemAsync(CancellationToken cancellationToken)
{
// This runs once at startup with system privileges
// Perfect for: seeding data, initialization, migrations
}
}
3. SystemBackgroundService Base Class (for continuous background tasks)¶
Use when you need a long-running background service with system privileges:
public class MyBackgroundWorker : SystemBackgroundService
{
public MyBackgroundWorker(ILogger<MyBackgroundWorker> logger) : base(logger) { }
protected override async Task ExecuteAsSystemAsync(CancellationToken stoppingToken)
{
// This runs continuously with system privileges
// Perfect for: data harvesters, scheduled jobs, monitoring
while (!stoppingToken.IsCancellationRequested)
{
await ProcessDataAsync();
await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken);
}
}
}
Configuration¶
Startup Registration¶
// In Program.cs
builder.Services.AddEventSourcingInfrastructure(configuration); // Command security + ISecurityMapping registration
builder.Services.AddReadModelInfrastructure(assemblies); // Query infrastructure with automatic security wrapping
Note: Security is automatically enabled when both methods are called in order:
- AddEventSourcingInfrastructure() registers security mappings and wraps command execution with authorization
- AddReadModelInfrastructure() automatically detects the security mapping and wraps query execution with authorization
- No additional configuration is required - security is enabled by default
Graceful Degradation¶
If AddReadModelInfrastructure() is called without AddEventSourcingInfrastructure():
- Queries will execute without authorization checks
- This allows for testing scenarios or gradual migration
- A warning should be logged in production environments
Assembly Discovery¶
Security requirements are automatically discovered from:
- LBS.Domain.Core assembly
- LBS.Domain.Fantasy assembly
- LBS.EventSourcing assembly
Authorization Behaviors¶
Commands¶
- Default: Throw
CommandAuthorizationExceptionon failure - System Bypass: Allow system operations to bypass authorization
Queries¶
- ThrowException: Throw
QueryAuthorizationException - ReturnEmpty: Return empty results for unauthorized queries
- FilterResults: Return filtered results based on authorization
Testing¶
Use the provided test helpers for authorization testing:
// Set up test user with specific roles
using (AuthorizationTestHelpers.SetTestUser(RoleDefinition.Admin))
{
// Test code with admin user
}
// Set up system context
using (AuthorizationTestHelpers.SetSystemUser())
{
// Test system operations
}
// Test with no security
services.AddNoSecurity();
Error Handling¶
Command Authorization Failures¶
try
{
await commandExecutor.ExecuteAsync(command);
}
catch (CommandAuthorizationException ex)
{
// ex.CommandType, ex.RequiredRoles, ex.UserRoles available
logger.LogWarning("Unauthorized command: {Message}", ex.Message);
}
Query Authorization Failures¶
Queries typically return empty results rather than throwing exceptions, but behavior is configurable per query type.
Performance Considerations¶
- Startup Discovery: Security requirements discovered once at startup
- Request Caching: User context cached per request via AsyncLocal
- Role Hierarchy: Efficient role checking using pre-computed hierarchy
Security Features¶
- Defense in Depth: Authorization at executor level, not just endpoints
- Consistent Enforcement: Same patterns for commands and queries
- System Operations: Secure bypass mechanism for internal operations
- Comprehensive Logging: Detailed audit trails for all authorization decisions
- Flexible Behaviors: Configurable responses to authorization failures
- Role Hierarchy: Supports role inheritance (Admin inherits Member permissions)
Migration Guide¶
Existing Commands/Queries¶
- Add
[RequiresRoles]attribute to commands/queries requiring authorization - System will automatically discover and enforce the requirements
- Test thoroughly with different user roles
Example Migration¶
Before:
After:
[RequiresRoles(RoleDefinition.UserManager, RoleDefinition.Admin)]
public sealed record CreateUserCommand : DomainCommand<UserId> { }
Troubleshooting¶
Common Issues¶
- Command/Query fails with authorization error
- Check if user has required roles
- Verify role hierarchy is correct
-
Check logs for detailed authorization decision
-
System operations failing
- Ensure system context is set:
using (UserContext.RunAsSystem()) -
Check if
AllowSystemBypassis enabled (default: true) -
Tests failing due to authorization
- Use
AuthorizationTestHelpers.SetTestUser()in tests - Or use
services.AddNoSecurity()for business logic tests
Logging¶
Enable debug logging to see detailed authorization decisions:
{
"Logging": {
"LogLevel": {
"LBS.Domain.Infrastructure.Command.SecurityCommandExecutor": "Debug",
"LBS.Domain.Infrastructure.Query.Services.SecurityQueryService": "Debug",
"LBS.Augment.Security.SecurityMapping": "Debug"
}
}
}
Best Practices¶
- Principle of Least Privilege: Only grant minimum required roles
- Explicit Authorization: Always declare authorization requirements explicitly
- Test Coverage: Test both authorized and unauthorized scenarios
- System Context: Use system context sparingly and only for legitimate system operations
- Role Hierarchy: Leverage role inheritance to reduce duplication - assign only the highest role needed
- Audit Logging: Monitor authorization decisions in production
- Role Assignment: Users should be assigned only their highest role (e.g., assign
Adminonly, notAdmin+Member) as inheritance provides lower-level permissions automatically
Future Enhancements¶
- Resource-based authorization (user can only access their own data)
- Policy-based authorization (complex business rules)
- Dynamic role assignment
- External authorization providers
- Performance optimizations (caching, background refresh)