fix(opcua): scope application audit receive permissions to validation
This commit is contained in:
@@ -4949,7 +4949,8 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Called outside <c>Lock</c>, like the <c>Server.ReportEvent</c> call it replaces; the decision
|
||||
/// takes no node-manager lock.
|
||||
/// briefly resolves the audit's source variable under the manager lock; permission evaluation
|
||||
/// and queueing happen outside that lock.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
private void ReportToServerObject(BaseEventState e)
|
||||
@@ -4966,11 +4967,122 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
foreach (var item in _allEventsItems.Keys)
|
||||
{
|
||||
if (ServerObjectEventFor(item, e) is not { } visible) continue;
|
||||
if (ServiceResult.IsBad(ValidateEventRolePermissions(item, visible))) continue;
|
||||
if (visible is AuditWriteUpdateEventState audit)
|
||||
{
|
||||
if (!ServiceResult.IsGood(ValidateApplicationAuditEventReceivePermissions(item, audit))) continue;
|
||||
}
|
||||
else if (ServiceResult.IsBad(ValidateEventRolePermissions(item, visible))) continue;
|
||||
item.QueueEvent(visible);
|
||||
}
|
||||
}
|
||||
|
||||
// Exact SDK 2.0.1 checks ReceiveEvents on the event type and source. The i=2100 defaults
|
||||
// exclude ordinary application readers. Compose that one permission only while our manual Read-filtered
|
||||
// delivery is being validated. SDK-generated audits use ordinary contexts and keep their defaults.
|
||||
internal void InstallApplicationAuditPermissions(BaseObjectTypeState auditType)
|
||||
{
|
||||
if (auditType.NodeId != ObjectTypeIds.AuditWriteUpdateEventType)
|
||||
throw new ArgumentException("Only AuditWriteUpdateEventType may receive this callback.", nameof(auditType));
|
||||
var previous = auditType.OnReadRolePermissions;
|
||||
auditType.OnReadRolePermissions = (ISystemContext context, NodeState node, ref ArrayOf<RolePermissionType> permissions) =>
|
||||
{
|
||||
var result = ReadAuditPermissions(previous, context, node, ref permissions);
|
||||
var audit = (context as ServerSystemContext)?.OperationContext as ApplicationAuditOperationContext;
|
||||
if (!ServiceResult.IsGood(result) || audit is null || node.NodeId != audit.EventType
|
||||
|| !CanComposeApplicationAuditPermissions(audit)) return result;
|
||||
|
||||
// Append fresh entries to a fresh value; neither the node's arrays nor existing entries change.
|
||||
permissions = [.. permissions, .. (audit.UserIdentity.GrantedRoleIds.ToArray() ?? []).Select(role => new RolePermissionType
|
||||
{
|
||||
RoleId = role,
|
||||
Permissions = (uint)PermissionType.ReceiveEvents,
|
||||
})];
|
||||
return result;
|
||||
};
|
||||
// Observe this attribute's veto as well: SDK metadata discards either attribute's read error.
|
||||
// Its callback result and value are passed through unchanged; no permissions are composed here.
|
||||
var previousUser = auditType.OnReadUserRolePermissions;
|
||||
auditType.OnReadUserRolePermissions = (ISystemContext context, NodeState node, ref ArrayOf<RolePermissionType> permissions) =>
|
||||
ReadAuditPermissions(previousUser, context, node, ref permissions);
|
||||
}
|
||||
|
||||
private static ServiceResult ReadAuditPermissions(NodeAttributeEventHandler<ArrayOf<RolePermissionType>>? previous,
|
||||
ISystemContext context, NodeState node, ref ArrayOf<RolePermissionType> permissions)
|
||||
{
|
||||
var audit = (context as ServerSystemContext)?.OperationContext as ApplicationAuditOperationContext;
|
||||
ServiceResult result;
|
||||
try { result = previous?.Invoke(context, node, ref permissions) ?? ServiceResult.Good; }
|
||||
catch (Exception error)
|
||||
{
|
||||
// ReadAttribute maps this exception to BadUnexpectedError, but metadata discards that status.
|
||||
audit?.RecordPermissionReadFailure(ServiceResult.Create(error, StatusCodes.BadUnexpectedError,
|
||||
"Failed to read non value attribute from node."));
|
||||
throw;
|
||||
}
|
||||
if (!ServiceResult.IsGood(result)) audit?.RecordPermissionReadFailure(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
internal ServiceResult ValidateApplicationAuditEventReceivePermissions(
|
||||
IEventMonitoredItem item, AuditWriteUpdateEventState audit)
|
||||
{
|
||||
var type = audit.EventType?.Value ?? NodeId.Null;
|
||||
var source = audit.SourceNode?.Value ?? NodeId.Null;
|
||||
if (type != ObjectTypeIds.AuditWriteUpdateEventType || !OwnsNamespace(source)
|
||||
|| !source.TryGetValue(out string _)) return StatusCodes.BadUserAccessDenied;
|
||||
BaseDataVariableState sourceVariable;
|
||||
lock (Lock)
|
||||
{
|
||||
if (!_variables.TryGetValue(MapKey(source), out sourceVariable!)) return StatusCodes.BadUserAccessDenied;
|
||||
}
|
||||
using var context = new ApplicationAuditOperationContext(item, type, source, sourceVariable);
|
||||
try
|
||||
{
|
||||
if (!CanComposeApplicationAuditPermissions(context)) return StatusCodes.BadUserAccessDenied;
|
||||
var result = ValidateEventReceivePermissions(context, type, source);
|
||||
// Metadata attribute reads discard callback errors; preserve a prior callback's veto.
|
||||
if (context.PermissionReadFailure is { } veto) return veto;
|
||||
// Source metadata callbacks may have changed authority after the type was checked.
|
||||
return CanComposeApplicationAuditPermissions(context) ? result : StatusCodes.BadUserAccessDenied;
|
||||
}
|
||||
finally
|
||||
{
|
||||
// The capability is gone before QueueEvent (including nested callbacks) can run.
|
||||
context.Revoke();
|
||||
}
|
||||
}
|
||||
|
||||
private bool CanComposeApplicationAuditPermissions(ApplicationAuditOperationContext context) =>
|
||||
context.IsActive && context.EventType == ObjectTypeIds.AuditWriteUpdateEventType
|
||||
&& IsCurrentApplicationAuditSource(context) && AclEnforced
|
||||
&& ReferenceEquals(context.UserIdentity, context.Item.EffectiveIdentity)
|
||||
&& (context.Session is null || ReferenceEquals(context.UserIdentity, context.Session.EffectiveIdentity))
|
||||
&& context.UserIdentity is RoleCarryingUserIdentity identity
|
||||
&& IsAuditDeliverableTo(context.Item) && Permits(identity, OpcUaOperation.Read, context.Source);
|
||||
|
||||
private bool IsCurrentApplicationAuditSource(ApplicationAuditOperationContext context)
|
||||
{
|
||||
lock (Lock) return _variables.TryGetValue(MapKey(context.Source), out var current)
|
||||
&& ReferenceEquals(current, context.SourceVariable);
|
||||
}
|
||||
|
||||
private sealed class ApplicationAuditOperationContext(
|
||||
IEventMonitoredItem item, NodeId eventType, NodeId source, BaseDataVariableState sourceVariable) : OperationContext(item)
|
||||
{
|
||||
private int _active = 1;
|
||||
internal IEventMonitoredItem Item { get; } = item;
|
||||
internal NodeId EventType { get; } = eventType;
|
||||
internal NodeId Source { get; } = source;
|
||||
internal BaseDataVariableState SourceVariable { get; } = sourceVariable;
|
||||
internal bool IsActive => Volatile.Read(ref _active) != 0;
|
||||
internal ServiceResult? PermissionReadFailure { get; private set; }
|
||||
internal void RecordPermissionReadFailure(ServiceResult result)
|
||||
{
|
||||
if (IsActive) PermissionReadFailure ??= result;
|
||||
}
|
||||
internal void Revoke() => Interlocked.Exchange(ref _active, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// What of a server-wide event <paramref name="item"/>'s session may receive with enforcement on: the
|
||||
/// event itself, a copy narrowed to what it may see, or nothing.
|
||||
|
||||
@@ -303,6 +303,17 @@ public sealed class OtOpcUaSdkServer : StandardServer
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async ValueTask OnNodeManagerStartedAsync(
|
||||
IServerInternal server, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await base.OnNodeManagerStartedAsync(server, cancellationToken).ConfigureAwait(false);
|
||||
var auditType = server.DiagnosticsNodeManager?
|
||||
.FindPredefinedNode<BaseObjectTypeState>(ObjectTypeIds.AuditWriteUpdateEventType)
|
||||
?? throw new InvalidOperationException("AuditWriteUpdateEventType is unavailable during server startup.");
|
||||
_otOpcUaNodeManager!.InstallApplicationAuditPermissions(auditType);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override MasterNodeManager CreateMasterNodeManager(
|
||||
IServerInternal server, ApplicationConfiguration configuration)
|
||||
|
||||
Reference in New Issue
Block a user