fix(management): elide DatabaseConnection ConnectionString from List/Get (arch-review C3)

Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
This commit is contained in:
Joseph Doherty
2026-07-10 05:15:55 -04:00
parent ecf8ac1b7d
commit d4d1732a9e
4 changed files with 83 additions and 6 deletions
@@ -3,5 +3,8 @@ namespace ZB.MOM.WW.ScadaBridge.Commons.Messages.Management;
public record ListDatabaseConnectionsCommand;
public record GetDatabaseConnectionCommand(int DatabaseConnectionId);
public record CreateDatabaseConnectionDefCommand(string Name, string ConnectionString);
public record UpdateDatabaseConnectionDefCommand(int DatabaseConnectionId, string Name, string ConnectionString);
// ConnectionString is nullable (arch-review C3): an omitted (null) value preserves
// the stored secret rather than wiping it — the connection string is elided from
// List/Get responses, so a round-trip must not require re-sending it.
public record UpdateDatabaseConnectionDefCommand(int DatabaseConnectionId, string Name, string? ConnectionString);
public record DeleteDatabaseConnectionDefCommand(int DatabaseConnectionId);
@@ -2637,16 +2637,30 @@ public class ManagementActor : ReceiveActor
// Database Connection Definition handlers
// ========================================================================
/// <summary>
/// Secret-elided DatabaseConnectionDefinition projection (arch-review C3): the
/// ADO.NET <c>ConnectionString</c> can embed the SQL password and must never
/// leave this boundary via List/Get responses. Presence is surfaced as
/// <c>HasConnectionString</c>. Mirrors <see cref="SmtpConfigPublicShape"/>.
/// </summary>
private static object DatabaseConnectionPublicShape(DatabaseConnectionDefinition d) => new
{
d.Id,
d.Name,
HasConnectionString = !string.IsNullOrEmpty(d.ConnectionString),
};
private static async Task<object?> HandleListDatabaseConnections(IServiceProvider sp)
{
var repo = sp.GetRequiredService<IExternalSystemRepository>();
return await repo.GetAllDatabaseConnectionsAsync();
return (await repo.GetAllDatabaseConnectionsAsync()).Select(DatabaseConnectionPublicShape).ToList();
}
private static async Task<object?> HandleGetDatabaseConnection(IServiceProvider sp, GetDatabaseConnectionCommand cmd)
{
var repo = sp.GetRequiredService<IExternalSystemRepository>();
return await repo.GetDatabaseConnectionByIdAsync(cmd.DatabaseConnectionId);
var def = await repo.GetDatabaseConnectionByIdAsync(cmd.DatabaseConnectionId);
return def is null ? null : DatabaseConnectionPublicShape(def);
}
private static async Task<object?> HandleCreateDatabaseConnection(IServiceProvider sp, CreateDatabaseConnectionDefCommand cmd, string user)
@@ -2656,7 +2670,7 @@ public class ManagementActor : ReceiveActor
await repo.AddDatabaseConnectionAsync(def);
await repo.SaveChangesAsync();
await AuditAsync(sp, user, "Create", "DatabaseConnection", def.Id.ToString(), def.Name, new { def.Id, def.Name });
return def;
return DatabaseConnectionPublicShape(def);
}
private static async Task<object?> HandleUpdateDatabaseConnection(IServiceProvider sp, UpdateDatabaseConnectionDefCommand cmd, string user)
@@ -2665,11 +2679,14 @@ public class ManagementActor : ReceiveActor
var def = await repo.GetDatabaseConnectionByIdAsync(cmd.DatabaseConnectionId)
?? throw new ManagementCommandException($"DatabaseConnection with ID {cmd.DatabaseConnectionId} not found.");
def.Name = cmd.Name;
def.ConnectionString = cmd.ConnectionString;
// Preserve-if-null: an update that omits ConnectionString leaves the stored
// secret intact — the connection string is elided from List/Get, so a
// round-trip must not be required to re-send it.
if (cmd.ConnectionString is not null) def.ConnectionString = cmd.ConnectionString;
await repo.UpdateDatabaseConnectionAsync(def);
await repo.SaveChangesAsync();
await AuditAsync(sp, user, "Update", "DatabaseConnection", def.Id.ToString(), def.Name, new { def.Id, def.Name });
return def;
return DatabaseConnectionPublicShape(def);
}
private static async Task<object?> HandleDeleteDatabaseConnection(IServiceProvider sp, DeleteDatabaseConnectionDefCommand cmd, string user)