/* ======================================================================== * 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.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Opc.Ua; using Opc.Ua.Server; namespace Quickstarts.ReferenceServer { /// /// Reference-server-specific that /// applies a few address-space tweaks needed by the OPC UA Conformance /// Test (CTT) suite. These tweaks intentionally live outside of the SDK /// because they are test-suite-specific and modify the standard nodeset: /// /// Populate RolePermissions/UserRolePermissions/AccessRestrictions /// on the Server node so CTT tests that read those optional /// attributes get a defined value instead of BadAttributeIdInvalid. /// Expose a single HasAddIn instance under the Server /// node so CTT tests that require at least one AddIn complete. /// Add an optional EngineeringUnits property to /// AnalogItemType (Part 8 §5.3.2.3) so CTT browse tests that /// expect this child don't skip. /// /// public sealed class ReferenceServerConfigurationNodeManager : ConfigurationNodeManager { /// /// Initializes the configuration node manager. /// public ReferenceServerConfigurationNodeManager( IServerInternal server, ApplicationConfiguration configuration) : base(server, configuration) { } /// public override async ValueTask CreateAddressSpaceAsync( IDictionary> externalReferences, CancellationToken cancellationToken = default) { await base.CreateAddressSpaceAsync(externalReferences, cancellationToken) .ConfigureAwait(false); ushort diagnosticsNamespaceIndex = Server.NamespaceUris .GetIndexOrAppend(Opc.Ua.Namespaces.OpcUa + "Diagnostics"); var addedNodes = new List(); // CTT: populate RolePermissions / UserRolePermissions / // AccessRestrictions on the Server node so clients that read // those optional attributes (per Part 5 §6.2) get a defined // value instead of BadAttributeIdInvalid. Anonymous gets // Browse + Read on the metadata; SecurityAdmin and // ConfigureAdmin get the full permission mask. This is purely // metadata exposure — it doesn't change runtime access // enforcement (which is governed by the per-child node // RolePermissions in the standard nodeset). The NodeState // reference is shared between the diagnostics and core node // managers, so attribute mutations propagate automatically. ServerObjectState serverNode = FindPredefinedNode( ObjectIds.Server); if (serverNode != null && serverNode.RolePermissions.IsNull) { const PermissionType browseAndRead = PermissionType.Browse | PermissionType.Read | PermissionType.ReceiveEvents; const PermissionType fullAdmin = PermissionType.Browse | PermissionType.ReadRolePermissions | PermissionType.WriteAttribute | PermissionType.WriteRolePermissions | PermissionType.WriteHistorizing | PermissionType.Read | PermissionType.Write | PermissionType.ReadHistory | PermissionType.InsertHistory | PermissionType.ModifyHistory | PermissionType.DeleteHistory | PermissionType.ReceiveEvents | PermissionType.Call | PermissionType.AddReference | PermissionType.RemoveReference | PermissionType.DeleteNode; ArrayOf permissions = new RolePermissionType[] { new() { RoleId = ObjectIds.WellKnownRole_Anonymous, Permissions = (uint)browseAndRead }, new() { RoleId = ObjectIds.WellKnownRole_AuthenticatedUser, Permissions = (uint)browseAndRead }, new() { RoleId = ObjectIds.WellKnownRole_SecurityAdmin, Permissions = (uint)fullAdmin }, new() { RoleId = ObjectIds.WellKnownRole_ConfigureAdmin, Permissions = (uint)fullAdmin } }.ToArrayOf(); serverNode.RolePermissions = permissions; serverNode.UserRolePermissions = permissions; serverNode.AccessRestrictions = AccessRestrictionType.None; } // CTT: expose a single AddIn instance under the Server node // so the conformance tests that expect at least one HasAddIn // reference (Address Space Base AddIn instance / inverse / target // is Object) can complete successfully. AddIn instances are an // optional extensibility point per Part 5 §6.3.6 — clients may // browse Server forward via HasAddIn (i=17604) and inspect the // returned objects. The instance is a plain BaseObjectState. if (serverNode != null) { var addIn = new BaseObjectState(serverNode) { SymbolicName = "ConformanceAddIn", ReferenceTypeId = ReferenceTypeIds.HasAddIn, TypeDefinitionId = ObjectTypeIds.BaseObjectType, NodeId = new NodeId("ConformanceAddIn", diagnosticsNamespaceIndex), BrowseName = new QualifiedName("ConformanceAddIn", diagnosticsNamespaceIndex), DisplayName = LocalizedText.From("ConformanceAddIn"), WriteMask = AttributeWriteMask.None, UserWriteMask = AttributeWriteMask.None, EventNotifier = EventNotifiers.None }; serverNode.AddReference(ReferenceTypeIds.HasAddIn, false, addIn.NodeId); addIn.AddReference(ReferenceTypeIds.HasAddIn, true, serverNode.NodeId); await AddPredefinedNodeAsync(SystemContext, addIn, cancellationToken) .ConfigureAwait(false); addedNodes.Add(addIn); } // CTT: declare the optional EngineeringUnits property on // VariableTypeIds.AnalogItemType so conformance tests that // browse the type for an "EngineeringUnits" child don't skip. // Per Part 8 §5.3.2.3 EngineeringUnits is an optional // PropertyType child of AnalogItemType — but the standard // NodeSet2.xml as shipped only declares EURange. Adding // EngineeringUnits as a HasProperty Optional child here // matches the spec and is harmless for other clients (the // property carries a default-constructed EUInformation // value). BaseVariableTypeState analogItemType = FindPredefinedNode( VariableTypeIds.AnalogItemType); if (analogItemType != null && analogItemType.FindChild(SystemContext, new QualifiedName(BrowseNames.EngineeringUnits, 0)) == null) { var euProperty = PropertyState .With>(analogItemType); euProperty.SymbolicName = BrowseNames.EngineeringUnits; euProperty.ReferenceTypeId = ReferenceTypeIds.HasProperty; euProperty.TypeDefinitionId = VariableTypeIds.PropertyType; euProperty.ModellingRuleId = ObjectIds.ModellingRule_Optional; euProperty.NodeId = new NodeId(BrowseNames.EngineeringUnits, diagnosticsNamespaceIndex); euProperty.BrowseName = new QualifiedName(BrowseNames.EngineeringUnits, 0); euProperty.DisplayName = LocalizedText.From(BrowseNames.EngineeringUnits); euProperty.DataType = DataTypeIds.EUInformation; euProperty.ValueRank = ValueRanks.Scalar; euProperty.AccessLevel = AccessLevels.CurrentRead; euProperty.UserAccessLevel = AccessLevels.CurrentRead; euProperty.Value = new EUInformation(); analogItemType.AddChild(euProperty); await AddPredefinedNodeAsync(SystemContext, euProperty, cancellationToken) .ConfigureAwait(false); addedNodes.Add(euProperty); } await RemovePubSubKeyServiceFoldersAsync(cancellationToken).ConfigureAwait(false); // Push any newly added namespace-0 nodes into the CoreNodeManager // so they are reachable via Browse from the standard address // space (base.CreateAddressSpaceAsync already performed the bulk // import; this top-up handles the CTT-only nodes). if (addedNodes.Count > 0) { await Server.CoreNodeManager.ImportNodesAsync( SystemContext, addedNodes, true, cancellationToken).ConfigureAwait(false); } } /// /// Issue #3719: the standard NodeSet declares SecurityGroups /// (i=15443) and KeyPushTargets (i=25440) under /// PublishSubscribe and each carries a set of mandatory methods /// (AddSecurityGroup, RemoveSecurityGroup, /// AddPushTarget, RemovePushTarget). The reference /// server does not implement PubSub key services, so the folders /// are removed entirely rather than carrying no-op method stubs /// that would only exist to satisfy the mandatory-method check. /// Both folders are optional under PublishSubscribe per /// Part 14, so removing them is spec-compliant. /// private async ValueTask RemovePubSubKeyServiceFoldersAsync(CancellationToken cancellationToken) { foreach (NodeId folderNodeId in s_optionalPubSubFolders) { if (FindPredefinedNode(folderNodeId) != null) { await DeleteNodeAsync(SystemContext, folderNodeId, cancellationToken) .ConfigureAwait(false); } } } private static readonly NodeId[] s_optionalPubSubFolders = [ // PublishSubscribe.SecurityGroups instance (NodeId i=15443). new NodeId(15443u), // PublishSubscribe.KeyPushTargets instance (NodeId i=25440). new NodeId(25440u) ]; } }