Add authentication and role-based write access control

Implements configurable user authentication (anonymous + username/password)
with pluggable credential provider (IUserAuthenticationProvider). Anonymous
writes can be disabled via AnonymousCanWrite setting while reads remain
open. Adds -U/-P flags to all CLI commands for authenticated sessions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Joseph Doherty
2026-03-27 02:14:37 -04:00
parent b27d355763
commit bbd043e97b
24 changed files with 499 additions and 34 deletions

View File

@@ -24,6 +24,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
private readonly PerformanceMetrics _metrics;
private readonly HistorianDataSource? _historianDataSource;
private readonly bool _alarmTrackingEnabled;
private readonly bool _anonymousCanWrite;
private readonly string _namespaceUri;
// NodeId → full_tag_reference for read/write resolution
@@ -190,7 +191,8 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
IMxAccessClient mxAccessClient,
PerformanceMetrics metrics,
HistorianDataSource? historianDataSource = null,
bool alarmTrackingEnabled = false)
bool alarmTrackingEnabled = false,
bool anonymousCanWrite = true)
: base(server, configuration, namespaceUri)
{
_namespaceUri = namespaceUri;
@@ -198,6 +200,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
_metrics = metrics;
_historianDataSource = historianDataSource;
_alarmTrackingEnabled = alarmTrackingEnabled;
_anonymousCanWrite = anonymousCanWrite;
// Wire up data change delivery
_mxAccessClient.OnTagValueChanged += OnMxAccessDataChange;
@@ -378,7 +381,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
new LocalizedText("en", alarmAttr.AttributeName + " Alarm"),
true);
condition.SourceNode.Value = sourceNodeId;
condition.SourceName.Value = alarmAttr.AttributeName;
condition.SourceName.Value = alarmAttr.FullTagReference.TrimEnd('[', ']');
condition.ConditionName.Value = alarmAttr.AttributeName;
condition.AutoReportStateChanges = true;
@@ -833,7 +836,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
new LocalizedText("en", alarmAttr.AttributeName + " Alarm"),
true);
condition.SourceNode.Value = sourceNodeId;
condition.SourceName.Value = alarmAttr.AttributeName;
condition.SourceName.Value = alarmAttr.FullTagReference.TrimEnd('[', ']');
condition.ConditionName.Value = alarmAttr.AttributeName;
condition.AutoReportStateChanges = true;
condition.SetEnableState(SystemContext, true);
@@ -1100,6 +1103,14 @@ 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))
{
errors[i] = new ServiceResult(StatusCodes.BadUserAccessDenied);
continue;
}
var nodeId = nodesToWrite[i].NodeId;
if (nodeId.NamespaceIndex != NamespaceIndex) continue;