using Microsoft.Extensions.Logging;
using ZB.MOM.WW.Secrets.Abstractions;
namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Config;
///
/// Resolves Gateway.ApiKeySecretRef to the actual API-key string. Five
/// forms supported, evaluated in order:
///
/// - env:NAME — reads Environment.GetEnvironmentVariable(NAME).
/// Throws when the variable is unset, so a misconfigured deployment fails
/// fast rather than silently sending an empty key.
/// - file:PATH — reads UTF-8 text from PATH, trimming
/// whitespace. Lets operators stash the key in an ACL'd file outside the
/// repo (the same pattern as the legacy .local/galaxy-host-secret.txt).
/// - dev:KEY — explicit cleartext literal. The dev: prefix
/// is a deliberate opt-in signal (dev box, parity rig) so the resolver
/// doesn't emit a warning; production should never use this arm.
/// - secret:NAME — resolves NAME through the shared
/// ZB.MOM.WW.Secrets (the encrypted-at-rest
/// store). Fail-closed: a secret: ref whose secret is absent/tombstoned
/// throws rather than falling through to the literal arm — the production path
/// that retires the cleartext dev:/literal-in-DB model.
/// - Anything else — used as the literal API key for back-compat with
/// configs that pre-date this resolver. When a logger is supplied the
/// resolver emits a startup warning so an operator who accidentally
/// committed a cleartext key sees it.
///
/// A future PR can swap any of these arms for a different backing store without
/// changing the call site.
///
///
/// Lives in the Contracts project so both the runtime GalaxyDriver and the
/// AdminUI GalaxyDriverBrowser (which intentionally don't reference each
/// other) share a single resolver rather than each maintaining a copy.
///
public static class GalaxySecretRef
{
///
/// Resolves the supplied secret reference. The secret:NAME arm resolves
/// through and is fail-closed (throws when the secret
/// is absent). When the ref falls through to the back-compat literal arm (an
/// unprefixed cleartext API key in DriverConfig JSON) and a
/// is supplied, emits a .
/// The dev: prefix is the explicit opt-in path that doesn't warn.
///
/// The secret reference string to resolve.
/// The shared secret resolver used by the secret: arm.
/// Optional logger for warning on cleartext keys.
/// Cancellation token for the async secret: resolution.
/// The resolved API-key string.
public static async Task ResolveApiKeyAsync(
string secretRef,
ISecretResolver resolver,
ILogger? logger = null,
CancellationToken ct = default)
{
ArgumentException.ThrowIfNullOrEmpty(secretRef);
ArgumentNullException.ThrowIfNull(resolver);
if (secretRef.StartsWith("env:", StringComparison.OrdinalIgnoreCase))
{
var name = secretRef[4..];
var value = Environment.GetEnvironmentVariable(name);
return !string.IsNullOrEmpty(value)
? value
: throw new InvalidOperationException(
$"Galaxy.Gateway.ApiKeySecretRef='{secretRef}' resolves to env var '{name}', but it is unset.");
}
if (secretRef.StartsWith("file:", StringComparison.OrdinalIgnoreCase))
{
var path = secretRef[5..];
if (!File.Exists(path))
{
throw new InvalidOperationException(
$"Galaxy.Gateway.ApiKeySecretRef='{secretRef}' points at '{path}', which doesn't exist.");
}
var contents = File.ReadAllText(path).Trim();
return !string.IsNullOrEmpty(contents)
? contents
: throw new InvalidOperationException(
$"Galaxy.Gateway.ApiKeySecretRef='{secretRef}' file '{path}' is empty.");
}
if (secretRef.StartsWith("dev:", StringComparison.OrdinalIgnoreCase))
{
// Explicit dev opt-in — no warning, the operator deliberately chose a
// cleartext literal (dev box, parity rig).
return secretRef[4..];
}
if (secretRef.StartsWith("secret:", StringComparison.OrdinalIgnoreCase))
{
// Production path: resolve the name through the shared encrypted secret store.
// Fail-closed — an absent/tombstoned secret throws rather than falling through
// to the literal arm (which would silently treat the ref string as the key).
var name = secretRef["secret:".Length..];
var value = await resolver.GetAsync(new SecretName(name), ct).ConfigureAwait(false);
return !string.IsNullOrEmpty(value)
? value
: throw new InvalidOperationException(
$"Galaxy.Gateway.ApiKeySecretRef='{secretRef}' resolves secret '{name}', but it is absent from the store (fail-closed).");
}
// Back-compat literal arm. An unprefixed string is treated as the literal
// API key — but emit a warning so an operator who accidentally committed a
// cleartext key into DriverConfig sees it. Use the dev: prefix to suppress
// this warning when the literal is intentional.
logger?.LogWarning(
"Galaxy.Gateway.ApiKeySecretRef is being treated as a literal cleartext API key. " +
"Prefer env:NAME, file:PATH, or the explicit dev:KEY prefix for dev rigs — " +
"a literal key in DriverConfig JSON is stored in cleartext in the central config DB.");
return secretRef;
}
}