Architecture remediation: P1 tier (process & hardening) #121
@@ -163,7 +163,15 @@ public sealed class SelfSignedCertificateProvider
|
|||||||
// temp file empty, harden its permissions, and only then write the PFX into
|
// temp file empty, harden its permissions, and only then write the PFX into
|
||||||
// the already-protected file. The temp path is in the same directory as the
|
// the already-protected file. The temp path is in the same directory as the
|
||||||
// target so the Move is atomic and preserves the hardened DACL/mode.
|
// target so the Move is atomic and preserves the hardened DACL/mode.
|
||||||
string temp = path + ".tmp";
|
//
|
||||||
|
// The temp name carries a unique suffix rather than a fixed "<path>.tmp": two
|
||||||
|
// processes (or two parallel callers) generating to the same target must not
|
||||||
|
// collide on one temp file. On Windows a fixed name makes the second writer's
|
||||||
|
// File.Create/Move fail with "the process cannot access the file ... because it
|
||||||
|
// is being used by another process"; a unique name lets each generation stage
|
||||||
|
// independently, and the final atomic Move (last-writer-wins) still yields a
|
||||||
|
// valid, equivalent certificate at the shared path.
|
||||||
|
string temp = $"{path}.{Guid.NewGuid():N}.tmp";
|
||||||
using (File.Create(temp)) { }
|
using (File.Create(temp)) { }
|
||||||
HardenPermissions(temp);
|
HardenPermissions(temp);
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
||||||
using ZB.MOM.WW.Auth.ApiKeys.Sqlite;
|
using ZB.MOM.WW.Auth.ApiKeys.Sqlite;
|
||||||
using ZB.MOM.WW.MxGateway.Server.Diagnostics;
|
using ZB.MOM.WW.MxGateway.Server.Diagnostics;
|
||||||
|
using ZB.MOM.WW.MxGateway.Tests.Security.Authentication;
|
||||||
|
|
||||||
namespace ZB.MOM.WW.MxGateway.Tests.Diagnostics;
|
namespace ZB.MOM.WW.MxGateway.Tests.Diagnostics;
|
||||||
|
|
||||||
@@ -17,15 +18,15 @@ public sealed class AuthStoreHealthCheckTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task Healthy_WhenStoreReachable()
|
public async Task Healthy_WhenStoreReachable()
|
||||||
{
|
{
|
||||||
var path = Path.Combine(Path.GetTempPath(), $"authcheck-{Guid.NewGuid():N}.db");
|
// Open a real SQLite file via the health check. TempDatabaseDirectory clears the
|
||||||
try
|
// Microsoft.Data.Sqlite connection pool on dispose before deleting the file; without
|
||||||
{
|
// that the pool keeps the .db handle open and the delete throws "used by another
|
||||||
var check = new AuthStoreHealthCheck(FactoryFor(path));
|
// process" on Windows (a latent full-suite flake — the file opens fine on macOS).
|
||||||
|
using TempDatabaseDirectory directory = TempDatabaseDirectory.Create("authcheck");
|
||||||
|
var check = new AuthStoreHealthCheck(FactoryFor(directory.DatabasePath()));
|
||||||
var result = await check.CheckHealthAsync(new HealthCheckContext());
|
var result = await check.CheckHealthAsync(new HealthCheckContext());
|
||||||
Assert.Equal(HealthStatus.Healthy, result.Status);
|
Assert.Equal(HealthStatus.Healthy, result.Status);
|
||||||
}
|
}
|
||||||
finally { if (File.Exists(path)) File.Delete(path); }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>The health check reports unhealthy when the database path cannot be opened.</summary>
|
/// <summary>The health check reports unhealthy when the database path cannot be opened.</summary>
|
||||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||||
|
|||||||
@@ -9,6 +9,10 @@ using ZB.MOM.WW.MxGateway.Server;
|
|||||||
|
|
||||||
namespace ZB.MOM.WW.MxGateway.Tests.Gateway;
|
namespace ZB.MOM.WW.MxGateway.Tests.Gateway;
|
||||||
|
|
||||||
|
// Sets process-global Kestrel/TLS environment variables that GatewayApplication.Build reads;
|
||||||
|
// serialized against all other collections so a parallel host-building test cannot inherit them
|
||||||
|
// mid-run and race on the generated-certificate file. See GlobalEnvironmentCollection.
|
||||||
|
[Collection(TestSupport.GlobalEnvironmentCollection.Name)]
|
||||||
public sealed class GatewayTlsBootstrapTests
|
public sealed class GatewayTlsBootstrapTests
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
namespace ZB.MOM.WW.MxGateway.Tests.TestSupport;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// xUnit collection for tests that mutate <em>process-global</em> state (environment variables
|
||||||
|
/// read by <c>GatewayApplication.Build</c>, e.g. <c>Kestrel__Endpoints__…</c> and
|
||||||
|
/// <c>MxGateway__Tls__SelfSignedCertPath</c>). <c>DisableParallelization</c> keeps such a test from
|
||||||
|
/// running concurrently with any other collection: otherwise a parallel host-building test inherits
|
||||||
|
/// the mutated variables mid-run and the two race on the same generated-certificate file (on Windows,
|
||||||
|
/// "the process cannot access the file … because it is being used by another process"). Membership is
|
||||||
|
/// deliberately narrow — only add classes that set/clear real environment variables, not ones that
|
||||||
|
/// pass configuration through <c>Build([...])</c> command-line args.
|
||||||
|
/// </summary>
|
||||||
|
[CollectionDefinition(Name, DisableParallelization = true)]
|
||||||
|
public sealed class GlobalEnvironmentCollection
|
||||||
|
{
|
||||||
|
/// <summary>The collection name applied via <c>[Collection(GlobalEnvironmentCollection.Name)]</c>.</summary>
|
||||||
|
public const string Name = "GlobalEnvironmentMutation";
|
||||||
|
}
|
||||||
@@ -3,7 +3,8 @@ using System.Runtime.CompilerServices;
|
|||||||
namespace ZB.MOM.WW.MxGateway.Tests.TestSupport;
|
namespace ZB.MOM.WW.MxGateway.Tests.TestSupport;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Defaults the host environment to Development for the whole test assembly.
|
/// Defaults the host environment to Development and isolates the test process's on-disk
|
||||||
|
/// gateway paths, for the whole test assembly.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// Many tests build the full gateway host through <c>GatewayApplication.Build</c> against the dev
|
/// Many tests build the full gateway host through <c>GatewayApplication.Build</c> against the dev
|
||||||
@@ -15,6 +16,15 @@ namespace ZB.MOM.WW.MxGateway.Tests.TestSupport;
|
|||||||
/// <c>GatewayOptionsValidator</c> with its <c>isProduction</c> constructor (no host environment) and
|
/// <c>GatewayOptionsValidator</c> with its <c>isProduction</c> constructor (no host environment) and
|
||||||
/// are unaffected; a test that needs Production can still pass <c>--environment=Production</c>, which
|
/// are unaffected; a test that needs Production can still pass <c>--environment=Production</c>, which
|
||||||
/// overrides this default.
|
/// overrides this default.
|
||||||
|
/// <para>
|
||||||
|
/// The self-signed-cert path is also defaulted to a per-process temp file. Otherwise every
|
||||||
|
/// full-host test that triggers HTTPS-cert generation writes the shared
|
||||||
|
/// <c>CommonApplicationData/MxGateway/certs/gateway-selfsigned.pfx</c> default — parallel xUnit
|
||||||
|
/// test classes then collide on that path's temp file (Windows: "the process cannot access the
|
||||||
|
/// file ... because it is being used by another process"), and on a shared CI/dev box the suite
|
||||||
|
/// also fights the deployed gateway service for the same file. Pointing at a per-process path
|
||||||
|
/// isolates the suite: the first host-building test generates the cert, the rest load it.
|
||||||
|
/// </para>
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
internal static class TestHostEnvironmentInitializer
|
internal static class TestHostEnvironmentInitializer
|
||||||
{
|
{
|
||||||
@@ -25,5 +35,17 @@ internal static class TestHostEnvironmentInitializer
|
|||||||
{
|
{
|
||||||
Environment.SetEnvironmentVariable("ASPNETCORE_ENVIRONMENT", "Development");
|
Environment.SetEnvironmentVariable("ASPNETCORE_ENVIRONMENT", "Development");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(Environment.GetEnvironmentVariable("MxGateway__Tls__SelfSignedCertPath")))
|
||||||
|
{
|
||||||
|
// ProcessId keeps the path stable within this test process (so a valid cert
|
||||||
|
// generated by the first host-building test is reused by the rest) yet unique
|
||||||
|
// across processes (so concurrent test runs / the deployed service never share it).
|
||||||
|
string certPath = Path.Combine(
|
||||||
|
Path.GetTempPath(),
|
||||||
|
$"mxgw-tests-{Environment.ProcessId}",
|
||||||
|
"gateway-selfsigned.pfx");
|
||||||
|
Environment.SetEnvironmentVariable("MxGateway__Tls__SelfSignedCertPath", certPath);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user