diff --git a/src/ZB.MOM.WW.OtOpcUa.Server/OpcUa/OpcUaApplicationHost.cs b/src/ZB.MOM.WW.OtOpcUa.Server/OpcUa/OpcUaApplicationHost.cs index 2dc9c4f..be26535 100644 --- a/src/ZB.MOM.WW.OtOpcUa.Server/OpcUa/OpcUaApplicationHost.cs +++ b/src/ZB.MOM.WW.OtOpcUa.Server/OpcUa/OpcUaApplicationHost.cs @@ -118,7 +118,8 @@ public sealed class OpcUaApplicationHost : IAsyncDisposable _server = new OtOpcUaServer(_driverHost, _authenticator, _pipelineBuilder, _loggerFactory, authzGate: _authzGate, scopeResolver: _scopeResolver, tierLookup: _tierLookup, resilienceConfigLookup: _resilienceConfigLookup, - virtualReadable: _virtualReadable, scriptedAlarmReadable: _scriptedAlarmReadable); + virtualReadable: _virtualReadable, scriptedAlarmReadable: _scriptedAlarmReadable, + anonymousRoles: _options.AnonymousRoles); await _application.Start(_server).ConfigureAwait(false); _logger.LogInformation("OPC UA server started — endpoint={Endpoint} driverCount={Count}", diff --git a/src/ZB.MOM.WW.OtOpcUa.Server/OpcUa/OpcUaServerOptions.cs b/src/ZB.MOM.WW.OtOpcUa.Server/OpcUa/OpcUaServerOptions.cs index 34bcd09..3ebc1b0 100644 --- a/src/ZB.MOM.WW.OtOpcUa.Server/OpcUa/OpcUaServerOptions.cs +++ b/src/ZB.MOM.WW.OtOpcUa.Server/OpcUa/OpcUaServerOptions.cs @@ -85,4 +85,15 @@ public sealed class OpcUaServerOptions /// LdapOptions.Enabled = false, UserName token attempts are rejected. /// public LdapOptions Ldap { get; init; } = new(); + + /// + /// Roles granted to anonymous OPC UA sessions. Default empty — anonymous clients can + /// read FreeAccess attributes but cannot write Operate/Tune/ + /// Configure tags ( rejects the empty role set). + /// Dev + smoke-test deployments that need anonymous writes populate this with the + /// role names they want, e.g. ["WriteOperate"] to match v1's anonymous-can- + /// operate default. Production deployments leave it empty + route operators through + /// UserName auth. + /// + public IReadOnlyList AnonymousRoles { get; init; } = []; } diff --git a/src/ZB.MOM.WW.OtOpcUa.Server/OpcUa/OtOpcUaServer.cs b/src/ZB.MOM.WW.OtOpcUa.Server/OpcUa/OtOpcUaServer.cs index 2b0e313..49dd62a 100644 --- a/src/ZB.MOM.WW.OtOpcUa.Server/OpcUa/OtOpcUaServer.cs +++ b/src/ZB.MOM.WW.OtOpcUa.Server/OpcUa/OtOpcUaServer.cs @@ -34,6 +34,15 @@ public sealed class OtOpcUaServer : StandardServer private readonly IReadable? _virtualReadable; private readonly IReadable? _scriptedAlarmReadable; + /// + /// Roles granted to anonymous sessions. When non-empty, + /// wraps AnonymousIdentityToken in a carrying + /// these roles so 's write-authz check passes for + /// matching classifications. Empty (the default) preserves the pre-existing behaviour + /// of rejecting anonymous writes at Operate or higher. + /// + private readonly IReadOnlyList _anonymousRoles; + private readonly ILoggerFactory _loggerFactory; private readonly List _driverNodeManagers = new(); @@ -47,7 +56,8 @@ public sealed class OtOpcUaServer : StandardServer Func? tierLookup = null, Func? resilienceConfigLookup = null, IReadable? virtualReadable = null, - IReadable? scriptedAlarmReadable = null) + IReadable? scriptedAlarmReadable = null, + IReadOnlyList? anonymousRoles = null) { _driverHost = driverHost; _authenticator = authenticator; @@ -58,6 +68,7 @@ public sealed class OtOpcUaServer : StandardServer _resilienceConfigLookup = resilienceConfigLookup; _virtualReadable = virtualReadable; _scriptedAlarmReadable = scriptedAlarmReadable; + _anonymousRoles = anonymousRoles ?? []; _loggerFactory = loggerFactory; } @@ -112,7 +123,9 @@ public sealed class OtOpcUaServer : StandardServer switch (args.NewIdentity) { case AnonymousIdentityToken: - args.Identity = new UserIdentity(); // anonymous + args.Identity = _anonymousRoles.Count == 0 + ? new UserIdentity() // anonymous, no roles — production default + : new RoleBasedIdentity("(anonymous)", "Anonymous", _anonymousRoles); return; case UserNameIdentityToken user: diff --git a/src/ZB.MOM.WW.OtOpcUa.Server/Program.cs b/src/ZB.MOM.WW.OtOpcUa.Server/Program.cs index f02a385..0738536 100644 --- a/src/ZB.MOM.WW.OtOpcUa.Server/Program.cs +++ b/src/ZB.MOM.WW.OtOpcUa.Server/Program.cs @@ -83,6 +83,7 @@ var opcUaOptions = new OpcUaServerOptions SecurityProfile = Enum.TryParse(opcUaSection.GetValue("SecurityProfile"), true, out var p) ? p : OpcUaSecurityProfile.None, Ldap = ldapOptions, + AnonymousRoles = opcUaSection.GetSection("AnonymousRoles").Get() ?? [], }; builder.Services.AddSingleton(options);