feat(dcl): Layer D OpcUaGlobalOptions for app-wide identity + cert paths

New deployment-wide options bound from the "OpcUa" section of appsettings.json:
- ApplicationName (default "ScadaLink-DCL")
- TrustedIssuerStorePath / TrustedPeerStorePath / RejectedCertificateStorePath

Empty paths fall back to Path.GetTempPath()/ScadaLink/pki/* so dev runs work
without explicit config — same defaults the hardcoded values previously used.

Wiring:
- ServiceCollectionExtensions binds OpcUaGlobalOptions to the OpcUa section.
- DataConnectionFactory takes IOptions<OpcUaGlobalOptions> and constructs
  RealOpcUaClientFactory with the snapshot.
- RealOpcUaClient(globalOptions) replaces the hardcoded ApplicationName and
  the three CertificateTrustList store paths in ApplicationConfiguration.
- Parameterless ctors on factory and client preserved for the existing test
  suite (32/32 DCL tests still green).
This commit is contained in:
Joseph Doherty
2026-05-12 02:27:58 -04:00
parent e6a5b558f3
commit 8faaa8fe2b
4 changed files with 51 additions and 6 deletions
@@ -17,6 +17,12 @@ public class RealOpcUaClient : IOpcUaClient
private readonly Dictionary<string, Action<string, object?, DateTime, uint>> _callbacks = new();
private volatile bool _connectionLostFired;
private OpcUaConnectionOptions _options = new();
private readonly OpcUaGlobalOptions _globalOptions;
public RealOpcUaClient(OpcUaGlobalOptions? globalOptions = null)
{
_globalOptions = globalOptions ?? new OpcUaGlobalOptions();
}
public bool IsConnected => _session?.Connected ?? false;
public event Action? ConnectionLost;
@@ -34,15 +40,17 @@ public class RealOpcUaClient : IOpcUaClient
var appConfig = new ApplicationConfiguration
{
ApplicationName = "ScadaLink-DCL",
ApplicationName = string.IsNullOrWhiteSpace(_globalOptions.ApplicationName)
? "ScadaLink-DCL"
: _globalOptions.ApplicationName,
ApplicationType = ApplicationType.Client,
SecurityConfiguration = new SecurityConfiguration
{
AutoAcceptUntrustedCertificates = opts.AutoAcceptUntrustedCerts,
ApplicationCertificate = new CertificateIdentifier(),
TrustedIssuerCertificates = new CertificateTrustList { StorePath = Path.Combine(Path.GetTempPath(), "ScadaLink", "pki", "issuers") },
TrustedPeerCertificates = new CertificateTrustList { StorePath = Path.Combine(Path.GetTempPath(), "ScadaLink", "pki", "trusted") },
RejectedCertificateStore = new CertificateTrustList { StorePath = Path.Combine(Path.GetTempPath(), "ScadaLink", "pki", "rejected") }
TrustedIssuerCertificates = new CertificateTrustList { StorePath = ResolveStorePath(_globalOptions.TrustedIssuerStorePath, "issuers") },
TrustedPeerCertificates = new CertificateTrustList { StorePath = ResolveStorePath(_globalOptions.TrustedPeerStorePath, "trusted") },
RejectedCertificateStore = new CertificateTrustList { StorePath = ResolveStorePath(_globalOptions.RejectedCertificateStorePath, "rejected") }
},
ClientConfiguration = new ClientConfiguration { DefaultSessionTimeout = opts.SessionTimeoutMs },
TransportQuotas = new TransportQuotas { OperationTimeout = opts.OperationTimeoutMs }
@@ -270,6 +278,11 @@ public class RealOpcUaClient : IOpcUaClient
"BOTH" => TimestampsToReturn.Both,
_ => TimestampsToReturn.Source
};
private static string ResolveStorePath(string configured, string fallbackLeaf) =>
string.IsNullOrWhiteSpace(configured)
? Path.Combine(Path.GetTempPath(), "ScadaLink", "pki", fallbackLeaf)
: configured;
}
/// <summary>
@@ -277,5 +290,13 @@ public class RealOpcUaClient : IOpcUaClient
/// </summary>
public class RealOpcUaClientFactory : IOpcUaClientFactory
{
public IOpcUaClient Create() => new RealOpcUaClient();
private readonly OpcUaGlobalOptions _globalOptions;
public RealOpcUaClientFactory() : this(new OpcUaGlobalOptions()) { }
public RealOpcUaClientFactory(OpcUaGlobalOptions globalOptions)
{
_globalOptions = globalOptions;
}
public IOpcUaClient Create() => new RealOpcUaClient(_globalOptions);
}