Files
lmxopcua/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient/NullSecretResolver.cs
T
Joseph Doherty 9bb237b794 feat(secrets): G-2b resolve OpcUaClient secret: Password/UserCertificatePassword (Task 8)
Resolve secret:-prefixed Password + UserCertificatePassword through the shared
ISecretResolver, fail-closed on absent, retiring the cleartext-in-DB path. The
driver-registry factory is synchronous (Func<string,string,IDriver>), so resolution
is done lazily in the async session-open (InitializeAsync, before BuildUserIdentity)
rather than at deserialize — mirroring Task 7's Galaxy pattern and matching its
re-resolve-on-reconnect behavior. Both consumers (username Password and the
certificate-password LoadPkcs12 path via BuildCertificateIdentity) see the resolved
connect-scoped options; _options stays raw (secret: refs intact), no long-lived
plaintext field.

Scope corrections vs the plan (verified against v3): the probe is unauthenticated
GetEndpoints-only and never reads either credential, so it is NOT a resolution site
(comment added, no dead code); OpcUaClientDriverOptions was a sealed class, converted
to sealed record for the with-expression (no positional params → identical JSON; no
reference-equality/dict-key/ToString-log usages → no behavior/leak change).

ISecretResolver threaded via factory Register/CreateInstance + DriverFactoryBootstrap
(real resolver from DI); NullSecretResolver null-object backs test/parse paths only,
fail-closed on secret: refs. TDD: 4 helper tests RED→GREEN; 142 OpcUaClient tests pass.
2026-07-16 18:14:12 -04:00

27 lines
1.1 KiB
C#

using ZB.MOM.WW.Secrets.Abstractions;
namespace ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient;
/// <summary>
/// A null-object <see cref="ISecretResolver"/> that resolves every name to <c>null</c>
/// (absent). Backs the driver's default (test/parse-only) construction path so callers that
/// never author a <c>secret:</c> credential ref need not thread a real resolver. A production
/// <c>OpcUaClientDriver</c> always receives the DI-registered resolver via the factory; a
/// <c>secret:</c> ref resolved against this null object throws fail-closed (the secret is
/// reported absent), which is the correct behaviour for a mis-wired deployment — the
/// <c>secret:</c> literal is never sent verbatim as a password.
/// </summary>
internal sealed class NullSecretResolver : ISecretResolver
{
/// <summary>The shared singleton instance.</summary>
public static readonly NullSecretResolver Instance = new();
private NullSecretResolver()
{
}
/// <inheritdoc />
public Task<string?> GetAsync(SecretName name, CancellationToken ct) =>
Task.FromResult<string?>(null);
}