/* ======================================================================== * 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.Generic; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; namespace Opc.Ua.Server.AliasNames { /// /// Standalone OPC UA Part 17 (Alias Names) node manager. Builds an /// address-space tree of AliasNameCategoryType instances from /// an 's /// , wires the typed /// OnCallAsync handlers of the generated /// FindAliasMethodState/FindAliasVerboseMethodState/AddAliasesToCategoryMethodState/DeleteAliasesFromCategoryMethodState /// children, and (when configured) registers the store with the /// server-wide so that the /// standard well-known Aliases (i=23470) / /// TagVariables (i=23479) / Topics (i=23488) nodes also /// dispatch through it. /// /// /// /// Apps opt in by adding this manager to their server's node-manager /// list. The manager owns a single namespace /// () under /// which it creates its category and alias instances; standard /// well-known categories owned by DiagnosticsNodeManager are /// not duplicated — only their methods are wired through the /// registry. /// /// /// All four Part 17 methods are exposed only when the corresponding /// flag is set on the /// . Mutating methods /// (AddAliasesToCategory/DeleteAliasesFromCategory) /// default to requiring the SecurityAdmin role on a /// SignAndEncrypt channel; override /// /// to opt out. /// /// public class AliasNameNodeManager : AsyncCustomNodeManager { /// /// Initializes a new instance. /// /// The server internal interface. /// The application configuration. /// The pluggable alias-name backend; the /// manager builds its address space from /// and dispatches /// every Part 17 method through it. /// Optional tunables; defaults applied /// when null. public AliasNameNodeManager( IServerInternal server, ApplicationConfiguration configuration, IAliasNameStore store, AliasNameNodeManagerOptions? options = null) : base(server, configuration, ResolveNamespaceUri(options)) { Store = store ?? throw new ArgumentNullException(nameof(store)); Options = options ?? new AliasNameNodeManagerOptions(); m_aliasLogger = server.Telemetry.CreateLogger(); m_registry = ResolveServerRegistry(server); m_localCategoryDispatcher = new AliasNameStoreRegistry(); m_localCategoryDispatcher.Register(Store); } /// /// The backing . /// public IAliasNameStore Store { get; } /// /// The tunables in use. /// public AliasNameNodeManagerOptions Options { get; } /// public override NodeId New(ISystemContext context, NodeState node) { // Preserve any caller-assigned NodeId that already lives in our // namespace; otherwise mint a sequential numeric id. if (!node.NodeId.IsNull && node.NodeId.NamespaceIndex == NamespaceIndex) { return node.NodeId; } uint id = Utils.IncrementIdentifier(ref m_nextNodeId); return new NodeId(id, NamespaceIndex); } /// public override async ValueTask CreateAddressSpaceAsync( IDictionary> externalReferences, CancellationToken cancellationToken = default) { await base.CreateAddressSpaceAsync(externalReferences, cancellationToken) .ConfigureAwait(false); foreach (AliasNameCategoryDescriptor root in Store.RootCategories) { AliasNameCategoryState rootState = BuildCategoryTree(root); m_rootCategoryStates[root.NodeId] = rootState; if (Options.LinkToStandardAliasesObject) { AddExternalReference( ObjectIds.Aliases, ReferenceTypeIds.Organizes, isInverse: false, rootState.NodeId, externalReferences); rootState.AddReference( ReferenceTypeIds.Organizes, isInverse: true, ObjectIds.Aliases); } await AddPredefinedNodeAsync( SystemContext, rootState, cancellationToken) .ConfigureAwait(false); } if (Options.RegisterWithServerRegistry && m_registry != null) { try { m_registry.Register(Store); m_registeredWithServer = true; } catch (InvalidOperationException ex) { m_aliasLogger.LogWarning(ex, "AliasNameStore could not be registered with " + "the server-wide registry; standard well-known " + "Aliases methods will not dispatch through it."); } } Store.Changed += OnStoreChanged; } /// protected override void Dispose(bool disposing) { if (disposing) { Store.Changed -= OnStoreChanged; if (m_registeredWithServer && m_registry != null) { m_registry.Unregister(Store); m_registeredWithServer = false; } m_localCategoryDispatcher.Dispose(); } base.Dispose(disposing); } /// /// Recursively builds an /// instance for the supplied descriptor — including any optional /// children declared by its capabilities and any nested /// sub-categories — and wires every method's typed /// OnCallAsync handler to the dispatcher. /// protected virtual AliasNameCategoryState BuildCategoryTree( AliasNameCategoryDescriptor descriptor) { // CreateInstanceOf... returns a typed instance with the // mandatory FindAlias child already created. We then override // the identity attributes and add the optional Part 17 // children we have been asked to expose. AliasNameCategoryState category = SystemContext.CreateInstanceOfAliasNameCategoryType(); category.NodeId = descriptor.NodeId; category.BrowseName = descriptor.BrowseName; category.DisplayName = new LocalizedText(descriptor.BrowseName.Name); category.SymbolicName = descriptor.BrowseName.Name!; category.TypeDefinitionId = ObjectTypeIds.AliasNameCategoryType; category.ReferenceTypeId = ReferenceTypeIds.Organizes; AliasNameCapabilities cap = descriptor.Capabilities; if ((cap & AliasNameCapabilities.FindAliasVerbose) != 0) { category.AddFindAliasVerbose(SystemContext); } if ((cap & AliasNameCapabilities.LastChange) != 0) { category.AddLastChange(SystemContext); } if ((cap & AliasNameCapabilities.AddAliasesToCategory) != 0) { category.AddAddAliasesToCategory(SystemContext); } if ((cap & AliasNameCapabilities.DeleteAliasesFromCategory) != 0) { category.AddDeleteAliasesFromCategory(SystemContext); } foreach (AliasNameCategoryDescriptor child in descriptor.SubCategories) { AliasNameCategoryState childState = BuildCategoryTree(child); childState.ReferenceTypeId = ReferenceTypeIds.Organizes; category.AddChild(childState); } // Auto-assign NodeIds to every child not pre-pinned in our // namespace. category.AssignNodeIds(SystemContext, []); // Wire method handlers (after node ids are stable). WireCategoryHandlers(descriptor.NodeId, category); // Seed LastChange. category.LastChange?.Value = Store.GetLastChange(descriptor.NodeId) ?? 0u; return category; } private void WireCategoryHandlers(NodeId categoryId, AliasNameCategoryState category) { category.FindAlias?.OnCallAsync = (ctx, method, objId, pattern, refType, ct) => AliasNameMethodDispatcher.FindAliasAsync( m_localCategoryDispatcher, Server.TypeTree, objId.IsNull ? categoryId : objId, pattern, refType, ct); category.FindAliasVerbose?.OnCallAsync = (ctx, method, objId, pattern, refType, ct) => AliasNameMethodDispatcher.FindAliasVerboseAsync( m_localCategoryDispatcher, Server.TypeTree, objId.IsNull ? categoryId : objId, pattern, refType, ct); category.AddAliasesToCategory?.OnCallAsync = (ctx, method, objId, names, targets, servers, refType, ct) => DispatchAddAsync(ctx, categoryId, objId, names, targets, servers, refType, ct); category.DeleteAliasesFromCategory?.OnCallAsync = (ctx, method, objId, names, targets, ct) => DispatchDeleteAsync(ctx, categoryId, objId, names, targets, ct); // Recurse into sub-category children (already wired through // BuildCategoryTree, but their state objects sit on this // node's children list — wire those too). var children = new List(); category.GetChildren(SystemContext, children); foreach (BaseInstanceState child in children) { if (child is AliasNameCategoryState childCategory && !ReferenceEquals(childCategory, category)) { WireCategoryHandlers(childCategory.NodeId, childCategory); } } } private ValueTask DispatchAddAsync( ISystemContext context, NodeId fallbackCategoryId, NodeId objectId, ArrayOf aliasNames, ArrayOf targetNodes, ArrayOf targetServers, NodeId targetReferenceType, CancellationToken ct) { if (Options.RequireSecurityAdminForMutations && !HasSecureAdminAccess(context)) { return new ValueTask( new AddAliasesToCategoryMethodStateResult { ServiceResult = new ServiceResult(StatusCodes.BadUserAccessDenied), ErrorCodes = default }); } return AliasNameMethodDispatcher.AddAliasesAsync( m_localCategoryDispatcher, objectId.IsNull ? fallbackCategoryId : objectId, aliasNames, targetNodes, targetServers, targetReferenceType, ct); } private ValueTask DispatchDeleteAsync( ISystemContext context, NodeId fallbackCategoryId, NodeId objectId, ArrayOf aliasNames, ArrayOf targetNodes, CancellationToken ct) { if (Options.RequireSecurityAdminForMutations && !HasSecureAdminAccess(context)) { return new ValueTask( new DeleteAliasesFromCategoryMethodStateResult { ServiceResult = new ServiceResult(StatusCodes.BadUserAccessDenied), ErrorCodes = default }); } return AliasNameMethodDispatcher.DeleteAliasesAsync( m_localCategoryDispatcher, objectId.IsNull ? fallbackCategoryId : objectId, aliasNames, targetNodes, ct); } private void OnStoreChanged(object? sender, AliasStoreChangedEventArgs e) { lock (m_lock) { if (!m_rootCategoryStates.TryGetValue(e.CategoryId, out AliasNameCategoryState? root)) { root = FindCategoryNode(e.CategoryId); } if (root?.LastChange != null) { root.LastChange.Value = e.LastChange; root.LastChange.ClearChangeMasks(SystemContext, false); } } } private AliasNameCategoryState? FindCategoryNode(NodeId categoryId) { foreach (AliasNameCategoryState root in m_rootCategoryStates.Values) { AliasNameCategoryState? found = FindRecursive(root, categoryId); if (found != null) { return found; } } return null; } private AliasNameCategoryState? FindRecursive( AliasNameCategoryState node, NodeId target) { if (node.NodeId == target) { return node; } var children = new List(); node.GetChildren(SystemContext, children); foreach (BaseInstanceState child in children) { if (child is AliasNameCategoryState sub) { AliasNameCategoryState? hit = FindRecursive(sub, target); if (hit != null) { return hit; } } } return null; } private static bool HasSecureAdminAccess(ISystemContext context) { if (context is SessionSystemContext { OperationContext: OperationContext op } session) { if (op.ChannelContext?.EndpointDescription?.SecurityMode != MessageSecurityMode.SignAndEncrypt) { return false; } return session.UserIdentity?.GrantedRoleIds .Contains(ObjectIds.WellKnownRole_SecurityAdmin) == true; } return false; } private static string ResolveNamespaceUri(AliasNameNodeManagerOptions? options) { return options?.NamespaceUri ?? new AliasNameNodeManagerOptions().NamespaceUri; } private static IAliasNameStoreRegistry? ResolveServerRegistry(IServerInternal server) { return (server as IAliasNameStoreRegistryProvider)?.AliasNameStoreRegistry; } private readonly ILogger m_aliasLogger; private readonly IAliasNameStoreRegistry? m_registry; /// /// Always-available dispatcher that wraps just this manager's /// store so the standalone manager works even when the host /// server does not implement IAliasNameStoreRegistryProvider. /// private readonly AliasNameStoreRegistry m_localCategoryDispatcher; private readonly Dictionary m_rootCategoryStates = []; private bool m_registeredWithServer; private uint m_nextNodeId; private readonly Lock m_lock = new(); } }