fix(SEC-33,SEC-34): host-meaningful path rooting; verification-cache invalidate race

SEC-33: make rooting host-meaningful and stop shipping foreign-platform literals.
- Delete IsRootedForAnyPlatform; AddIfNotRooted now uses Path.IsPathRooted (current OS).
- Promote AddIfNotRooted/AddIfInvalidPath to shared GatewayConfigPathRules so the new
  Galaxy validator reuses them and the two validators cannot drift.
- Remove Authentication:SqlitePath and Galaxy:SnapshotCachePath Windows literals from
  appsettings.json; the CommonApplicationData-derived code defaults take over. The
  Galaxy default is seeded as a configuration value before AddZbGalaxyRepository
  (SnapshotCachePath is init-only, so a PostConfigure mutation cannot compile).
- New GalaxyRepositoryOptionsValidator (ValidateOnStart) enforces a valid, host-rooted
  SnapshotCachePath when PersistSnapshot is true.
- Root-cause the stray junk-named auth DB: host start eagerly builds
  AuthSqliteConnectionFactory; under the non-rooted Windows literal on macOS SQLite
  wrote it relative to the test bin CWD. The three real-host-start tests now pin
  SqlitePath to a temp path.

SEC-34: verification cache Invalidate-vs-in-flight-repopulation race closed with a
per-key generation counter (bump-before-evict, snapshot-then-recheck). The expiry
cap (window 2) takes the documented fallback: the library verification identity
carries no ExpiresUtc, so the cache cannot cap at the key's expiry (donor-library ask).

GWC-24 rider: cap MxGateway:Events:QueueCapacity at int.MaxValue/2 so the derived
checked(2 * EventChannelCapacity) in WorkerClient cannot overflow at session creation.

SEC-35 (doc-only): note IsProduction() env-name semantics in GatewayConfiguration.md.

Docs updated same commit (GatewayConfiguration.md, Authentication.md) and tracking
registers/change-log flipped (00-tracking.md, 40-security-dashboard.md).
This commit is contained in:
Joseph Doherty
2026-08-07 06:36:01 -04:00
parent 3f854d6cbf
commit 7e7f7cad84
15 changed files with 548 additions and 112 deletions
@@ -0,0 +1,82 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using ZB.MOM.WW.GalaxyRepository;
using ZB.MOM.WW.MxGateway.Server;
using ZB.MOM.WW.MxGateway.Server.Configuration;
namespace ZB.MOM.WW.MxGateway.Tests.Configuration;
/// <summary>
/// Gateway-owned validation of the shared <see cref="GalaxyRepositoryOptions"/> (SEC-33): the Galaxy
/// package binds the options but ships no validator, so the gateway rejects a persisted snapshot
/// whose path is blank, invalid, or non-rooted on the running host, and supplies a rooted per-OS
/// default when the shipped config leaves the path blank.
/// </summary>
public sealed class GalaxyRepositoryOptionsValidatorTests
{
/// <summary>Verifies a blank snapshot path with persistence enabled fails validation.</summary>
[Fact]
public void Validate_Fails_WhenPersistSnapshotAndPathBlank()
{
GalaxyRepositoryOptions options = new() { PersistSnapshot = true, SnapshotCachePath = "" };
ValidateOptionsResult result = new GalaxyRepositoryOptionsValidator().Validate(null, options);
Assert.True(result.Failed);
Assert.Contains(result.Failures!, f => f.Contains("MxGateway:Galaxy:SnapshotCachePath"));
}
/// <summary>Verifies a non-rooted snapshot path with persistence enabled fails validation.</summary>
[Fact]
public void Validate_Fails_WhenPersistSnapshotAndPathNotRooted()
{
GalaxyRepositoryOptions options = new()
{
PersistSnapshot = true,
SnapshotCachePath = "galaxy-snapshot.json",
};
ValidateOptionsResult result = new GalaxyRepositoryOptionsValidator().Validate(null, options);
Assert.True(result.Failed);
Assert.Contains(
result.Failures!,
f => f.Contains("MxGateway:Galaxy:SnapshotCachePath") && f.Contains("rooted"));
}
/// <summary>Verifies a blank path passes when persistence is disabled (the path is never used).</summary>
[Fact]
public void Validate_Succeeds_WhenPersistSnapshotDisabled()
{
GalaxyRepositoryOptions options = new() { PersistSnapshot = false, SnapshotCachePath = "" };
ValidateOptionsResult result = new GalaxyRepositoryOptionsValidator().Validate(null, options);
Assert.True(result.Succeeded);
}
/// <summary>Verifies a valid, host-rooted snapshot path with persistence enabled passes.</summary>
[Fact]
public void Validate_Succeeds_WhenPersistSnapshotAndPathRooted()
{
GalaxyRepositoryOptions options = new()
{
PersistSnapshot = true,
SnapshotCachePath = Path.Combine(Path.GetTempPath(), "galaxy-snapshot.json"),
};
ValidateOptionsResult result = new GalaxyRepositoryOptionsValidator().Validate(null, options);
Assert.True(result.Succeeded);
}
/// <summary>
/// Verifies the gateway supplies a rooted per-OS default when the shipped config leaves
/// SnapshotCachePath blank, so the removed appsettings literal is not needed and validation
/// passes on any host (SEC-33). Resolving the options triggers the registered validator.
/// </summary>
[Fact]
public void Build_DefaultsBlankSnapshotCachePathToRootedDefault()
{
using WebApplication app = GatewayApplication.Build([]);
GalaxyRepositoryOptions options =
app.Services.GetRequiredService<IOptions<GalaxyRepositoryOptions>>().Value;
Assert.False(string.IsNullOrWhiteSpace(options.SnapshotCachePath));
Assert.True(Path.IsPathRooted(options.SnapshotCachePath));
}
}