Add LDAP authentication with role-based OPC UA permissions

Replace static user list with GLAuth LDAP authentication. Group
membership (ReadOnly, ReadWrite, AlarmAck) maps to granular OPC UA
permissions for write and alarm-ack operations. Anonymous can still
browse and read but not write.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Joseph Doherty
2026-03-28 18:57:30 -04:00
parent 9d3599fbb6
commit 74107ea95e
16 changed files with 726 additions and 17 deletions

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Text;
using Opc.Ua;
@@ -29,6 +30,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
private readonly RedundancyConfiguration _redundancyConfig;
private readonly string? _applicationUri;
private readonly ServiceLevelCalculator _serviceLevelCalculator = new ServiceLevelCalculator();
private readonly ConcurrentDictionary<string, IReadOnlyList<string>> _userAppRoles = new ConcurrentDictionary<string, IReadOnlyList<string>>(StringComparer.OrdinalIgnoreCase);
private LmxNodeManager? _nodeManager;
/// <summary>
@@ -36,6 +38,22 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
/// </summary>
public LmxNodeManager? NodeManager => _nodeManager;
/// <summary>
/// Returns the application-level roles cached for the given username, or null if no roles are stored.
/// Called by LmxNodeManager to enforce per-role write and alarm-ack permissions.
/// </summary>
public IReadOnlyList<string>? GetUserAppRoles(string? username)
{
if (username != null && _userAppRoles.TryGetValue(username, out var roles))
return roles;
return null;
}
/// <summary>
/// Gets whether LDAP role-based access control is active.
/// </summary>
public bool LdapRolesEnabled => _authProvider is Domain.IRoleProvider;
/// <summary>
/// Gets the number of active OPC UA sessions currently connected to the server.
/// </summary>
@@ -69,7 +87,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
{
var namespaceUri = $"urn:{_galaxyName}:LmxOpcUa";
_nodeManager = new LmxNodeManager(server, configuration, namespaceUri, _mxAccessClient, _metrics,
_historianDataSource, _alarmTrackingEnabled, _authConfig.AnonymousCanWrite);
_historianDataSource, _alarmTrackingEnabled, _authConfig.AnonymousCanWrite,
LdapRolesEnabled ? (Func<string?, IReadOnlyList<string>?>)GetUserAppRoles : null,
LdapRolesEnabled);
var nodeManagers = new List<INodeManager> { _nodeManager };
return new MasterNodeManager(server, configuration, null, nodeManagers.ToArray());
@@ -206,10 +226,23 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
throw new ServiceResultException(StatusCodes.BadUserAccessDenied, "Invalid username or password");
}
var roles = new List<Role> { Role.AuthenticatedUser };
// Resolve LDAP-based roles when the provider supports it
if (_authProvider is Domain.IRoleProvider roleProvider)
{
var appRoles = roleProvider.GetUserRoles(userNameToken.UserName);
_userAppRoles[userNameToken.UserName] = appRoles;
Log.Information("User {Username} authenticated with roles [{Roles}]",
userNameToken.UserName, string.Join(", ", appRoles));
}
else
{
Log.Information("User {Username} authenticated", userNameToken.UserName);
}
args.Identity = new RoleBasedIdentity(
new UserIdentity(userNameToken),
new List<Role> { Role.AuthenticatedUser });
Log.Information("User {Username} authenticated", userNameToken.UserName);
new UserIdentity(userNameToken), roles);
return;
}