From 8faaa8fe2be4e1b09a7ac97b84187bbd20389c26 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Tue, 12 May 2026 02:27:58 -0400 Subject: [PATCH] feat(dcl): Layer D OpcUaGlobalOptions for app-wide identity + cert paths MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 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). --- .../Adapters/RealOpcUaClient.cs | 31 ++++++++++++++++--- .../DataConnectionFactory.cs | 8 ++++- .../OpcUaGlobalOptions.cs | 15 +++++++++ .../ServiceCollectionExtensions.cs | 3 ++ 4 files changed, 51 insertions(+), 6 deletions(-) create mode 100644 src/ScadaLink.DataConnectionLayer/OpcUaGlobalOptions.cs diff --git a/src/ScadaLink.DataConnectionLayer/Adapters/RealOpcUaClient.cs b/src/ScadaLink.DataConnectionLayer/Adapters/RealOpcUaClient.cs index 5202031..d3d2f08 100644 --- a/src/ScadaLink.DataConnectionLayer/Adapters/RealOpcUaClient.cs +++ b/src/ScadaLink.DataConnectionLayer/Adapters/RealOpcUaClient.cs @@ -17,6 +17,12 @@ public class RealOpcUaClient : IOpcUaClient private readonly Dictionary> _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; } /// @@ -277,5 +290,13 @@ public class RealOpcUaClient : IOpcUaClient /// 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); } diff --git a/src/ScadaLink.DataConnectionLayer/DataConnectionFactory.cs b/src/ScadaLink.DataConnectionLayer/DataConnectionFactory.cs index d721453..82a6ef2 100644 --- a/src/ScadaLink.DataConnectionLayer/DataConnectionFactory.cs +++ b/src/ScadaLink.DataConnectionLayer/DataConnectionFactory.cs @@ -1,4 +1,5 @@ using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using ScadaLink.Commons.Interfaces.Protocol; using ScadaLink.DataConnectionLayer.Adapters; @@ -14,12 +15,17 @@ public class DataConnectionFactory : IDataConnectionFactory private readonly ILoggerFactory _loggerFactory; public DataConnectionFactory(ILoggerFactory loggerFactory) + : this(loggerFactory, Options.Create(new OpcUaGlobalOptions())) { } + + public DataConnectionFactory(ILoggerFactory loggerFactory, IOptions opcUaGlobalOptions) { _loggerFactory = loggerFactory; + var globalOptions = opcUaGlobalOptions.Value; // Register built-in protocols RegisterAdapter("OpcUa", details => new OpcUaDataConnection( - new RealOpcUaClientFactory(), _loggerFactory.CreateLogger())); + new RealOpcUaClientFactory(globalOptions), + _loggerFactory.CreateLogger())); } /// diff --git a/src/ScadaLink.DataConnectionLayer/OpcUaGlobalOptions.cs b/src/ScadaLink.DataConnectionLayer/OpcUaGlobalOptions.cs new file mode 100644 index 0000000..71376bf --- /dev/null +++ b/src/ScadaLink.DataConnectionLayer/OpcUaGlobalOptions.cs @@ -0,0 +1,15 @@ +namespace ScadaLink.DataConnectionLayer; + +/// +/// Deployment-wide OPC UA application identity. Bound from the "OpcUa" section +/// of appsettings.json. Per-endpoint behavior lives on OpcUaEndpointConfig. +/// Empty paths fall back to a default under Path.GetTempPath() so dev runs +/// work without explicit configuration. +/// +public class OpcUaGlobalOptions +{ + public string ApplicationName { get; set; } = "ScadaLink-DCL"; + public string TrustedIssuerStorePath { get; set; } = ""; + public string TrustedPeerStorePath { get; set; } = ""; + public string RejectedCertificateStorePath { get; set; } = ""; +} diff --git a/src/ScadaLink.DataConnectionLayer/ServiceCollectionExtensions.cs b/src/ScadaLink.DataConnectionLayer/ServiceCollectionExtensions.cs index 28fd163..82cde4b 100644 --- a/src/ScadaLink.DataConnectionLayer/ServiceCollectionExtensions.cs +++ b/src/ScadaLink.DataConnectionLayer/ServiceCollectionExtensions.cs @@ -9,6 +9,9 @@ public static class ServiceCollectionExtensions services.AddOptions() .BindConfiguration("DataConnectionLayer"); + services.AddOptions() + .BindConfiguration("OpcUa"); + // WP-34: Register the factory for protocol extensibility services.AddSingleton();