feat(artifacts): central ships each site's referenced secrets (ciphertext) and fails closed on a missing one
ArtifactDeploymentService now resolves each site's secret: references (via the new SiteSecretsCollector, wrapping ISecretStore) into DeployArtifactsCommand.Secrets, never plaintext. A referenced secret that is absent or tombstoned fails that site's deploy before anything is sent, with a named error, while the rest of the batch still deploys; ComputeCurrentArtifactHashesAsync instead folds a MissingSecrets marker into the hash so such a site reads Out of date. Central logs what it ships (name@revision) and errors on what's missing, never material. Also updates the two other direct ArtifactDeploymentService construction sites (PendingUpdatesEvaluatorTests, CentralUI SitesPageTests) for the new constructor parameter.
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Artifacts;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types.Secrets;
|
||||
using ZB.MOM.WW.Secrets.Abstractions;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.DeploymentManager;
|
||||
|
||||
/// <summary>
|
||||
/// Result of <see cref="SiteSecretsCollector.CollectAsync"/>: the resolved ciphertext rows for a
|
||||
/// site's referenced secrets, and the references that could not be resolved (absent or
|
||||
/// tombstoned) at central.
|
||||
/// </summary>
|
||||
public sealed record SiteSecretsCollection(
|
||||
IReadOnlyList<SecretArtifact> Secrets,
|
||||
IReadOnlyList<SecretReference> Missing);
|
||||
|
||||
/// <summary>
|
||||
/// Central half of design §3.1/§3.3: derives which secrets a site's data connections reference
|
||||
/// (via <see cref="SecretReferenceScanner"/>) and resolves each one against the central
|
||||
/// <see cref="ISecretStore"/>, verbatim, as its ciphertext <see cref="StoredSecret"/> row. Never
|
||||
/// decrypts and never sees plaintext — the store's <see cref="ISecretStore.GetAsync"/> already
|
||||
/// returns only the encrypted representation.
|
||||
/// </summary>
|
||||
public sealed class SiteSecretsCollector(ISecretStore store)
|
||||
{
|
||||
/// <summary>
|
||||
/// Scans <paramref name="connections"/> for <c>secret:<name></c> references and
|
||||
/// resolves each distinct name against the central secret store.
|
||||
/// </summary>
|
||||
/// <param name="connections">The site's data-connection artifacts to scan.</param>
|
||||
/// <param name="ct">A token to cancel the operation.</param>
|
||||
/// <returns>
|
||||
/// The resolved rows (mapped field-for-field from <see cref="StoredSecret"/>) and the
|
||||
/// references that were absent or tombstoned in the store.
|
||||
/// </returns>
|
||||
public async Task<SiteSecretsCollection> CollectAsync(
|
||||
IEnumerable<DataConnectionArtifact> connections, CancellationToken ct)
|
||||
{
|
||||
var secrets = new List<SecretArtifact>();
|
||||
var missing = new List<SecretReference>();
|
||||
|
||||
foreach (var reference in SecretReferenceScanner.Scan(connections))
|
||||
{
|
||||
var row = await store.GetAsync(new SecretName(reference.Name), ct);
|
||||
if (row is null || row.IsDeleted)
|
||||
{
|
||||
missing.Add(reference);
|
||||
continue;
|
||||
}
|
||||
|
||||
secrets.Add(new SecretArtifact(
|
||||
row.Name.Value,
|
||||
row.ContentType.ToString(),
|
||||
row.Ciphertext,
|
||||
row.Nonce,
|
||||
row.Tag,
|
||||
row.WrappedDek,
|
||||
row.WrapNonce,
|
||||
row.WrapTag,
|
||||
row.KekId,
|
||||
row.Revision,
|
||||
row.CreatedUtc,
|
||||
row.UpdatedUtc,
|
||||
row.Description));
|
||||
}
|
||||
|
||||
return new SiteSecretsCollection(secrets, missing);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user