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

@@ -25,6 +25,8 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
private readonly HistorianDataSource? _historianDataSource;
private readonly bool _alarmTrackingEnabled;
private readonly bool _anonymousCanWrite;
private readonly Func<string?, IReadOnlyList<string>?>? _appRoleLookup;
private readonly bool _ldapRolesEnabled;
private readonly string _namespaceUri;
// NodeId → full_tag_reference for read/write resolution
@@ -192,7 +194,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
PerformanceMetrics metrics,
HistorianDataSource? historianDataSource = null,
bool alarmTrackingEnabled = false,
bool anonymousCanWrite = true)
bool anonymousCanWrite = true,
Func<string?, IReadOnlyList<string>?>? appRoleLookup = null,
bool ldapRolesEnabled = false)
: base(server, configuration, namespaceUri)
{
_namespaceUri = namespaceUri;
@@ -201,6 +205,8 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
_historianDataSource = historianDataSource;
_alarmTrackingEnabled = alarmTrackingEnabled;
_anonymousCanWrite = anonymousCanWrite;
_appRoleLookup = appRoleLookup;
_ldapRolesEnabled = ldapRolesEnabled;
// Wire up data change delivery
_mxAccessClient.OnTagValueChanged += OnMxAccessDataChange;
@@ -467,6 +473,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
private ServiceResult OnAlarmAcknowledge(
ISystemContext context, ConditionState condition, byte[] eventId, LocalizedText comment)
{
if (!HasAlarmAckPermission(context))
return new ServiceResult(StatusCodes.BadUserAccessDenied);
var alarmInfo = _alarmInAlarmTags.Values
.FirstOrDefault(a => a.ConditionNode == condition);
if (alarmInfo == null)
@@ -1103,9 +1112,8 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
if (errors[i] != null && errors[i].StatusCode == StatusCodes.BadNotWritable)
continue;
// Enforce role-based write access: reject anonymous writes when AnonymousCanWrite is false
if (!_anonymousCanWrite && context.UserIdentity?.GrantedRoleIds != null &&
!context.UserIdentity.GrantedRoleIds.Contains(ObjectIds.WellKnownRole_AuthenticatedUser))
// Enforce role-based write access
if (!HasWritePermission(context))
{
errors[i] = new ServiceResult(StatusCodes.BadUserAccessDenied);
continue;
@@ -1155,6 +1163,39 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
}
}
private bool HasWritePermission(OperationContext context)
{
// When LDAP roles are active, check for ReadWrite role
if (_ldapRolesEnabled && _appRoleLookup != null)
{
var username = context.UserIdentity?.GetIdentityToken() is UserNameIdentityToken token ? token.UserName : null;
var roles = _appRoleLookup(username);
if (roles == null) return false; // unknown user
return roles.Contains("ReadWrite");
}
// Legacy behavior: reject anonymous writes when AnonymousCanWrite is false
if (!_anonymousCanWrite && context.UserIdentity?.GrantedRoleIds != null &&
!context.UserIdentity.GrantedRoleIds.Contains(ObjectIds.WellKnownRole_AuthenticatedUser))
{
return false;
}
return true;
}
private bool HasAlarmAckPermission(ISystemContext context)
{
if (!_ldapRolesEnabled || _appRoleLookup == null)
return true; // no LDAP restrictions without LDAP roles
var opContext = context as SystemContext;
var username = opContext?.UserIdentity?.GetIdentityToken() is UserNameIdentityToken token ? token.UserName : null;
var roles = _appRoleLookup(username);
if (roles == null) return false;
return roles.Contains("AlarmAck");
}
private bool TryApplyArrayElementWrite(string tagRef, object? writeValue, string indexRange, out object updatedArray)
{
updatedArray = null!;