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

@@ -1,3 +1,4 @@
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.DataProtection.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using ScadaLink.Commons.Entities.Audit;
@@ -15,10 +16,24 @@ namespace ScadaLink.ConfigurationDatabase;
public class ScadaLinkDbContext : DbContext, IDataProtectionKeyContext
{
private readonly IDataProtectionProvider? _dataProtectionProvider;
public ScadaLinkDbContext(DbContextOptions<ScadaLinkDbContext> options) : base(options)
{
}
/// <summary>
/// Creates a context with an explicit Data Protection provider used to encrypt
/// secret-bearing configuration columns at rest. The runtime resolves this overload
/// via DI; design-time tooling uses the single-argument overload.
/// </summary>
public ScadaLinkDbContext(DbContextOptions<ScadaLinkDbContext> options, IDataProtectionProvider dataProtectionProvider)
: base(options)
{
_dataProtectionProvider = dataProtectionProvider
?? throw new ArgumentNullException(nameof(dataProtectionProvider));
}
// Templates
public DbSet<Template> Templates => Set<Template>();
public DbSet<TemplateAttribute> TemplateAttributes => Set<TemplateAttribute>();
@@ -73,5 +88,38 @@ public class ScadaLinkDbContext : DbContext, IDataProtectionKeyContext
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.ApplyConfigurationsFromAssembly(typeof(ScadaLinkDbContext).Assembly);
ApplySecretColumnEncryption(modelBuilder);
}
/// <summary>
/// Applies encryption-at-rest to columns that hold authentication secrets
/// (SMTP credentials, external-system auth config, database connection strings)
/// so they are never persisted as plaintext.
/// </summary>
/// <remarks>
/// When no Data Protection provider is supplied (design-time <c>dotnet ef</c> tooling,
/// which only emits schema and never reads or writes secret data), an ephemeral provider
/// is used. The encrypted-column type is <c>nvarchar</c> either way, so the generated
/// schema is identical regardless of which provider is in effect. The runtime path always
/// receives the DI-registered provider whose keys are persisted to this database.
/// </remarks>
private void ApplySecretColumnEncryption(ModelBuilder modelBuilder)
{
IDataProtectionProvider provider = _dataProtectionProvider ?? new EphemeralDataProtectionProvider();
var converter = new EncryptedStringConverter(
provider.CreateProtector(EncryptedStringConverter.ProtectorPurpose));
modelBuilder.Entity<SmtpConfiguration>()
.Property(s => s.Credentials)
.HasConversion(converter);
modelBuilder.Entity<ExternalSystemDefinition>()
.Property(e => e.AuthConfiguration)
.HasConversion(converter);
modelBuilder.Entity<DatabaseConnectionDefinition>()
.Property(d => d.ConnectionString)
.HasConversion((Microsoft.EntityFrameworkCore.Storage.ValueConversion.ValueConverter)converter);
}
}