namespace ZB.MOM.WW.Secrets.Replicator.SqlServer; /// /// Configuration for the shared SQL-Server secret store and its hub replication. Bound from a /// configuration section by the AddZbSecretsSqlServer* extensions. /// public sealed class SqlServerSecretsOptions { private string _schemaName = SqlServerSecretsSchema.DefaultSchemaName; /// /// Connection string for the shared secret database. Required. Supply it through a /// ${secret:} / secret: reference like every other connection string in the family /// — the shared store holds ciphertext only, but its credentials are still credentials. /// public string ConnectionString { get; set; } = string.Empty; /// /// SQL schema the secret tables live in. Defaults to zbsecrets so the secret tables never /// collide with an application's own dbo objects when they share a database. /// /// /// The value is not a plain identifier (letters, digits, and underscore, not starting with a /// digit, at most 128 characters). /// /// /// Validated on assignment rather than at use: the schema name is interpolated into DDL and /// query text (T-SQL cannot parameterize an object name), so the allow-list is the injection /// boundary and it must be impossible to get an unvalidated value into the property at all. /// public string SchemaName { get => _schemaName; set => _schemaName = ValidateIdentifier(value); } /// /// How often the sync service reconciles the local store against the hub, in hub-replication /// mode. Defaults to 30 seconds. Ignored in shared-store mode (there is nothing to reconcile). /// public TimeSpan SyncInterval { get; set; } = TimeSpan.FromSeconds(30); /// /// When (the default), hub-replication mode runs a reconcile at startup /// before the first interval elapses, so a node that was down while a peer wrote does not serve /// stale secrets for a whole interval after it comes back. /// public bool SyncOnStartup { get; set; } = true; /// /// Command timeout applied to every store operation. Defaults to 30 seconds — long enough for a /// contended shared database, short enough that a hung hub surfaces as an error rather than /// stalling a resolve indefinitely. /// public TimeSpan CommandTimeout { get; set; } = TimeSpan.FromSeconds(30); /// /// Throws if the options are unusable. Called by the DI extensions at registration time so a /// misconfigured node fails at startup rather than on the first secret resolve. /// /// The connection string is missing, or an interval is not positive. public void Validate() { if (string.IsNullOrWhiteSpace(ConnectionString)) { throw new InvalidOperationException( "SQL-Server secret store requires a ConnectionString (Secrets:SqlServer:ConnectionString)."); } if (SyncInterval <= TimeSpan.Zero) { throw new InvalidOperationException( $"SQL-Server secret replication SyncInterval must be positive (was {SyncInterval})."); } if (CommandTimeout <= TimeSpan.Zero) { throw new InvalidOperationException( $"SQL-Server secret store CommandTimeout must be positive (was {CommandTimeout})."); } } /// /// Strict allow-list for a SQL identifier that will be interpolated into DDL or query text. /// Rejects everything that is not [A-Za-z_][A-Za-z0-9_]{0,127} — no brackets, quotes, /// dots, or spaces — so no bracket-escape (]]) trick can break out of the quoting applied /// at the use site. Uses the ASCII-only character tests deliberately, so homoglyph and /// full-width look-alikes are rejected rather than accepted as "letters". /// /// The candidate identifier. /// The same value, once validated. /// The value is not a plain identifier. internal static string ValidateIdentifier(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); if (value.Length > 128) { throw new ArgumentException( $"SQL schema name must be at most 128 characters (was {value.Length}).", nameof(value)); } if (!char.IsAsciiLetter(value[0]) && value[0] != '_') { throw new ArgumentException( $"SQL schema name must start with a letter or underscore (was '{value}').", nameof(value)); } foreach (char c in value) { if (!char.IsAsciiLetterOrDigit(c) && c != '_') { throw new ArgumentException( $"SQL schema name may contain only letters, digits, and underscore (was '{value}').", nameof(value)); } } return value; } }