Add post-quantum cryptography support with PqSoftCryptoProvider
Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
wine-csp-build / Build Wine CSP Image (push) Has been cancelled
Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
wine-csp-build / Build Wine CSP Image (push) Has been cancelled
- Implemented PqSoftCryptoProvider for software-only post-quantum algorithms (Dilithium3, Falcon512) using BouncyCastle. - Added PqSoftProviderOptions and PqSoftKeyOptions for configuration. - Created unit tests for Dilithium3 and Falcon512 signing and verification. - Introduced EcdsaPolicyCryptoProvider for compliance profiles (FIPS/eIDAS) with explicit allow-lists. - Added KcmvpHashOnlyProvider for KCMVP baseline compliance. - Updated project files and dependencies for new libraries and testing frameworks.
This commit is contained in:
@@ -0,0 +1,251 @@
|
||||
using System.Security.Claims;
|
||||
using System.Text.Json;
|
||||
using System.Text.RegularExpressions;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace StellaOps.Policy.Engine.Tenancy;
|
||||
|
||||
/// <summary>
|
||||
/// Middleware that extracts tenant context from request headers and validates tenant access.
|
||||
/// Per RLS design at docs/modules/policy/prep/tenant-rls.md.
|
||||
/// </summary>
|
||||
public sealed partial class TenantContextMiddleware
|
||||
{
|
||||
private readonly RequestDelegate _next;
|
||||
private readonly TenantContextOptions _options;
|
||||
private readonly ILogger<TenantContextMiddleware> _logger;
|
||||
|
||||
// Valid tenant/project ID pattern: alphanumeric, dashes, underscores
|
||||
[GeneratedRegex("^[a-zA-Z0-9_-]+$", RegexOptions.Compiled)]
|
||||
private static partial Regex ValidIdPattern();
|
||||
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
|
||||
WriteIndented = false
|
||||
};
|
||||
|
||||
public TenantContextMiddleware(
|
||||
RequestDelegate next,
|
||||
IOptions<TenantContextOptions> options,
|
||||
ILogger<TenantContextMiddleware> logger)
|
||||
{
|
||||
_next = next ?? throw new ArgumentNullException(nameof(next));
|
||||
_options = options?.Value ?? new TenantContextOptions();
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
public async Task InvokeAsync(HttpContext context, ITenantContextAccessor tenantContextAccessor)
|
||||
{
|
||||
// Skip tenant validation for excluded paths
|
||||
if (!_options.Enabled || IsExcludedPath(context.Request.Path))
|
||||
{
|
||||
await _next(context);
|
||||
return;
|
||||
}
|
||||
|
||||
var validationResult = ValidateTenantContext(context);
|
||||
|
||||
if (!validationResult.IsValid)
|
||||
{
|
||||
await WriteTenantErrorResponse(context, validationResult);
|
||||
return;
|
||||
}
|
||||
|
||||
// Set tenant context for the request
|
||||
tenantContextAccessor.TenantContext = validationResult.Context;
|
||||
|
||||
using (_logger.BeginScope(new Dictionary<string, object?>
|
||||
{
|
||||
["tenant_id"] = validationResult.Context?.TenantId,
|
||||
["project_id"] = validationResult.Context?.ProjectId
|
||||
}))
|
||||
{
|
||||
await _next(context);
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsExcludedPath(PathString path)
|
||||
{
|
||||
var pathValue = path.Value ?? string.Empty;
|
||||
return _options.ExcludedPaths.Any(excluded =>
|
||||
pathValue.StartsWith(excluded, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
private TenantValidationResult ValidateTenantContext(HttpContext context)
|
||||
{
|
||||
// Extract tenant header
|
||||
var tenantHeader = context.Request.Headers[TenantContextConstants.TenantHeader].FirstOrDefault();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(tenantHeader))
|
||||
{
|
||||
if (_options.RequireTenantHeader)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Missing required {Header} header for {Path}",
|
||||
TenantContextConstants.TenantHeader,
|
||||
context.Request.Path);
|
||||
|
||||
return TenantValidationResult.Failure(
|
||||
TenantContextConstants.MissingTenantHeaderErrorCode,
|
||||
$"The {TenantContextConstants.TenantHeader} header is required.");
|
||||
}
|
||||
|
||||
// Use default tenant ID when header is not required
|
||||
tenantHeader = TenantContextConstants.DefaultTenantId;
|
||||
}
|
||||
|
||||
// Validate tenant ID format
|
||||
if (!IsValidTenantId(tenantHeader))
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Invalid tenant ID format: {TenantId}",
|
||||
tenantHeader);
|
||||
|
||||
return TenantValidationResult.Failure(
|
||||
TenantContextConstants.InvalidTenantIdErrorCode,
|
||||
"Invalid tenant ID format. Must be alphanumeric with dashes and underscores.");
|
||||
}
|
||||
|
||||
// Extract project header (optional)
|
||||
var projectHeader = context.Request.Headers[TenantContextConstants.ProjectHeader].FirstOrDefault();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(projectHeader) && !IsValidProjectId(projectHeader))
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Invalid project ID format: {ProjectId}",
|
||||
projectHeader);
|
||||
|
||||
return TenantValidationResult.Failure(
|
||||
TenantContextConstants.InvalidTenantIdErrorCode,
|
||||
"Invalid project ID format. Must be alphanumeric with dashes and underscores.");
|
||||
}
|
||||
|
||||
// Determine write permission from scopes/claims
|
||||
var canWrite = DetermineWritePermission(context);
|
||||
|
||||
// Extract actor ID
|
||||
var actorId = ExtractActorId(context);
|
||||
|
||||
var tenantContext = TenantContext.ForTenant(
|
||||
tenantHeader,
|
||||
string.IsNullOrWhiteSpace(projectHeader) ? null : projectHeader,
|
||||
canWrite,
|
||||
actorId);
|
||||
|
||||
_logger.LogDebug(
|
||||
"Tenant context established: tenant={TenantId}, project={ProjectId}, canWrite={CanWrite}, actor={ActorId}",
|
||||
tenantContext.TenantId,
|
||||
tenantContext.ProjectId ?? "(none)",
|
||||
tenantContext.CanWrite,
|
||||
tenantContext.ActorId ?? "(anonymous)");
|
||||
|
||||
return TenantValidationResult.Success(tenantContext);
|
||||
}
|
||||
|
||||
private bool IsValidTenantId(string tenantId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(tenantId))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (tenantId.Length > _options.MaxTenantIdLength)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return ValidIdPattern().IsMatch(tenantId);
|
||||
}
|
||||
|
||||
private bool IsValidProjectId(string projectId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(projectId))
|
||||
{
|
||||
return true; // Project ID is optional
|
||||
}
|
||||
|
||||
if (projectId.Length > _options.MaxProjectIdLength)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return ValidIdPattern().IsMatch(projectId);
|
||||
}
|
||||
|
||||
private static bool DetermineWritePermission(HttpContext context)
|
||||
{
|
||||
var user = context.User;
|
||||
if (user?.Identity?.IsAuthenticated != true)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for write-related scopes
|
||||
var hasWriteScope = user.Claims.Any(c =>
|
||||
c.Type == "scope" &&
|
||||
(c.Value.Contains("policy:write", StringComparison.OrdinalIgnoreCase) ||
|
||||
c.Value.Contains("policy:edit", StringComparison.OrdinalIgnoreCase) ||
|
||||
c.Value.Contains("policy:activate", StringComparison.OrdinalIgnoreCase)));
|
||||
|
||||
if (hasWriteScope)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check for admin role
|
||||
var hasAdminRole = user.IsInRole("admin") ||
|
||||
user.IsInRole("policy-admin") ||
|
||||
user.HasClaim("role", "admin") ||
|
||||
user.HasClaim("role", "policy-admin");
|
||||
|
||||
return hasAdminRole;
|
||||
}
|
||||
|
||||
private static string? ExtractActorId(HttpContext context)
|
||||
{
|
||||
var user = context.User;
|
||||
|
||||
// Try standard claims
|
||||
var actorId = user?.FindFirst(ClaimTypes.NameIdentifier)?.Value
|
||||
?? user?.FindFirst(ClaimTypes.Upn)?.Value
|
||||
?? user?.FindFirst("sub")?.Value
|
||||
?? user?.FindFirst("client_id")?.Value;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(actorId))
|
||||
{
|
||||
return actorId;
|
||||
}
|
||||
|
||||
// Fall back to header
|
||||
if (context.Request.Headers.TryGetValue("X-StellaOps-Actor", out var header) &&
|
||||
!string.IsNullOrWhiteSpace(header))
|
||||
{
|
||||
return header.ToString();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static async Task WriteTenantErrorResponse(HttpContext context, TenantValidationResult result)
|
||||
{
|
||||
context.Response.StatusCode = StatusCodes.Status400BadRequest;
|
||||
context.Response.ContentType = "application/json";
|
||||
|
||||
var errorResponse = new TenantErrorResponse(
|
||||
result.ErrorCode ?? "UNKNOWN_ERROR",
|
||||
result.ErrorMessage ?? "An unknown error occurred.",
|
||||
context.Request.Path.Value ?? "/");
|
||||
|
||||
await context.Response.WriteAsync(
|
||||
JsonSerializer.Serialize(errorResponse, JsonOptions));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Error response for tenant validation failures.
|
||||
/// </summary>
|
||||
internal sealed record TenantErrorResponse(
|
||||
string ErrorCode,
|
||||
string Message,
|
||||
string Path);
|
||||
@@ -0,0 +1,233 @@
|
||||
namespace StellaOps.Policy.Engine.Tenancy;
|
||||
|
||||
/// <summary>
|
||||
/// Constants for tenant context headers and GUCs (PostgreSQL Grand Unified Configuration).
|
||||
/// Per RLS design at docs/modules/policy/prep/tenant-rls.md.
|
||||
/// </summary>
|
||||
public static class TenantContextConstants
|
||||
{
|
||||
/// <summary>
|
||||
/// HTTP header for tenant ID (mandatory).
|
||||
/// </summary>
|
||||
public const string TenantHeader = "X-Stella-Tenant";
|
||||
|
||||
/// <summary>
|
||||
/// HTTP header for project ID (optional).
|
||||
/// </summary>
|
||||
public const string ProjectHeader = "X-Stella-Project";
|
||||
|
||||
/// <summary>
|
||||
/// PostgreSQL GUC for tenant ID.
|
||||
/// </summary>
|
||||
public const string TenantGuc = "app.tenant_id";
|
||||
|
||||
/// <summary>
|
||||
/// PostgreSQL GUC for project ID.
|
||||
/// </summary>
|
||||
public const string ProjectGuc = "app.project_id";
|
||||
|
||||
/// <summary>
|
||||
/// PostgreSQL GUC for write permission.
|
||||
/// </summary>
|
||||
public const string CanWriteGuc = "app.can_write";
|
||||
|
||||
/// <summary>
|
||||
/// Default tenant ID for legacy data migration.
|
||||
/// </summary>
|
||||
public const string DefaultTenantId = "public";
|
||||
|
||||
/// <summary>
|
||||
/// Error code for missing tenant header (deterministic).
|
||||
/// </summary>
|
||||
public const string MissingTenantHeaderErrorCode = "POLICY_TENANT_HEADER_REQUIRED";
|
||||
|
||||
/// <summary>
|
||||
/// Error code for invalid tenant ID format.
|
||||
/// </summary>
|
||||
public const string InvalidTenantIdErrorCode = "POLICY_TENANT_ID_INVALID";
|
||||
|
||||
/// <summary>
|
||||
/// Error code for tenant access denied (403).
|
||||
/// </summary>
|
||||
public const string TenantAccessDeniedErrorCode = "POLICY_TENANT_ACCESS_DENIED";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents the current tenant and project context for a request.
|
||||
/// </summary>
|
||||
public sealed record TenantContext
|
||||
{
|
||||
/// <summary>
|
||||
/// The tenant ID for the current request.
|
||||
/// </summary>
|
||||
public required string TenantId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The project ID for the current request (optional; null for tenant-wide operations).
|
||||
/// </summary>
|
||||
public string? ProjectId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the current request has write permission.
|
||||
/// </summary>
|
||||
public bool CanWrite { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The actor ID (user or system) making the request.
|
||||
/// </summary>
|
||||
public string? ActorId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Timestamp when the context was created.
|
||||
/// </summary>
|
||||
public DateTimeOffset CreatedAt { get; init; } = DateTimeOffset.UtcNow;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a tenant context for a specific tenant.
|
||||
/// </summary>
|
||||
public static TenantContext ForTenant(string tenantId, string? projectId = null, bool canWrite = false, string? actorId = null)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(tenantId);
|
||||
|
||||
return new TenantContext
|
||||
{
|
||||
TenantId = tenantId,
|
||||
ProjectId = projectId,
|
||||
CanWrite = canWrite,
|
||||
ActorId = actorId,
|
||||
CreatedAt = DateTimeOffset.UtcNow
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Options for tenant context middleware configuration.
|
||||
/// </summary>
|
||||
public sealed class TenantContextOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Configuration section name.
|
||||
/// </summary>
|
||||
public const string SectionName = "PolicyEngine:Tenancy";
|
||||
|
||||
/// <summary>
|
||||
/// Whether tenant validation is enabled (default: true).
|
||||
/// </summary>
|
||||
public bool Enabled { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Whether to require tenant header on all endpoints (default: true).
|
||||
/// When false, missing tenant header defaults to <see cref="TenantContextConstants.DefaultTenantId"/>.
|
||||
/// </summary>
|
||||
public bool RequireTenantHeader { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Paths to exclude from tenant validation (e.g., health checks).
|
||||
/// </summary>
|
||||
public List<string> ExcludedPaths { get; set; } = new()
|
||||
{
|
||||
"/healthz",
|
||||
"/readyz",
|
||||
"/.well-known"
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Maximum length for tenant ID (default: 256).
|
||||
/// </summary>
|
||||
public int MaxTenantIdLength { get; set; } = 256;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum length for project ID (default: 256).
|
||||
/// </summary>
|
||||
public int MaxProjectIdLength { get; set; } = 256;
|
||||
|
||||
/// <summary>
|
||||
/// Whether to allow multi-tenant queries (default: false).
|
||||
/// When true, users with appropriate scopes can query across tenants.
|
||||
/// </summary>
|
||||
public bool AllowMultiTenantQueries { get; set; } = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Interface for accessing the current tenant context.
|
||||
/// </summary>
|
||||
public interface ITenantContextAccessor
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the current tenant context.
|
||||
/// </summary>
|
||||
TenantContext? TenantContext { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Default implementation of <see cref="ITenantContextAccessor"/> using AsyncLocal.
|
||||
/// </summary>
|
||||
public sealed class TenantContextAccessor : ITenantContextAccessor
|
||||
{
|
||||
private static readonly AsyncLocal<TenantContextHolder> _tenantContextCurrent = new();
|
||||
|
||||
/// <inheritdoc />
|
||||
public TenantContext? TenantContext
|
||||
{
|
||||
get => _tenantContextCurrent.Value?.Context;
|
||||
set
|
||||
{
|
||||
var holder = _tenantContextCurrent.Value;
|
||||
if (holder is not null)
|
||||
{
|
||||
// Clear current context trapped in the AsyncLocals, as its done.
|
||||
holder.Context = null;
|
||||
}
|
||||
|
||||
if (value is not null)
|
||||
{
|
||||
// Use an object to hold the context in the AsyncLocal,
|
||||
// so it can be cleared in all ExecutionContexts when its cleared.
|
||||
_tenantContextCurrent.Value = new TenantContextHolder { Context = value };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class TenantContextHolder
|
||||
{
|
||||
public TenantContext? Context;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result of tenant context validation.
|
||||
/// </summary>
|
||||
public sealed record TenantValidationResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether the validation succeeded.
|
||||
/// </summary>
|
||||
public bool IsValid { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Error code if validation failed.
|
||||
/// </summary>
|
||||
public string? ErrorCode { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Error message if validation failed.
|
||||
/// </summary>
|
||||
public string? ErrorMessage { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The validated tenant context if successful.
|
||||
/// </summary>
|
||||
public TenantContext? Context { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a successful validation result.
|
||||
/// </summary>
|
||||
public static TenantValidationResult Success(TenantContext context) =>
|
||||
new() { IsValid = true, Context = context };
|
||||
|
||||
/// <summary>
|
||||
/// Creates a failed validation result.
|
||||
/// </summary>
|
||||
public static TenantValidationResult Failure(string errorCode, string errorMessage) =>
|
||||
new() { IsValid = false, ErrorCode = errorCode, ErrorMessage = errorMessage };
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
|
||||
namespace StellaOps.Policy.Engine.Tenancy;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for registering tenant context services.
|
||||
/// </summary>
|
||||
public static class TenantContextServiceCollectionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds tenant context services to the service collection.
|
||||
/// </summary>
|
||||
public static IServiceCollection AddTenantContext(this IServiceCollection services)
|
||||
{
|
||||
services.TryAddSingleton<ITenantContextAccessor, TenantContextAccessor>();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds tenant context services with configuration.
|
||||
/// </summary>
|
||||
public static IServiceCollection AddTenantContext(
|
||||
this IServiceCollection services,
|
||||
Action<TenantContextOptions> configure)
|
||||
{
|
||||
services.Configure(configure);
|
||||
return services.AddTenantContext();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds tenant context services with configuration from configuration section.
|
||||
/// </summary>
|
||||
public static IServiceCollection AddTenantContext(
|
||||
this IServiceCollection services,
|
||||
IConfiguration configuration,
|
||||
string sectionName = TenantContextOptions.SectionName)
|
||||
{
|
||||
services.Configure<TenantContextOptions>(configuration.GetSection(sectionName));
|
||||
return services.AddTenantContext();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for configuring tenant context middleware.
|
||||
/// </summary>
|
||||
public static class TenantContextApplicationBuilderExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds the tenant context middleware to the application pipeline.
|
||||
/// This middleware extracts tenant/project headers and validates tenant access.
|
||||
/// </summary>
|
||||
public static IApplicationBuilder UseTenantContext(this IApplicationBuilder app)
|
||||
{
|
||||
return app.UseMiddleware<TenantContextMiddleware>();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for endpoint routing to apply tenant requirements.
|
||||
/// </summary>
|
||||
public static class TenantContextEndpointExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Requires tenant context for the endpoint group.
|
||||
/// </summary>
|
||||
public static RouteGroupBuilder RequireTenantContext(this RouteGroupBuilder group)
|
||||
{
|
||||
group.AddEndpointFilter<TenantContextEndpointFilter>();
|
||||
return group;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a tenant context requirement filter to a route handler.
|
||||
/// </summary>
|
||||
public static RouteHandlerBuilder RequireTenantContext(this RouteHandlerBuilder builder)
|
||||
{
|
||||
builder.AddEndpointFilter<TenantContextEndpointFilter>();
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Endpoint filter that validates tenant context is present.
|
||||
/// </summary>
|
||||
internal sealed class TenantContextEndpointFilter : IEndpointFilter
|
||||
{
|
||||
public async ValueTask<object?> InvokeAsync(
|
||||
EndpointFilterInvocationContext context,
|
||||
EndpointFilterDelegate next)
|
||||
{
|
||||
var tenantAccessor = context.HttpContext.RequestServices
|
||||
.GetService<ITenantContextAccessor>();
|
||||
|
||||
if (tenantAccessor?.TenantContext is null)
|
||||
{
|
||||
return Results.Problem(
|
||||
title: "Tenant context required",
|
||||
detail: $"The {TenantContextConstants.TenantHeader} header is required for this endpoint.",
|
||||
statusCode: StatusCodes.Status400BadRequest,
|
||||
extensions: new Dictionary<string, object?>
|
||||
{
|
||||
["error_code"] = TenantContextConstants.MissingTenantHeaderErrorCode
|
||||
});
|
||||
}
|
||||
|
||||
return await next(context);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user