Adds Security section to appsettings.json with configurable OPC UA transport profiles (None, Basic256Sha256-Sign, Basic256Sha256-SignAndEncrypt), certificate policy settings, and a configurable BindAddress for the OPC UA endpoint. Defaults preserve backward compatibility. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
112 lines
5.8 KiB
C#
112 lines
5.8 KiB
C#
using System.Linq;
|
|
using Serilog;
|
|
using ZB.MOM.WW.LmxOpcUa.Host.OpcUa;
|
|
|
|
namespace ZB.MOM.WW.LmxOpcUa.Host.Configuration
|
|
{
|
|
/// <summary>
|
|
/// Validates and logs effective configuration at startup. (SVC-003, SVC-005)
|
|
/// </summary>
|
|
public static class ConfigurationValidator
|
|
{
|
|
private static readonly ILogger Log = Serilog.Log.ForContext(typeof(ConfigurationValidator));
|
|
|
|
/// <summary>
|
|
/// Validates the effective host configuration and writes the resolved values to the startup log before service initialization continues.
|
|
/// </summary>
|
|
/// <param name="config">The bound service configuration that drives OPC UA hosting, MXAccess connectivity, Galaxy queries, and dashboard behavior.</param>
|
|
/// <returns><see langword="true"/> when the required settings are present and within supported bounds; otherwise, <see langword="false"/>.</returns>
|
|
public static bool ValidateAndLog(AppConfiguration config)
|
|
{
|
|
bool valid = true;
|
|
|
|
Log.Information("=== Effective Configuration ===");
|
|
|
|
// OPC UA
|
|
Log.Information("OpcUa.BindAddress={BindAddress}, Port={Port}, EndpointPath={EndpointPath}, ServerName={ServerName}, GalaxyName={GalaxyName}",
|
|
config.OpcUa.BindAddress, config.OpcUa.Port, config.OpcUa.EndpointPath, config.OpcUa.ServerName, config.OpcUa.GalaxyName);
|
|
Log.Information("OpcUa.MaxSessions={MaxSessions}, SessionTimeoutMinutes={SessionTimeout}",
|
|
config.OpcUa.MaxSessions, config.OpcUa.SessionTimeoutMinutes);
|
|
|
|
if (config.OpcUa.Port < 1 || config.OpcUa.Port > 65535)
|
|
{
|
|
Log.Error("OpcUa.Port must be between 1 and 65535");
|
|
valid = false;
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(config.OpcUa.GalaxyName))
|
|
{
|
|
Log.Error("OpcUa.GalaxyName must not be empty");
|
|
valid = false;
|
|
}
|
|
|
|
// MxAccess
|
|
Log.Information("MxAccess.ClientName={ClientName}, ReadTimeout={ReadTimeout}s, WriteTimeout={WriteTimeout}s, MaxConcurrent={MaxConcurrent}",
|
|
config.MxAccess.ClientName, config.MxAccess.ReadTimeoutSeconds, config.MxAccess.WriteTimeoutSeconds,
|
|
config.MxAccess.MaxConcurrentOperations);
|
|
Log.Information("MxAccess.MonitorInterval={MonitorInterval}s, AutoReconnect={AutoReconnect}, ProbeTag={ProbeTag}, ProbeStaleThreshold={ProbeStale}s",
|
|
config.MxAccess.MonitorIntervalSeconds, config.MxAccess.AutoReconnect,
|
|
config.MxAccess.ProbeTag ?? "(none)", config.MxAccess.ProbeStaleThresholdSeconds);
|
|
|
|
if (string.IsNullOrWhiteSpace(config.MxAccess.ClientName))
|
|
{
|
|
Log.Error("MxAccess.ClientName must not be empty");
|
|
valid = false;
|
|
}
|
|
|
|
// Galaxy Repository
|
|
Log.Information("GalaxyRepository.ConnectionString={ConnectionString}, ChangeDetectionInterval={ChangeInterval}s, CommandTimeout={CmdTimeout}s, ExtendedAttributes={ExtendedAttributes}",
|
|
config.GalaxyRepository.ConnectionString, config.GalaxyRepository.ChangeDetectionIntervalSeconds,
|
|
config.GalaxyRepository.CommandTimeoutSeconds, config.GalaxyRepository.ExtendedAttributes);
|
|
|
|
if (string.IsNullOrWhiteSpace(config.GalaxyRepository.ConnectionString))
|
|
{
|
|
Log.Error("GalaxyRepository.ConnectionString must not be empty");
|
|
valid = false;
|
|
}
|
|
|
|
// Dashboard
|
|
Log.Information("Dashboard.Enabled={Enabled}, Port={Port}, RefreshInterval={Refresh}s",
|
|
config.Dashboard.Enabled, config.Dashboard.Port, config.Dashboard.RefreshIntervalSeconds);
|
|
|
|
// Security
|
|
Log.Information("Security.Profiles=[{Profiles}], AutoAcceptClientCertificates={AutoAccept}, RejectSHA1={RejectSHA1}, MinKeySize={MinKeySize}",
|
|
string.Join(", ", config.Security.Profiles), config.Security.AutoAcceptClientCertificates,
|
|
config.Security.RejectSHA1Certificates, config.Security.MinimumCertificateKeySize);
|
|
|
|
if (config.Security.PkiRootPath != null)
|
|
Log.Information("Security.PkiRootPath={PkiRootPath}", config.Security.PkiRootPath);
|
|
if (config.Security.CertificateSubject != null)
|
|
Log.Information("Security.CertificateSubject={CertificateSubject}", config.Security.CertificateSubject);
|
|
|
|
var unknownProfiles = config.Security.Profiles
|
|
.Where(p => !SecurityProfileResolver.ValidProfileNames.Contains(p, System.StringComparer.OrdinalIgnoreCase))
|
|
.ToList();
|
|
if (unknownProfiles.Count > 0)
|
|
{
|
|
Log.Warning("Unknown security profile(s): {Profiles}. Valid values: {ValidProfiles}",
|
|
string.Join(", ", unknownProfiles), string.Join(", ", SecurityProfileResolver.ValidProfileNames));
|
|
}
|
|
|
|
if (config.Security.MinimumCertificateKeySize < 2048)
|
|
{
|
|
Log.Error("Security.MinimumCertificateKeySize must be at least 2048");
|
|
valid = false;
|
|
}
|
|
|
|
if (config.Security.AutoAcceptClientCertificates)
|
|
{
|
|
Log.Warning("Security.AutoAcceptClientCertificates is enabled — client certificate trust is not enforced. Set to false in production");
|
|
}
|
|
|
|
if (config.Security.Profiles.Count == 1 && config.Security.Profiles[0].Equals("None", System.StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
Log.Warning("Only the 'None' security profile is configured — transport security is disabled");
|
|
}
|
|
|
|
Log.Information("=== Configuration {Status} ===", valid ? "Valid" : "INVALID");
|
|
return valid;
|
|
}
|
|
}
|
|
}
|