109 lines
4.9 KiB
C#
109 lines
4.9 KiB
C#
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, the references that could not be resolved (absent or
|
|
/// tombstoned) at central, and — when the central secret store itself could not be read — the
|
|
/// error that stopped resolution.
|
|
/// </summary>
|
|
/// <param name="Secrets">The resolved rows.</param>
|
|
/// <param name="Missing">References that are absent or tombstoned in the store.</param>
|
|
/// <param name="StoreError">
|
|
/// <see langword="null"/> when every reference was looked up. Otherwise the store read failed
|
|
/// (e.g. the shared SQL Server secret store is unreachable), resolution stopped
|
|
/// at the first failure, and <see cref="Secrets"/>/<see cref="Missing"/> are INCOMPLETE — the
|
|
/// caller must treat this site as undeployable and its current hash as unknown. The message is
|
|
/// the exception type and message only; it never contains secret material.
|
|
/// </param>
|
|
public sealed record SiteSecretsCollection(
|
|
IReadOnlyList<SecretArtifact> Secrets,
|
|
IReadOnlyList<SecretReference> Missing,
|
|
string? StoreError = null);
|
|
|
|
/// <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>
|
|
/// <param name="lookupCache">
|
|
/// Optional per-sweep memo of store lookups, keyed by secret name (a <see langword="null"/>
|
|
/// value records "absent"). A multi-site caller passes ONE dictionary for the whole sweep so
|
|
/// a secret referenced by N sites is read from the store once, not N times. Only successful
|
|
/// lookups are recorded; a store read failure is never cached. Not thread-safe — the
|
|
/// callers build their per-site commands serially.
|
|
/// </param>
|
|
/// <returns>
|
|
/// The resolved rows (mapped field-for-field from <see cref="StoredSecret"/>) and the
|
|
/// references that were absent or tombstoned in the store. A store read failure is
|
|
/// returned as <see cref="SiteSecretsCollection.StoreError"/> rather than thrown, so one
|
|
/// site's lookup failing never aborts a whole multi-site sweep; only
|
|
/// cancellation of <paramref name="ct"/> propagates. A site whose connections reference no
|
|
/// secret never touches the store at all.
|
|
/// </returns>
|
|
public async Task<SiteSecretsCollection> CollectAsync(
|
|
IEnumerable<DataConnectionArtifact> connections, CancellationToken ct,
|
|
IDictionary<string, StoredSecret?>? lookupCache = null)
|
|
{
|
|
var secrets = new List<SecretArtifact>();
|
|
var missing = new List<SecretReference>();
|
|
|
|
foreach (var reference in SecretReferenceScanner.Scan(connections))
|
|
{
|
|
StoredSecret? row;
|
|
try
|
|
{
|
|
if (lookupCache is null || !lookupCache.TryGetValue(reference.Name, out row))
|
|
{
|
|
row = await store.GetAsync(new SecretName(reference.Name), ct);
|
|
if (lookupCache is not null)
|
|
lookupCache[reference.Name] = row;
|
|
}
|
|
}
|
|
catch (Exception ex) when (!ct.IsCancellationRequested)
|
|
{
|
|
return new SiteSecretsCollection(
|
|
secrets, missing,
|
|
$"Central secret store could not be read while resolving '{reference.Name}' " +
|
|
$"({ex.GetType().Name}: {ex.Message})");
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|