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.
This commit is contained in:
+23
-5
@@ -2,6 +2,7 @@ using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Hosting;
|
||||
using ZB.MOM.WW.Secrets.Abstractions;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient;
|
||||
|
||||
@@ -31,22 +32,37 @@ public static class OpcUaClientDriverFactoryExtensions
|
||||
Converters = { new JsonStringEnumConverter() },
|
||||
};
|
||||
|
||||
/// <summary>Register the OpcUaClient factory with the driver registry.</summary>
|
||||
/// <summary>Register the OpcUaClient factory with the driver registry, threading the shared secret resolver.</summary>
|
||||
/// <param name="registry">The driver factory registry to register with.</param>
|
||||
/// <param name="loggerFactory">Optional logger factory used to create per-instance loggers.</param>
|
||||
public static void Register(DriverFactoryRegistry registry, ILoggerFactory? loggerFactory = null)
|
||||
/// <param name="secretResolver">
|
||||
/// The shared secret resolver (from DI) used to resolve the <c>secret:</c> arm of each
|
||||
/// instance's <c>Password</c> / <c>UserCertificatePassword</c> at session-open. Defaults to
|
||||
/// the <see cref="NullSecretResolver"/> null-object for the test/standalone path — callers
|
||||
/// that need a working <c>secret:</c> credential ref must thread the DI resolver.
|
||||
/// </param>
|
||||
public static void Register(
|
||||
DriverFactoryRegistry registry,
|
||||
ILoggerFactory? loggerFactory = null,
|
||||
ISecretResolver? secretResolver = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(registry);
|
||||
registry.Register(DriverTypeName, (id, json) => CreateInstance(id, json, loggerFactory));
|
||||
var resolver = secretResolver ?? NullSecretResolver.Instance;
|
||||
registry.Register(DriverTypeName, (id, json) => CreateInstance(id, json, loggerFactory, resolver));
|
||||
}
|
||||
|
||||
/// <summary>Public for the Server-side bootstrapper + test consumers.</summary>
|
||||
/// <param name="driverInstanceId">The unique identifier for the driver instance.</param>
|
||||
/// <param name="driverConfigJson">The JSON configuration string for the driver.</param>
|
||||
/// <param name="loggerFactory">Optional logger factory for the per-instance logger.</param>
|
||||
/// <param name="secretResolver">
|
||||
/// Optional shared secret resolver for the driver's <c>secret:</c> credential arm; defaults
|
||||
/// to the <see cref="NullSecretResolver"/> null-object (fail-closed on a <c>secret:</c> ref).
|
||||
/// </param>
|
||||
/// <returns>A configured <see cref="OpcUaClientDriver"/>.</returns>
|
||||
public static OpcUaClientDriver CreateInstance(
|
||||
string driverInstanceId, string driverConfigJson, ILoggerFactory? loggerFactory = null)
|
||||
string driverInstanceId, string driverConfigJson, ILoggerFactory? loggerFactory = null,
|
||||
ISecretResolver? secretResolver = null)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(driverInstanceId);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(driverConfigJson);
|
||||
@@ -55,6 +71,8 @@ public static class OpcUaClientDriverFactoryExtensions
|
||||
?? throw new InvalidOperationException(
|
||||
$"OpcUaClient driver config for '{driverInstanceId}' deserialised to null");
|
||||
|
||||
return new OpcUaClientDriver(options, driverInstanceId, loggerFactory?.CreateLogger<OpcUaClientDriver>());
|
||||
return new OpcUaClientDriver(
|
||||
options, driverInstanceId, loggerFactory?.CreateLogger<OpcUaClientDriver>(),
|
||||
secretResolver ?? NullSecretResolver.Instance);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user