using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Opc.Ua; using Opc.Ua.Server; using ZB.MOM.WW.OtOpcUa.Core.Abstractions; using ZB.MOM.WW.OtOpcUa.Core.Hosting; using ZB.MOM.WW.OtOpcUa.Core.OpcUa; using ZB.MOM.WW.OtOpcUa.Core.Resilience; using ZB.MOM.WW.OtOpcUa.Server.Security; namespace ZB.MOM.WW.OtOpcUa.Server.OpcUa; /// /// subclass that wires one per /// registered driver from . Anonymous endpoint on /// opc.tcp://0.0.0.0:4840, no security — PR 16 minimum-viable scope; LDAP + security /// profiles are deferred to their own PR on top of this. /// public sealed class OtOpcUaServer : StandardServer { private readonly DriverHost _driverHost; private readonly IUserAuthenticator _authenticator; private readonly DriverResiliencePipelineBuilder _pipelineBuilder; private readonly AuthorizationGate? _authzGate; private readonly NodeScopeResolver? _scopeResolver; private readonly Func? _tierLookup; private readonly Func? _resilienceConfigLookup; private readonly ILoggerFactory _loggerFactory; private readonly List _driverNodeManagers = new(); public OtOpcUaServer( DriverHost driverHost, IUserAuthenticator authenticator, DriverResiliencePipelineBuilder pipelineBuilder, ILoggerFactory loggerFactory, AuthorizationGate? authzGate = null, NodeScopeResolver? scopeResolver = null, Func? tierLookup = null, Func? resilienceConfigLookup = null) { _driverHost = driverHost; _authenticator = authenticator; _pipelineBuilder = pipelineBuilder; _authzGate = authzGate; _scopeResolver = scopeResolver; _tierLookup = tierLookup; _resilienceConfigLookup = resilienceConfigLookup; _loggerFactory = loggerFactory; } /// /// Read-only snapshot of the driver node managers materialized at server start. Used by /// the generic-driver-node-manager-driven discovery flow after the server starts — the /// host walks each entry and invokes /// GenericDriverNodeManager.BuildAddressSpaceAsync(manager) passing the manager /// as its own . /// public IReadOnlyList DriverNodeManagers => _driverNodeManagers; protected override MasterNodeManager CreateMasterNodeManager(IServerInternal server, ApplicationConfiguration configuration) { foreach (var driverId in _driverHost.RegisteredDriverIds) { var driver = _driverHost.GetDriver(driverId); if (driver is null) continue; var logger = _loggerFactory.CreateLogger(); // Per-driver resilience options: tier comes from lookup (Phase 6.1 Stream B.1 // DriverTypeRegistry in the prod wire-up) or falls back to Tier A. ResilienceConfig // JSON comes from the DriverInstance row via the optional lookup Func; parser // layers JSON overrides on top of tier defaults (Phase 6.1 Stream A.2). var tier = _tierLookup?.Invoke(driver.DriverType) ?? DriverTier.A; var resilienceJson = _resilienceConfigLookup?.Invoke(driver.DriverInstanceId); var options = DriverResilienceOptionsParser.ParseOrDefaults(tier, resilienceJson, out var diag); if (diag is not null) logger.LogWarning("ResilienceConfig parse diagnostic for driver {DriverId}: {Diag}", driver.DriverInstanceId, diag); var invoker = new CapabilityInvoker(_pipelineBuilder, driver.DriverInstanceId, () => options, driver.DriverType); var manager = new DriverNodeManager(server, configuration, driver, invoker, logger, authzGate: _authzGate, scopeResolver: _scopeResolver); _driverNodeManagers.Add(manager); } return new MasterNodeManager(server, configuration, null, _driverNodeManagers.ToArray()); } protected override void OnServerStarted(IServerInternal server) { base.OnServerStarted(server); // Hook UserName / Anonymous token validation here. Anonymous passes through; UserName // is validated against the IUserAuthenticator (LDAP in production). Rejected identities // throw ServiceResultException which the stack translates to Bad_IdentityTokenInvalid. server.SessionManager.ImpersonateUser += OnImpersonateUser; } private void OnImpersonateUser(Session session, ImpersonateEventArgs args) { switch (args.NewIdentity) { case AnonymousIdentityToken: args.Identity = new UserIdentity(); // anonymous return; case UserNameIdentityToken user: { var result = _authenticator.AuthenticateAsync( user.UserName, user.DecryptedPassword, CancellationToken.None) .GetAwaiter().GetResult(); if (!result.Success) { throw ServiceResultException.Create( StatusCodes.BadUserAccessDenied, "Invalid username or password ({0})", result.Error ?? "no detail"); } args.Identity = new RoleBasedIdentity(user.UserName, result.DisplayName, result.Roles); return; } default: throw ServiceResultException.Create( StatusCodes.BadIdentityTokenInvalid, "Unsupported user identity token type: {0}", args.NewIdentity?.GetType().Name ?? "null"); } } /// /// Tiny UserIdentity carrier that preserves the resolved roles so downstream node /// managers can gate writes by role via session.Identity. Anonymous identity still /// uses the stack's default. /// private sealed class RoleBasedIdentity : UserIdentity, IRoleBearer { public IReadOnlyList Roles { get; } public string? Display { get; } public RoleBasedIdentity(string userName, string? displayName, IReadOnlyList roles) : base(userName, "") { Display = displayName; Roles = roles; } } protected override ServerProperties LoadServerProperties() => new() { ManufacturerName = "OtOpcUa", ProductName = "OtOpcUa.Server", ProductUri = "urn:OtOpcUa:Server", SoftwareVersion = "2.0.0", BuildNumber = "0", BuildDate = DateTime.UtcNow, }; }