fix(configuration-database): resolve ConfigurationDatabase-002..007 — remove hardcoded sa creds, fail-fast no-arg DI, encrypt secret columns, resilient audit serialization

This commit is contained in:
Joseph Doherty
2026-05-16 21:11:24 -04:00
parent 8fc04d43c2
commit 0c82ffcbe6
17 changed files with 2029 additions and 40 deletions

View File

@@ -15,10 +15,28 @@ public static class ServiceCollectionExtensions
/// </summary>
public static IServiceCollection AddConfigurationDatabase(this IServiceCollection services, string connectionString)
{
services.AddDbContext<ScadaLinkDbContext>(options =>
// The DbContext is constructed via the (options, IDataProtectionProvider) overload so
// secret-bearing configuration columns are encrypted at rest. AddDataProtection below
// registers IDataProtectionProvider as a singleton; resolving it here does not recurse
// because key-ring loading is lazy (first Protect/Unprotect), not triggered by
// CreateProtector during model building.
services.AddDbContext<ScadaLinkDbContext>((serviceProvider, options) =>
{
options.UseSqlServer(connectionString)
.ConfigureWarnings(w => w.Ignore(
Microsoft.EntityFrameworkCore.Diagnostics.RelationalEventId.PendingModelChangesWarning)));
Microsoft.EntityFrameworkCore.Diagnostics.RelationalEventId.PendingModelChangesWarning));
});
// AddDbContext registers ScadaLinkDbContext via EF's activator, which only injects
// DbContextOptions. Override that registration (last registration wins for resolution)
// with a factory that also supplies the IDataProtectionProvider, so the encrypting
// value converter for secret columns is always wired up at runtime.
services.AddScoped(serviceProvider =>
{
var options = serviceProvider.GetRequiredService<DbContextOptions<ScadaLinkDbContext>>();
var protectionProvider = serviceProvider.GetRequiredService<IDataProtectionProvider>();
return new ScadaLinkDbContext(options, protectionProvider);
});
services.AddScoped<ISecurityRepository, SecurityRepository>();
services.AddScoped<ICentralUiRepository, CentralUiRepository>();
@@ -38,13 +56,27 @@ public static class ServiceCollectionExtensions
}
/// <summary>
/// Registers the ScadaLinkDbContext with no connection string (for backward compatibility / Phase 0 stubs).
/// This overload is a no-op placeholder; callers should migrate to the overload that accepts a connection string.
/// Obsolete parameterless overload. This previously registered nothing, which meant a
/// central node wired up with it failed late and opaquely — the first repository
/// resolution threw a DI exception far from the actual misconfiguration. Use
/// <see cref="AddConfigurationDatabase(IServiceCollection, string)"/> and pass the
/// configured connection string.
/// </summary>
/// <exception cref="InvalidOperationException">
/// Always thrown. The connection string is required; there is no valid no-op registration.
/// </exception>
[Obsolete(
"AddConfigurationDatabase() with no connection string registers nothing and is not a " +
"valid configuration. Call AddConfigurationDatabase(connectionString) instead.",
error: true)]
public static IServiceCollection AddConfigurationDatabase(this IServiceCollection services)
{
// Retained for backward compatibility during migration.
// Site nodes do not use the configuration database, so this is intentionally a no-op.
return services;
// Defence-in-depth: even if a caller suppresses the compile-time obsolete error,
// fail fast at wire-up time rather than silently registering nothing and surfacing
// an opaque DI resolution failure much later.
throw new InvalidOperationException(
"AddConfigurationDatabase() requires a connection string. Call " +
"AddConfigurationDatabase(connectionString) with the configured " +
"'ScadaLink:Database:ConfigurationDb' value.");
}
}