diff --git a/Applications/ConsoleReferenceServer/Ctt.ReferenceServer.Config.xml b/Applications/ConsoleReferenceServer/Ctt.ReferenceServer.Config.xml index 51a8631630..c04150b419 100644 --- a/Applications/ConsoleReferenceServer/Ctt.ReferenceServer.Config.xml +++ b/Applications/ConsoleReferenceServer/Ctt.ReferenceServer.Config.xml @@ -267,7 +267,7 @@ - %LocalApplicationData%/OPC Foundation/GDS/gdsdb.json + diff --git a/Applications/ConsoleReferenceServer/GdsNodeManagerFactory.cs b/Applications/ConsoleReferenceServer/GdsNodeManagerFactory.cs index c73f8c386d..edeb3de83c 100644 --- a/Applications/ConsoleReferenceServer/GdsNodeManagerFactory.cs +++ b/Applications/ConsoleReferenceServer/GdsNodeManagerFactory.cs @@ -81,7 +81,9 @@ public INodeManager Create(IServerInternal server, ApplicationConfiguration conf Directory.CreateDirectory(databaseDir); } - var database = JsonApplicationsDatabase.Load(databaseStorePath); + LinqApplicationsDatabase database = string.IsNullOrEmpty(databaseDir) + ? new LinqApplicationsDatabase() + : new JsonApplicationsDatabase(databaseStorePath); return new ApplicationsNodeManager( server, diff --git a/Applications/ConsoleReferenceServer/Quickstarts.ReferenceServer.Config.xml b/Applications/ConsoleReferenceServer/Quickstarts.ReferenceServer.Config.xml index 8e801e37cb..e86c3ee155 100644 --- a/Applications/ConsoleReferenceServer/Quickstarts.ReferenceServer.Config.xml +++ b/Applications/ConsoleReferenceServer/Quickstarts.ReferenceServer.Config.xml @@ -394,7 +394,7 @@ - %LocalApplicationData%/OPC Foundation/GDS/gdsdb.json + diff --git a/Applications/UAReferenceServer.ctt.xml b/Applications/UAReferenceServer.ctt.xml index 970a9b61b0..f4fe6a61da 100644 --- a/Applications/UAReferenceServer.ctt.xml +++ b/Applications/UAReferenceServer.ctt.xml @@ -16799,7 +16799,7 @@ - + @@ -16843,7 +16843,7 @@ - + @@ -16887,7 +16887,7 @@ - + @@ -16931,7 +16931,7 @@ - + @@ -16975,7 +16975,7 @@ - + @@ -17019,7 +17019,7 @@ - + @@ -17063,7 +17063,7 @@ - + @@ -17107,7 +17107,7 @@ - + diff --git a/Libraries/Opc.Ua.Gds.Server.Common/ApplicationsDatabase/ApplicationsDatabaseBase.cs b/Libraries/Opc.Ua.Gds.Server.Common/ApplicationsDatabase/ApplicationsDatabaseBase.cs index 4ae062b532..caa3e58ccf 100644 --- a/Libraries/Opc.Ua.Gds.Server.Common/ApplicationsDatabase/ApplicationsDatabaseBase.cs +++ b/Libraries/Opc.Ua.Gds.Server.Common/ApplicationsDatabase/ApplicationsDatabaseBase.cs @@ -30,6 +30,8 @@ using System; using System.Collections.Generic; using System.Text; +using Opc.Ua.Security; +using Opc.Ua.Types; namespace Opc.Ua.Gds.Server.Database { @@ -115,7 +117,9 @@ public virtual NodeId RegisterApplication(ApplicationRecordDataType application) if (application.ServerCapabilities.IsEmpty) { - application.ServerCapabilities = ["NA"]; + throw new ArgumentException( + "At least one ServerCapability must be provided.", + nameof(application)); } } else if (!application.DiscoveryUrls.IsEmpty) @@ -161,6 +165,11 @@ public virtual ApplicationRecordDataType GetApplication(NodeId applicationId) public virtual ApplicationRecordDataType[] FindApplications(string applicationUri) { + if (string.IsNullOrWhiteSpace(applicationUri)) + { + throw new ServiceResultException( + StatusCodes.BadInvalidArgument); + } return null; } @@ -190,6 +199,20 @@ public virtual ApplicationDescription[] QueryApplications( { lastCounterResetTime = DateTimeUtc.MinValue; nextRecordId = 0; + + if (applicationType > 2) + { + throw new ServiceResultException( + StatusCodes.BadInvalidArgument); + } + + if (serverCapabilities.Contains("NA", StringComparer.OrdinalIgnoreCase) && + serverCapabilities.Count > 1) + { + throw new ServiceResultException( + StatusCodes.BadInvalidArgument); + } + return null; } @@ -332,7 +355,7 @@ protected Guid GetNodeIdGuid(NodeId nodeId) if (NamespaceIndex != nodeId.NamespaceIndex || !nodeId.TryGetValue(out Guid id)) { - throw new ServiceResultException(StatusCodes.BadNodeIdUnknown); + throw new ServiceResultException(StatusCodes.BadNotFound); } return id; @@ -367,7 +390,7 @@ protected void ValidateApplicationNodeId(NodeId nodeId) if ((nodeId.IdType != IdType.Guid && nodeId.IdType != IdType.String) || NamespaceIndex != nodeId.NamespaceIndex) { - throw new ServiceResultException(StatusCodes.BadNodeIdUnknown); + throw new ServiceResultException(StatusCodes.BadNotFound); } } diff --git a/Libraries/Opc.Ua.Gds.Server.Common/ApplicationsDatabase/LinqApplicationsDatabase.cs b/Libraries/Opc.Ua.Gds.Server.Common/ApplicationsDatabase/LinqApplicationsDatabase.cs index 016e30ecc3..4aba023c77 100644 --- a/Libraries/Opc.Ua.Gds.Server.Common/ApplicationsDatabase/LinqApplicationsDatabase.cs +++ b/Libraries/Opc.Ua.Gds.Server.Common/ApplicationsDatabase/LinqApplicationsDatabase.cs @@ -109,6 +109,7 @@ public override void Initialize() public override NodeId RegisterApplication(ApplicationRecordDataType application) { + bool isNewEntry = application.ApplicationId.IsNull; NodeId appNodeId = base.RegisterApplication(application); if (appNodeId.IsNull) { @@ -119,19 +120,36 @@ public override NodeId RegisterApplication(ApplicationRecordDataType application lock (Lock) { + bool existingApplication = ( + from ii in Applications + where ii.ApplicationUri == application.ApplicationUri + select ii + ).Any(); + + if (existingApplication && isNewEntry) + { + throw new ServiceResultException( + StatusCodes.BadEntryExists, + "An application with the same application URI is already registered."); + } + Application record = null; if (applicationId != Guid.Empty) { - IEnumerable results = - from ii in Applications + record = + (from ii in Applications where ii.ApplicationId == applicationId - select ii; - - record = results.SingleOrDefault(); + select ii).SingleOrDefault(); if (record != null) { + if (record.ApplicationUri != application.ApplicationUri) + { + throw new ServiceResultException( + StatusCodes.BadWriteNotSupported); + } + var endpoints = ( from ii in ServerEndpoints where ii.ApplicationId == record.ApplicationId @@ -221,9 +239,9 @@ public override void UnregisterApplication(NodeId applicationId) Application application = (from ii in Applications where ii.ApplicationId == id select ii) .SingleOrDefault() - ?? throw new ArgumentException( - "A record with the specified application id does not exist.", - nameof(applicationId)); + ?? throw new ServiceResultException( + StatusCodes.BadNotFound, + "A record with the specified application id does not exist."); IEnumerable certificateRequests = from ii in CertificateRequests @@ -323,6 +341,8 @@ from ii in ServerEndpoints public override ApplicationRecordDataType[] FindApplications(string applicationUri) { + base.FindApplications(applicationUri); + lock (Lock) { IEnumerable results = @@ -495,6 +515,7 @@ from ii in ServerEndpoints { if (maxRecordsToReturn != 0 && records.Count >= maxRecordsToReturn) { + nextRecordId = result.ID + 1; break; } @@ -524,7 +545,6 @@ from ii in ApplicationNames DiscoveryProfileUri = null, DiscoveryUrls = discoveryUrls }); - nextRecordId = lastID + 1; } return [.. records]; } diff --git a/Libraries/Opc.Ua.Gds.Server.Common/ApplicationsNodeManager.cs b/Libraries/Opc.Ua.Gds.Server.Common/ApplicationsNodeManager.cs index c41cec7421..03e8c3cb4b 100644 --- a/Libraries/Opc.Ua.Gds.Server.Common/ApplicationsNodeManager.cs +++ b/Libraries/Opc.Ua.Gds.Server.Common/ApplicationsNodeManager.cs @@ -65,7 +65,7 @@ public ApplicationsNodeManager( ApplicationConfiguration configuration, IApplicationsDatabase database, ICertificateRequest request, - ICertificateGroup certificateGroup, + ICertificateGroup certificateGroupFactory, bool autoApprove = false) : base( server, @@ -105,7 +105,7 @@ public ApplicationsNodeManager( m_autoApprove = autoApprove; m_database = database; m_request = request; - m_certificateGroupFactory = certificateGroup; + m_certificateGroupFactory = certificateGroupFactory; m_certificateGroups = []; try @@ -652,7 +652,14 @@ private ServiceResult OnRegisterApplication( m_logger.LogInformation("OnRegisterApplication: {ApplicationUri}", application.ApplicationUri); - applicationId = m_database.RegisterApplication(application); + try + { + applicationId = m_database.RegisterApplication(application); + } + catch (ArgumentException ex) + { + throw new ServiceResultException(StatusCodes.BadInvalidArgument, ex); + } if (!applicationId.IsNull) { @@ -687,7 +694,14 @@ private ServiceResult OnUpdateApplication( LocalizedText.From("The application id does not exist.")); } - m_database.RegisterApplication(application); + try + { + m_database.RegisterApplication(application); + } + catch (ArgumentException ex) + { + throw new ServiceResultException(StatusCodes.BadInvalidArgument, ex); + } ArrayOf inputArguments = [Variant.FromStructure(application)]; Server.ReportApplicationRegistrationChangedAuditEvent( @@ -806,6 +820,7 @@ private ServiceResult OnFindApplications( { AuthorizationHelper.HasAuthorization(context, AuthorizationHelper.AuthenticatedUser); m_logger.LogInformation("OnFindApplications: {ApplicationUri}", applicationUri); + applications = m_database.FindApplications(applicationUri); return ServiceResult.Good; } @@ -822,7 +837,20 @@ private ServiceResult OnGetApplication( AuthorizationHelper.AuthenticatedUserOrSelfAdmin, applicationId); m_logger.LogInformation("OnGetApplication: {ApplicationId}", applicationId); - application = m_database.GetApplication(applicationId); + + try + { + application = m_database.GetApplication(applicationId); + } + catch (ArgumentException ex) + { + throw new ServiceResultException(StatusCodes.BadInvalidArgument, ex); + } + + if (application == null) + { + throw new ServiceResultException(StatusCodes.BadNotFound); + } return ServiceResult.Good; } diff --git a/Libraries/Opc.Ua.Gds.Server.Common/Diagnostics/AuditEvents.cs b/Libraries/Opc.Ua.Gds.Server.Common/Diagnostics/AuditEvents.cs index 4fb67b10fb..b3f4b27c90 100644 --- a/Libraries/Opc.Ua.Gds.Server.Common/Diagnostics/AuditEvents.cs +++ b/Libraries/Opc.Ua.Gds.Server.Common/Diagnostics/AuditEvents.cs @@ -67,7 +67,7 @@ internal static void ReportCertificateDeliveredAuditEvent( e.SetChildValue( systemContext, Ua.BrowseNames.SourceName, - "Method/UpdateCertificate", + "Method/FinishRequest", false); e.SetChildValue( systemContext, @@ -148,7 +148,7 @@ internal static void ReportCertificateRequestedAuditEvent( e.SetChildValue( systemContext, Ua.BrowseNames.SourceName, - "Method/UpdateCertificate", + "Method/StartNewKeyPairRequest", false); e.SetChildValue( systemContext, @@ -180,7 +180,7 @@ internal static void ReportCertificateRequestedAuditEvent( { logger.LogError( ex, - "Error while reporting CertificateDeliveredAuditEventState event."); + "Error while reporting CertificateRequestedAuditEventState event."); } } @@ -216,14 +216,21 @@ internal static void ReportApplicationRegistrationChangedAuditEvent( e.SetChildValue( systemContext, Ua.BrowseNames.SourceName, - "Method/UpdateCertificate", + "Method/RegisterApplication", false); + e.SetChildValue( systemContext, Ua.BrowseNames.LocalTime, TimeZoneDataType.Local, false); + e.SetChildValue( + systemContext, + Ua.BrowseNames.ActionTimeStamp, + DateTimeUtc.Now, + false); + e.SetChildValue(systemContext, Ua.BrowseNames.MethodId, method?.NodeId ?? default, false); e.SetChildValue( systemContext, @@ -237,7 +244,7 @@ internal static void ReportApplicationRegistrationChangedAuditEvent( { logger.LogError( ex, - "Error while reporting CertificateDeliveredAuditEventState event."); + "Error while reporting ApplicationRegistrationChangedAuditEventState event."); } } } diff --git a/Libraries/Opc.Ua.Server/Diagnostics/ParsedNodeId.cs b/Libraries/Opc.Ua.Server/Diagnostics/ParsedNodeId.cs index 3bde7c539a..79d6a0fd4e 100644 --- a/Libraries/Opc.Ua.Server/Diagnostics/ParsedNodeId.cs +++ b/Libraries/Opc.Ua.Server/Diagnostics/ParsedNodeId.cs @@ -41,7 +41,6 @@ namespace Opc.Ua.Server /// for the Root Node. The ComponentPath is constructed from the SymbolicNames /// of one or more children of the Root Node. /// - [Obsolete("Will be removed in a future version")] public class ParsedNodeId { /// diff --git a/Libraries/Opc.Ua.Server/NodeManager/Adapters/AsyncNodeManagerAdapter.cs b/Libraries/Opc.Ua.Server/NodeManager/Adapters/AsyncNodeManagerAdapter.cs index 9b3cc93563..9c734186c0 100644 --- a/Libraries/Opc.Ua.Server/NodeManager/Adapters/AsyncNodeManagerAdapter.cs +++ b/Libraries/Opc.Ua.Server/NodeManager/Adapters/AsyncNodeManagerAdapter.cs @@ -132,6 +132,21 @@ public ValueTask CallAsync( return default; } + /// + public ValueTask FindMethodStateAsync( + OperationContext context, + CallMethodRequest methodToCall, + CancellationToken cancellationToken = default) + { + if (SyncNodeManager is INodeManager3 nodeManager) + { + MethodState method = nodeManager.FindMethodState(context, methodToCall); + return new ValueTask(method); + } + + return new ValueTask((MethodState)null); + } + /// public ValueTask ConditionRefreshAsync( OperationContext context, diff --git a/Libraries/Opc.Ua.Server/NodeManager/Adapters/SyncNodeManagerAdapter.cs b/Libraries/Opc.Ua.Server/NodeManager/Adapters/SyncNodeManagerAdapter.cs index 36b4e4b5bd..527a9befa7 100644 --- a/Libraries/Opc.Ua.Server/NodeManager/Adapters/SyncNodeManagerAdapter.cs +++ b/Libraries/Opc.Ua.Server/NodeManager/Adapters/SyncNodeManagerAdapter.cs @@ -289,6 +289,14 @@ public NodeMetadata GetPermissionMetadata( .AsTask().GetAwaiter().GetResult(); } + /// + public MethodState FindMethodState( + OperationContext context, + CallMethodRequest methodToCall) + { + return m_nodeManager.FindMethodStateAsync(context, methodToCall).AsTask().GetAwaiter().GetResult(); + } + /// public ServiceResult ValidateEventRolePermissions(IEventMonitoredItem monitoredItem, IFilterTarget filterTarget) { diff --git a/Libraries/Opc.Ua.Server/NodeManager/AsyncCustomNodeManager.cs b/Libraries/Opc.Ua.Server/NodeManager/AsyncCustomNodeManager.cs index 61ff9aafaa..7f06b67871 100644 --- a/Libraries/Opc.Ua.Server/NodeManager/AsyncCustomNodeManager.cs +++ b/Libraries/Opc.Ua.Server/NodeManager/AsyncCustomNodeManager.cs @@ -3215,7 +3215,74 @@ protected virtual async ValueTask HistoryDeleteEventsAsync( } /// - /// Asycnhronously calls a method defined on an object. + /// Resolves the effective method for a call request. + /// + public virtual async ValueTask FindMethodStateAsync( + OperationContext context, + CallMethodRequest methodToCall, + CancellationToken cancellationToken = default) + { + if (methodToCall == null || methodToCall.ObjectId.IsNull || methodToCall.MethodId.IsNull) + { + return null; + } + + ServerSystemContext systemContext = SystemContext.Copy(context); + IDictionary operationCache = new NodeIdDictionary(); + + NodeHandle handle = await GetManagerHandleAsync( + systemContext, + methodToCall.ObjectId, + operationCache, + cancellationToken).ConfigureAwait(false); + + if (handle == null) + { + return null; + } + + NodeState source = await ValidateNodeAsync(systemContext, handle, operationCache, cancellationToken).ConfigureAwait(false); + + if (source == null) + { + return null; + } + + MethodState method; + lock (source) + { + method = source.FindMethod(systemContext, methodToCall.MethodId); + } + + if (method != null) + { + return method; + } + + bool referenceExists; + lock (source) + { + referenceExists = source.ReferenceExists( + ReferenceTypeIds.HasComponent, + false, + methodToCall.MethodId); + } + + if (referenceExists) + { + method = FindPredefinedNode(methodToCall.MethodId); + } + + if (method == null && source is BaseInstanceState instanceState) + { + method = FindMethodInTypeHierarchy(systemContext, instanceState.TypeDefinitionId, methodToCall.MethodId); + } + + return method; + } + + /// + /// Asynchronously calls a method defined on an object. /// public virtual async ValueTask CallAsync( OperationContext context, @@ -3255,51 +3322,21 @@ public virtual async ValueTask CallAsync( methodToCall.Processed = true; // validate the source node. - NodeState source = await ValidateNodeAsync(systemContext, handle, operationCache, cancellationToken).ConfigureAwait(false); - - if (source == null) + if (await ValidateNodeAsync(systemContext, handle, operationCache, cancellationToken).ConfigureAwait(false) == null) { errors[ii] = StatusCodes.BadNodeIdUnknown; continue; } - // find the method. - lock (source) - { - method = source.FindMethod(systemContext, methodToCall.MethodId); - } + method = await FindMethodStateAsync( + context, + methodToCall, + cancellationToken).ConfigureAwait(false); if (method == null) { - bool referenceExists; - lock (source) - { - referenceExists = source.ReferenceExists( - ReferenceTypeIds.HasComponent, - false, - methodToCall.MethodId); - } - - // check for loose coupling. - if (referenceExists) - { - method = FindPredefinedNode( - methodToCall.MethodId); - } - - // Per OPC UA spec Part 4 section 5.12.2.2: the ObjectType of the Object - // or a super type of that ObjectType may also be the source of a HasComponent - // reference to the method. - if (method == null && source is BaseInstanceState instanceState) - { - method = FindMethodInTypeHierarchy(systemContext, instanceState.TypeDefinitionId, methodToCall.MethodId); - } - - if (method == null) - { - errors[ii] = StatusCodes.BadMethodInvalid; - continue; - } + errors[ii] = StatusCodes.BadMethodInvalid; + continue; } // validate the role permissions for method to be executed, diff --git a/Libraries/Opc.Ua.Server/NodeManager/CustomNodeManager.cs b/Libraries/Opc.Ua.Server/NodeManager/CustomNodeManager.cs index 38fdd5c333..c8004d67e7 100644 --- a/Libraries/Opc.Ua.Server/NodeManager/CustomNodeManager.cs +++ b/Libraries/Opc.Ua.Server/NodeManager/CustomNodeManager.cs @@ -3068,6 +3068,64 @@ public virtual void Call( #pragma warning restore CA2012 // Use ValueTasks correctly } + /// + /// Resolves the effective method for a call request. + /// + public virtual MethodState FindMethodState( + OperationContext context, + CallMethodRequest methodToCall) + { + if (methodToCall == null || methodToCall.ObjectId.IsNull || methodToCall.MethodId.IsNull) + { + return null; + } + + ServerSystemContext systemContext = SystemContext.Copy(context); + IDictionary operationCache = new NodeIdDictionary(); + + NodeHandle handle = GetManagerHandle( + systemContext, + methodToCall.ObjectId, + operationCache); + + if (handle == null) + { + return null; + } + + lock (Lock) + { + NodeState source = ValidateNode(systemContext, handle, operationCache); + + if (source == null) + { + return null; + } + + MethodState method = source.FindMethod(systemContext, methodToCall.MethodId); + + if (method != null) + { + return method; + } + + if (source.ReferenceExists( + ReferenceTypeIds.HasComponent, + false, + methodToCall.MethodId)) + { + method = FindPredefinedNode(methodToCall.MethodId); + } + + if (method == null && source is BaseInstanceState instanceState) + { + method = FindMethodInTypeHierarchy(systemContext, instanceState.TypeDefinitionId, methodToCall.MethodId); + } + + return method; + } + } + /// /// Calls a method on the specified nodes. /// @@ -3111,42 +3169,17 @@ protected virtual async ValueTask CallInternalAsync( methodToCall.Processed = true; // validate the source node. - NodeState source = ValidateNode(systemContext, handle, operationCache); - - if (source == null) + if (ValidateNode(systemContext, handle, operationCache) == null) { errors[ii] = StatusCodes.BadNodeIdUnknown; continue; } - // find the method. - method = source.FindMethod(systemContext, methodToCall.MethodId); - + method = FindMethodState(context, methodToCall); if (method == null) { - // check for loose coupling. - if (source.ReferenceExists( - ReferenceTypeIds.HasComponent, - false, - methodToCall.MethodId)) - { - method = FindPredefinedNode( - methodToCall.MethodId); - } - - // Per OPC UA spec Part 4 section 5.12.2.2: the ObjectType of the Object - // or a super type of that ObjectType may also be the source of a HasComponent - // reference to the method. - if (method == null && source is BaseInstanceState instanceState) - { - method = FindMethodInTypeHierarchy(systemContext, instanceState.TypeDefinitionId, methodToCall.MethodId); - } - - if (method == null) - { - errors[ii] = StatusCodes.BadMethodInvalid; - continue; - } + errors[ii] = StatusCodes.BadMethodInvalid; + continue; } // validate the role permissions for method to be executed, diff --git a/Libraries/Opc.Ua.Server/NodeManager/INodeManager.cs b/Libraries/Opc.Ua.Server/NodeManager/INodeManager.cs index ae96c94267..79def06a6a 100644 --- a/Libraries/Opc.Ua.Server/NodeManager/INodeManager.cs +++ b/Libraries/Opc.Ua.Server/NodeManager/INodeManager.cs @@ -393,6 +393,13 @@ ServiceResult ValidateRolePermissions( OperationContext operationContext, NodeId nodeId, PermissionType requestedPermission); + + /// + /// Resolves the effective for a call request. + /// + MethodState FindMethodState( + OperationContext context, + CallMethodRequest methodToCall); } /// @@ -711,6 +718,14 @@ public interface IAsyncNodeManager : IModifyMonitoredItemsAsyncNodeManager, ICreateMonitoredItemsAsyncNodeManager { + /// + /// Resolves the effective for a call request. + /// + ValueTask FindMethodStateAsync( + OperationContext context, + CallMethodRequest methodToCall, + CancellationToken cancellationToken = default); + /// /// Returns the NamespaceUris for the Nodes belonging to the NodeManager. /// diff --git a/Libraries/Opc.Ua.Server/NodeManager/MasterNodeManager.cs b/Libraries/Opc.Ua.Server/NodeManager/MasterNodeManager.cs index 6082007492..e3c172b344 100644 --- a/Libraries/Opc.Ua.Server/NodeManager/MasterNodeManager.cs +++ b/Libraries/Opc.Ua.Server/NodeManager/MasterNodeManager.cs @@ -1337,6 +1337,7 @@ private static void PrepareValidationCache( Type listType = typeof(T); if (listType != typeof(ReadValueId) && + listType != typeof(WriteValue) && listType != typeof(BrowseDescription) && listType != typeof(CallMethodRequest)) { @@ -2084,6 +2085,10 @@ await nodeManager.HistoryReadAsync( var results = new List(count); var diagnosticInfos = new List(count); + PrepareValidationCache( + nodesToWrite, + out Dictionary uniqueNodesReadAttributes); + // add placeholder for each result. bool validItems = false; @@ -2093,7 +2098,12 @@ await nodeManager.HistoryReadAsync( DiagnosticInfo diagnosticInfo = null; // pre-validate and pre-parse parameter. Validate also access rights and role permissions - ServiceResult error = await ValidateWriteRequestAsync(context, nodesToWrite[ii], cancellationToken) + ServiceResult error = await ValidateWriteRequestAsync( + context, + nodesToWrite[ii], + uniqueNodesReadAttributes, + false, + cancellationToken) .ConfigureAwait(false); // return error status. @@ -2325,6 +2335,10 @@ await nodeManager.HistoryUpdateAsync( 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; @@ -2339,7 +2353,14 @@ await nodeManager.HistoryUpdateAsync( } // validate request parameters. - errors[ii] = ValidateCallRequestItem(context, methodsToCall[ii]); + 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])) { @@ -3521,11 +3542,14 @@ protected static ServiceResult ValidateMonitoredItemModifyRequest( } /// - /// Validates a call request item parameter. It validates also access rights and role permissions + /// Validates a call request item parameter and checks access rights and role permissions. /// - protected ServiceResult ValidateCallRequestItem( + protected async ValueTask ValidateCallRequestItemAsync( OperationContext operationContext, - CallMethodRequest callMethodRequest) + CallMethodRequest callMethodRequest, + Dictionary uniqueNodesReadAttributes = null, + bool permissionsOnly = false, + CancellationToken cancellationToken = default) { // check for null structure. if (callMethodRequest == null) @@ -3545,6 +3569,28 @@ protected ServiceResult ValidateCallRequestItem( return StatusCodes.BadMethodInvalid; } + (_, IAsyncNodeManager nodeManager) = await GetManagerHandleAsync(callMethodRequest.ObjectId, cancellationToken) + .ConfigureAwait(false); + + // 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; } @@ -3592,6 +3638,8 @@ protected async ValueTask ValidateReadRequestAsync( protected async ValueTask ValidateWriteRequestAsync( OperationContext operationContext, WriteValue writeValue, + Dictionary uniqueNodesServiceAttributes = null, + bool permissionsOnly = false, CancellationToken cancellationToken = default) { ServiceResult serviceResult = WriteValue.Validate(writeValue); @@ -3617,8 +3665,8 @@ protected async ValueTask ValidateWriteRequestAsync( operationContext, writeValue.NodeId, requestedPermission, - null, - true, + uniqueNodesServiceAttributes, + permissionsOnly, cancellationToken) .ConfigureAwait(false); } diff --git a/Tests/Opc.Ua.Aot.Tests/GdsClientAotTests.cs b/Tests/Opc.Ua.Aot.Tests/GdsClientAotTests.cs index abef35778f..994e685f8b 100644 --- a/Tests/Opc.Ua.Aot.Tests/GdsClientAotTests.cs +++ b/Tests/Opc.Ua.Aot.Tests/GdsClientAotTests.cs @@ -124,8 +124,9 @@ public async Task UpdateApplicationAsync() .ConfigureAwait(false); appRecord.ApplicationId = id; - const string updatedUri = appUri + "/v2"; - appRecord.ApplicationUri = updatedUri; + const string updatedProductUri = + "http://opcfoundation.org/AotTest/Product/Updated"; + appRecord.ProductUri = updatedProductUri; await fixture.GdsClient .UpdateApplicationAsync(appRecord) .ConfigureAwait(false); @@ -135,7 +136,7 @@ await fixture.GdsClient .ConfigureAwait(false); await Assert.That(result).IsNotNull(); - await Assert.That(result.ApplicationUri).IsEqualTo(updatedUri); + await Assert.That(result.ProductUri).IsEqualTo(updatedProductUri); // cleanup await fixture.GdsClient diff --git a/Tests/Opc.Ua.Aot.Tests/GdsTestFixture.cs b/Tests/Opc.Ua.Aot.Tests/GdsTestFixture.cs index 58cfcdd7ef..e05eae4ea6 100644 --- a/Tests/Opc.Ua.Aot.Tests/GdsTestFixture.cs +++ b/Tests/Opc.Ua.Aot.Tests/GdsTestFixture.cs @@ -158,7 +158,10 @@ await Task.Delay(UnsecureRandom.Shared.Next(100, 1000)) StoreType = CertificateStoreType.Directory, StorePath = Path.Combine(m_pkiRoot, "rejected") }, - AutoAcceptUntrustedCertificates = true + AutoAcceptUntrustedCertificates = true, + RejectSHA1SignedCertificates = false, + RejectUnknownRevocationStatus = true, + MinimumCertificateKeySize = 1024 }, TransportQuotas = new TransportQuotas(), ClientConfiguration = new ClientConfiguration(), @@ -171,6 +174,26 @@ await m_clientConfiguration.ValidateAsync(ApplicationType.Client) m_clientConfiguration.SecurityConfiguration, Telemetry); m_clientConfiguration.CertificateManager.AcceptError = static (cert, err) => true; + var clientApplication = new ApplicationInstance(Telemetry) + { + ApplicationName = m_clientConfiguration.ApplicationName, + ApplicationType = ApplicationType.Client, + ApplicationConfiguration = m_clientConfiguration + }; + try + { + bool haveAppCertificate = await clientApplication + .CheckApplicationInstanceCertificatesAsync(true).ConfigureAwait(false); + if (!haveAppCertificate) + { + throw new InvalidOperationException("Client application certificate invalid!"); + } + } + finally + { + await clientApplication.DisposeAsync().ConfigureAwait(false); + } + // Create the GDS client with admin credentials GdsClient = new GlobalDiscoveryServerClient( m_clientConfiguration); @@ -192,12 +215,37 @@ await discoveryClient.CloseAsync(CancellationToken.None) EndpointDescription selectedEndpoint = null; foreach (EndpointDescription ep in endpoints) { - if (ep.SecurityPolicyUri == SecurityPolicies.None) + if (ep.SecurityMode == MessageSecurityMode.SignAndEncrypt && + ep.SecurityPolicyUri == SecurityPolicies.Aes256_Sha256_RsaPss) { selectedEndpoint = ep; break; } } + if (selectedEndpoint == null) + { + foreach (EndpointDescription ep in endpoints) + { + if (ep.SecurityMode == MessageSecurityMode.SignAndEncrypt && + (ep.SecurityPolicyUri == SecurityPolicies.Aes128_Sha256_RsaOaep || + ep.SecurityPolicyUri == SecurityPolicies.Basic256Sha256)) + { + selectedEndpoint = ep; + break; + } + } + } + if (selectedEndpoint == null) + { + foreach (EndpointDescription ep in endpoints) + { + if (ep.SecurityMode == MessageSecurityMode.SignAndEncrypt) + { + selectedEndpoint = ep; + break; + } + } + } selectedEndpoint ??= endpoints[0]; GdsClient.Endpoint = new ConfiguredEndpoint( @@ -296,12 +344,16 @@ private async Task StartGdsServerAsync(int port) }; ArrayOf applicationCerts = - ApplicationConfigurationBuilder - .CreateDefaultApplicationCertificates( - "CN=GDS AOT Test Server, O=OPC Foundation, " + - "DC=localhost", - CertificateStoreType.Directory, - m_gdsRoot); + [ + new CertificateIdentifier + { + StoreType = CertificateStoreType.Directory, + StorePath = m_gdsRoot, + SubjectName = + "CN=GDS AOT Test Server, O=OPC Foundation, DC=localhost", + CertificateType = ObjectTypeIds.RsaSha256ApplicationCertificateType + } + ]; m_serverApplication = new ApplicationInstance(Telemetry) { diff --git a/Tests/Opc.Ua.Gds.Tests/ApplicationsDatabaseBaseTests.cs b/Tests/Opc.Ua.Gds.Tests/ApplicationsDatabaseBaseTests.cs new file mode 100644 index 0000000000..fbfe0436a6 --- /dev/null +++ b/Tests/Opc.Ua.Gds.Tests/ApplicationsDatabaseBaseTests.cs @@ -0,0 +1,161 @@ +/* ======================================================================== + * 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 NUnit.Framework; +using Opc.Ua.Gds.Server.Database; + +namespace Opc.Ua.Gds.Tests +{ + [TestFixture] + [Category("GDS")] + [SetCulture("en-us")] + [SetUICulture("en-us")] + [Parallelizable] + public class ApplicationsDatabaseBaseTests + { + [Test] + public void RegisterApplicationNullThrows() + { + var database = new TestApplicationsDatabase(); + + Assert.That( + () => database.RegisterApplication(null), + Throws.TypeOf()); + } + + [Test] + public void RegisterApplicationInvalidApplicationUriThrows() + { + var database = new TestApplicationsDatabase(); + ApplicationRecordDataType application = CreateValidServerApplication(); + application.ApplicationUri = "not a uri"; + + Assert.That( + () => database.RegisterApplication(application), + Throws.TypeOf()); + } + + [Test] + public void RegisterApplicationServerWithoutDiscoveryUrlsThrows() + { + var database = new TestApplicationsDatabase(); + ApplicationRecordDataType application = CreateValidServerApplication(); + application.DiscoveryUrls = []; + + Assert.That( + () => database.RegisterApplication(application), + Throws.TypeOf()); + } + + [Test] + public void RegisterApplicationClientWithDiscoveryUrlsThrows() + { + var database = new TestApplicationsDatabase(); + ApplicationRecordDataType application = CreateValidServerApplication(); + application.ApplicationType = ApplicationType.Client; + application.ServerCapabilities = []; + application.DiscoveryUrls = ["opc.tcp://localhost:4840"]; + + Assert.That( + () => database.RegisterApplication(application), + Throws.TypeOf()); + } + + [Test] + public void RegisterApplicationValidServerReturnsDefaultNodeId() + { + var database = new TestApplicationsDatabase(); + ApplicationRecordDataType application = CreateValidServerApplication(); + + NodeId applicationId = database.RegisterApplication(application); + + Assert.That(applicationId.IsNull, Is.True); + } + + [Test] + public void RegisterApplicationValidServerWithDiscoveryUrlsDoesNotThrow() + { + var database = new TestApplicationsDatabase(); + ApplicationRecordDataType application = CreateValidServerApplication(); + + Assert.That( + () => database.RegisterApplication(application), + Throws.Nothing); + } + + [Test] + public void FindApplicationsWhitespaceThrows() + { + var database = new TestApplicationsDatabase(); + + Assert.That( + () => database.FindApplications(" "), + Throws.TypeOf() + .With.Property(nameof(ServiceResultException.StatusCode)).EqualTo(StatusCodes.BadInvalidArgument)); + } + + [Test] + public void QueryApplicationsInvalidTypeThrows() + { + var database = new TestApplicationsDatabase(); + + Assert.That( + () => database.QueryApplications( + 0, + 10, + null, + null, + 3, + null, + [], + out _, + out _), + Throws.TypeOf() + .With.Property(nameof(ServiceResultException.StatusCode)).EqualTo(StatusCodes.BadInvalidArgument)); + } + + private static ApplicationRecordDataType CreateValidServerApplication() + { + return new ApplicationRecordDataType + { + ApplicationUri = "urn:test:application", + ApplicationType = ApplicationType.Server, + ApplicationNames = [new LocalizedText("en", "TestApp")], + ProductUri = "urn:test:product", + DiscoveryUrls = ["opc.tcp://localhost:4840"], + ServerCapabilities = ["LDS"] + }; + } + + private sealed class TestApplicationsDatabase : ApplicationsDatabaseBase + { + } + } +} diff --git a/Tests/Opc.Ua.Gds.Tests/ClientTest.cs b/Tests/Opc.Ua.Gds.Tests/ClientTest.cs index f295769f09..a0610259bc 100644 --- a/Tests/Opc.Ua.Gds.Tests/ClientTest.cs +++ b/Tests/Opc.Ua.Gds.Tests/ClientTest.cs @@ -219,31 +219,9 @@ public async Task RegisterDuplicateGoodApplicationsAsync() var newRecord = (ApplicationRecordDataType)application.ApplicationRecord .MemberwiseClone(); newRecord.ApplicationId = default; - NodeId id = await m_gdsClient.GDSClient.RegisterApplicationAsync(newRecord).ConfigureAwait(false); - Assert.That(id.IsNull, Is.False); - Assert.That(id.IdType, Is.EqualTo(IdType.Guid).Or.EqualTo(IdType.String)); - newRecord.ApplicationId = id; - ArrayOf applicationDataRecords = await m_gdsClient.GDSClient - .FindApplicationAsync( - newRecord.ApplicationUri).ConfigureAwait(false); - Assert.That(applicationDataRecords.IsNull, Is.False); - bool newIdFound = false; - bool registeredIdFound = false; - foreach (ApplicationRecordDataType applicationDataRecord in applicationDataRecords.ToList()) - { - if (applicationDataRecord.ApplicationId == newRecord.ApplicationId) - { - await m_gdsClient.GDSClient.UnregisterApplicationAsync(id).ConfigureAwait(false); - newIdFound = true; - } - else if (applicationDataRecord.ApplicationId == application.ApplicationRecord - .ApplicationId) - { - registeredIdFound = true; - } - } - Assert.That(newIdFound, Is.True); - Assert.That(registeredIdFound, Is.True); + ServiceResultException serviceResultException = Assert.ThrowsAsync( + async () => await m_gdsClient.GDSClient.RegisterApplicationAsync(newRecord).ConfigureAwait(false)); + Assert.That(serviceResultException.StatusCode, Is.EqualTo(StatusCodes.BadEntryExists)); } } @@ -288,12 +266,9 @@ public async Task UpdateGoodApplicationsAsync() var updatedApplicationRecord = (ApplicationRecordDataType) application.ApplicationRecord.MemberwiseClone(); updatedApplicationRecord.ApplicationUri += "update"; - await m_gdsClient.GDSClient.UpdateApplicationAsync(updatedApplicationRecord).ConfigureAwait(false); - ArrayOf result = await m_gdsClient.GDSClient.FindApplicationAsync( - updatedApplicationRecord.ApplicationUri).ConfigureAwait(false); - await m_gdsClient.GDSClient.UpdateApplicationAsync(application.ApplicationRecord).ConfigureAwait(false); - Assert.That(result.IsNull, Is.False); - Assert.That(result.Count, Is.GreaterThanOrEqualTo(1), "Couldn't find updated application record"); + ServiceResultException serviceResultException = Assert.ThrowsAsync( + async () => await m_gdsClient.GDSClient.UpdateApplicationAsync(updatedApplicationRecord).ConfigureAwait(false)); + Assert.That(serviceResultException.StatusCode, Is.EqualTo(StatusCodes.BadWriteNotSupported)); } } @@ -773,7 +748,6 @@ public async Task QueryServersByProductUriAsync() } } - [Test] [Order(480)] public async Task QueryAllApplicationsAsync() { diff --git a/Tests/Opc.Ua.Server.Tests/AsyncCustomNodeManagerTests.cs b/Tests/Opc.Ua.Server.Tests/AsyncCustomNodeManagerTests.cs index f79d8489cd..f1f715213f 100644 --- a/Tests/Opc.Ua.Server.Tests/AsyncCustomNodeManagerTests.cs +++ b/Tests/Opc.Ua.Server.Tests/AsyncCustomNodeManagerTests.cs @@ -1302,6 +1302,78 @@ public async Task CallAsync_InvokesMethodFromObjectTypeAsync() Assert.That(syncResults[0].OutputArguments[0].GetInt32(), Is.EqualTo(42)); } + [Test] + public async Task FindMethodStateAsyncResolvesMethodFromSuperTypeOfObjectType() + { + using TestableAsyncCustomNodeManager manager = CreateManager(); + ServerSystemContext context = manager.SystemContext; + ushort nsIdx = manager.NamespaceIndexes[0]; + + var baseType = new BaseObjectTypeState + { + NodeId = new NodeId("ResolverBaseType", nsIdx), + BrowseName = new QualifiedName("ResolverBaseType", nsIdx), + SuperTypeId = NodeId.Null + }; + var derivedType = new BaseObjectTypeState + { + NodeId = new NodeId("ResolverDerivedType", nsIdx), + BrowseName = new QualifiedName("ResolverDerivedType", nsIdx), + SuperTypeId = baseType.NodeId + }; + var instance = new BaseObjectState(null) + { + NodeId = new NodeId("ResolverDerivedInstance", nsIdx), + BrowseName = new QualifiedName("ResolverDerivedInstance", nsIdx), + TypeDefinitionId = derivedType.NodeId + }; + try + { + var baseMethod = new MethodState(baseType) + { + NodeId = new NodeId("ResolverBaseMethod", nsIdx), + BrowseName = new QualifiedName("ResolverBaseMethod", nsIdx) + }; + baseType.AddChild(baseMethod); + + // Initialize after relationships are set up so predefined state reflects the full type hierarchy. + baseType.CreateAsPredefinedNode(context); + derivedType.CreateAsPredefinedNode(context); + instance.CreateAsPredefinedNode(context); + + await manager.AddPredefinedNodePublicAsync(context, baseType).ConfigureAwait(false); + await manager.AddPredefinedNodePublicAsync(context, derivedType).ConfigureAwait(false); + await manager.AddNodeAsync(context, default, instance).ConfigureAwait(false); + + m_mockServer.Object.TypeTree.AddSubtype(baseType.NodeId, NodeId.Null); + m_mockServer.Object.TypeTree.AddSubtype(derivedType.NodeId, baseType.NodeId); + + var request = new CallMethodRequest + { + ObjectId = instance.NodeId, + MethodId = baseMethod.NodeId, + InputArguments = [] + }; + var operationContext = new OperationContext(new RequestHeader(), null, RequestType.Call, RequestLifetime.None); + + MethodState method = await manager.FindMethodStateAsync(operationContext, request).ConfigureAwait(false); + Assert.That(method, Is.Not.Null); + Assert.That(method.NodeId, Is.EqualTo(baseMethod.NodeId)); + + var syncNodeManager = manager.SyncNodeManager as INodeManager3; + Assert.That(syncNodeManager, Is.Not.Null); + MethodState syncMethod = syncNodeManager.FindMethodState(operationContext, request); + Assert.That(syncMethod, Is.Not.Null); + Assert.That(syncMethod.NodeId, Is.EqualTo(baseMethod.NodeId)); + } + finally + { + DisposeIfNeeded(instance); + DisposeIfNeeded(derivedType); + DisposeIfNeeded(baseType); + } + } + [Test] public async Task CallAsync_InvokesMethodFromSuperTypeOfObjectTypeAsync() { @@ -3778,6 +3850,14 @@ private void SetupMasterNodeManager(TestableAsyncCustomNodeManager manager) return new ValueTask<(object handle, IAsyncNodeManager nodeManager)>((handle, manager)); }); } + + private static void DisposeIfNeeded(object value) + { + if (value is IDisposable disposable) + { + disposable.Dispose(); + } + } } public class TestableAsyncCustomNodeManager : AsyncCustomNodeManager