f40409a688
Tracking IDs are review bookkeeping that rots in shipped source; the rationale they tagged is kept. Comment-only change (CommentChecker 458 -> 16 false positives).
155 lines
6.6 KiB
C#
155 lines
6.6 KiB
C#
using ZB.MOM.WW.Configuration;
|
|
|
|
namespace ZB.MOM.WW.MxGateway.Server.Configuration;
|
|
|
|
/// <summary>
|
|
/// Shared filesystem-path validation primitives used by more than one options validator
|
|
/// (<see cref="GatewayOptionsValidator"/> and <see cref="GalaxyRepositoryOptionsValidator"/>).
|
|
/// Both the auth credential store and the Galaxy snapshot are written by the running gateway
|
|
/// process, so both must reject paths the host cannot use — the rules live here once so the two
|
|
/// validators cannot drift.
|
|
/// </summary>
|
|
internal static class GatewayConfigPathRules
|
|
{
|
|
/// <summary>
|
|
/// Fails validation when <paramref name="value"/> is not an absolute (rooted) path <em>on the
|
|
/// host running the validator</em>. Security-sensitive paths (the auth DB, the self-signed
|
|
/// private key, the Galaxy snapshot) must be absolute: a non-rooted value silently resolves
|
|
/// against the launch working directory, so the store moves with the CWD and can leak into the
|
|
/// source tree. Rooting is checked with <see cref="Path.IsPathRooted(string)"/> — the current
|
|
/// OS — so a Windows drive/UNC literal on a Unix host fails fast at startup rather than being
|
|
/// blessed and then written as a junk-named relative file. Reject
|
|
/// rather than auto-root; silent relocation of a credential store is worse than a boot error.
|
|
/// Blank is handled by the caller's required-field check and is not treated as non-rooted here.
|
|
/// </summary>
|
|
/// <param name="value">The configured path value.</param>
|
|
/// <param name="message">The failure message to record when the value is not rooted.</param>
|
|
/// <param name="builder">The validation builder accumulating failures.</param>
|
|
public static void AddIfNotRooted(string? value, string message, ValidationBuilder builder)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(value))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!Path.IsPathRooted(value))
|
|
{
|
|
builder.Add(message);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Fails validation when <paramref name="value"/> is non-blank but not a syntactically valid
|
|
/// filesystem path (as judged by <see cref="Path.GetFullPath(string)"/>). Blank values are the
|
|
/// caller's required-field concern and pass here.
|
|
/// </summary>
|
|
/// <param name="value">The configured path value.</param>
|
|
/// <param name="message">The failure message to record when the value is not a valid path.</param>
|
|
/// <param name="builder">The validation builder accumulating failures.</param>
|
|
public static void AddIfInvalidPath(string? value, string message, ValidationBuilder builder)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(value))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!TryGetFullPath(value, out _))
|
|
{
|
|
builder.Add(message);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Fails validation when <paramref name="value"/> resolves to a location inside
|
|
/// <paramref name="contentRoot"/> — the directory the application runs from.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// <b>Rooted is not the same as safe, and this is the rule that closes the gap.</b>
|
|
/// <see cref="AddIfNotRooted"/> stops a store drifting with the working directory, but an
|
|
/// absolute path <em>inside the app directory</em> passes it cleanly — and that is what failed
|
|
/// in production on 2026-08-09. The upgrade procedure renames the app directory to
|
|
/// <c>Server.bak.*</c> and unpacks a new one; a store living there is renamed away with it, the
|
|
/// process then creates a fresh empty one at the same path, and nothing reports an error. All
|
|
/// API keys were lost and no gRPC client could authenticate for two days. The deploy itself was
|
|
/// executed correctly — the binaries were the point of the rename, and the store was collateral.
|
|
/// </para>
|
|
/// <para>
|
|
/// The same shape catches the dev-side symptom: a store under the content root lands in the
|
|
/// source tree, which is how <c>mxgateway-secrets.db</c> once tripped the repository's
|
|
/// tree-hygiene test.
|
|
/// </para>
|
|
/// <para>
|
|
/// Comparison is case-insensitive only on Windows. On a case-insensitive macOS volume this can
|
|
/// miss a violation that differs only in case, which is a missed warning in dev; assuming
|
|
/// case-insensitivity on Linux would instead reject a legitimate path, and a false startup
|
|
/// abort is the worse failure.
|
|
/// </para>
|
|
/// </remarks>
|
|
/// <param name="value">The configured path value.</param>
|
|
/// <param name="contentRoot">The application content root to test against.</param>
|
|
/// <param name="message">The failure message to record when the value is under the content root.</param>
|
|
/// <param name="builder">The validation builder accumulating failures.</param>
|
|
public static void AddIfUnderContentRoot(
|
|
string? value,
|
|
string? contentRoot,
|
|
string message,
|
|
ValidationBuilder builder)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(value) || string.IsNullOrWhiteSpace(contentRoot))
|
|
{
|
|
return;
|
|
}
|
|
|
|
// A malformed path is AddIfInvalidPath's message to report; staying silent here keeps one
|
|
// bad value from producing two failures that say different things about the same mistake.
|
|
if (!TryGetFullPath(value, out string fullValue) || !TryGetFullPath(contentRoot, out string fullRoot))
|
|
{
|
|
return;
|
|
}
|
|
|
|
fullRoot = fullRoot.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
|
|
|
|
StringComparison comparison = OperatingSystem.IsWindows()
|
|
? StringComparison.OrdinalIgnoreCase
|
|
: StringComparison.Ordinal;
|
|
|
|
// The separator is load-bearing: a bare prefix test would also match a sibling directory
|
|
// whose name merely starts with the root's ("/srv/app" against "/srv/app-data").
|
|
if (string.Equals(fullValue, fullRoot, comparison)
|
|
|| fullValue.StartsWith(fullRoot + Path.DirectorySeparatorChar, comparison))
|
|
{
|
|
builder.Add(message);
|
|
}
|
|
}
|
|
|
|
private static bool TryGetFullPath(string value, out string fullPath)
|
|
{
|
|
try
|
|
{
|
|
fullPath = Path.GetFullPath(value);
|
|
return true;
|
|
}
|
|
catch (ArgumentException)
|
|
{
|
|
fullPath = string.Empty;
|
|
return false;
|
|
}
|
|
catch (NotSupportedException)
|
|
{
|
|
fullPath = string.Empty;
|
|
return false;
|
|
}
|
|
catch (PathTooLongException)
|
|
{
|
|
fullPath = string.Empty;
|
|
return false;
|
|
}
|
|
catch (IOException)
|
|
{
|
|
fullPath = string.Empty;
|
|
return false;
|
|
}
|
|
}
|
|
}
|