From 7180daf1b4d1ac1634c3062552857e2b1b1fb1bd Mon Sep 17 00:00:00 2001 From: souvikghosh04 Date: Tue, 16 Jun 2026 18:38:56 +0530 Subject: [PATCH 1/4] Restrict POST /configuration endpoint to loopback The POST /configuration endpoint used in late-configured (hosted) mode now requires the request to originate from a loopback address. An optional bootstrap token (DAB_CONFIG_AUTH_TOKEN env var, X-DAB-CONFIG-AUTH header) can be required as an additional check. Backward compatible: in-process callers (TestServer) and loopback callers without a token continue to work. --- ...ConfigurationEndpointAuthorizationTests.cs | 166 ++++++++++++++++++ src/Service/Startup.cs | 62 +++++++ 2 files changed, 228 insertions(+) create mode 100644 src/Service.Tests/Configuration/ConfigurationEndpointAuthorizationTests.cs diff --git a/src/Service.Tests/Configuration/ConfigurationEndpointAuthorizationTests.cs b/src/Service.Tests/Configuration/ConfigurationEndpointAuthorizationTests.cs new file mode 100644 index 0000000000..33b8d432ee --- /dev/null +++ b/src/Service.Tests/Configuration/ConfigurationEndpointAuthorizationTests.cs @@ -0,0 +1,166 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Net; +using System.Net.Http; +using System.Net.Http.Json; +using System.Threading.Tasks; +using Azure.DataApiBuilder.Service.Controllers; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.TestHost; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using static Azure.DataApiBuilder.Service.Tests.Configuration.ConfigurationEndpoints; +using static Azure.DataApiBuilder.Service.Tests.Configuration.TestConfigFileReader; + +namespace Azure.DataApiBuilder.Service.Tests.Configuration; + +/// +/// Tests for the security mitigation on the POST /configuration endpoint (CWE-306). +/// Verifies that the endpoint is restricted to loopback addresses and optionally +/// gated behind a bootstrap token (DAB_CONFIG_AUTH_TOKEN / X-DAB-CONFIG-AUTH header). +/// +[TestClass] +public class ConfigurationEndpointAuthorizationTests +{ + /// + /// Validates IsConfigurationRequestAuthorized across the full matrix of inputs: + /// remote IP (loopback v4/v6, private/public, null/in-process), configured bootstrap + /// token, and provided X-DAB-CONFIG-AUTH header value. + /// + /// + /// Source IP for the simulated request. Use null to model an in-process call + /// (e.g. TestServer) where there is no underlying TCP connection. + /// + /// Value to set DAB_CONFIG_AUTH_TOKEN to, or null to leave unset. + /// Value of the X-DAB-CONFIG-AUTH header, or null to omit. + /// Expected authorization result. + [DataTestMethod] + // --- No bootstrap token configured --- + [DataRow("127.0.0.1", null, null, true, DisplayName = "Loopback IPv4, no token => allow")] + [DataRow("::1", null, null, true, DisplayName = "Loopback IPv6, no token => allow")] + [DataRow(null, null, null, true, DisplayName = "In-process (null IP), no token => allow")] + [DataRow("192.168.1.100", null, null, false, DisplayName = "Private IPv4, no token => deny")] + [DataRow("10.0.0.1", null, null, false, DisplayName = "Private IPv4 (10/8), no token => deny")] + [DataRow("172.17.0.1", null, null, false, DisplayName = "Docker bridge IP, no token => deny")] + [DataRow("203.0.113.50", null, null, false, DisplayName = "Public IPv4, no token => deny")] + // --- Bootstrap token configured --- + [DataRow("127.0.0.1", "secret", "secret", true, DisplayName = "Loopback + correct token => allow")] + [DataRow("127.0.0.1", "secret", "wrong", false, DisplayName = "Loopback + wrong token => deny")] + [DataRow("127.0.0.1", "secret", null, false, DisplayName = "Loopback + missing header => deny")] + [DataRow("192.168.1.50", "secret", "secret", false, DisplayName = "Non-loopback + correct token => still deny")] + [DataRow(null, "secret", null, false, DisplayName = "In-process + missing header => deny")] + [DataRow(null, "secret", "secret", true, DisplayName = "In-process + correct token => allow")] + public void IsConfigurationRequestAuthorized_Matrix( + string remoteIp, + string configuredToken, + string providedHeader, + bool expected) + { + // Arrange + Environment.SetEnvironmentVariable(Startup.CONFIG_AUTH_TOKEN_ENV_VAR, configuredToken); + + DefaultHttpContext httpContext = new(); + httpContext.Connection.RemoteIpAddress = remoteIp is null ? null : IPAddress.Parse(remoteIp); + if (providedHeader is not null) + { + httpContext.Request.Headers[Startup.CONFIG_AUTH_HEADER] = providedHeader; + } + + // Act + bool result = Startup.IsConfigurationRequestAuthorized(httpContext); + + // Assert + Assert.AreEqual(expected, result); + } + + /// + /// End-to-end test against the full middleware pipeline via TestServer. Drives both + /// /configuration (v1) and /configuration/v2 with every token combination and verifies + /// the HTTP status code returned by the security middleware. + /// + /// The endpoint being tested. + /// Value to set DAB_CONFIG_AUTH_TOKEN to, or null to leave unset. + /// Value of the X-DAB-CONFIG-AUTH header, or null to omit. + /// Expected HTTP status code from the POST. + [DataTestMethod, TestCategory(TestCategory.COSMOSDBNOSQL)] + // No token configured -- backward-compatible loopback success + [DataRow(CONFIGURATION_ENDPOINT, null, null, HttpStatusCode.OK)] + [DataRow(CONFIGURATION_ENDPOINT_V2, null, null, HttpStatusCode.OK)] + // Token configured and correct -- allowed + [DataRow(CONFIGURATION_ENDPOINT, "integration-token", "integration-token", HttpStatusCode.OK)] + [DataRow(CONFIGURATION_ENDPOINT_V2, "integration-token", "integration-token", HttpStatusCode.OK)] + // Token configured but wrong -- forbidden + [DataRow(CONFIGURATION_ENDPOINT, "correct-token", "wrong-token", HttpStatusCode.Forbidden)] + [DataRow(CONFIGURATION_ENDPOINT_V2, "correct-token", "wrong-token", HttpStatusCode.Forbidden)] + // Token configured but header missing -- forbidden + [DataRow(CONFIGURATION_ENDPOINT, "required-token", null, HttpStatusCode.Forbidden)] + [DataRow(CONFIGURATION_ENDPOINT_V2, "required-token", null, HttpStatusCode.Forbidden)] + public async Task EndToEnd_PostConfiguration_StatusCodeMatrix( + string configurationEndpoint, + string configuredToken, + string providedHeader, + HttpStatusCode expectedStatus) + { + // Arrange + Environment.SetEnvironmentVariable(Startup.CONFIG_AUTH_TOKEN_ENV_VAR, configuredToken); + + using TestServer server = new(Program.CreateWebHostFromInMemoryUpdatableConfBuilder(Array.Empty())); + using HttpClient httpClient = server.CreateClient(); + + HttpRequestMessage request = new(HttpMethod.Post, configurationEndpoint) + { + Content = BuildPostContent(configurationEndpoint), + }; + if (providedHeader is not null) + { + request.Headers.Add(Startup.CONFIG_AUTH_HEADER, providedHeader); + } + + // Act + HttpResponseMessage postResult = await httpClient.SendAsync(request); + + // Assert + Assert.AreEqual(expectedStatus, postResult.StatusCode); + } + + [TestCleanup] + public void Cleanup() + { + Environment.SetEnvironmentVariable(Startup.CONFIG_AUTH_TOKEN_ENV_VAR, null); + } + + /// + /// Builds the JSON post body appropriate for the given configuration endpoint version. + /// + private static JsonContent BuildPostContent(string endpoint) + { + Config.ObjectModel.RuntimeConfig config = ReadCosmosConfigurationFromFile() with { Schema = "@env('schema')" }; + const string graphqlSchema = @" + type Entity { + id: ID! + name: String! + } + "; + + if (endpoint == CONFIGURATION_ENDPOINT) + { + return JsonContent.Create(new ConfigurationPostParameters( + Configuration: config.ToJson(), + Schema: graphqlSchema, + ConnectionString: "AccountEndpoint=https://localhost:8081/;AccountKey=C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==", + AccessToken: null)); + } + + if (endpoint == CONFIGURATION_ENDPOINT_V2) + { + return JsonContent.Create(new ConfigurationPostParametersV2( + Configuration: config.ToJson(), + ConfigurationOverrides: "{}", + Schema: graphqlSchema, + AccessToken: null)); + } + + throw new ArgumentException($"Unknown endpoint: {endpoint}", nameof(endpoint)); + } +} diff --git a/src/Service/Startup.cs b/src/Service/Startup.cs index 876b63378e..a427e74584 100644 --- a/src/Service/Startup.cs +++ b/src/Service/Startup.cs @@ -7,6 +7,8 @@ using System.Net; using System.Net.Http; using System.Net.Http.Headers; +using System.Security.Cryptography; +using System.Text; using System.Threading.Tasks; using Azure.DataApiBuilder.Auth; using Azure.DataApiBuilder.Config; @@ -89,6 +91,19 @@ public class Startup(IConfiguration configuration, ILogger logger) public static AzureLogAnalyticsOptions AzureLogAnalyticsOptions = new(); public static FileSinkOptions FileSinkOptions = new(); public const string NO_HTTPS_REDIRECT_FLAG = "--no-https-redirect"; + + /// + /// Environment variable name for the optional bootstrap token that gates access to the + /// POST /configuration endpoint in late-configured (hosted) mode. When set, the request + /// must include an X-DAB-CONFIG-AUTH header whose value matches this token. + /// + internal const string CONFIG_AUTH_TOKEN_ENV_VAR = "DAB_CONFIG_AUTH_TOKEN"; + + /// + /// Header name used to supply the configuration bootstrap token. + /// + internal const string CONFIG_AUTH_HEADER = "X-DAB-CONFIG-AUTH"; + private readonly HotReloadEventHandler _hotReloadEventHandler = new(); private RuntimeConfigProvider? _configProvider; private ILogger _logger = logger; @@ -949,6 +964,10 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env, RuntimeC { context.Response.StatusCode = StatusCodes.Status409Conflict; } + else if (!IsConfigurationRequestAuthorized(context)) + { + context.Response.StatusCode = StatusCodes.Status403Forbidden; + } else { await next.Invoke(); @@ -1479,6 +1498,49 @@ private static bool IsUIEnabled(RuntimeConfig? runtimeConfig, IWebHostEnvironmen return (runtimeConfig is not null && runtimeConfig.IsDevelopmentMode()) || env.IsDevelopment(); } + /// + /// Determines whether a POST /configuration request is authorized by enforcing: + /// 1. The request must originate from a loopback address (localhost / 127.0.0.1 / ::1). + /// 2. If the DAB_CONFIG_AUTH_TOKEN environment variable is set, the request must include + /// an X-DAB-CONFIG-AUTH header whose value matches the token (constant-time comparison). + /// This mitigates Missing Authentication for Critical Function" by preventing + /// unauthenticated network-remote callers from hijacking the runtime configuration. + /// + internal static bool IsConfigurationRequestAuthorized(HttpContext context) + { + // Defense 1: Restrict to loopback addresses only. + // A null RemoteIpAddress indicates an in-process call (e.g. TestServer) + // where there is no underlying TCP connection — by definition this cannot + // be a remote attacker, so we treat it as loopback-equivalent. A real + // network-borne request always carries a TCP source IP. + IPAddress? remoteIp = context.Connection.RemoteIpAddress; + if (remoteIp is not null && !IPAddress.IsLoopback(remoteIp)) + { + return false; + } + + // Defense 2: If a bootstrap token is configured, require it in the request header. + string? expectedToken = Environment.GetEnvironmentVariable(CONFIG_AUTH_TOKEN_ENV_VAR); + if (!string.IsNullOrEmpty(expectedToken)) + { + string? providedToken = context.Request.Headers[CONFIG_AUTH_HEADER].FirstOrDefault(); + if (string.IsNullOrEmpty(providedToken)) + { + return false; + } + + // Use fixed-time comparison to prevent timing attacks. + if (!CryptographicOperations.FixedTimeEquals( + Encoding.UTF8.GetBytes(expectedToken), + Encoding.UTF8.GetBytes(providedToken))) + { + return false; + } + } + + return true; + } + /// /// Checks whether On-Behalf-Of (OBO) authentication is configured by verifying that /// the required environment variables are set and the config has user-delegated auth enabled. From 7579bbb4c4c48abe66ee365494f1ead30020d5b7 Mon Sep 17 00:00:00 2001 From: souvikghosh04 Date: Tue, 16 Jun 2026 19:02:06 +0530 Subject: [PATCH 2/4] Address PR review: normalize IPv4-mapped IPv6 loopback, doc typo, test - Handle ::ffff:127.0.0.1 (dual-stack Kestrel) as loopback by calling MapToIPv4 first - Fix stray quote in IsConfigurationRequestAuthorized XML doc - Add matrix row covering IPv4-mapped IPv6 loopback --- .../ConfigurationEndpointAuthorizationTests.cs | 1 + src/Service/Startup.cs | 18 +++++++++++++----- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/Service.Tests/Configuration/ConfigurationEndpointAuthorizationTests.cs b/src/Service.Tests/Configuration/ConfigurationEndpointAuthorizationTests.cs index 33b8d432ee..e4bf03b45e 100644 --- a/src/Service.Tests/Configuration/ConfigurationEndpointAuthorizationTests.cs +++ b/src/Service.Tests/Configuration/ConfigurationEndpointAuthorizationTests.cs @@ -39,6 +39,7 @@ public class ConfigurationEndpointAuthorizationTests // --- No bootstrap token configured --- [DataRow("127.0.0.1", null, null, true, DisplayName = "Loopback IPv4, no token => allow")] [DataRow("::1", null, null, true, DisplayName = "Loopback IPv6, no token => allow")] + [DataRow("::ffff:127.0.0.1", null, null, true, DisplayName = "IPv4-mapped IPv6 loopback (dual-stack Kestrel), no token => allow")] [DataRow(null, null, null, true, DisplayName = "In-process (null IP), no token => allow")] [DataRow("192.168.1.100", null, null, false, DisplayName = "Private IPv4, no token => deny")] [DataRow("10.0.0.1", null, null, false, DisplayName = "Private IPv4 (10/8), no token => deny")] diff --git a/src/Service/Startup.cs b/src/Service/Startup.cs index a427e74584..520a95b90d 100644 --- a/src/Service/Startup.cs +++ b/src/Service/Startup.cs @@ -1500,11 +1500,12 @@ private static bool IsUIEnabled(RuntimeConfig? runtimeConfig, IWebHostEnvironmen /// /// Determines whether a POST /configuration request is authorized by enforcing: - /// 1. The request must originate from a loopback address (localhost / 127.0.0.1 / ::1). + /// 1. The request must originate from a loopback address (localhost / 127.0.0.1 / ::1 + /// or its IPv4-mapped IPv6 form such as ::ffff:127.0.0.1). /// 2. If the DAB_CONFIG_AUTH_TOKEN environment variable is set, the request must include /// an X-DAB-CONFIG-AUTH header whose value matches the token (constant-time comparison). - /// This mitigates Missing Authentication for Critical Function" by preventing - /// unauthenticated network-remote callers from hijacking the runtime configuration. + /// This prevents unauthenticated network-remote callers from supplying the runtime + /// configuration during the late-configured bootstrap window. /// internal static bool IsConfigurationRequestAuthorized(HttpContext context) { @@ -1514,9 +1515,16 @@ internal static bool IsConfigurationRequestAuthorized(HttpContext context) // be a remote attacker, so we treat it as loopback-equivalent. A real // network-borne request always carries a TCP source IP. IPAddress? remoteIp = context.Connection.RemoteIpAddress; - if (remoteIp is not null && !IPAddress.IsLoopback(remoteIp)) + if (remoteIp is not null) { - return false; + // Kestrel often listens on dual-stack IPv6, so an IPv4 loopback caller can + // arrive as the IPv4-mapped IPv6 form (e.g. ::ffff:127.0.0.1). IPAddress.IsLoopback + // does not treat those as loopback, so normalize to IPv4 first. + IPAddress effectiveIp = remoteIp.IsIPv4MappedToIPv6 ? remoteIp.MapToIPv4() : remoteIp; + if (!IPAddress.IsLoopback(effectiveIp)) + { + return false; + } } // Defense 2: If a bootstrap token is configured, require it in the request header. From d0b0c0819a39d9332663924e064f61a55d4deeb0 Mon Sep 17 00:00:00 2001 From: souvikghosh04 Date: Tue, 16 Jun 2026 20:48:59 +0530 Subject: [PATCH 3/4] test: isolate ConfigurationEndpointAuthorization tests from env pollution Other tests in the assembly (e.g. TestLoadingLocalCosmosSettings) set ASPNETCORE_ENVIRONMENT and DAB_ENVIRONMENT without cleanup. Those env vars caused FileSystemRuntimeConfigLoader to find an environment-specific dab-config.*.json on disk and auto-initialize the runtime, which made POST /configuration return 409 Conflict before the new security middleware was exercised, breaking the EndToEnd matrix in CI. Snapshot and clear ASPNETCORE_ENVIRONMENT and DAB_ENVIRONMENT before creating the TestServer, and restore them in a finally block. Verified locally by setting ASPNETCORE_ENVIRONMENT=CosmosDb_NoSql in the parent shell and rerunning all 22 tests successfully. --- ...ConfigurationEndpointAuthorizationTests.cs | 46 +++++++++++++------ 1 file changed, 32 insertions(+), 14 deletions(-) diff --git a/src/Service.Tests/Configuration/ConfigurationEndpointAuthorizationTests.cs b/src/Service.Tests/Configuration/ConfigurationEndpointAuthorizationTests.cs index e4bf03b45e..f5973f60f4 100644 --- a/src/Service.Tests/Configuration/ConfigurationEndpointAuthorizationTests.cs +++ b/src/Service.Tests/Configuration/ConfigurationEndpointAuthorizationTests.cs @@ -104,25 +104,43 @@ public async Task EndToEnd_PostConfiguration_StatusCodeMatrix( HttpStatusCode expectedStatus) { // Arrange + // Other tests in the assembly (e.g. TestLoadingLocalCosmosSettings) set + // ASPNETCORE_ENVIRONMENT / DAB_ENVIRONMENT without cleanup. Those env vars cause + // FileSystemRuntimeConfigLoader to find an environment-specific dab-config.*.json on + // disk and auto-initialize the runtime, which makes POST /configuration return + // 409 Conflict before our security middleware can be exercised. Snapshot and clear + // them so each test starts from an uninitialized runtime. + string originalAspNetEnv = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT"); + string originalDabEnv = Environment.GetEnvironmentVariable("DAB_ENVIRONMENT"); + Environment.SetEnvironmentVariable("ASPNETCORE_ENVIRONMENT", null); + Environment.SetEnvironmentVariable("DAB_ENVIRONMENT", null); Environment.SetEnvironmentVariable(Startup.CONFIG_AUTH_TOKEN_ENV_VAR, configuredToken); - using TestServer server = new(Program.CreateWebHostFromInMemoryUpdatableConfBuilder(Array.Empty())); - using HttpClient httpClient = server.CreateClient(); - - HttpRequestMessage request = new(HttpMethod.Post, configurationEndpoint) + try { - Content = BuildPostContent(configurationEndpoint), - }; - if (providedHeader is not null) + using TestServer server = new(Program.CreateWebHostFromInMemoryUpdatableConfBuilder(Array.Empty())); + using HttpClient httpClient = server.CreateClient(); + + HttpRequestMessage request = new(HttpMethod.Post, configurationEndpoint) + { + Content = BuildPostContent(configurationEndpoint), + }; + if (providedHeader is not null) + { + request.Headers.Add(Startup.CONFIG_AUTH_HEADER, providedHeader); + } + + // Act + HttpResponseMessage postResult = await httpClient.SendAsync(request); + + // Assert + Assert.AreEqual(expectedStatus, postResult.StatusCode); + } + finally { - request.Headers.Add(Startup.CONFIG_AUTH_HEADER, providedHeader); + Environment.SetEnvironmentVariable("ASPNETCORE_ENVIRONMENT", originalAspNetEnv); + Environment.SetEnvironmentVariable("DAB_ENVIRONMENT", originalDabEnv); } - - // Act - HttpResponseMessage postResult = await httpClient.SendAsync(request); - - // Assert - Assert.AreEqual(expectedStatus, postResult.StatusCode); } [TestCleanup] From 79c2aebea1f09f40c03fbc0cf4c214f47c8a17b2 Mon Sep 17 00:00:00 2001 From: souvikghosh04 Date: Wed, 17 Jun 2026 10:03:41 +0530 Subject: [PATCH 4/4] test: snapshot/restore env vars and expand non-loopback matrix Addresses two review comments on PR #3669: 1. (anushakolan) Move DAB_CONFIG_AUTH_TOKEN, ASPNETCORE_ENVIRONMENT, and DAB_ENVIRONMENT snapshot/restore into TestInitialize/TestCleanup so any pre-existing values are preserved verbatim. Previously TestCleanup reset DAB_CONFIG_AUTH_TOKEN to null which could perturb other tests. 2. (RubenCerna2079) Add explicit unit-test matrix rows for private/public IPv4 with correct token, wrong token, and missing header so the 'non-loopback is always denied regardless of token' contract is regression-locked. Matrix grows from 14 to 18 rows; total tests 22 -> 26. Verified locally by setting both ASPNETCORE_ENVIRONMENT=CosmosDb_NoSql and DAB_CONFIG_AUTH_TOKEN=preexisting-value in the parent shell: all 26 tests pass and both env vars retain their original values after the run. --- ...ConfigurationEndpointAuthorizationTests.cs | 97 +++++++++++-------- 1 file changed, 58 insertions(+), 39 deletions(-) diff --git a/src/Service.Tests/Configuration/ConfigurationEndpointAuthorizationTests.cs b/src/Service.Tests/Configuration/ConfigurationEndpointAuthorizationTests.cs index f5973f60f4..fc9eb9a68e 100644 --- a/src/Service.Tests/Configuration/ConfigurationEndpointAuthorizationTests.cs +++ b/src/Service.Tests/Configuration/ConfigurationEndpointAuthorizationTests.cs @@ -23,6 +23,42 @@ namespace Azure.DataApiBuilder.Service.Tests.Configuration; [TestClass] public class ConfigurationEndpointAuthorizationTests { + // Names of environment variables this test class manipulates. Snapshotted in + // TestInitialize and restored in TestCleanup so each test starts and ends from + // the same global state regardless of what other tests in the assembly do. + private const string ASPNETCORE_ENVIRONMENT_VAR = "ASPNETCORE_ENVIRONMENT"; + private const string DAB_ENVIRONMENT_VAR = "DAB_ENVIRONMENT"; + + private string _originalAuthToken; + private string _originalAspNetEnvironment; + private string _originalDabEnvironment; + + [TestInitialize] + public void Initialize() + { + // Snapshot the originals so they can be restored verbatim in TestCleanup. + _originalAuthToken = Environment.GetEnvironmentVariable(Startup.CONFIG_AUTH_TOKEN_ENV_VAR); + _originalAspNetEnvironment = Environment.GetEnvironmentVariable(ASPNETCORE_ENVIRONMENT_VAR); + _originalDabEnvironment = Environment.GetEnvironmentVariable(DAB_ENVIRONMENT_VAR); + + // Other tests in the assembly (e.g. TestLoadingLocalCosmosSettings) set + // ASPNETCORE_ENVIRONMENT / DAB_ENVIRONMENT without cleanup. Those env vars cause + // FileSystemRuntimeConfigLoader to find an environment-specific dab-config.*.json on + // disk and auto-initialize the runtime, which makes POST /configuration return + // 409 Conflict before our security middleware can be exercised. Clear them so each + // test starts from an uninitialized runtime; the originals are restored in cleanup. + Environment.SetEnvironmentVariable(ASPNETCORE_ENVIRONMENT_VAR, null); + Environment.SetEnvironmentVariable(DAB_ENVIRONMENT_VAR, null); + } + + [TestCleanup] + public void Cleanup() + { + Environment.SetEnvironmentVariable(Startup.CONFIG_AUTH_TOKEN_ENV_VAR, _originalAuthToken); + Environment.SetEnvironmentVariable(ASPNETCORE_ENVIRONMENT_VAR, _originalAspNetEnvironment); + Environment.SetEnvironmentVariable(DAB_ENVIRONMENT_VAR, _originalDabEnvironment); + } + /// /// Validates IsConfigurationRequestAuthorized across the full matrix of inputs: /// remote IP (loopback v4/v6, private/public, null/in-process), configured bootstrap @@ -49,7 +85,11 @@ public class ConfigurationEndpointAuthorizationTests [DataRow("127.0.0.1", "secret", "secret", true, DisplayName = "Loopback + correct token => allow")] [DataRow("127.0.0.1", "secret", "wrong", false, DisplayName = "Loopback + wrong token => deny")] [DataRow("127.0.0.1", "secret", null, false, DisplayName = "Loopback + missing header => deny")] - [DataRow("192.168.1.50", "secret", "secret", false, DisplayName = "Non-loopback + correct token => still deny")] + [DataRow("192.168.1.50", "secret", "secret", false, DisplayName = "Private IPv4 + correct token => still deny (non-loopback)")] + [DataRow("192.168.1.50", "secret", "wrong", false, DisplayName = "Private IPv4 + wrong token => deny")] + [DataRow("203.0.113.50", "secret", "secret", false, DisplayName = "Public IPv4 + correct token => still deny (non-loopback)")] + [DataRow("203.0.113.50", "secret", "wrong", false, DisplayName = "Public IPv4 + wrong token => deny")] + [DataRow("203.0.113.50", "secret", null, false, DisplayName = "Public IPv4 + missing header => deny")] [DataRow(null, "secret", null, false, DisplayName = "In-process + missing header => deny")] [DataRow(null, "secret", "secret", true, DisplayName = "In-process + correct token => allow")] public void IsConfigurationRequestAuthorized_Matrix( @@ -103,50 +143,29 @@ public async Task EndToEnd_PostConfiguration_StatusCodeMatrix( string providedHeader, HttpStatusCode expectedStatus) { - // Arrange - // Other tests in the assembly (e.g. TestLoadingLocalCosmosSettings) set - // ASPNETCORE_ENVIRONMENT / DAB_ENVIRONMENT without cleanup. Those env vars cause - // FileSystemRuntimeConfigLoader to find an environment-specific dab-config.*.json on - // disk and auto-initialize the runtime, which makes POST /configuration return - // 409 Conflict before our security middleware can be exercised. Snapshot and clear - // them so each test starts from an uninitialized runtime. - string originalAspNetEnv = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT"); - string originalDabEnv = Environment.GetEnvironmentVariable("DAB_ENVIRONMENT"); - Environment.SetEnvironmentVariable("ASPNETCORE_ENVIRONMENT", null); - Environment.SetEnvironmentVariable("DAB_ENVIRONMENT", null); + // Arrange -- ASPNETCORE_ENVIRONMENT / DAB_ENVIRONMENT are already cleared by + // TestInitialize so the runtime starts uninitialized and our middleware is + // exercised. Only the per-row auth token still needs to be set here; TestCleanup + // restores the original value. Environment.SetEnvironmentVariable(Startup.CONFIG_AUTH_TOKEN_ENV_VAR, configuredToken); - try + using TestServer server = new(Program.CreateWebHostFromInMemoryUpdatableConfBuilder(Array.Empty())); + using HttpClient httpClient = server.CreateClient(); + + HttpRequestMessage request = new(HttpMethod.Post, configurationEndpoint) { - using TestServer server = new(Program.CreateWebHostFromInMemoryUpdatableConfBuilder(Array.Empty())); - using HttpClient httpClient = server.CreateClient(); - - HttpRequestMessage request = new(HttpMethod.Post, configurationEndpoint) - { - Content = BuildPostContent(configurationEndpoint), - }; - if (providedHeader is not null) - { - request.Headers.Add(Startup.CONFIG_AUTH_HEADER, providedHeader); - } - - // Act - HttpResponseMessage postResult = await httpClient.SendAsync(request); - - // Assert - Assert.AreEqual(expectedStatus, postResult.StatusCode); - } - finally + Content = BuildPostContent(configurationEndpoint), + }; + if (providedHeader is not null) { - Environment.SetEnvironmentVariable("ASPNETCORE_ENVIRONMENT", originalAspNetEnv); - Environment.SetEnvironmentVariable("DAB_ENVIRONMENT", originalDabEnv); + request.Headers.Add(Startup.CONFIG_AUTH_HEADER, providedHeader); } - } - [TestCleanup] - public void Cleanup() - { - Environment.SetEnvironmentVariable(Startup.CONFIG_AUTH_TOKEN_ENV_VAR, null); + // Act + HttpResponseMessage postResult = await httpClient.SendAsync(request); + + // Assert + Assert.AreEqual(expectedStatus, postResult.StatusCode); } ///