using System.Security.Cryptography; using System.Text; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using SVSim.EmulatedEntrypoint.Configuration; namespace SVSim.EmulatedEntrypoint.Infrastructure; /// /// Gates a controller or action on a shared secret carried in the X-Admin-Secret header, /// compared against . Runs as an authorization filter so /// unauthorized requests short-circuit before model binding. /// /// Fail-closed: if the configured secret is null/empty the endpoint is treated as disabled and /// every request gets a 401 (with a warning logged once per request). This means a deployment /// that forgets to set the secret leaves the endpoint locked, not open. /// [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true)] public sealed class RequireAdminSecretAttribute : Attribute, IAuthorizationFilter { public const string HeaderName = "X-Admin-Secret"; public void OnAuthorization(AuthorizationFilterContext context) { var services = context.HttpContext.RequestServices; var options = services.GetRequiredService>().Value; var logger = services.GetRequiredService>(); if (string.IsNullOrWhiteSpace(options.ImportSecret)) { logger.LogWarning( "Rejecting request to {Path}: Admin:ImportSecret is not configured. " + "Set it in appsettings (or the NPGSQL_ADMIN__IMPORTSECRET env var) to enable the endpoint.", context.HttpContext.Request.Path); context.Result = new UnauthorizedResult(); return; } if (!context.HttpContext.Request.Headers.TryGetValue(HeaderName, out var provided) || provided.Count == 0 || string.IsNullOrEmpty(provided[0])) { context.Result = new UnauthorizedResult(); return; } var providedBytes = Encoding.UTF8.GetBytes(provided[0]!); var expectedBytes = Encoding.UTF8.GetBytes(options.ImportSecret); if (!CryptographicOperations.FixedTimeEquals(providedBytes, expectedBytes)) { context.Result = new UnauthorizedResult(); } } }