3cc8069150
SqlDriverFactoryExtensions turns a deployed DriverConfig blob into a live SqlDriver, and SqlConnectionStringResolver resolves the authored connectionStringRef NAME from Sql__ConnectionStrings__<ref> so credentials never ride in a config blob (design §8.2). Three behaviours worth naming: - String-enum guard. JsonOptions carries JsonStringEnumConverter + camelCase, so `provider` parses whether authored as a name or an ordinal and always round-trips as the NAME — the systemic AdminUI defect where a page serialised an enum numerically against a string-typed DTO. - Config validation lands here because nothing below does it. operationTimeout must be STRICTLY greater than commandTimeout (design §8.3); the reader and the driver stay deliberately usable with the pair inverted so the frozen-database tests can prove the client-side bound fires. Non-positive timeouts / poll interval and maxConcurrentGroups < 1 are rejected too. - Credential hygiene. The resolved connection string reaches the provider and nothing else: messages name the ref, the environment variable, or SqlDriver.Endpoint's credential-free server/database rendering. Both a log-leak and an exception-leak test cover it. DTO changes: - Adds RawTags (List<RawTagEntry>), following the Modbus precedent — the deploy artifact delivers a driver's tags this way and the DTO had no way to receive them, so an authored Sql tag could never reach the driver. - Deletes SqlProbeDto + the `probe` key. It had no consumer: SqlDriver was specified without a background probe loop, and deliberately so — its IHostConnectivityProbe state is a by-product of the Initialize liveness check and of every poll, i.e. a statement about traffic that actually happened rather than a synthetic ping. On-demand connectivity is Task 10's SqlDriverProbe. Shipping a config key that silently does nothing is worse than not shipping it; UnmappedMemberHandling.Skip means a blob that still carries `probe` keeps parsing. allowWrites stays inert (SqlDriver implements no write capability) but an authored `true` now WARNS rather than passing silently — the flag cannot do harm, an operator who believes writes are enabled can. Claude-Session: https://claude.ai/code/session_01GASWkNEi68FSCtvr6rLoEW
63 lines
3.9 KiB
C#
63 lines
3.9 KiB
C#
namespace ZB.MOM.WW.OtOpcUa.Driver.Sql;
|
|
|
|
/// <summary>
|
|
/// Resolves a <c>Sql</c> driver config's <c>connectionStringRef</c> — a NAME — into the connection string
|
|
/// it stands for (design §8.2). The deployed artifact carries only the name, so database credentials never
|
|
/// ride in a config blob, never land in the central config DB, and never appear in a deployment diff.
|
|
/// <para><b>The environment is read directly, on purpose.</b> A driver factory is invoked through
|
|
/// <c>DriverFactoryRegistry</c>'s <c>(driverInstanceId, driverConfigJson)</c> closure — there is no
|
|
/// <c>IConfiguration</c>, no <c>IServiceProvider</c> and no ambient scope at that seam, and widening the
|
|
/// registry's signature for one driver would change every driver in the tree. The
|
|
/// <c>Sql__ConnectionStrings__<ref></c> spelling is exactly the key .NET's environment-variable
|
|
/// configuration provider would bind to <c>Sql:ConnectionStrings:<ref></c>, so an operator can set it
|
|
/// the same way they set every other secret and a later move onto <c>IConfiguration</c> needs no
|
|
/// re-provisioning.</para>
|
|
/// <para><b>Nothing here ever emits the resolved value.</b> Messages name the environment
|
|
/// <em>variable</em>, which is not a secret and is the operator's next action; the value itself is returned
|
|
/// to exactly one caller and is otherwise unmentioned.</para>
|
|
/// </summary>
|
|
public static class SqlConnectionStringResolver
|
|
{
|
|
/// <summary>
|
|
/// The environment-variable prefix a <c>connectionStringRef</c> is appended to. The double underscore
|
|
/// is .NET's configuration hierarchy separator, so this is the environment spelling of
|
|
/// <c>Sql:ConnectionStrings:<ref></c>.
|
|
/// </summary>
|
|
public const string EnvironmentVariablePrefix = "Sql__ConnectionStrings__";
|
|
|
|
/// <summary>The environment variable a given <c>connectionStringRef</c> is read from.</summary>
|
|
/// <param name="connectionStringRef">The authored reference name.</param>
|
|
/// <returns>The full environment-variable name.</returns>
|
|
/// <exception cref="ArgumentException"><paramref name="connectionStringRef"/> is null, empty or whitespace.</exception>
|
|
public static string EnvironmentVariableFor(string connectionStringRef)
|
|
{
|
|
ArgumentException.ThrowIfNullOrWhiteSpace(connectionStringRef);
|
|
return string.Concat(EnvironmentVariablePrefix, connectionStringRef);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resolves <paramref name="connectionStringRef"/> to its connection string.
|
|
/// <para>An absent <b>or blank</b> variable is a half-provisioned deployment and throws: handing an
|
|
/// empty string to the provider would surface as an unintelligible ADO.NET error at the first poll,
|
|
/// far from the missing secret that actually caused it.</para>
|
|
/// </summary>
|
|
/// <param name="connectionStringRef">The authored reference name.</param>
|
|
/// <returns>The resolved connection string. <b>Treat as a secret</b> — never log it.</returns>
|
|
/// <exception cref="ArgumentException"><paramref name="connectionStringRef"/> is null, empty or whitespace.</exception>
|
|
/// <exception cref="InvalidOperationException">The environment variable is absent or blank.</exception>
|
|
public static string Resolve(string connectionStringRef)
|
|
{
|
|
var variable = EnvironmentVariableFor(connectionStringRef);
|
|
var value = Environment.GetEnvironmentVariable(variable);
|
|
if (string.IsNullOrWhiteSpace(value))
|
|
{
|
|
throw new InvalidOperationException(
|
|
$"Sql connection string '{connectionStringRef}' is not provisioned: set the environment " +
|
|
$"variable '{variable}' on every node that runs this driver. The connection string is " +
|
|
$"deliberately not carried in the deployed configuration.");
|
|
}
|
|
|
|
return value;
|
|
}
|
|
}
|