Files
lmxopcua/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient/OpcUaClientDriverFactoryExtensions.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

79 lines
4.4 KiB
C#

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;
/// <summary>
/// Registers the OPC UA Client driver with the <see cref="DriverFactoryRegistry"/>. The driver's
/// <c>DriverConfig</c> JSON deserialises directly into <see cref="OpcUaClientDriverOptions"/>
/// (the same shape <see cref="OpcUaClientDriverProbe"/> reads), so no separate DTO is needed —
/// unlike the protocol drivers, this driver has no pre-declared "Tags" DTO path to retire.
/// v3: the deploy artifact's authored raw tags bind straight onto
/// <see cref="OpcUaClientDriverOptions.RawTags"/> (a <c>RawTagEntry</c> list) through the same
/// deserialiser; the <c>EndpointUrl</c> binding is unchanged (endpoint→DeviceConfig is Wave C).
/// Mirrors <c>ModbusDriverFactoryExtensions</c> / <c>GalaxyDriverFactoryExtensions</c>.
/// </summary>
public static class OpcUaClientDriverFactoryExtensions
{
/// <summary>Driver type name — matches <c>DriverInstance.DriverType</c> values.</summary>
public const string DriverTypeName = "OpcUaClient";
// Match OpcUaClientDriverProbe exactly so factory + probe parse a config identically.
// The JsonStringEnumConverter lets enum-valued knobs (SecurityMode / SecurityPolicy /
// AuthType / TargetNamespaceKind) be authored as their string names — the natural form
// for human-edited + AdminUI-emitted DriverConfig JSON.
private static readonly JsonSerializerOptions JsonOptions = new()
{
PropertyNameCaseInsensitive = true,
UnmappedMemberHandling = JsonUnmappedMemberHandling.Skip,
Converters = { new JsonStringEnumConverter() },
};
/// <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>
/// <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);
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,
ISecretResolver? secretResolver = null)
{
ArgumentException.ThrowIfNullOrWhiteSpace(driverInstanceId);
ArgumentException.ThrowIfNullOrWhiteSpace(driverConfigJson);
var options = JsonSerializer.Deserialize<OpcUaClientDriverOptions>(driverConfigJson, JsonOptions)
?? throw new InvalidOperationException(
$"OpcUaClient driver config for '{driverInstanceId}' deserialised to null");
return new OpcUaClientDriver(
options, driverInstanceId, loggerFactory?.CreateLogger<OpcUaClientDriver>(),
secretResolver ?? NullSecretResolver.Instance);
}
}