Skip to content

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:

  1. REQUIRED: [RequiresRoles] Attribute
  2. ALL DomainCommand<T> implementations MUST have the [RequiresRoles] attribute
  3. Commands without this attribute will FAIL security validation tests
  4. Example: [RequiresRoles(RoleDefinition.Admin)]

  5. FORBIDDEN: Public Role on Commands

  6. Commands CANNOT use RoleDefinition.Public
  7. Commands modify state and must ALWAYS require authentication
  8. Use IPublicQuery for publicly accessible read operations instead

  9. REQUIRED: Valid Roles Only

  10. All roles must exist in RoleDefinition.All
  11. Invalid roles will FAIL security validation tests

Enforced by: CommandSecurityAttributeTests.cs

Query Security Rules

All queries that retrieve data MUST follow these rules:

  1. REQUIRED: Security Marker Interface
  2. ALL queries MUST implement either ISecureQuery OR IPublicQuery
  3. Queries CANNOT implement ISearchQuery directly (ambiguous security)
  4. This makes security requirements explicit and prevents accidents

  5. ISecureQuery Requirements (for authenticated queries):

  6. MUST have [RequiresRoles] attribute with specific roles
  7. CANNOT use RoleDefinition.Public (use IPublicQuery instead)
  8. Example: [RequiresRoles(RoleDefinition.Member)]

  9. IPublicQuery Requirements (for public queries):

  10. Should NOT have [RequiresRoles] attribute
  11. If [RequiresRoles] is present, can only contain RoleDefinition.Public
  12. No authentication required, but can optionally enhance data for authenticated users

  13. REQUIRED: Valid Roles Only

  14. All roles must exist in RoleDefinition.All
  15. 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:

  1. REQUIRED: [RequiresRoles] Attribute
  2. ALL IContract implementations MUST have the [RequiresRoles] attribute
  3. Can use RoleDefinition.Public for publicly accessible contracts

  4. REQUIRED: Valid Roles Only

  5. 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)

using (UserContext.RunAsSystem())
{
    await commandExecutor.ExecuteAsync(systemCommand);
}

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 CommandAuthorizationException on 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

  1. Defense in Depth: Authorization at executor level, not just endpoints
  2. Consistent Enforcement: Same patterns for commands and queries
  3. System Operations: Secure bypass mechanism for internal operations
  4. Comprehensive Logging: Detailed audit trails for all authorization decisions
  5. Flexible Behaviors: Configurable responses to authorization failures
  6. Role Hierarchy: Supports role inheritance (Admin inherits Member permissions)

Migration Guide

Existing Commands/Queries

  1. Add [RequiresRoles] attribute to commands/queries requiring authorization
  2. System will automatically discover and enforce the requirements
  3. Test thoroughly with different user roles

Example Migration

Before:

public sealed record CreateUserCommand : DomainCommand<UserId> { }

After:

[RequiresRoles(RoleDefinition.UserManager, RoleDefinition.Admin)]
public sealed record CreateUserCommand : DomainCommand<UserId> { }

Troubleshooting

Common Issues

  1. Command/Query fails with authorization error
  2. Check if user has required roles
  3. Verify role hierarchy is correct
  4. Check logs for detailed authorization decision

  5. System operations failing

  6. Ensure system context is set: using (UserContext.RunAsSystem())
  7. Check if AllowSystemBypass is enabled (default: true)

  8. Tests failing due to authorization

  9. Use AuthorizationTestHelpers.SetTestUser() in tests
  10. 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

  1. Principle of Least Privilege: Only grant minimum required roles
  2. Explicit Authorization: Always declare authorization requirements explicitly
  3. Test Coverage: Test both authorized and unauthorized scenarios
  4. System Context: Use system context sparingly and only for legitimate system operations
  5. Role Hierarchy: Leverage role inheritance to reduce duplication - assign only the highest role needed
  6. Audit Logging: Monitor authorization decisions in production
  7. Role Assignment: Users should be assigned only their highest role (e.g., assign Admin only, not Admin + 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)