/* ======================================================================== * Copyright (c) 2005-2025 The OPC Foundation, Inc. All rights reserved. * * OPC Foundation MIT License 1.00 * * Permission is hereby granted, free of charge, to any person * obtaining a copy of this software and associated documentation * files (the "Software"), to deal in the Software without * restriction, including without limitation the rights to use, * copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following * conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * * The complete license agreement can be found here: * http://opcfoundation.org/License/MIT/1.00/ * ======================================================================*/ using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; namespace Opc.Ua.Server { /// public class MasterNodeManager : IDisposable, IMasterNodeManager { /// /// Initializes the object with default values. /// public MasterNodeManager( IServerInternal server, ApplicationConfiguration configuration, string? dynamicNamespaceUri, params INodeManager[] additionalManagers) : this(server, configuration, dynamicNamespaceUri, null, additionalManagers) { } /// /// Initializes the object with default values. /// public MasterNodeManager( IServerInternal server, ApplicationConfiguration configuration, string? dynamicNamespaceUri, params IAsyncNodeManager[] additionalManagers) : this(server, configuration, dynamicNamespaceUri, additionalManagers, null) { } /// /// Initializes the object with default values. /// public MasterNodeManager( IServerInternal server, ApplicationConfiguration configuration, string? dynamicNamespaceUri, IEnumerable? additionalManagers, IEnumerable? additionalSyncManagers) { if (configuration == null) { throw new ArgumentNullException(nameof(configuration)); } Server = server ?? throw new ArgumentNullException(nameof(server)); m_logger = server.Telemetry.CreateLogger(); m_nodeManagers = []; m_maxContinuationPointsPerBrowse = (uint)configuration.ServerConfiguration! .MaxBrowseContinuationPoints; // ensure the dynamic namespace uris. int dynamicNamespaceIndex = 1; if (!string.IsNullOrEmpty(dynamicNamespaceUri)) { dynamicNamespaceIndex = server.NamespaceUris.GetIndex(dynamicNamespaceUri!); if (dynamicNamespaceIndex == -1) { dynamicNamespaceIndex = server.NamespaceUris.Append(dynamicNamespaceUri!); } } // need to build a table of NamespaceIndexes and their NodeManagers. List registeredManagers; var namespaceManagers = new Dictionary> { [0] = [], [1] = registeredManagers = [] }; // always add the diagnostics and configuration node manager to the start of the list. IConfigurationNodeManager configurationAndDiagnosticsManager = server.MainNodeManagerFactory.CreateConfigurationNodeManager(); RegisterNodeManager( configurationAndDiagnosticsManager, registeredManagers, namespaceManagers); // add the core node manager second because the diagnostics node manager takes priority. // always add the core node manager to the second of the list. ICoreNodeManager coreNodeManager = server.MainNodeManagerFactory.CreateCoreNodeManager((ushort)dynamicNamespaceIndex); m_nodeManagers.Add(coreNodeManager); // register core node manager for default UA namespace. namespaceManagers[0].Add(m_nodeManagers[1]); // register core node manager for built-in server namespace. namespaceManagers[1].Add(m_nodeManagers[1]); // add the custom NodeManagers provided by the application. if (additionalManagers != null) { foreach (IAsyncNodeManager nodeManager in additionalManagers) { RegisterNodeManager(nodeManager, registeredManagers, namespaceManagers); } } if (additionalSyncManagers != null) { foreach (INodeManager nodeManager in additionalSyncManagers) { RegisterNodeManager(nodeManager.ToAsyncNodeManager(), registeredManagers, namespaceManagers); } } // build NamespaceManagersDictionary from local dictionary. foreach (KeyValuePair> namespaceManager in namespaceManagers) { NamespaceManagers.TryAdd(namespaceManager.Key, namespaceManager.Value.AsReadOnly()); } } /// /// Registers the node manager with the master node manager. /// private void RegisterNodeManager( IAsyncNodeManager nodeManager, List registeredManagers, Dictionary> namespaceManagers) { m_nodeManagers.Add(nodeManager); // ensure the NamespaceUris supported by the NodeManager are in the Server's NamespaceTable. if (nodeManager.NamespaceUris != null) { foreach (string namespaceUri in nodeManager.NamespaceUris) { // look up the namespace uri. int index = Server.NamespaceUris.GetIndex(namespaceUri); if (index == -1) { index = Server.NamespaceUris.Append(namespaceUri); } // add manager to list for the namespace. if (!namespaceManagers.TryGetValue(index, out registeredManagers!)) { namespaceManagers[index] = registeredManagers = []; } registeredManagers.Add(nodeManager); } } } /// /// Frees any unmanaged resources. /// public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } /// /// An overrideable version of the Dispose. /// protected virtual void Dispose(bool disposing) { if (disposing && !m_disposed) { m_disposed = true; m_namespaceManagersSemaphoreSlim.Dispose(); m_startupShutdownSemaphoreSlim.Wait(); List nodeManagers = [.. m_nodeManagers]; m_nodeManagers.Clear(); m_startupShutdownSemaphoreSlim.Dispose(); foreach (IAsyncNodeManager nodeManager in nodeManagers) { (nodeManager as IDisposable)?.Dispose(); } } } /// /// Adds a reference to the table of external references. /// /// /// This is a convenience function used by custom NodeManagers. /// public static void CreateExternalReference( IDictionary> externalReferences, NodeId sourceId, NodeId referenceTypeId, bool isInverse, NodeId targetId) { var reference = new ReferenceNode { ReferenceTypeId = referenceTypeId, IsInverse = isInverse, TargetId = targetId }; if (!externalReferences.TryGetValue(sourceId, out IList? references)) { externalReferences[sourceId] = references = []; } references!.Add(reference); } /// /// Determine the required history access permission depending on the HistoryUpdateDetails /// /// The HistoryUpdateDetails passed in /// The corresponding history access permission protected static PermissionType DetermineHistoryAccessPermission( HistoryUpdateDetails historyUpdateDetails) { Type detailsType = historyUpdateDetails.GetType(); if (detailsType == typeof(UpdateDataDetails)) { var updateDataDetails = (UpdateDataDetails)historyUpdateDetails; return GetHistoryPermissionType(updateDataDetails.PerformInsertReplace); } else if (detailsType == typeof(UpdateStructureDataDetails)) { var updateStructureDataDetails = (UpdateStructureDataDetails)historyUpdateDetails; return GetHistoryPermissionType(updateStructureDataDetails.PerformInsertReplace); } else if (detailsType == typeof(UpdateEventDetails)) { var updateEventDetails = (UpdateEventDetails)historyUpdateDetails; return GetHistoryPermissionType(updateEventDetails.PerformInsertReplace); } else if (detailsType == typeof(DeleteRawModifiedDetails) || detailsType == typeof(DeleteAtTimeDetails) || detailsType == typeof(DeleteEventDetails)) { return PermissionType.DeleteHistory; } return PermissionType.ModifyHistory; } /// /// Determine the History PermissionType depending on PerformUpdateType /// /// The corresponding PermissionType protected static PermissionType GetHistoryPermissionType(PerformUpdateType updateType) { switch (updateType) { case PerformUpdateType.Insert: return PermissionType.InsertHistory; case PerformUpdateType.Update: return PermissionType.InsertHistory | PermissionType.ModifyHistory; case PerformUpdateType.Replace: case PerformUpdateType.Remove: return PermissionType.ModifyHistory; default: Debug.Fail($"Unexpected update type {updateType}"); return PermissionType.ModifyHistory; } } /// public ICoreNodeManager? CoreNodeManager => m_nodeManagers[1] as ICoreNodeManager; /// public IDiagnosticsNodeManager? DiagnosticsNodeManager => m_nodeManagers[0] as IDiagnosticsNodeManager; /// public IConfigurationNodeManager? ConfigurationNodeManager => m_nodeManagers[0] as IConfigurationNodeManager; /// public virtual async ValueTask StartupAsync(CancellationToken cancellationToken = default) { await m_startupShutdownSemaphoreSlim.WaitAsync(cancellationToken).ConfigureAwait(false); try { m_logger.LogInformation( Utils.TraceMasks.StartStop, "MasterNodeManager.Startup - NodeManagers={Count}", m_nodeManagers.Count); // create the address spaces. var externalReferences = new Dictionary>(); foreach (IAsyncNodeManager nodeManager in m_nodeManagers) { try { await nodeManager.CreateAddressSpaceAsync(externalReferences, cancellationToken) .ConfigureAwait(false); } catch (Exception e) { m_logger.LogError( e, "Unexpected error creating address space for NodeManager ={NodeManager}.", nodeManager); throw; } } // update external references. foreach (IAsyncNodeManager nodeManager in m_nodeManagers) { try { await nodeManager.AddReferencesAsync(externalReferences, cancellationToken) .ConfigureAwait(false); } catch (Exception e) { m_logger.LogError( e, "Unexpected error adding references for NodeManager ={NodeManager}.", nodeManager); throw; } } } finally { m_startupShutdownSemaphoreSlim.Release(); } } /// public virtual async ValueTask SessionClosingAsync( OperationContext context, NodeId sessionId, bool deleteSubscriptions, CancellationToken cancellationToken = default) { await m_startupShutdownSemaphoreSlim.WaitAsync(cancellationToken).ConfigureAwait(false); try { foreach (IAsyncNodeManager nodeManager in m_nodeManagers) { try { await nodeManager.SessionClosingAsync(context, sessionId, deleteSubscriptions, cancellationToken) .ConfigureAwait(false); } catch (Exception e) { m_logger.LogError( e, "Unexpected error closing session for NodeManager ={NodeManager}.", nodeManager); } } } finally { m_startupShutdownSemaphoreSlim.Release(); } } /// public virtual async ValueTask SessionActivatedAsync( OperationContext context, NodeId sessionId, CancellationToken cancellationToken = default) { foreach (IAsyncNodeManager nodeManager in m_nodeManagers) { try { await nodeManager.SessionActivatedAsync(context, sessionId, cancellationToken) .ConfigureAwait(false); } catch (Exception e) { m_logger.LogError( e, "Unexpected error notifying node manager of session activation for NodeManager={NodeManager}.", nodeManager); } } } /// public virtual async ValueTask ShutdownAsync(CancellationToken cancellationToken = default) { await m_startupShutdownSemaphoreSlim.WaitAsync(cancellationToken).ConfigureAwait(false); try { m_logger.LogInformation( Utils.TraceMasks.StartStop, "MasterNodeManager.Shutdown - NodeManagers={Count}", m_nodeManagers.Count); foreach (IAsyncNodeManager nodeManager in m_nodeManagers) { await nodeManager.DeleteAddressSpaceAsync(cancellationToken) .ConfigureAwait(false); } } finally { m_startupShutdownSemaphoreSlim.Release(); } } /// public void RegisterNamespaceManager(string namespaceUri, INodeManager nodeManager) { RegisterNamespaceManager(namespaceUri, nodeManager.ToAsyncNodeManager()); } /// public void RegisterNamespaceManager(string namespaceUri, IAsyncNodeManager nodeManager) { if (string.IsNullOrEmpty(namespaceUri)) { throw new ArgumentNullException(nameof(namespaceUri)); } if (nodeManager == null) { throw new ArgumentNullException(nameof(nodeManager)); } // look up the namespace uri. int index = Server.NamespaceUris.GetIndex(namespaceUri); if (index < 0) { index = Server.NamespaceUris.Append(namespaceUri); } m_namespaceManagersSemaphoreSlim.Wait(); try { NamespaceManagers.AddOrUpdate( index, [nodeManager], (key, existingNodeManagers) => { var nodeManagers = existingNodeManagers.ToList(); nodeManagers.Add(nodeManager); return nodeManagers.AsReadOnly(); }); } finally { m_namespaceManagersSemaphoreSlim.Release(); } } /// public bool UnregisterNamespaceManager(string namespaceUri, INodeManager nodeManager) { return UnregisterNamespaceManager(namespaceUri, null, nodeManager); } /// public bool UnregisterNamespaceManager(string namespaceUri, IAsyncNodeManager nodeManager) { return UnregisterNamespaceManager(namespaceUri, nodeManager, null); } private bool UnregisterNamespaceManager(string namespaceUri, IAsyncNodeManager? asyncNodeManager, INodeManager? nodeManager) { if (string.IsNullOrEmpty(namespaceUri)) { throw new ArgumentNullException(nameof(namespaceUri)); } if (nodeManager == null && asyncNodeManager == null) { throw new ArgumentNullException(nameof(nodeManager)); } // look up the namespace uri. int namespaceIndex = Server.NamespaceUris.GetIndex(namespaceUri); if (namespaceIndex < 0) { return false; } m_namespaceManagersSemaphoreSlim.Wait(); try { if (!NamespaceManagers.TryGetValue(namespaceIndex, out IReadOnlyList? readOnlyNodeManagers)) { return false; } var nodeManagers = readOnlyNodeManagers!.ToList(); int nodeManagersFound; IAsyncNodeManager? nodeManagerToRemove = asyncNodeManager; if (nodeManagerToRemove is null) { nodeManagersFound = nodeManagers.RemoveAll(manager => manager.SyncNodeManager == nodeManager); } else { nodeManagersFound = nodeManagers.Remove(nodeManagerToRemove) ? 1 : 0; } if (nodeManagers.Count == 0) { NamespaceManagers.TryRemove(namespaceIndex, out _); } else { NamespaceManagers[namespaceIndex] = nodeManagers.AsReadOnly(); } return nodeManagersFound > 0; } finally { m_namespaceManagersSemaphoreSlim.Release(); } } /// public virtual object? GetManagerHandle(NodeId nodeId, out INodeManager? nodeManager) { object? handle; nodeManager = null; // null node ids have no manager. if (nodeId.IsNull) { return null; } // use the namespace index to select the node manager. int index = nodeId.NamespaceIndex; // check if node managers are registered - use the core node manager if unknown. if (!NamespaceManagers.TryGetValue(index, out IReadOnlyList? nodeManagers)) { handle = m_nodeManagers[1].SyncNodeManager.GetManagerHandle(nodeId); if (handle != null) { nodeManager = m_nodeManagers[1].SyncNodeManager; return handle; } return null; } foreach (IAsyncNodeManager asyncNodeManager in nodeManagers!) { handle = asyncNodeManager.SyncNodeManager.GetManagerHandle(nodeId); if (handle != null) { nodeManager = asyncNodeManager.SyncNodeManager; return handle; } } // node not recognized. return null; } /// [Obsolete("Use GetManagerHandleAsync instead.")] public virtual object? GetManagerHandle(NodeId nodeId, out IAsyncNodeManager? nodeManager) { (object? handle, IAsyncNodeManager? nodeManager) result = GetManagerHandleAsync(nodeId).AsTask().GetAwaiter().GetResult(); nodeManager = result.nodeManager; return result.handle; } /// public virtual async ValueTask<(object? handle, IAsyncNodeManager? nodeManager)> GetManagerHandleAsync(NodeId nodeId, CancellationToken cancellationToken = default) { object? handle; // null node ids have no manager. if (nodeId.IsNull) { return (null, null); } // use the namespace index to select the node manager. int index = nodeId.NamespaceIndex; // check if node managers are registered - use the core node manager if unknown. if (!NamespaceManagers.TryGetValue(index, out IReadOnlyList? nodeManagers)) { handle = await m_nodeManagers[1].GetManagerHandleAsync(nodeId, cancellationToken) .ConfigureAwait(false); if (handle != null) { return (handle, m_nodeManagers[1]); } return (null, null); } foreach (IAsyncNodeManager asyncNodeManager in nodeManagers!) { handle = await asyncNodeManager.GetManagerHandleAsync(nodeId, cancellationToken) .ConfigureAwait(false); if (handle != null) { return (handle, asyncNodeManager); } } // node not recognized. return (null, null); } /// /// Adds the references to the target. /// [Obsolete("Use AddReferencesAsync instead.")] public virtual void AddReferences(NodeId sourceId, IList references) { AddReferencesAsync(sourceId, references).AsTask().GetAwaiter().GetResult(); } /// public virtual async ValueTask AddReferencesAsync(NodeId sourceId, IList references, CancellationToken cancellationToken = default) { // find source node. (object? sourceHandle, IAsyncNodeManager? nodeManager) = await GetManagerHandleAsync(sourceId, cancellationToken) .ConfigureAwait(false); if (sourceHandle == null) { return; } var map = new Dictionary> { { sourceId, references } }; await nodeManager!.AddReferencesAsync(map, cancellationToken) .ConfigureAwait(false); } /// /// Deletes the references to the target. /// [Obsolete("Use DeleteReferencesAsync")] public virtual void DeleteReferences(NodeId targetId, IList references) { DeleteReferencesAsync(targetId, references).AsTask().GetAwaiter().GetResult(); } /// public virtual async ValueTask DeleteReferencesAsync(NodeId targetId, IList references, CancellationToken cancellationToken = default) { foreach (ReferenceNode reference in references.OfType()) { var sourceId = ExpandedNodeId.ToNodeId(reference.TargetId, Server.NamespaceUris); // find source node. (object? sourceHandle, IAsyncNodeManager? nodeManager) = await GetManagerHandleAsync(sourceId, cancellationToken) .ConfigureAwait(false); if (sourceHandle == null) { continue; } // delete the reference. await nodeManager!.DeleteReferenceAsync( sourceHandle, reference.ReferenceTypeId, !reference.IsInverse, targetId, false, cancellationToken) .ConfigureAwait(false); } } /// [Obsolete("Use RemoveReferencesAsync instead.")] public void RemoveReferences(List referencesToRemove) { RemoveReferencesAsync(referencesToRemove).AsTask().GetAwaiter().GetResult(); } /// public async ValueTask RemoveReferencesAsync(List referencesToRemove, CancellationToken cancellationToken = default) { for (int ii = 0; ii < referencesToRemove.Count; ii++) { LocalReference reference = referencesToRemove[ii]; // find source node. (object? sourceHandle, IAsyncNodeManager? nodeManager) = await GetManagerHandleAsync( reference.SourceId, cancellationToken) .ConfigureAwait(false); if (sourceHandle == null) { continue; } // delete the reference. await nodeManager!.DeleteReferenceAsync( sourceHandle, reference.ReferenceTypeId, reference.IsInverse, reference.TargetId, false, cancellationToken) .ConfigureAwait(false); } } /// public virtual async ValueTask<(ArrayOf results, ArrayOf diagnosticInfos)> AddNodesAsync( OperationContext context, ArrayOf nodesToAdd, CancellationToken cancellationToken = default) { if (context == null) { throw new ArgumentNullException(nameof(context)); } var results = new AddNodesResult[nodesToAdd.Count]; var diagnosticInfos = new DiagnosticInfo[nodesToAdd.Count]; bool anyDiagnostics = false; for (int ii = 0; ii < nodesToAdd.Count; ii++) { AddNodesItem item = nodesToAdd[ii]; (ServiceResult result, NodeId addedNodeId) = await DispatchAddNodeAsync( context, item, cancellationToken).ConfigureAwait(false); results[ii] = new AddNodesResult { StatusCode = result.StatusCode, AddedNodeId = addedNodeId }; if (ServiceResult.IsBad(result)) { anyDiagnostics = true; diagnosticInfos[ii] = new DiagnosticInfo( result, context.DiagnosticsMask, false, context.StringTable, m_logger); } } return (results.ToArrayOf(), anyDiagnostics ? diagnosticInfos.ToArrayOf() : default); } /// public virtual async ValueTask<(ArrayOf results, ArrayOf diagnosticInfos)> DeleteNodesAsync( OperationContext context, ArrayOf nodesToDelete, CancellationToken cancellationToken = default) { if (context == null) { throw new ArgumentNullException(nameof(context)); } var results = new StatusCode[nodesToDelete.Count]; var diagnosticInfos = new DiagnosticInfo[nodesToDelete.Count]; bool anyDiagnostics = false; for (int ii = 0; ii < nodesToDelete.Count; ii++) { DeleteNodesItem item = nodesToDelete[ii]; ServiceResult result = await DispatchDeleteNodeAsync( context, item, cancellationToken).ConfigureAwait(false); results[ii] = result.StatusCode; if (ServiceResult.IsBad(result)) { anyDiagnostics = true; diagnosticInfos[ii] = new DiagnosticInfo( result, context.DiagnosticsMask, false, context.StringTable, m_logger); } } return (results.ToArrayOf(), anyDiagnostics ? diagnosticInfos.ToArrayOf() : default); } /// public virtual async ValueTask<(ArrayOf results, ArrayOf diagnosticInfos)> AddReferencesAsync( OperationContext context, ArrayOf referencesToAdd, CancellationToken cancellationToken = default) { if (context == null) { throw new ArgumentNullException(nameof(context)); } var results = new StatusCode[referencesToAdd.Count]; var diagnosticInfos = new DiagnosticInfo[referencesToAdd.Count]; bool anyDiagnostics = false; for (int ii = 0; ii < referencesToAdd.Count; ii++) { AddReferencesItem item = referencesToAdd[ii]; ServiceResult result = await DispatchAddReferenceAsync( context, item, cancellationToken).ConfigureAwait(false); results[ii] = result.StatusCode; if (ServiceResult.IsBad(result)) { anyDiagnostics = true; diagnosticInfos[ii] = new DiagnosticInfo( result, context.DiagnosticsMask, false, context.StringTable, m_logger); } } return (results.ToArrayOf(), anyDiagnostics ? diagnosticInfos.ToArrayOf() : default); } /// public virtual async ValueTask<(ArrayOf results, ArrayOf diagnosticInfos)> DeleteReferencesAsync( OperationContext context, ArrayOf referencesToDelete, CancellationToken cancellationToken = default) { if (context == null) { throw new ArgumentNullException(nameof(context)); } var results = new StatusCode[referencesToDelete.Count]; var diagnosticInfos = new DiagnosticInfo[referencesToDelete.Count]; bool anyDiagnostics = false; for (int ii = 0; ii < referencesToDelete.Count; ii++) { DeleteReferencesItem item = referencesToDelete[ii]; ServiceResult result = await DispatchDeleteReferenceAsync( context, item, cancellationToken).ConfigureAwait(false); results[ii] = result.StatusCode; if (ServiceResult.IsBad(result)) { anyDiagnostics = true; diagnosticInfos[ii] = new DiagnosticInfo( result, context.DiagnosticsMask, false, context.StringTable, m_logger); } } return (results.ToArrayOf(), anyDiagnostics ? diagnosticInfos.ToArrayOf() : default); } private async ValueTask<(ServiceResult result, NodeId addedNodeId)> DispatchAddNodeAsync( OperationContext context, AddNodesItem item, CancellationToken cancellationToken) { if (item == null) { return (new ServiceResult(StatusCodes.BadNothingToDo), NodeId.Null); } if (item.BrowseName.IsNull) { return (new ServiceResult(StatusCodes.BadBrowseNameInvalid), NodeId.Null); } if (item.ParentNodeId.IsNull) { return (new ServiceResult(StatusCodes.BadParentNodeIdInvalid), NodeId.Null); } if (item.ReferenceTypeId.IsNull || !Server.TypeTree.IsKnown(item.ReferenceTypeId)) { return (new ServiceResult(StatusCodes.BadReferenceTypeIdInvalid), NodeId.Null); } if (!Server.TypeTree.IsTypeOf( item.ReferenceTypeId, ReferenceTypeIds.HierarchicalReferences)) { return (new ServiceResult(StatusCodes.BadReferenceNotAllowed), NodeId.Null); } var parentNodeId = ExpandedNodeId.ToNodeId(item.ParentNodeId, Server.NamespaceUris); if (parentNodeId.IsNull) { return (new ServiceResult(StatusCodes.BadParentNodeIdInvalid), NodeId.Null); } // Routing per spec: // - When RequestedNewNodeId is provided, dispatch to that namespace's // owner (the namespace dictates the responsible NodeManager). // - Otherwise, prefer the parent's owner. If the parent's owner has // not opted in to NodeManagement, fall back to the first // opted-in NodeManager so cross-NodeManager AddNodes under a // read-only parent (e.g. ObjectsFolder) is still serviceable. INodeManagementAsyncNodeManager? nodeManagement = null; if (!item.RequestedNewNodeId.IsNull) { var requestedNodeId = ExpandedNodeId.ToNodeId( item.RequestedNewNodeId, Server.NamespaceUris); if (requestedNodeId.IsNull) { return (new ServiceResult(StatusCodes.BadNodeIdRejected), NodeId.Null); } nodeManagement = FindNodeManagementOwner(requestedNodeId.NamespaceIndex); if (nodeManagement == null || !nodeManagement.AllowNodeManagement) { return (new ServiceResult(StatusCodes.BadUserAccessDenied), NodeId.Null); } } else { (object? parentHandle, IAsyncNodeManager? parentOwner) = await GetManagerHandleAsync(parentNodeId, cancellationToken).ConfigureAwait(false); if (parentHandle == null || parentOwner == null) { return (new ServiceResult(StatusCodes.BadParentNodeIdInvalid), NodeId.Null); } if (parentOwner.AllowNodeManagement) { nodeManagement = parentOwner; } else { nodeManagement = FindFirstOptedInNodeManager(); if (nodeManagement == null) { return (new ServiceResult(StatusCodes.BadUserAccessDenied), NodeId.Null); } } } try { return await nodeManagement.AddNodeAsync(context, item, cancellationToken) .ConfigureAwait(false); } catch (ServiceResultException ex) { return (new ServiceResult(ex), NodeId.Null); } } private async ValueTask DispatchDeleteNodeAsync( OperationContext context, DeleteNodesItem item, CancellationToken cancellationToken) { if (item == null) { return new ServiceResult(StatusCodes.BadNothingToDo); } if (item.NodeId.IsNull) { return new ServiceResult(StatusCodes.BadNodeIdInvalid); } (object? handle, IAsyncNodeManager? owner) = await GetManagerHandleAsync(item.NodeId, cancellationToken).ConfigureAwait(false); if (handle == null || owner == null) { return new ServiceResult(StatusCodes.BadNodeIdUnknown); } if (!owner.AllowNodeManagement) { return new ServiceResult(StatusCodes.BadUserAccessDenied); } try { return await owner.DeleteNodeAsync(context, item, cancellationToken) .ConfigureAwait(false); } catch (ServiceResultException ex) { return new ServiceResult(ex); } } private async ValueTask DispatchAddReferenceAsync( OperationContext context, AddReferencesItem item, CancellationToken cancellationToken) { if (item == null) { return new ServiceResult(StatusCodes.BadNothingToDo); } if (item.SourceNodeId.IsNull) { return new ServiceResult(StatusCodes.BadSourceNodeIdInvalid); } if (item.TargetNodeId.IsNull) { return new ServiceResult(StatusCodes.BadTargetNodeIdInvalid); } if (item.ReferenceTypeId.IsNull || !Server.TypeTree.IsKnown(item.ReferenceTypeId)) { return new ServiceResult(StatusCodes.BadReferenceTypeIdInvalid); } (object? sourceHandle, IAsyncNodeManager? sourceOwner) = await GetManagerHandleAsync(item.SourceNodeId, cancellationToken).ConfigureAwait(false); if (sourceHandle == null || sourceOwner == null) { return new ServiceResult(StatusCodes.BadSourceNodeIdInvalid); } if (!sourceOwner.AllowNodeManagement) { return new ServiceResult(StatusCodes.BadUserAccessDenied); } ServiceResult sourceResult; try { sourceResult = await sourceOwner.AddReferenceAsync(context, item, cancellationToken) .ConfigureAwait(false); } catch (ServiceResultException ex) { return new ServiceResult(ex); } if (ServiceResult.IsBad(sourceResult)) { return sourceResult; } // Best-effort: write the complementary edge into the target's owning manager // when the target is local. Failures here are logged but do not change the // source-side success so the source half stays consistent. var targetNodeId = ExpandedNodeId.ToNodeId(item.TargetNodeId, Server.NamespaceUris); if (!targetNodeId.IsNull) { (object? targetHandle, IAsyncNodeManager? targetOwner) = await GetManagerHandleAsync( targetNodeId, cancellationToken).ConfigureAwait(false); if (targetHandle != null && targetOwner != null && !ReferenceEquals(targetOwner, sourceOwner) && targetOwner.AllowNodeManagement) { var inverseItem = new AddReferencesItem { SourceNodeId = targetNodeId, ReferenceTypeId = item.ReferenceTypeId, IsForward = !item.IsForward, TargetServerUri = item.TargetServerUri, TargetNodeId = item.SourceNodeId, TargetNodeClass = item.TargetNodeClass }; try { ServiceResult inverseResult = await targetOwner.AddReferenceAsync( context, inverseItem, cancellationToken).ConfigureAwait(false); if (ServiceResult.IsBad(inverseResult)) { m_logger.LogWarning( "AddReferences: failed to mirror inverse edge {RefType} from {Source} to {Target} on owning NodeManager: {Status}", item.ReferenceTypeId, item.SourceNodeId, item.TargetNodeId, inverseResult.StatusCode); } } catch (ServiceResultException ex) { m_logger.LogWarning( ex, "AddReferences: failed to mirror inverse edge {RefType} from {Source} to {Target} on owning NodeManager.", item.ReferenceTypeId, item.SourceNodeId, item.TargetNodeId); } } } return sourceResult; } private async ValueTask DispatchDeleteReferenceAsync( OperationContext context, DeleteReferencesItem item, CancellationToken cancellationToken) { if (item == null) { return new ServiceResult(StatusCodes.BadNothingToDo); } if (item.SourceNodeId.IsNull) { return new ServiceResult(StatusCodes.BadSourceNodeIdInvalid); } if (item.ReferenceTypeId.IsNull || !Server.TypeTree.IsKnown(item.ReferenceTypeId)) { return new ServiceResult(StatusCodes.BadReferenceTypeIdInvalid); } (object? sourceHandle, IAsyncNodeManager? sourceOwner) = await GetManagerHandleAsync(item.SourceNodeId, cancellationToken).ConfigureAwait(false); if (sourceHandle == null || sourceOwner == null) { return new ServiceResult(StatusCodes.BadSourceNodeIdInvalid); } if (!sourceOwner.AllowNodeManagement) { return new ServiceResult(StatusCodes.BadUserAccessDenied); } ServiceResult sourceResult; try { sourceResult = await sourceOwner.DeleteReferenceAsync(context, item, cancellationToken) .ConfigureAwait(false); } catch (ServiceResultException ex) { return new ServiceResult(ex); } if (ServiceResult.IsBad(sourceResult)) { return sourceResult; } if (!item.DeleteBidirectional) { return sourceResult; } // Cross-manager: remove the complementary edge on the target's owner. var targetNodeId = ExpandedNodeId.ToNodeId(item.TargetNodeId, Server.NamespaceUris); if (targetNodeId.IsNull) { return sourceResult; } (object? targetHandle2, IAsyncNodeManager? targetOwner2) = await GetManagerHandleAsync( targetNodeId, cancellationToken).ConfigureAwait(false); if (targetHandle2 != null && targetOwner2 != null && !ReferenceEquals(targetOwner2, sourceOwner) && targetOwner2.AllowNodeManagement) { var inverseItem = new DeleteReferencesItem { SourceNodeId = targetNodeId, ReferenceTypeId = item.ReferenceTypeId, IsForward = !item.IsForward, TargetNodeId = item.SourceNodeId, DeleteBidirectional = false }; try { ServiceResult inverseResult = await targetOwner2.DeleteReferenceAsync( context, inverseItem, cancellationToken).ConfigureAwait(false); if (ServiceResult.IsBad(inverseResult)) { m_logger.LogWarning( "DeleteReferences: failed to mirror inverse delete {RefType} from {Source} to {Target} on owning NodeManager: {Status}", item.ReferenceTypeId, item.SourceNodeId, item.TargetNodeId, inverseResult.StatusCode); } } catch (ServiceResultException ex) { m_logger.LogWarning( ex, "DeleteReferences: failed to mirror inverse delete {RefType} from {Source} to {Target} on owning NodeManager.", item.ReferenceTypeId, item.SourceNodeId, item.TargetNodeId); } } return sourceResult; } /// /// Returns the first NodeManager registered against the given namespace /// index, or null when no NodeManager owns the namespace. Since /// is aggregated into /// every owner exposes the surface; /// callers should still check . /// private INodeManagementAsyncNodeManager? FindNodeManagementOwner(ushort namespaceIndex) { if (!NamespaceManagers.TryGetValue(namespaceIndex, out IReadOnlyList? nodeManagers)) { return null; } return nodeManagers.Count > 0 ? nodeManagers[0] : null; } /// /// Returns the first registered NodeManager that has opted in to /// NodeManagement, or null if no NodeManager has done so. Used /// as the cross-NodeManager AddNodes fallback when the parent's /// owning NodeManager does not allow NodeManagement. /// private INodeManagementAsyncNodeManager? FindFirstOptedInNodeManager() { for (int ii = 0; ii < m_nodeManagers.Count; ii++) { if (m_nodeManagers[ii].AllowNodeManagement) { return m_nodeManagers[ii]; } } return null; } /// public virtual void RegisterNodes( OperationContext context, ArrayOf nodesToRegister, out ArrayOf registeredNodeIds) { // return the node id provided. registeredNodeIds = nodesToRegister; m_logger.LogTrace( Utils.TraceMasks.ServiceDetail, "MasterNodeManager.RegisterNodes - Count={Count}", nodesToRegister.Count); // it is up to the node managers to assign the handles. /* List processedNodes = new List(new bool[itemsToDelete.Count]); for (int ii = 0; ii < m_nodeManagers.Count; ii++) { m_nodeManagers[ii].RegisterNodes( context, nodesToRegister, registeredNodeIds, processedNodes); } */ } /// public virtual void UnregisterNodes( OperationContext context, ArrayOf nodesToUnregister) { m_logger.LogTrace( Utils.TraceMasks.ServiceDetail, "MasterNodeManager.UnregisterNodes - Count={Count}", nodesToUnregister.Count); // it is up to the node managers to assign the handles. /* List processedNodes = new List(new bool[itemsToDelete.Count]); for (int ii = 0; ii < m_nodeManagers.Count; ii++) { m_nodeManagers[ii].RegisterNodes( context, nodesToUnregister, processedNodes); } */ } /// /// Translates a start node id plus a relative paths into a node id. /// /// is null. /// [Obsolete("Use TranslateBrowsePathsToNodeIdsAsync instead.")] public virtual void TranslateBrowsePathsToNodeIds( OperationContext context, ArrayOf browsePaths, out ArrayOf results, out ArrayOf diagnosticInfos) { (results, diagnosticInfos) = TranslateBrowsePathsToNodeIdsAsync( context, browsePaths).AsTask().GetAwaiter().GetResult(); } /// public virtual async ValueTask<(ArrayOf results, ArrayOf diagnosticInfos)> TranslateBrowsePathsToNodeIdsAsync( OperationContext context, ArrayOf browsePaths, CancellationToken cancellationToken = default) { bool diagnosticsExist = false; var results = new List(browsePaths.Count); var diagnosticInfos = new List(browsePaths.Count); for (int ii = 0; ii < browsePaths.Count; ii++) { // check if request has timed out or been cancelled. if (StatusCode.IsBad(context.OperationStatus)) { throw new ServiceResultException(context.OperationStatus); } BrowsePath browsePath = browsePaths[ii]; var result = new BrowsePathResult { StatusCode = StatusCodes.Good }; results.Add(result); ServiceResult error; // need to trap unexpected exceptions to handle bugs in the node managers. try { error = await TranslateBrowsePathAsync(context, browsePath, result, cancellationToken) .ConfigureAwait(false); } catch (Exception e) { error = ServiceResult.Create( e, StatusCodes.BadUnexpectedError, "Unexpected error translating browse path."); } if (ServiceResult.IsGood(error)) { // check for no match. if (result.Targets.Count == 0) { error = StatusCodes.BadNoMatch; } // put a placeholder for diagnostics. else if ((context.DiagnosticsMask & DiagnosticsMasks.OperationAll) != 0) { diagnosticInfos.Add(null!); } } // check for error. if (error != null && error.Code != StatusCodes.Good) { result.StatusCode = error.StatusCode; if ((context.DiagnosticsMask & DiagnosticsMasks.OperationAll) != 0) { DiagnosticInfo? diagnosticInfo = ServerUtils.CreateDiagnosticInfo( Server, context, error, m_logger); diagnosticInfos.Add(diagnosticInfo!); diagnosticsExist = true; } } } // clear the diagnostics array if no diagnostics requested or no errors occurred. UpdateDiagnostics(context, diagnosticsExist, ref diagnosticInfos); return (results, diagnosticInfos); } /// /// Updates the diagnostics return parameter. /// protected void UpdateDiagnostics( OperationContext context, bool diagnosticsExist, ref List diagnosticInfos) { if (diagnosticsExist && context.StringTable.Count == 0) { diagnosticsExist = false; for (int ii = 0; !diagnosticsExist && ii < diagnosticInfos.Count; ii++) { DiagnosticInfo? diagnosticInfo = diagnosticInfos[ii]; int depth = 0; while (diagnosticInfo != null && depth++ < DiagnosticInfo.MaxInnerDepth) { if (!string.IsNullOrEmpty(diagnosticInfo.AdditionalInfo)) { diagnosticsExist = true; break; } diagnosticInfo = diagnosticInfo.InnerDiagnosticInfo; } } } if (!diagnosticsExist) { diagnosticInfos = null!; } } /// /// Translates a browse path. /// protected async ValueTask TranslateBrowsePathAsync( OperationContext context, BrowsePath browsePath, BrowsePathResult result, CancellationToken cancellationToken) { Debug.Assert(browsePath != null); Debug.Assert(result != null); // check for valid start node. (object? sourceHandle, IAsyncNodeManager? nodeManager) = await GetManagerHandleAsync( browsePath!.StartingNode, cancellationToken) .ConfigureAwait(false); if (sourceHandle == null) { return StatusCodes.BadNodeIdUnknown; } // check the relative path. RelativePath relativePath = browsePath.RelativePath; if (relativePath.Elements.IsEmpty) { return StatusCodes.BadNothingToDo; } for (int ii = 0; ii < relativePath.Elements.Count; ii++) { RelativePathElement element = relativePath.Elements[ii]; if (element == null || relativePath.Elements[ii].TargetName.IsNull) { return StatusCodes.BadBrowseNameInvalid; } if (element.ReferenceTypeId.IsNull) { element.ReferenceTypeId = ReferenceTypeIds.References; element.IncludeSubtypes = true; } } // validate access rights and role permissions ServiceResult serviceResult = await ValidatePermissionsAsync( context, nodeManager, sourceHandle, PermissionType.Browse, null, true, cancellationToken) .ConfigureAwait(false); if (ServiceResult.IsGood(serviceResult)) { List targets = []; // translate path only if validation is passing await TranslateBrowsePathAsync( context, nodeManager, sourceHandle, relativePath, targets, 0, cancellationToken) .ConfigureAwait(false); result!.Targets = targets; } return serviceResult; } /// /// Recursively processes the elements in the RelativePath starting at the specified index. /// /// private async ValueTask TranslateBrowsePathAsync( OperationContext context, IAsyncNodeManager? nodeManager, object? sourceHandle, RelativePath relativePath, List targets, int index, CancellationToken cancellationToken) { Debug.Assert(nodeManager != null); Debug.Assert(sourceHandle != null); Debug.Assert(relativePath != null); Debug.Assert(targets != null); // check for end of list. if (index < 0 || index >= relativePath!.Elements.Count) { return; } // follow the next hop. RelativePathElement element = relativePath.Elements[index]; // check for valid reference type. if (!element.IncludeSubtypes && element.ReferenceTypeId.IsNull) { return; } // check for valid target name. if (element.TargetName.IsNull) { throw new ServiceResultException(StatusCodes.BadBrowseNameInvalid); } var targetIds = new List(); var externalTargetIds = new List(); try { await nodeManager!.TranslateBrowsePathAsync( context, sourceHandle!, element, targetIds, externalTargetIds, cancellationToken) .ConfigureAwait(false); } catch (Exception e) { m_logger.LogError(e, "Unexpected error translating browse path."); return; } // must check the browse name on all external targets. for (int ii = 0; ii < externalTargetIds.Count; ii++) { // get the browse name from another node manager. var description = new ReferenceDescription(); await UpdateReferenceDescriptionAsync( context, externalTargetIds[ii], NodeClass.Unspecified, BrowseResultMask.BrowseName, description, cancellationToken) .ConfigureAwait(false); // add to list if target name matches. if (description.BrowseName == element.TargetName) { bool found = false; for (int jj = 0; jj < targetIds.Count; jj++) { if (targetIds[jj] == externalTargetIds[ii]) { found = true; break; } } if (!found) { targetIds.Add(externalTargetIds[ii]); } } } // check if done after a final hop. if (index == relativePath.Elements.Count - 1) { for (int ii = 0; ii < targetIds.Count; ii++) { // Check the role permissions for target nodes (object? targetHandle, IAsyncNodeManager? targetNodeManager) = await GetManagerHandleAsync( ExpandedNodeId.ToNodeId(targetIds[ii], Server.NamespaceUris), cancellationToken) .ConfigureAwait(false); if (targetHandle != null && targetNodeManager != null) { NodeMetadata nodeMetadata = await targetNodeManager.GetNodeMetadataAsync( context, targetHandle, BrowseResultMask.All, cancellationToken) .ConfigureAwait(false); ServiceResult serviceResult = ValidateRolePermissions( context, nodeMetadata, PermissionType.Browse, m_logger); if (ServiceResult.IsBad(serviceResult)) { // Remove target node without role permissions. continue; } } var target = new BrowsePathTarget { TargetId = targetIds[ii], RemainingPathIndex = uint.MaxValue }; targets!.Add(target); } return; } // process next hops. for (int ii = 0; ii < targetIds.Count; ii++) { ExpandedNodeId targetId = targetIds[ii]; // check for external reference. if (targetId.IsAbsolute) { var target = new BrowsePathTarget { TargetId = targetId, RemainingPathIndex = (uint)(index + 1) }; targets!.Add(target); continue; } // check for valid start node. (sourceHandle, nodeManager) = await GetManagerHandleAsync((NodeId)targetId, cancellationToken) .ConfigureAwait(false); if (sourceHandle == null) { continue; } // recursively follow hops. await TranslateBrowsePathAsync( context, nodeManager, sourceHandle, relativePath, targets!, index + 1, cancellationToken) .ConfigureAwait(false); } } /// public virtual async ValueTask<(ArrayOf results, ArrayOf diagnosticInfos)> BrowseAsync( OperationContext context, ViewDescription view, uint maxReferencesPerNode, ArrayOf nodesToBrowse, CancellationToken cancellationToken = default) { if (context == null) { throw new ArgumentNullException(nameof(context)); } if (view != null && !view.ViewId.IsNull) { (object? viewHandle, IAsyncNodeManager? viewManager) = await GetManagerHandleAsync(view.ViewId, cancellationToken) .ConfigureAwait(false); if (viewHandle == null || viewManager == null) { throw new ServiceResultException(StatusCodes.BadViewIdUnknown); } NodeMetadata metadata = await viewManager.GetNodeMetadataAsync( context, viewHandle, BrowseResultMask.NodeClass, cancellationToken) .ConfigureAwait(false); if (metadata == null || metadata.NodeClass != NodeClass.View) { throw new ServiceResultException(StatusCodes.BadViewIdUnknown); } // validate access rights and role permissions ServiceResult validationResult = await ValidatePermissionsAsync( context, viewManager, viewHandle, PermissionType.Browse, null, true, cancellationToken) .ConfigureAwait(false); if (ServiceResult.IsBad(validationResult)) { throw new ServiceResultException(validationResult); } view.Handle = viewHandle; } bool diagnosticsExist = false; var results = new List(nodesToBrowse.Count); var diagnosticInfos = new List(nodesToBrowse.Count); uint continuationPointsAssigned = 0; for (int ii = 0; ii < nodesToBrowse.Count; ii++) { // check if request has timed out or been cancelled. if (StatusCode.IsBad(context.OperationStatus)) { // release all allocated continuation points. foreach (BrowseResult current in results) { if (current != null && !current.ContinuationPoint.IsEmpty) { ContinuationPoint? cp = context.Session .RestoreContinuationPoint(current.ContinuationPoint); cp?.Dispose(); } } throw new ServiceResultException(context.OperationStatus); } BrowseDescription nodeToBrowse = nodesToBrowse[ii]; // initialize result. var result = new BrowseResult { StatusCode = StatusCodes.Good }; results.Add(result); ServiceResult error; // need to trap unexpected exceptions to handle bugs in the node managers. try { error = await BrowseAsync( context, view, maxReferencesPerNode, continuationPointsAssigned < m_maxContinuationPointsPerBrowse, nodeToBrowse, result, cancellationToken).ConfigureAwait(false); } catch (Exception e) { error = ServiceResult.Create( e, StatusCodes.BadUnexpectedError, "Unexpected error browsing node."); } // check for continuation point. if (!result.ContinuationPoint.IsEmpty) { continuationPointsAssigned++; } // check for error. result.StatusCode = error.StatusCode; if ((context.DiagnosticsMask & DiagnosticsMasks.OperationAll) != 0) { DiagnosticInfo? diagnosticInfo = null; if (error != null && error.Code != StatusCodes.Good) { diagnosticInfo = ServerUtils.CreateDiagnosticInfo(Server, context, error, m_logger); diagnosticsExist = true; } diagnosticInfos.Add(diagnosticInfo!); } } // clear the diagnostics array if no diagnostics requested or no errors occurred. UpdateDiagnostics(context, diagnosticsExist, ref diagnosticInfos); return (results, diagnosticInfos); } /// /// Prepare a cache per NodeManager and unique NodeId that holds the attributes needed to validate the AccessRestrictions and RolePermissions. /// This cache is then used in subsequenct calls to avoid triggering unnecessary time consuming callbacks. /// The current services that benefit from this are the Read service /// /// One of the following types used in the service calls: /// ReadValueId used in the Read service /// The collection of nodes on which the service operates uppon /// The resulting cache that holds the values of the AccessRestrictions and RolePermissions attributes needed for Read service /// private static void PrepareValidationCache( ArrayOf nodesList, out Dictionary uniqueNodesServiceAttributes) { var uniqueNodes = new HashSet(); Type listType = typeof(T); if (listType != typeof(ReadValueId) && listType != typeof(WriteValue) && listType != typeof(BrowseDescription) && listType != typeof(CallMethodRequest)) { throw new ArgumentException( "Provided List nodesCollection is of wrong type, T should be type BrowseDescription, ReadValueId or CallMethodRequest", nameof(nodesList)); } for (int i = 0; i < nodesList.Count; i++) { NodeId nodeId = default; if (listType == typeof(ReadValueId)) { nodeId = (nodesList[i] as ReadValueId)?.NodeId ?? default; } else if (listType == typeof(BrowseDescription)) { nodeId = (nodesList[i] as BrowseDescription)?.NodeId ?? default; } else if (listType == typeof(CallMethodRequest)) { nodeId = (nodesList[i] as CallMethodRequest)?.ObjectId ?? default; } if (!nodeId.IsNull) { uniqueNodes.Add(nodeId); } } // uniqueNodesReadAttributes is the place where the attributes for each unique nodeId are kept on the services uniqueNodesServiceAttributes = []; foreach (NodeId uniqueNode in uniqueNodes) { uniqueNodesServiceAttributes.Add(uniqueNode, []); } } /// public virtual async ValueTask<(ArrayOf results, ArrayOf diagnosticInfos)> BrowseNextAsync( OperationContext context, bool releaseContinuationPoints, ArrayOf continuationPoints, CancellationToken cancellationToken = default) { if (context == null) { throw new ArgumentNullException(nameof(context)); } bool diagnosticsExist = false; var results = new List(continuationPoints.Count); var diagnosticInfos = new List(continuationPoints.Count); uint continuationPointsAssigned = 0; for (int ii = 0; ii < continuationPoints.Count; ii++) { ContinuationPoint? cp; // check if request has timed out or been canceled. if (StatusCode.IsBad(context.OperationStatus)) { // release all allocated continuation points. foreach (BrowseResult current in results) { if (current != null && !current.ContinuationPoint.IsEmpty) { cp = context.Session .RestoreContinuationPoint(current.ContinuationPoint); cp?.Dispose(); } } throw new ServiceResultException(context.OperationStatus); } // find the continuation point. cp = context.Session.RestoreContinuationPoint(continuationPoints[ii]); // validate access rights and role permissions if (cp != null) { ServiceResult validationResult = await ValidatePermissionsAsync( context, cp.Manager, cp.NodeToBrowse, PermissionType.Browse, null, true, cancellationToken) .ConfigureAwait(false); if (ServiceResult.IsBad(validationResult)) { var badResult = new BrowseResult { StatusCode = validationResult.Code }; results.Add(badResult); // put placeholder for diagnostics diagnosticInfos.Add(null!); continue; } } // initialize result. var result = new BrowseResult { StatusCode = StatusCodes.Good }; results.Add(result); // check if simply releasing the continuation point. if (releaseContinuationPoints) { cp?.Dispose(); continue; } ServiceResult? error = null; // check if continuation point has expired. if (cp == null) { error = StatusCodes.BadContinuationPointInvalid; } if (cp != null) { // need to trap unexpected exceptions to handle bugs in the node managers. try { ArrayOf references = result.References; (error, cp, references) = await FetchReferencesAsync( context, continuationPointsAssigned < m_maxContinuationPointsPerBrowse, cp!, references, cancellationToken) .ConfigureAwait(false); result.References = references; } catch (Exception e) { error = ServiceResult.Create( e, StatusCodes.BadUnexpectedError, "Unexpected error browsing node."); } // check for continuation point. if (!result.ContinuationPoint.IsEmpty) { continuationPointsAssigned++; } } // check for error. result.StatusCode = error!.StatusCode; if ((context.DiagnosticsMask & DiagnosticsMasks.OperationAll) != 0) { DiagnosticInfo? diagnosticInfo = null; if (error != null && error.Code != StatusCodes.Good) { diagnosticInfo = ServerUtils.CreateDiagnosticInfo( Server, context, error, m_logger); diagnosticsExist = true; } diagnosticInfos.Add(diagnosticInfo!); } // check for continuation point. if (cp != null) { result.StatusCode = StatusCodes.Good; result.ContinuationPoint = cp.Id.ToByteArray().ToByteString(); } } // clear the diagnostics array if no diagnostics requested or no errors occurred. UpdateDiagnostics(context, diagnosticsExist, ref diagnosticInfos); return (results, diagnosticInfos); } /// /// Returns the set of references that meet the filter criteria. /// protected async ValueTask BrowseAsync( OperationContext context, ViewDescription? view, uint maxReferencesPerNode, bool assignContinuationPoint, BrowseDescription nodeToBrowse, BrowseResult result, CancellationToken cancellationToken = default) { Debug.Assert(context != null); Debug.Assert(nodeToBrowse != null); Debug.Assert(result != null); // find node manager that owns the node. (object? handle, IAsyncNodeManager? nodeManager) = await GetManagerHandleAsync(nodeToBrowse!.NodeId, cancellationToken) .ConfigureAwait(false); if (handle == null) { return StatusCodes.BadNodeIdUnknown; } if (!nodeToBrowse.ReferenceTypeId.IsNull && !Server.TypeTree.IsKnown(nodeToBrowse.ReferenceTypeId)) { return StatusCodes.BadReferenceTypeIdInvalid; } if (nodeToBrowse.BrowseDirection is < BrowseDirection.Forward or > BrowseDirection.Both) { return StatusCodes.BadBrowseDirectionInvalid; } // validate access rights and role permissions ServiceResult validationResult = await ValidatePermissionsAsync( context!, nodeManager, handle, PermissionType.Browse, null, true, cancellationToken) .ConfigureAwait(false); if (ServiceResult.IsBad(validationResult)) { return validationResult; } // create a continuation point. ContinuationPoint? tempCp = null; try { tempCp = new ContinuationPoint { Manager = nodeManager!, View = view, NodeToBrowse = handle, MaxResultsToReturn = maxReferencesPerNode, BrowseDirection = nodeToBrowse.BrowseDirection, ReferenceTypeId = nodeToBrowse.ReferenceTypeId, IncludeSubtypes = nodeToBrowse.IncludeSubtypes, NodeClassMask = nodeToBrowse.NodeClassMask, ResultMask = (BrowseResultMask)nodeToBrowse.ResultMask, Index = 0, Data = null }; ContinuationPoint? cp = tempCp; // check if reference type left unspecified. if (cp.ReferenceTypeId.IsNull) { cp.ReferenceTypeId = ReferenceTypeIds.References; cp.IncludeSubtypes = true; } // loop until browse is complete or max results. ArrayOf references = result!.References; ServiceResult error; (error, cp, references) = await FetchReferencesAsync( context!, assignContinuationPoint, cp!, references, cancellationToken) .ConfigureAwait(false); tempCp = null; // ownership transferred to FetchReferencesAsync result.References = references; // save continuation point. if (cp != null) { result.StatusCode = StatusCodes.Good; result.ContinuationPoint = cp.Id.ToByteArray().ToByteString(); } // all is good. return error; } finally { tempCp?.Dispose(); } } /// /// Loops until browse is complete for max results reached. /// protected async ValueTask<( ServiceResult serviceResult, ContinuationPoint? cp, ArrayOf references )> FetchReferencesAsync( OperationContext context, bool assignContinuationPoint, ContinuationPoint cp, ArrayOf references, CancellationToken cancellationToken = default) { Debug.Assert(context != null); Debug.Assert(cp != null); IAsyncNodeManager nodeManager = cp!.Manager; var nodeClassMask = (NodeClass)cp.NodeClassMask; BrowseResultMask resultMask = cp.ResultMask; var referenceList = references.ToList(); ContinuationPoint? currentCp = cp; // loop until browse is complete or max results. while (currentCp != null) { currentCp = await nodeManager.BrowseAsync(context!, currentCp, referenceList, cancellationToken) .ConfigureAwait(false); var referencesToKeep = new List(referenceList.Count); // check for incomplete reference descriptions. for (int ii = 0; ii < referenceList.Count; ii++) { ReferenceDescription reference = referenceList[ii]; // check if filtering must be applied. if (reference.Unfiltered) { // ignore unknown external references. if (reference.NodeId.IsAbsolute) { continue; } // update the description. bool include = await UpdateReferenceDescriptionAsync( context!, (NodeId)reference.NodeId, nodeClassMask, resultMask, reference, cancellationToken) .ConfigureAwait(false); if (!include) { continue; } } // add to list. referencesToKeep.Add(reference); } // replace list. referenceList = referencesToKeep; // check if browse limit reached. if (currentCp != null && referenceList.Count >= currentCp.MaxResultsToReturn) { if (!assignContinuationPoint) { return (StatusCodes.BadNoContinuationPoints, currentCp, referenceList); } currentCp.Id = Guid.NewGuid(); context!.Session!.SaveContinuationPoint(currentCp); break; } } // all is good. return (ServiceResult.Good, currentCp, referenceList); } /// /// Updates the reference description with the node attributes. /// /// is null. private async ValueTask UpdateReferenceDescriptionAsync( OperationContext context, NodeId targetId, NodeClass nodeClassMask, BrowseResultMask resultMask, ReferenceDescription description, CancellationToken cancellationToken = default) { if (targetId.IsNull) { throw new ArgumentNullException(nameof(targetId)); } if (description == null) { throw new ArgumentNullException(nameof(description)); } // find node manager that owns the node. (object? handle, IAsyncNodeManager? nodeManager) = await GetManagerHandleAsync(targetId, cancellationToken) .ConfigureAwait(false); // dangling reference - nothing more to do. if (handle == null) { return false; } // fetch the node attributes. NodeMetadata metadata = await nodeManager!.GetNodeMetadataAsync(context, handle, resultMask, cancellationToken) .ConfigureAwait(false); if (metadata == null) { return false; } // check nodeclass filter. if (nodeClassMask != NodeClass.Unspecified && ((int)metadata.NodeClass & (int)nodeClassMask) == 0) { return false; } // update attributes. description.NodeId = metadata.NodeId; description.SetTargetAttributes( resultMask, metadata.NodeClass, metadata.BrowseName, metadata.DisplayName, metadata.TypeDefinition); description.Unfiltered = false; return true; } /// public async ValueTask FindNodeInAddressSpaceAsync(NodeId nodeId, CancellationToken cancellationToken = default) { if (nodeId.IsNull) { return null; } // search node id in all node managers foreach (IAsyncNodeManager nodeManager in AsyncNodeManagers) { if ((await nodeManager.GetManagerHandleAsync(nodeId, cancellationToken).ConfigureAwait(false)) is not NodeHandle handle) { continue; } return handle.Node; } return null; } /// public virtual async ValueTask<(ArrayOf values, ArrayOf diagnosticInfos)> ReadAsync( OperationContext context, double maxAge, TimestampsToReturn timestampsToReturn, ArrayOf nodesToRead, CancellationToken cancellationToken = default) { if (maxAge < 0) { throw new ServiceResultException(StatusCodes.BadMaxAgeInvalid); } if (timestampsToReturn is < TimestampsToReturn.Source or > TimestampsToReturn.Neither) { throw new ServiceResultException(StatusCodes.BadTimestampsToReturnInvalid); } bool diagnosticsExist = false; var values = new List(nodesToRead.Count); var diagnosticInfos = new List(nodesToRead.Count); // create empty list of errors. var errors = new List(values.Count); for (int ii = 0; ii < nodesToRead.Count; ii++) { errors.Add(null!); } // add placeholder for each result. bool validItems = false; m_logger.LogTrace( Utils.TraceMasks.ServiceDetail, "MasterNodeManager.Read - Count={Count}", nodesToRead.Count); PrepareValidationCache( nodesToRead, out Dictionary uniqueNodesReadAttributes); for (int ii = 0; ii < nodesToRead.Count; ii++) { // add default value to values collection values.Add(default); // add placeholder for diagnostics diagnosticInfos.Add(null!); // pre-validate and pre-parse parameter. errors[ii] = (await ValidateReadRequestAsync( context, nodesToRead[ii], uniqueNodesReadAttributes, false, cancellationToken) .ConfigureAwait(false))!; // return error status. if (ServiceResult.IsBad(errors[ii])) { nodesToRead[ii].Processed = true; } // found at least one valid item. else { nodesToRead[ii].Processed = false; validItems = true; } } // call each node manager. if (validItems) { foreach (IAsyncNodeManager nodeManager in m_nodeManagers) { await nodeManager.ReadAsync( context, maxAge, nodesToRead, values, errors, cancellationToken).ConfigureAwait(false); } } // process results. for (int ii = 0; ii < nodesToRead.Count; ii++) { DataValue value = values[ii]; // set an error code for nodes that were not handled by any node manager. if (!nodesToRead[ii].Processed) { value = values[ii] = DataValue.FromStatusCode( StatusCodes.BadNodeIdUnknown, DateTime.UtcNow); errors[ii] = new ServiceResult(values[ii].StatusCode); } // update the diagnostic info and ensure the status code in the data value is the same as the error code. if (errors[ii] != null && errors[ii].Code != StatusCodes.Good) { if (value.IsNull) { value = values[ii] = DataValue.FromStatusCode(errors[ii].Code, DateTime.UtcNow); } value = values[ii] = value.WithStatus(errors[ii].Code); if ((context.DiagnosticsMask & DiagnosticsMasks.OperationAll) != 0) { diagnosticInfos[ii] = ServerUtils.CreateDiagnosticInfo( Server, context, errors[ii], m_logger)!; diagnosticsExist = true; } } // apply the timestamp filters. if (timestampsToReturn is not TimestampsToReturn.Server and not TimestampsToReturn.Both) { value = values[ii] = value.WithServerTimestamp(DateTimeUtc.MinValue); } if (timestampsToReturn is not TimestampsToReturn.Source and not TimestampsToReturn.Both) { _ = values[ii] = value.WithSourceTimestamp(DateTimeUtc.MinValue); } } // clear the diagnostics array if no diagnostics requested or no errors occurred. UpdateDiagnostics(context, diagnosticsExist, ref diagnosticInfos); return (values, diagnosticInfos); } /// public virtual async ValueTask<(ArrayOf values, ArrayOf diagnosticInfos)> HistoryReadAsync( OperationContext context, ExtensionObject historyReadDetails, TimestampsToReturn timestampsToReturn, bool releaseContinuationPoints, ArrayOf nodesToRead, CancellationToken cancellationToken = default) { // validate history details parameter. if (historyReadDetails.IsNull) { throw new ServiceResultException(StatusCodes.BadHistoryOperationInvalid); } if (!historyReadDetails.TryGetValue(out HistoryReadDetails? details)) { throw new ServiceResultException(StatusCodes.BadHistoryOperationInvalid); } // create result lists. bool diagnosticsExist = false; var results = new List(nodesToRead.Count); var diagnosticInfos = new List(nodesToRead.Count); // pre-validate items. bool validItems = false; // create empty list of errors. var errors = new List(results.Count); for (int ii = 0; ii < nodesToRead.Count; ii++) { errors.Add(null!); } for (int ii = 0; ii < nodesToRead.Count; ii++) { // Limit permission restrictions to Client initiated service call HistoryReadResult? result = null; DiagnosticInfo? diagnosticInfo = null; // pre-validate and pre-parse parameter. errors[ii] = (await ValidateHistoryReadRequestAsync(context, nodesToRead[ii], cancellationToken) .ConfigureAwait(false))!; // return error status. if (ServiceResult.IsBad(errors[ii])) { nodesToRead[ii].Processed = true; result = new HistoryReadResult { StatusCode = errors[ii].Code }; // add diagnostics if requested. if ((context.DiagnosticsMask & DiagnosticsMasks.OperationAll) != 0) { diagnosticInfo = ServerUtils.CreateDiagnosticInfo( Server, context, errors[ii], m_logger); diagnosticsExist = true; } } // found at least one valid item. else { nodesToRead[ii].Processed = false; validItems = true; } results.Add(result!); diagnosticInfos.Add(diagnosticInfo!); } // call each node manager. if (validItems) { foreach (IAsyncNodeManager nodeManager in m_nodeManagers) { await nodeManager.HistoryReadAsync( context, details!, timestampsToReturn, releaseContinuationPoints, nodesToRead, results, errors, cancellationToken).ConfigureAwait(false); } for (int ii = 0; ii < nodesToRead.Count; ii++) { HistoryReadResult result = results[ii]; // set an error code for nodes that were not handled by any node manager. if (!nodesToRead[ii].Processed) { nodesToRead[ii].Processed = true; result = results[ii] = new HistoryReadResult(); result.StatusCode = StatusCodes.BadNodeIdUnknown; errors[ii] = results[ii].StatusCode; } // update the diagnostic info and ensure the status code in the result is the same as the error code. if (errors[ii] != null && errors[ii].Code != StatusCodes.Good) { result ??= results[ii] = new HistoryReadResult(); result.StatusCode = errors[ii].Code; // add diagnostics if requested. if ((context.DiagnosticsMask & DiagnosticsMasks.OperationAll) != 0) { diagnosticInfos[ii] = ServerUtils.CreateDiagnosticInfo( Server, context, errors[ii], m_logger)!; diagnosticsExist = true; } } } } // clear the diagnostics array if no diagnostics requested or no errors occurred. UpdateDiagnostics(context, diagnosticsExist, ref diagnosticInfos); return (results, diagnosticInfos); } /// public virtual async ValueTask<(ArrayOf results, ArrayOf diagnosticInfos)> WriteAsync( OperationContext context, ArrayOf nodesToWrite, CancellationToken cancellationToken = default) { if (context == null) { throw new ArgumentNullException(nameof(context)); } int count = nodesToWrite.Count; bool diagnosticsExist = false; var results = new List(count); var diagnosticInfos = new List(count); PrepareValidationCache( nodesToWrite, out Dictionary uniqueNodesReadAttributes); // add placeholder for each result. bool validItems = false; for (int ii = 0; ii < count; ii++) { StatusCode result = StatusCodes.Good; DiagnosticInfo? diagnosticInfo = null; // pre-validate and pre-parse parameter. Validate also access rights and role permissions ServiceResult? error = await ValidateWriteRequestAsync( context, nodesToWrite[ii], uniqueNodesReadAttributes, false, cancellationToken) .ConfigureAwait(false); // return error status. if (ServiceResult.IsBad(error)) { nodesToWrite[ii].Processed = true; result = error!.Code; // add diagnostics if requested. if ((context.DiagnosticsMask & DiagnosticsMasks.OperationAll) != 0) { diagnosticInfo = ServerUtils.CreateDiagnosticInfo(Server, context, error, m_logger); diagnosticsExist = true; } } // found at least one valid item. else { nodesToWrite[ii].Processed = false; validItems = true; } results.Add(result); diagnosticInfos.Add(diagnosticInfo!); } // call each node manager. if (validItems) { var errors = new List(count); errors.AddRange(new ServiceResult[count]); foreach (IAsyncNodeManager nodeManager in m_nodeManagers) { await nodeManager.WriteAsync( context, nodesToWrite, errors, cancellationToken).ConfigureAwait(false); } for (int ii = 0; ii < nodesToWrite.Count; ii++) { if (!nodesToWrite[ii].Processed) { errors[ii] = StatusCodes.BadNodeIdUnknown; } if (errors[ii] != null && errors[ii].Code != StatusCodes.Good) { results[ii] = errors[ii].Code; // add diagnostics if requested. if ((context.DiagnosticsMask & DiagnosticsMasks.OperationAll) != 0) { diagnosticInfos[ii] = ServerUtils.CreateDiagnosticInfo( Server, context, errors[ii], m_logger)!; diagnosticsExist = true; } } ServerUtils.ReportWriteValue( nodesToWrite[ii].NodeId, nodesToWrite[ii].Value, results[ii]); } } // clear the diagnostics array if no diagnostics requested or no errors occurred. UpdateDiagnostics(context, diagnosticsExist, ref diagnosticInfos); return (results, diagnosticInfos); } /// public virtual async ValueTask<(ArrayOf results, ArrayOf diagnosticInfos)> HistoryUpdateAsync( OperationContext context, ArrayOf historyUpdateDetails, CancellationToken cancellationToken = default) { Type? detailsType = null; var nodesToUpdate = new List(); // verify that all extension objects in the list have the same type. foreach (ExtensionObject details in historyUpdateDetails) { if (details.IsNull) { continue; } if (!details.TryGetValue(out HistoryUpdateDetails? historyUpdateDetail)) { nodesToUpdate.Add(null!); // Retain old behavior continue; } detailsType = historyUpdateDetail!.GetType(); nodesToUpdate.Add(historyUpdateDetail); } // create result lists. bool diagnosticsExist = false; var results = new List(nodesToUpdate.Count); var diagnosticInfos = new List(nodesToUpdate.Count); // pre-validate items. bool validItems = false; // create empty list of errors. var errors = new List(results.Count); for (int ii = 0; ii < nodesToUpdate.Count; ii++) { errors.Add(null!); } for (int ii = 0; ii < nodesToUpdate.Count; ii++) { HistoryUpdateResult? result = null; DiagnosticInfo? diagnosticInfo = null; // check the type of details parameter. ServiceResult? error; if (nodesToUpdate[ii].GetType() != detailsType) { error = StatusCodes.BadHistoryOperationInvalid; } // pre-validate and pre-parse parameter. else { error = await ValidateHistoryUpdateRequestAsync(context, nodesToUpdate[ii], cancellationToken) .ConfigureAwait(false); } // return error status. if (ServiceResult.IsBad(error)) { nodesToUpdate[ii].Processed = true; result = new HistoryUpdateResult { StatusCode = error!.Code }; // add diagnostics if requested. if ((context.DiagnosticsMask & DiagnosticsMasks.OperationAll) != 0) { diagnosticInfo = ServerUtils.CreateDiagnosticInfo(Server, context, error, m_logger); diagnosticsExist = true; } } // found at least one valid item. else { nodesToUpdate[ii].Processed = false; validItems = true; } results.Add(result!); diagnosticInfos.Add(diagnosticInfo!); } // call each node manager. if (validItems) { foreach (IAsyncNodeManager nodeManager in m_nodeManagers) { await nodeManager.HistoryUpdateAsync( context, detailsType!, nodesToUpdate, results, errors, cancellationToken).ConfigureAwait(false); } for (int ii = 0; ii < nodesToUpdate.Count; ii++) { HistoryUpdateResult result = results[ii]; // set an error code for nodes that were not handled by any node manager. if (!nodesToUpdate[ii].Processed) { nodesToUpdate[ii].Processed = true; result = results[ii] = new HistoryUpdateResult(); result.StatusCode = StatusCodes.BadNodeIdUnknown; errors[ii] = result.StatusCode; } // update the diagnostic info and ensure the status code in the result is the same as the error code. if (errors[ii] != null && errors[ii].Code != StatusCodes.Good) { result ??= results[ii] = new HistoryUpdateResult(); result.StatusCode = errors[ii].Code; // add diagnostics if requested. if ((context.DiagnosticsMask & DiagnosticsMasks.OperationAll) != 0) { diagnosticInfos[ii] = ServerUtils.CreateDiagnosticInfo( Server, context, errors[ii], m_logger)!; diagnosticsExist = true; } } } } // clear the diagnostics array if no diagnostics requested or no errors occurred. UpdateDiagnostics(context, diagnosticsExist, ref diagnosticInfos); return (results, diagnosticInfos); } /// public virtual async ValueTask<(ArrayOf results, ArrayOf diagnosticInfos)> CallAsync( OperationContext context, ArrayOf methodsToCall, CancellationToken cancellationToken = default) { if (context == null) { throw new ArgumentNullException(nameof(context)); } bool diagnosticsExist = false; var results = new List(methodsToCall.Count); var diagnosticInfos = new List(methodsToCall.Count); var errors = new List(methodsToCall.Count); PrepareValidationCache( methodsToCall, out Dictionary uniqueNodesReadAttributes); // add placeholder for each result. bool validItems = false; for (int ii = 0; ii < methodsToCall.Count; ii++) { results.Add(null!); errors.Add(null!); if ((context.DiagnosticsMask & DiagnosticsMasks.OperationAll) != 0) { diagnosticInfos.Add(null!); } // validate request parameters. errors[ii] = await ValidateCallRequestItemAsync( context, methodsToCall[ii], uniqueNodesReadAttributes, // With resolver-based method lookup and an attribute cache, GetPermissionMetadataAsync // reads and caches the required permission attributes without the permissionsOnly shortcut. false, cancellationToken).ConfigureAwait(false); if (ServiceResult.IsBad(errors[ii])) { methodsToCall[ii].Processed = true; // add diagnostics if requested. if ((context.DiagnosticsMask & DiagnosticsMasks.OperationAll) != 0) { diagnosticInfos[ii] = ServerUtils.CreateDiagnosticInfo( Server, context, errors[ii], m_logger)!; diagnosticsExist = true; } continue; } // found at least one valid item. validItems = true; methodsToCall[ii].Processed = false; } // call each node manager. if (validItems) { foreach (IAsyncNodeManager nodeManager in m_nodeManagers) { await nodeManager.CallAsync( context, methodsToCall, results, errors, cancellationToken).ConfigureAwait(false); } } for (int ii = 0; ii < methodsToCall.Count; ii++) { // set an error code for calls that were not handled by any node manager. if (!methodsToCall[ii].Processed) { results[ii] = new CallMethodResult(); errors[ii] = StatusCodes.BadNodeIdUnknown; } // update the diagnostic info and ensure the status code in the result is the same as the error code. if (errors[ii] != null && errors[ii].Code != StatusCodes.Good) { if (results[ii] == null) { results[ii] = new CallMethodResult(); } results[ii].StatusCode = errors[ii].Code; // add diagnostics if requested. if ((context.DiagnosticsMask & DiagnosticsMasks.OperationAll) != 0) { diagnosticInfos[ii] = ServerUtils.CreateDiagnosticInfo( Server, context, errors[ii], m_logger)!; diagnosticsExist = true; } } } // clear the diagnostics array if no diagnostics requested or no errors occurred. UpdateDiagnostics(context, diagnosticsExist, ref diagnosticInfos); return (results, diagnosticInfos); } /// public virtual async ValueTask ConditionRefreshAsync( OperationContext context, IList monitoredItems, CancellationToken cancellationToken = default) { foreach (IAsyncNodeManager nodeManager in m_nodeManagers) { try { await nodeManager.ConditionRefreshAsync(context, monitoredItems, cancellationToken) .ConfigureAwait(false); } catch (Exception e) { m_logger.LogError(e, "Error calling ConditionRefreshAsync on AsyncNodeManager."); } } } /// /// Creates a set of monitored items. /// /// is null. /// /// [Obsolete("Use CreateMonitoredItemsAsync")] public virtual void CreateMonitoredItems( OperationContext context, uint subscriptionId, double publishingInterval, TimestampsToReturn timestampsToReturn, ArrayOf itemsToCreate, IList errors, IList filterResults, IList monitoredItems, bool createDurable) { CreateMonitoredItemsAsync( context, subscriptionId, publishingInterval, timestampsToReturn, itemsToCreate, errors, filterResults, monitoredItems, createDurable).AsTask().GetAwaiter().GetResult(); } /// public virtual async ValueTask CreateMonitoredItemsAsync( OperationContext context, uint subscriptionId, double publishingInterval, TimestampsToReturn timestampsToReturn, ArrayOf itemsToCreate, IList errors, IList filterResults, IList monitoredItems, bool createDurable, CancellationToken cancellationToken = default) { if (context == null) { throw new ArgumentNullException(nameof(context)); } if (errors == null) { throw new ArgumentNullException(nameof(errors)); } if (filterResults == null) { throw new ArgumentNullException(nameof(filterResults)); } if (monitoredItems == null) { throw new ArgumentNullException(nameof(monitoredItems)); } if (publishingInterval < 0) { throw new ArgumentOutOfRangeException(nameof(publishingInterval)); } if (timestampsToReturn is < TimestampsToReturn.Source or > TimestampsToReturn.Neither) { throw new ServiceResultException(StatusCodes.BadTimestampsToReturnInvalid); } // add placeholder for each result. bool validItems = false; for (int ii = 0; ii < itemsToCreate.Count; ii++) { // validate request parameters. errors[ii] = (await ValidateMonitoredItemCreateRequestAsync(context, itemsToCreate[ii], cancellationToken) .ConfigureAwait(false))!; if (ServiceResult.IsBad(errors[ii])) { itemsToCreate[ii].Processed = true; continue; } // found at least one valid item. validItems = true; itemsToCreate[ii].Processed = false; } // call each node manager. if (validItems) { // create items for event filters. await CreateMonitoredItemsForEventsAsync( context, subscriptionId, publishingInterval, timestampsToReturn, itemsToCreate, errors, filterResults, monitoredItems, createDurable, m_monitoredItemIdFactory, cancellationToken) .ConfigureAwait(false); // create items for data access. foreach (IAsyncNodeManager nodeManager in m_nodeManagers) { await nodeManager.CreateMonitoredItemsAsync( context, subscriptionId, publishingInterval, timestampsToReturn, itemsToCreate, errors, filterResults, monitoredItems, createDurable, m_monitoredItemIdFactory, cancellationToken) .ConfigureAwait(false); } // fill results for unknown nodes. for (int ii = 0; ii < errors.Count; ii++) { if (!itemsToCreate[ii].Processed) { errors[ii] = new ServiceResult(StatusCodes.BadNodeIdUnknown); } } } } /// /// Create monitored items for event subscriptions. /// private async ValueTask CreateMonitoredItemsForEventsAsync( OperationContext context, uint subscriptionId, double publishingInterval, TimestampsToReturn timestampsToReturn, ArrayOf itemsToCreate, IList errors, IList filterResults, IList monitoredItems, bool createDurable, MonitoredItemIdFactory monitoredItemIdFactory, CancellationToken cancellationToken = default) { for (int ii = 0; ii < itemsToCreate.Count; ii++) { MonitoredItemCreateRequest itemToCreate = itemsToCreate[ii]; if (!itemToCreate.Processed) { // all event subscriptions required an event filter. if (!itemToCreate.RequestedParameters.Filter.TryGetValue(out EventFilter? filter)) { continue; } itemToCreate.Processed = true; // only the value attribute may be used with an event subscription. if (itemToCreate.ItemToMonitor.AttributeId != Attributes.EventNotifier) { errors[ii] = StatusCodes.BadFilterNotAllowed; continue; } // the index range parameter has no meaning for event subscriptions. if (!string.IsNullOrEmpty(itemToCreate.ItemToMonitor.IndexRange)) { errors[ii] = StatusCodes.BadIndexRangeInvalid; continue; } // the data encoding has no meaning for event subscriptions. if (!itemToCreate.ItemToMonitor.DataEncoding.IsNull) { errors[ii] = StatusCodes.BadDataEncodingInvalid; continue; } // validate the event filter. EventFilter.Result result = filter!.Validate( new FilterContext(Server.NamespaceUris, Server.TypeTree, context, Server.Telemetry)); if (ServiceResult.IsBad(result.Status)) { errors[ii] = result.Status; filterResults[ii] = result.ToEventFilterResult( context.DiagnosticsMask, context.StringTable, m_logger); continue; } // check if a valid node. (object? handle, IAsyncNodeManager? nodeManager) = await GetManagerHandleAsync( itemToCreate.ItemToMonitor.NodeId, cancellationToken) .ConfigureAwait(false); if (handle == null) { errors[ii] = StatusCodes.BadNodeIdUnknown; continue; } NodeMetadata nodeMetadata = await nodeManager!.GetNodeMetadataAsync( context, handle, BrowseResultMask.All, cancellationToken) .ConfigureAwait(false); errors[ii] = ValidateRolePermissions( context, nodeMetadata, PermissionType.ReceiveEvents, m_logger); if (ServiceResult.IsBad(errors[ii])) { continue; } IEventMonitoredItem monitoredItem = Server.EventManager.CreateMonitoredItem( context, nodeManager, handle, subscriptionId, monitoredItemIdFactory, timestampsToReturn, publishingInterval, itemToCreate, filter, createDurable); // subscribe to all node managers. if (itemToCreate.ItemToMonitor.NodeId == Objects.Server) { foreach (IAsyncNodeManager manager in m_nodeManagers) { try { await manager.SubscribeToAllEventsAsync( context, subscriptionId, monitoredItem, false, cancellationToken) .ConfigureAwait(false); } catch (Exception e) { m_logger.LogError( e, "NodeManager threw an exception subscribing to all events. NodeManager={NodeManager}", manager); } } } // only subscribe to the node manager that owns the node. else { ServiceResult error = await nodeManager.SubscribeToEventsAsync( context, handle, subscriptionId, monitoredItem, false, cancellationToken) .ConfigureAwait(false); if (ServiceResult.IsBad(error)) { Server.EventManager.DeleteMonitoredItem(monitoredItem.Id); errors[ii] = error; continue; } } monitoredItems[ii] = monitoredItem; errors[ii] = StatusCodes.Good; } } } /// public virtual async ValueTask RestoreMonitoredItemsAsync( IList itemsToRestore, IList monitoredItems, IUserIdentity savedOwnerIdentity, CancellationToken cancellationToken = default) { if (itemsToRestore == null) { throw new ArgumentNullException(nameof(itemsToRestore)); } if (monitoredItems == null) { throw new ArgumentNullException(nameof(monitoredItems)); } if (Server.IsRunning) { throw new InvalidOperationException( "Subscription restore can only occur on startup"); } // create items for event filters. await RestoreMonitoredItemsForEventsAsync(itemsToRestore, monitoredItems, cancellationToken) .ConfigureAwait(false); // create items for data access. foreach (IAsyncNodeManager nodeManager in m_nodeManagers) { await nodeManager.RestoreMonitoredItemsAsync( itemsToRestore, monitoredItems, savedOwnerIdentity, cancellationToken) .ConfigureAwait(false); } m_monitoredItemIdFactory.SetStartValue(itemsToRestore.Max(i => i.Id)); } /// /// Restore monitored items for event subscriptions. /// private async ValueTask RestoreMonitoredItemsForEventsAsync( IList itemsToRestore, IList monitoredItems, CancellationToken cancellationToken = default) { for (int ii = 0; ii < itemsToRestore.Count; ii++) { IStoredMonitoredItem item = itemsToRestore[ii]; if (!item.IsRestored) { // all event subscriptions required an event filter. if (item.OriginalFilter is not EventFilter) { continue; } item.IsRestored = true; // check if a valid node. (object? handle, IAsyncNodeManager? nodeManager) = await GetManagerHandleAsync(item.NodeId, cancellationToken) .ConfigureAwait(false); if (handle == null) { continue; } IEventMonitoredItem monitoredItem = Server.EventManager.RestoreMonitoredItem( nodeManager!, handle, item); // subscribe to all node managers. if (item.NodeId == Objects.Server) { foreach (IAsyncNodeManager manager in m_nodeManagers) { try { await manager.SubscribeToAllEventsAsync( new OperationContext(monitoredItem), monitoredItem.SubscriptionId, monitoredItem, false, cancellationToken) .ConfigureAwait(false); } catch (Exception e) { m_logger.LogError( e, "NodeManager threw an exception subscribing to all events. NodeManager={NodeManager}", manager); } } } // only subscribe to the node manager that owns the node. else { ServiceResult error = await nodeManager!.SubscribeToEventsAsync( new OperationContext(monitoredItem), handle, monitoredItem.SubscriptionId, monitoredItem, false, cancellationToken) .ConfigureAwait(false); if (ServiceResult.IsBad(error)) { Server.EventManager.DeleteMonitoredItem(monitoredItem.Id); continue; } } monitoredItems[ii] = monitoredItem; } } } /// /// Modifies a set of monitored items. /// /// is null. /// [Obsolete("Use ModifyMonitoredItemsAsync")] public virtual void ModifyMonitoredItems( OperationContext context, TimestampsToReturn timestampsToReturn, IList monitoredItems, ArrayOf itemsToModify, IList errors, IList filterResults) { ModifyMonitoredItemsAsync( context, timestampsToReturn, monitoredItems, itemsToModify, errors, filterResults).AsTask().GetAwaiter().GetResult(); } /// public virtual async ValueTask ModifyMonitoredItemsAsync( OperationContext context, TimestampsToReturn timestampsToReturn, IList monitoredItems, ArrayOf itemsToModify, IList errors, IList filterResults, CancellationToken cancellationToken = default) { if (context == null) { throw new ArgumentNullException(nameof(context)); } if (monitoredItems == null) { throw new ArgumentNullException(nameof(monitoredItems)); } if (errors == null) { throw new ArgumentNullException(nameof(errors)); } if (filterResults == null) { throw new ArgumentNullException(nameof(filterResults)); } if (timestampsToReturn is < TimestampsToReturn.Source or > TimestampsToReturn.Neither) { throw new ServiceResultException(StatusCodes.BadTimestampsToReturnInvalid); } bool validItems = false; for (int ii = 0; ii < itemsToModify.Count; ii++) { // check for errors. if (ServiceResult.IsBad(errors[ii]) || monitoredItems[ii] == null) { itemsToModify[ii].Processed = true; continue; } // validate request parameters. errors[ii] = ValidateMonitoredItemModifyRequest(itemsToModify[ii])!; if (ServiceResult.IsBad(errors[ii])) { itemsToModify[ii].Processed = true; continue; } // found at least one valid item. validItems = true; itemsToModify[ii].Processed = false; } // call each node manager. if (validItems) { // modify items for event filters. await ModifyMonitoredItemsForEventsAsync( context, timestampsToReturn, monitoredItems, itemsToModify, errors, filterResults, cancellationToken) .ConfigureAwait(false); // let each node manager figure out which items it owns. foreach (IAsyncNodeManager nodeManager in m_nodeManagers) { await nodeManager.ModifyMonitoredItemsAsync( context, timestampsToReturn, monitoredItems, itemsToModify, errors, filterResults, cancellationToken) .ConfigureAwait(false); } // update results. for (int ii = 0; ii < errors.Count; ii++) { if (!itemsToModify[ii].Processed) { errors[ii] = new ServiceResult(StatusCodes.BadMonitoredItemIdInvalid); } } } } /// /// Modify monitored items for event subscriptions. /// private async ValueTask ModifyMonitoredItemsForEventsAsync( OperationContext context, TimestampsToReturn timestampsToReturn, IList monitoredItems, ArrayOf itemsToModify, IList errors, IList filterResults, CancellationToken cancellationToken = default) { for (int ii = 0; ii < itemsToModify.Count; ii++) { // all event subscriptions are handled by the event manager. if (monitoredItems[ii] is not IEventMonitoredItem monitoredItem || (monitoredItem.MonitoredItemType & MonitoredItemTypeMask.Events) == 0) { continue; } MonitoredItemModifyRequest itemToModify = itemsToModify[ii]; itemToModify.Processed = true; // check for a valid filter. if (itemToModify.RequestedParameters.Filter.IsNull) { errors[ii] = StatusCodes.BadEventFilterInvalid; continue; } // all event subscriptions required an event filter. if (!itemToModify.RequestedParameters.Filter.TryGetValue(out EventFilter? filter)) { errors[ii] = StatusCodes.BadEventFilterInvalid; continue; } // validate the event filter. EventFilter.Result result = filter!.Validate( new FilterContext(Server.NamespaceUris, Server.TypeTree, context, Server.Telemetry)); if (ServiceResult.IsBad(result.Status)) { errors[ii] = result.Status; filterResults[ii] = result.ToEventFilterResult( context.DiagnosticsMask, context.StringTable, m_logger); continue; } // modify the item. Server.EventManager.ModifyMonitoredItem( context, monitoredItem, timestampsToReturn, itemToModify, filter); // subscribe to all node managers. if ((monitoredItem.MonitoredItemType & MonitoredItemTypeMask.AllEvents) != 0) { foreach (IAsyncNodeManager nodeManager in m_nodeManagers) { await nodeManager.SubscribeToAllEventsAsync( context, monitoredItem.SubscriptionId, monitoredItem, false, cancellationToken) .ConfigureAwait(false); } } // only subscribe to the node manager that owns the node. else { await monitoredItem.NodeManager.SubscribeToEventsAsync( context, monitoredItem.ManagerHandle, monitoredItem.SubscriptionId, monitoredItem, false, cancellationToken).ConfigureAwait(false); } errors[ii] = StatusCodes.Good; } } /// /// Transfers a set of monitored items. /// /// is null. [Obsolete("User TransferMonitoredItemsAsync")] public virtual void TransferMonitoredItems( OperationContext context, bool sendInitialValues, IList monitoredItems, IList errors) { TransferMonitoredItemsAsync( context, sendInitialValues, monitoredItems, errors).AsTask().GetAwaiter().GetResult(); } /// public virtual async ValueTask TransferMonitoredItemsAsync( OperationContext context, bool sendInitialValues, IList monitoredItems, IList errors, CancellationToken cancellationToken = default) { if (context == null) { throw new ArgumentNullException(nameof(context)); } if (monitoredItems == null) { throw new ArgumentNullException(nameof(monitoredItems)); } if (errors == null) { throw new ArgumentNullException(nameof(errors)); } var processedItems = new List(monitoredItems.Count); // preset results for unknown nodes for (int ii = 0; ii < monitoredItems.Count; ii++) { processedItems.Add(monitoredItems[ii] == null); errors[ii] = StatusCodes.BadMonitoredItemIdInvalid; } // call each node manager. foreach (IAsyncNodeManager nodeManager in m_nodeManagers) { await nodeManager.TransferMonitoredItemsAsync( context, sendInitialValues, monitoredItems, processedItems, errors, cancellationToken) .ConfigureAwait(false); } } /// /// Deletes a set of monitored items. /// /// is null. [Obsolete("Use DeleteMonitoredItemsAsync")] public virtual void DeleteMonitoredItems( OperationContext context, uint subscriptionId, IList itemsToDelete, IList errors) { DeleteMonitoredItemsAsync( context, subscriptionId, itemsToDelete, errors).AsTask().GetAwaiter().GetResult(); } /// public virtual async ValueTask DeleteMonitoredItemsAsync( OperationContext context, uint subscriptionId, IList itemsToDelete, IList errors, CancellationToken cancellationToken = default) { if (context == null) { throw new ArgumentNullException(nameof(context)); } if (itemsToDelete == null) { throw new ArgumentNullException(nameof(itemsToDelete)); } if (errors == null) { throw new ArgumentNullException(nameof(errors)); } var processedItems = new List(itemsToDelete.Count); for (int ii = 0; ii < itemsToDelete.Count; ii++) { processedItems.Add(ServiceResult.IsBad(errors[ii]) || itemsToDelete[ii] == null); } // delete items for event filters. await DeleteMonitoredItemsForEventsAsync( context, subscriptionId, itemsToDelete, processedItems, errors, cancellationToken) .ConfigureAwait(false); // call each node manager. foreach (IAsyncNodeManager nodeManager in m_nodeManagers) { await nodeManager.DeleteMonitoredItemsAsync( context, itemsToDelete, processedItems, errors, cancellationToken) .ConfigureAwait(false); } // fill results for unknown nodes. for (int ii = 0; ii < errors.Count; ii++) { if (!processedItems[ii]) { errors[ii] = StatusCodes.BadMonitoredItemIdInvalid; } } } /// /// Delete monitored items for event subscriptions. /// private async ValueTask DeleteMonitoredItemsForEventsAsync( OperationContext context, uint subscriptionId, IList monitoredItems, List processedItems, IList errors, CancellationToken cancellationToken = default) { for (int ii = 0; ii < monitoredItems.Count; ii++) { // all event subscriptions are handled by the event manager. if (monitoredItems[ii] is not IEventMonitoredItem monitoredItem || (monitoredItem.MonitoredItemType & MonitoredItemTypeMask.Events) == 0) { continue; } processedItems[ii] = true; // unsubscribe to all node managers. if ((monitoredItem.MonitoredItemType & MonitoredItemTypeMask.AllEvents) != 0) { foreach (IAsyncNodeManager nodeManager in m_nodeManagers) { await nodeManager.SubscribeToAllEventsAsync( context, subscriptionId, monitoredItem, true, cancellationToken) .ConfigureAwait(false); } } // only unsubscribe to the node manager that owns the node. else { await monitoredItem.NodeManager.SubscribeToEventsAsync( context, monitoredItem.ManagerHandle, subscriptionId, monitoredItem, true, cancellationToken).ConfigureAwait(false); } // delete the item. Server.EventManager.DeleteMonitoredItem(monitoredItem.Id); // success. errors[ii] = StatusCodes.Good; } } /// public virtual async ValueTask SetMonitoringModeAsync( OperationContext context, MonitoringMode monitoringMode, IList itemsToModify, IList errors, CancellationToken cancellationToken = default) { if (context == null) { throw new ArgumentNullException(nameof(context)); } if (itemsToModify == null) { throw new ArgumentNullException(nameof(itemsToModify)); } if (errors == null) { throw new ArgumentNullException(nameof(errors)); } // call each node manager. var processedItems = new List(itemsToModify.Count); for (int ii = 0; ii < itemsToModify.Count; ii++) { processedItems.Add(ServiceResult.IsBad(errors[ii]) || itemsToModify[ii] == null); } // delete items for event filters. SetMonitoringModeForEvents( context, monitoringMode, itemsToModify, processedItems, errors); foreach (IAsyncNodeManager nodeManager in m_nodeManagers) { await nodeManager.SetMonitoringModeAsync( context, monitoringMode, itemsToModify, processedItems, errors, cancellationToken) .ConfigureAwait(false); } // fill results for unknown nodes. for (int ii = 0; ii < errors.Count; ii++) { if (!processedItems[ii]) { errors[ii] = StatusCodes.BadMonitoredItemIdInvalid; } } } /// /// Delete monitored items for event subscriptions. /// private static void SetMonitoringModeForEvents( OperationContext context, MonitoringMode monitoringMode, IList monitoredItems, List processedItems, IList errors) { for (int ii = 0; ii < monitoredItems.Count; ii++) { // all event subscriptions are handled by the event manager. if (monitoredItems[ii] is not IEventMonitoredItem monitoredItem || (monitoredItem.MonitoredItemType & MonitoredItemTypeMask.Events) == 0) { continue; } processedItems[ii] = true; // set the monitoring mode. monitoredItem.SetMonitoringMode(monitoringMode); // success. errors[ii] = StatusCodes.Good; } } /// /// The server that the node manager belongs to. /// protected IServerInternal Server { get; } /// public IReadOnlyList AsyncNodeManagers => m_nodeManagers; /// public IReadOnlyList NodeManagers => m_nodeManagers.ConvertAll(m => m.SyncNodeManager); /// /// The namespace managers being managed /// internal ConcurrentDictionary> NamespaceManagers { get; } = []; /// /// Validates a monitoring attributes parameter. /// protected static ServiceResult? ValidateMonitoringAttributes(MonitoringParameters attributes) { // check for null structure. if (attributes == null) { return new ServiceResult(StatusCodes.BadStructureMissing); } // If a filter was specified, it needs to be a known filter structure. if (!attributes.Filter.IsNull && !attributes.Filter.TryGetValue(out MonitoringFilter? _)) { return new ServiceResult(StatusCodes.BadMonitoredItemFilterInvalid); } // passed basic validation. return null; } /// /// Validates a monitoring filter. /// protected static ServiceResult? ValidateMonitoringFilter(ExtensionObject filter) { // check that no filter is specified for non-value attributes. if (!filter.IsNull) { // validate data change filter. if (filter.TryGetValue(out DataChangeFilter? datachangeFilter)) { ServiceResult error = datachangeFilter.Validate(); if (ServiceResult.IsBad(error)) { return error; } } } // passed basic validation. return null; } /// /// Validates a monitored item create request parameter. /// protected async ValueTask ValidateMonitoredItemCreateRequestAsync( OperationContext operationContext, MonitoredItemCreateRequest item, CancellationToken cancellationToken = default) { // check for null structure. if (item == null) { return new ServiceResult(StatusCodes.BadStructureMissing); } // validate read value id component. Validate also access rights and permissions ServiceResult? error = await ValidateReadRequestAsync( operationContext, item.ItemToMonitor, null, true, cancellationToken) .ConfigureAwait(false); if (ServiceResult.IsBad(error)) { return error; } // check for valid monitoring mode. if ((int)item.MonitoringMode is < 0 or > ((int)MonitoringMode.Reporting)) { return new ServiceResult(StatusCodes.BadMonitoringModeInvalid); } // check for null structure. MonitoringParameters attributes = item.RequestedParameters; error = ValidateMonitoringAttributes(attributes); if (ServiceResult.IsBad(error)) { return error; } // check that no filter is specified for non-value attributes. if (item.ItemToMonitor.AttributeId is not Attributes.Value and not Attributes .EventNotifier) { if (!attributes.Filter.IsNull) { return new ServiceResult(StatusCodes.BadFilterNotAllowed); } } else { error = ValidateMonitoringFilter(attributes.Filter); if (ServiceResult.IsBad(error)) { return error; } } // passed basic validation. return null; } /// /// Validates a monitored item modify request parameter. /// protected static ServiceResult? ValidateMonitoredItemModifyRequest( MonitoredItemModifyRequest item) { // check for null structure. if (item == null) { return new ServiceResult(StatusCodes.BadStructureMissing); } // check for null structure. MonitoringParameters attributes = item.RequestedParameters; ServiceResult? error = ValidateMonitoringAttributes(attributes); if (ServiceResult.IsBad(error)) { return error; } // validate monitoring filter. error = ValidateMonitoringFilter(attributes.Filter); if (ServiceResult.IsBad(error)) { return error; } // passed basic validation. return null; } /// /// Validates a call request item parameter and checks access rights and role permissions. /// protected async ValueTask ValidateCallRequestItemAsync( OperationContext operationContext, CallMethodRequest callMethodRequest, Dictionary? uniqueNodesReadAttributes = null, bool permissionsOnly = false, CancellationToken cancellationToken = default) { // check for null structure. if (callMethodRequest == null) { return StatusCodes.BadStructureMissing; } // check object id. if (callMethodRequest.ObjectId.IsNull) { return StatusCodes.BadNodeIdInvalid; } // check method id. if (callMethodRequest.MethodId.IsNull) { return StatusCodes.BadMethodInvalid; } (_, IAsyncNodeManager? nodeManager) = await GetManagerHandleAsync(callMethodRequest.ObjectId, cancellationToken) .ConfigureAwait(false); if (nodeManager == null) { return StatusCodes.BadNodeIdUnknown; } // Method resolution fallback is provided by the IAsyncNodeManager adapter path. MethodState method = await nodeManager.FindMethodStateAsync( operationContext, callMethodRequest, cancellationToken).ConfigureAwait(false); if (method != null) { // check access rights and role permissions return await ValidatePermissionsAsync( operationContext, method.NodeId, PermissionType.Call, uniqueNodesReadAttributes, permissionsOnly, cancellationToken) .ConfigureAwait(false); } return StatusCodes.Good; } /// /// Validates a Read or MonitoredItemCreate request. It validates also access rights and role permissions /// protected async ValueTask ValidateReadRequestAsync( OperationContext operationContext, ReadValueId readValueId, Dictionary? uniqueNodesReadAttributes = null, bool permissionsOnly = false, CancellationToken cancellationToken = default) { ServiceResult? serviceResult = ReadValueId.Validate(readValueId); if (ServiceResult.IsGood(serviceResult)) { //any attribute other than Value or RolePermissions PermissionType requestedPermission = PermissionType.Browse; if (readValueId.AttributeId == Attributes.RolePermissions) { requestedPermission = PermissionType.ReadRolePermissions; } else if (readValueId.AttributeId == Attributes.Value) { requestedPermission = PermissionType.Read; } // check access rights and role permissions serviceResult = await ValidatePermissionsAsync( operationContext, readValueId.NodeId, requestedPermission, uniqueNodesReadAttributes, permissionsOnly, cancellationToken) .ConfigureAwait(false); } return serviceResult; } /// /// Validates a Write request. It validates also access rights and role permissions /// protected async ValueTask ValidateWriteRequestAsync( OperationContext operationContext, WriteValue writeValue, Dictionary? uniqueNodesServiceAttributes = null, bool permissionsOnly = false, CancellationToken cancellationToken = default) { ServiceResult? serviceResult = WriteValue.Validate(writeValue); if (ServiceResult.IsGood(serviceResult)) { PermissionType requestedPermission = PermissionType.WriteAttribute; //any attribute other than Value, RolePermissions or Historizing if (writeValue.AttributeId == Attributes.RolePermissions) { requestedPermission = PermissionType.WriteRolePermissions; } else if (writeValue.AttributeId == Attributes.Historizing) { requestedPermission = PermissionType.WriteHistorizing; } else if (writeValue.AttributeId == Attributes.Value) { requestedPermission = PermissionType.Write; } // check access rights and permissions serviceResult = await ValidatePermissionsAsync( operationContext, writeValue.NodeId, requestedPermission, uniqueNodesServiceAttributes, permissionsOnly, cancellationToken) .ConfigureAwait(false); } return serviceResult; } /// /// Validates a HistoryRead request. It validates also access rights and role permissions /// protected async ValueTask ValidateHistoryReadRequestAsync( OperationContext operationContext, HistoryReadValueId historyReadValueId, CancellationToken cancellationToken = default) { ServiceResult? serviceResult = HistoryReadValueId.Validate(historyReadValueId); if (ServiceResult.IsGood(serviceResult)) { // check access rights and permissions serviceResult = await ValidatePermissionsAsync( operationContext, historyReadValueId.NodeId, PermissionType.ReadHistory, null, true, cancellationToken) .ConfigureAwait(false); } return serviceResult; } /// /// Validates a HistoryUpdate request. It validates also access rights and role permissions /// protected async ValueTask ValidateHistoryUpdateRequestAsync( OperationContext operationContext, HistoryUpdateDetails historyUpdateDetails, CancellationToken cancellationToken = default) { ServiceResult? serviceResult = HistoryUpdateDetails.Validate(historyUpdateDetails); if (ServiceResult.IsGood(serviceResult)) { // check access rights and permissions PermissionType requiredPermission = DetermineHistoryAccessPermission( historyUpdateDetails); serviceResult = await ValidatePermissionsAsync( operationContext, historyUpdateDetails.NodeId, requiredPermission, null, true, cancellationToken).ConfigureAwait(false); } return serviceResult; } /// /// Check if the Base NodeClass attributes and NameSpace meta-data attributes /// are valid for the given operation context of the specified node. /// /// The Operation Context /// The node whose attributes are validated /// The requested permission /// The cache holding the values of the attributes neeeded to be used in subsequent calls /// Only the AccessRestrictions and RolePermission attributes are read. Should be false if uniqueNodesServiceAttributes is not null /// The Cancellation Token /// StatusCode Good if permission is granted, BadUserAccessDenied if not granted /// or a bad status code describing the validation process failure protected async ValueTask ValidatePermissionsAsync( OperationContext context, NodeId nodeId, PermissionType requestedPermision, Dictionary? uniqueNodesServiceAttributes = null, bool permissionsOnly = false, CancellationToken cancellationToken = default) { if (context.Session != null) { (object? nodeHandle, IAsyncNodeManager? nodeManager) = await GetManagerHandleAsync(nodeId, cancellationToken) .ConfigureAwait(false); return await ValidatePermissionsAsync( context, nodeManager, nodeHandle, requestedPermision, uniqueNodesServiceAttributes, permissionsOnly, cancellationToken) .ConfigureAwait(false); } return StatusCodes.Good; } /// /// Check if the Base NodeClass attributes and NameSpace meta-data attributes /// are valid for the given operation context of the specified node. /// /// The Operation Context /// The node manager handling the nodeHandle /// The node handle of the node whose attributes are validated /// The requested permission /// The cache holding the values of the attributes neeeded to be used in subsequent calls /// Only the AccessRestrictions and RolePermission attributes are read. Should be false if uniqueNodesServiceAttributes is not null /// The Cancellation Token /// StatusCode Good if permission is granted, BadUserAccessDenied if not granted /// or a bad status code describing the validation process failure protected async ValueTask ValidatePermissionsAsync( OperationContext context, IAsyncNodeManager? nodeManager, object? nodeHandle, PermissionType requestedPermision, Dictionary? uniqueNodesServiceAttributes = null, bool permissionsOnly = false, CancellationToken cancellationToken = default) { ServiceResult serviceResult = StatusCodes.Good; // check if validation is necessary if (context.Session != null && nodeManager != null && nodeHandle != null) { // First attempt to retrieve just the Permission metadata with or without cache optimization // If it happens that nodemanager does not fully implement GetPermissionMetadata, // fallback to GetNodeMetadataAsync NodeMetadata? nodeMetadata = await nodeManager!.GetPermissionMetadataAsync(context, nodeHandle, BrowseResultMask.NodeClass, uniqueNodesServiceAttributes!, permissionsOnly, cancellationToken) .ConfigureAwait(false); // If not INodeManager2 or GetPermissionMetadata() returns null. nodeMetadata ??= await nodeManager.GetNodeMetadataAsync( context, nodeHandle, BrowseResultMask.NodeClass, cancellationToken) .ConfigureAwait(false); if (nodeMetadata != null) { // check RolePermissions serviceResult = ValidateRolePermissions( context, nodeMetadata, requestedPermision, m_logger); if (ServiceResult.IsGood(serviceResult)) { // check AccessRestrictions serviceResult = ValidateAccessRestrictions(context, nodeMetadata); } } } return serviceResult; } /// /// Validate the AccessRestrictions attribute /// /// The Operation Context /// Metadata /// Good if the AccessRestrictions passes the validation protected static ServiceResult ValidateAccessRestrictions( OperationContext context, NodeMetadata nodeMetadata) { ServiceResult serviceResult = StatusCodes.Good; // Type hierarchy nodes (ObjectType/VariableType and their children) // are universally accessible regardless of AccessRestrictions. if (nodeMetadata.IsPartOfTypeHierarchy) { return serviceResult; } AccessRestrictionType restrictions = AccessRestrictionType.None; if (nodeMetadata.AccessRestrictions != AccessRestrictionType.None) { restrictions = nodeMetadata.AccessRestrictions; } else if (nodeMetadata.DefaultAccessRestrictions != AccessRestrictionType.None) { restrictions = nodeMetadata.DefaultAccessRestrictions; } if (restrictions != AccessRestrictionType.None) { bool encryptionRequired = (restrictions & AccessRestrictionType.EncryptionRequired) == AccessRestrictionType.EncryptionRequired; bool signingRequired = (restrictions & AccessRestrictionType.SigningRequired) == AccessRestrictionType.SigningRequired; bool sessionRequired = (restrictions & AccessRestrictionType.SessionRequired) == AccessRestrictionType.SessionRequired; bool applyRestrictionsToBrowse = (restrictions & AccessRestrictionType.ApplyRestrictionsToBrowse) == AccessRestrictionType.ApplyRestrictionsToBrowse; bool browseOperation = context.RequestType is RequestType.Browse or RequestType.BrowseNext or RequestType.TranslateBrowsePathsToNodeIds; // Access restriction validation runs while a request is being processed, // which always carries a channel context with an endpoint description. EndpointDescription endpointDescription = context.ChannelContext!.EndpointDescription!; if (( encryptionRequired && endpointDescription.SecurityMode != MessageSecurityMode.SignAndEncrypt && endpointDescription.TransportProfileUri != Profiles.HttpsBinaryTransport && ((applyRestrictionsToBrowse && browseOperation) || !browseOperation) ) || ( signingRequired && endpointDescription.SecurityMode != MessageSecurityMode.Sign && endpointDescription.SecurityMode != MessageSecurityMode.SignAndEncrypt && endpointDescription.TransportProfileUri != Profiles.HttpsBinaryTransport && ((applyRestrictionsToBrowse && browseOperation) || !browseOperation) ) || (sessionRequired && context.Session == null)) { serviceResult = ServiceResult.Create( StatusCodes.BadSecurityModeInsufficient, "Access restricted to nodeId {0} due to insufficient security mode.", nodeMetadata.NodeId); } } return serviceResult; } /// /// Validates the role permissions /// protected internal static ServiceResult ValidateRolePermissions( OperationContext context, NodeMetadata nodeMetadata, PermissionType requestedPermission, ILogger? logger = null) { if (nodeMetadata == null || requestedPermission == PermissionType.None) { // no permission is required hence the validation passes return StatusCodes.Good; } // Type hierarchy nodes (ObjectType/VariableType and their children) // are universally accessible regardless of RolePermissions. if (nodeMetadata.IsPartOfTypeHierarchy) { return StatusCodes.Good; } // get the intersection of user role permissions and role permissions ArrayOf userRolePermissions = default; if (!nodeMetadata.UserRolePermissions.IsEmpty) { userRolePermissions = nodeMetadata.UserRolePermissions; } else if (!nodeMetadata.DefaultUserRolePermissions.IsEmpty) { userRolePermissions = nodeMetadata.DefaultUserRolePermissions; } ArrayOf rolePermissions; if (!nodeMetadata.RolePermissions.IsEmpty) { rolePermissions = nodeMetadata.RolePermissions; } else { rolePermissions = nodeMetadata.DefaultRolePermissions; } if (userRolePermissions.IsEmpty && rolePermissions.IsEmpty) { // there is no restriction from role permissions return StatusCodes.Good; } // group all permissions defined in rolePermissions by RoleId var roleIdPermissions = new Dictionary(); if (!rolePermissions.IsEmpty) { foreach (RolePermissionType rolePermission in rolePermissions) { if (roleIdPermissions.ContainsKey(rolePermission.RoleId)) { roleIdPermissions[rolePermission.RoleId] |= (PermissionType)rolePermission .Permissions; } else { roleIdPermissions[rolePermission.RoleId] = (PermissionType)rolePermission.Permissions; } } } // group all permissions defined in userRolePermissions by RoleId var roleIdPermissionsDefinedForUser = new Dictionary(); if (!userRolePermissions.IsEmpty) { foreach (RolePermissionType rolePermission in userRolePermissions) { if (roleIdPermissionsDefinedForUser.ContainsKey(rolePermission.RoleId)) { roleIdPermissionsDefinedForUser[rolePermission.RoleId] |= (PermissionType) rolePermission.Permissions; } else { roleIdPermissionsDefinedForUser[rolePermission.RoleId] = (PermissionType)rolePermission.Permissions; } } } Dictionary commonRoleIdPermissions; if (rolePermissions.IsEmpty) { // there were no role permissions defined for this node only user role permissions commonRoleIdPermissions = roleIdPermissionsDefinedForUser; } else if (userRolePermissions.IsEmpty) { // there were no user role permissions defined only role permissions for the node commonRoleIdPermissions = roleIdPermissions; } else { commonRoleIdPermissions = []; // intersect role permissions from node and user foreach (NodeId roleId in roleIdPermissions.Keys) { if (roleIdPermissionsDefinedForUser.TryGetValue( roleId, out PermissionType value)) { commonRoleIdPermissions[roleId] = roleIdPermissions[roleId] & value; } } } ArrayOf currentRoleIds = context?.UserIdentity?.GrantedRoleIds ?? default; if (currentRoleIds.IsEmpty) { logger?.LogDebug("Current user has no granted role."); return ServiceResult.Create( StatusCodes.BadUserAccessDenied, "Current user has no granted role."); } PermissionType userActualPermissions = PermissionType.None; foreach (NodeId currentRoleId in currentRoleIds) { if (commonRoleIdPermissions.TryGetValue(currentRoleId, out PermissionType value)) { userActualPermissions |= value; if ((value & requestedPermission) != PermissionType.None) { // there is one role that current session has na is listed in requested role return StatusCodes.Good; } } } logger?.LogDebug( "Role permissions validation failed for node {NodeId}. Requested: {RequestedPermission}, User has: {UserPermissions}", nodeMetadata.NodeId, requestedPermission, userActualPermissions); return ServiceResult.Create( StatusCodes.BadUserAccessDenied, "The requested permission {0} is not granted for node id {1}.", requestedPermission, nodeMetadata.NodeId); } private readonly ILogger m_logger; private readonly SemaphoreSlim m_startupShutdownSemaphoreSlim = new(1, 1); private readonly List m_nodeManagers; private readonly MonitoredItemIdFactory m_monitoredItemIdFactory = new(); private readonly uint m_maxContinuationPointsPerBrowse; private readonly SemaphoreSlim m_namespaceManagersSemaphoreSlim = new(1, 1); private bool m_disposed; } /// /// Stores a reference between NodeManagers that is needs to be created or deleted. /// public class LocalReference { /// /// Initializes the reference. /// public LocalReference( NodeId sourceId, NodeId referenceTypeId, bool isInverse, NodeId targetId) { SourceId = sourceId; ReferenceTypeId = referenceTypeId; IsInverse = isInverse; TargetId = targetId; } /// /// The source of the reference. /// public NodeId SourceId { get; } /// /// The type of reference. /// public NodeId ReferenceTypeId { get; } /// /// True if the reference is an inverse reference. /// public bool IsInverse { get; } /// /// The target of the reference. /// public NodeId TargetId { get; } } /// /// Represents a generator for unique monitored item ids. /// Call next() to retrieve the next valid monitoredItemId. /// /// This class provides a mechanism to generate sequential ids for monitored /// items. It is designed to ensure thread-safe incrementation of the identifier. public class MonitoredItemIdFactory { /// /// Initialize the MonitoredItemIdFactory with a new start value the ids start incrementing from. /// /// public void SetStartValue(uint firstId) { Utils.SetIdentifier(ref m_lastMonitoredItemId, firstId); } /// /// Get the next unique monitored item id. /// /// an uint that can be used as an id for a monitored item public uint GetNextId() { return Utils.IncrementIdentifier(ref m_lastMonitoredItemId); } private uint m_lastMonitoredItemId; } }