fix(management): elide DataConnection config secrets from responses and audit (arch-review C3)
Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
This commit is contained in:
@@ -1445,12 +1445,37 @@ public class ManagementActor : ReceiveActor
|
||||
// Data Connection handlers
|
||||
// ========================================================================
|
||||
|
||||
/// <summary>
|
||||
/// Secret-elided projection of a DataConnection (arch-review C3): the raw
|
||||
/// PrimaryConfiguration/BackupConfiguration JSON can embed OPC UA user and
|
||||
/// certificate passwords, and List/Get are readable by any authenticated user.
|
||||
/// Mirrors <see cref="SmtpConfigPublicShape"/>. The secret-bearing config blobs
|
||||
/// are scrubbed to the redaction sentinel; every non-config field is projected
|
||||
/// verbatim so callers keep the full shape they had before.
|
||||
/// </summary>
|
||||
private static object DataConnectionPublicShape(DataConnection c)
|
||||
{
|
||||
var (primary, s1) = ConfigSecretScrubber.Scrub(c.PrimaryConfiguration);
|
||||
var (backup, s2) = ConfigSecretScrubber.Scrub(c.BackupConfiguration);
|
||||
return new
|
||||
{
|
||||
c.Id,
|
||||
c.SiteId,
|
||||
c.Name,
|
||||
c.Protocol,
|
||||
c.FailoverRetryCount,
|
||||
PrimaryConfiguration = primary,
|
||||
BackupConfiguration = backup,
|
||||
HasSecrets = s1 || s2,
|
||||
};
|
||||
}
|
||||
|
||||
private static async Task<object?> HandleListDataConnections(IServiceProvider sp, ListDataConnectionsCommand cmd)
|
||||
{
|
||||
var repo = sp.GetRequiredService<ISiteRepository>();
|
||||
if (cmd.SiteId.HasValue)
|
||||
return await repo.GetDataConnectionsBySiteIdAsync(cmd.SiteId.Value);
|
||||
return await repo.GetAllDataConnectionsAsync();
|
||||
return (await repo.GetDataConnectionsBySiteIdAsync(cmd.SiteId.Value)).Select(DataConnectionPublicShape).ToList();
|
||||
return (await repo.GetAllDataConnectionsAsync()).Select(DataConnectionPublicShape).ToList();
|
||||
}
|
||||
|
||||
private static async Task<object?> HandleGetDataConnection(IServiceProvider sp, GetDataConnectionCommand cmd, AuthenticatedUser user)
|
||||
@@ -1459,7 +1484,7 @@ public class ManagementActor : ReceiveActor
|
||||
var conn = await repo.GetDataConnectionByIdAsync(cmd.DataConnectionId);
|
||||
if (conn != null)
|
||||
EnforceSiteScope(user, conn.SiteId);
|
||||
return conn;
|
||||
return conn is null ? null : DataConnectionPublicShape(conn);
|
||||
}
|
||||
|
||||
private static async Task<object?> HandleCreateDataConnection(IServiceProvider sp, CreateDataConnectionCommand cmd, string user)
|
||||
@@ -1473,8 +1498,9 @@ public class ManagementActor : ReceiveActor
|
||||
};
|
||||
await repo.AddDataConnectionAsync(conn);
|
||||
await repo.SaveChangesAsync();
|
||||
await AuditAsync(sp, user, "Create", "DataConnection", conn.Id.ToString(), conn.Name, conn);
|
||||
return conn;
|
||||
var publicShape = DataConnectionPublicShape(conn);
|
||||
await AuditAsync(sp, user, "Create", "DataConnection", conn.Id.ToString(), conn.Name, publicShape);
|
||||
return publicShape;
|
||||
}
|
||||
|
||||
private static async Task<object?> HandleUpdateDataConnection(IServiceProvider sp, UpdateDataConnectionCommand cmd, string user)
|
||||
@@ -1484,13 +1510,17 @@ public class ManagementActor : ReceiveActor
|
||||
?? throw new ManagementCommandException($"DataConnection with ID {cmd.DataConnectionId} not found.");
|
||||
conn.Name = cmd.Name;
|
||||
conn.Protocol = cmd.Protocol;
|
||||
conn.PrimaryConfiguration = cmd.PrimaryConfiguration;
|
||||
conn.BackupConfiguration = cmd.BackupConfiguration;
|
||||
// Sentinel-preserving merge: a client that round-trips a scrubbed config
|
||||
// (secrets replaced with the redaction sentinel) keeps the stored secret
|
||||
// rather than wiping it. Non-sentinel values overwrite as usual.
|
||||
conn.PrimaryConfiguration = ConfigSecretScrubber.MergeSentinels(cmd.PrimaryConfiguration, conn.PrimaryConfiguration);
|
||||
conn.BackupConfiguration = ConfigSecretScrubber.MergeSentinels(cmd.BackupConfiguration, conn.BackupConfiguration);
|
||||
conn.FailoverRetryCount = cmd.FailoverRetryCount;
|
||||
await repo.UpdateDataConnectionAsync(conn);
|
||||
await repo.SaveChangesAsync();
|
||||
await AuditAsync(sp, user, "Update", "DataConnection", conn.Id.ToString(), conn.Name, conn);
|
||||
return conn;
|
||||
var publicShape = DataConnectionPublicShape(conn);
|
||||
await AuditAsync(sp, user, "Update", "DataConnection", conn.Id.ToString(), conn.Name, publicShape);
|
||||
return publicShape;
|
||||
}
|
||||
|
||||
private static async Task<object?> HandleDeleteDataConnection(IServiceProvider sp, DeleteDataConnectionCommand cmd, string user)
|
||||
|
||||
Reference in New Issue
Block a user