Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 118 additions & 1 deletion Libraries/Opc.Ua.Server/Configuration/ConfigurationNodeManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,34 @@ protected override NodeState AddBehaviourToPredefinedNode(
return base.AddBehaviourToPredefinedNode(context, predefinedNode);
}

/// <summary>
/// Frees any unmanaged resources.
/// </summary>
protected override void Dispose(bool disposing)
{
if (disposing)
{
if (FindPredefinedNode<NamespacesState>(ObjectIds.Server_Namespaces)
is NamespacesState serverNamespacesNode)
{
serverNamespacesNode.StateChanged -= ServerNamespacesChanged;
}

foreach (NodeState node in PredefinedNodes.Values)
{
if (node is NamespaceMetadataState metadataState)
{
metadataState.StateChanged -= OnNamespaceChildrenChanged;
metadataState.DefaultRolePermissions?.StateChanged -= OnNamespaceDefaultPermissionsChanged;

metadataState.DefaultUserRolePermissions?.StateChanged -= OnNamespaceDefaultPermissionsChanged;
}
}
}

base.Dispose(disposing);
}

///<inheritdoc/>
public void CreateServerConfiguration(
ServerSystemContext systemContext,
Expand Down Expand Up @@ -313,6 +341,17 @@ .. configuration.ServerConfiguration.SupportedPrivateKeyFormats
is NamespacesState serverNamespacesNode)
{
serverNamespacesNode.StateChanged += ServerNamespacesChanged;

IList<BaseInstanceState> children = [];
serverNamespacesNode.GetChildren(systemContext, children);

foreach (BaseInstanceState child in children)
{
if (child is NamespaceMetadataState metadataState)
{
SubscribeToNamespaceDefaultPermissions(metadataState);
}
}
}
}

Expand Down Expand Up @@ -375,13 +414,19 @@ public NamespaceMetadataState CreateNamespaceMetadataState(string namespaceUri)
namespaceMetadataState.DisplayName = LocalizedText.From(namespaceUri);
namespaceMetadataState.SymbolicName = namespaceUri;
namespaceMetadataState.NamespaceUri.Value = namespaceUri;
namespaceMetadataState.AddDefaultRolePermissions(SystemContext);
namespaceMetadataState.AddDefaultUserRolePermissions(SystemContext);

// add node as child of ServerNamespaces and in predefined nodes
serverNamespacesNode.AddChild(namespaceMetadataState);
serverNamespacesNode.ClearChangeMasks(Server.DefaultSystemContext, true);
serverNamespacesNode.ClearChangeMasks(SystemContext, true);
AddPredefinedNode(SystemContext, namespaceMetadataState);
}

// Subscribe to the default permission properties so that any future changes
// trigger a DefaultPermissionsChanged notification to allow caches to be invalidated.
SubscribeToNamespaceDefaultPermissions(namespaceMetadataState);

return namespaceMetadataState;
}

Expand Down Expand Up @@ -1234,6 +1279,20 @@ private void ServerNamespacesChanged(
{
m_namespaceMetadataStates.Clear();
}

if (node is NamespacesState serverNamespacesNode)
{
IList<BaseInstanceState> children = [];
serverNamespacesNode.GetChildren(context, children);

foreach (BaseInstanceState child in children)
{
if (child is NamespaceMetadataState metadataState)
{
SubscribeToNamespaceDefaultPermissions(metadataState);
}
}
}
}
catch
{
Expand All @@ -1242,6 +1301,64 @@ private void ServerNamespacesChanged(
}
}

/// <summary>
/// Subscribes to the <c>StateChanged</c> events of the <c>DefaultRolePermissions</c>
/// and <c>DefaultUserRolePermissions</c> child nodes of a <see cref="NamespaceMetadataState"/>
/// to detect changes that require permission cache invalidation.
/// </summary>
private void SubscribeToNamespaceDefaultPermissions(NamespaceMetadataState namespaceMetadataState)
{
if (namespaceMetadataState.DefaultRolePermissions != null)
{
// unsubscribe first to avoid duplicate subscriptions if called multiple times
namespaceMetadataState.DefaultRolePermissions.StateChanged -= OnNamespaceDefaultPermissionsChanged;
namespaceMetadataState.DefaultRolePermissions.StateChanged += OnNamespaceDefaultPermissionsChanged;
}

if (namespaceMetadataState.DefaultUserRolePermissions != null)
{
namespaceMetadataState.DefaultUserRolePermissions.StateChanged -= OnNamespaceDefaultPermissionsChanged;
namespaceMetadataState.DefaultUserRolePermissions.StateChanged += OnNamespaceDefaultPermissionsChanged;
}

namespaceMetadataState.StateChanged -= OnNamespaceChildrenChanged;
namespaceMetadataState.StateChanged += OnNamespaceChildrenChanged;
}

/// <summary>
/// Handles children change on NamespaceMetadataState and resubscribes to the default permissions nodes
/// to ensure we are notified of changes on those nodes even if they are recreated.
/// </summary>
private void OnNamespaceChildrenChanged(
ISystemContext context,
NodeState node,
NodeStateChangeMasks changes)
{
if ((changes & NodeStateChangeMasks.Children) != 0 &&
node is NamespaceMetadataState namespaceMetadataState)
{
SubscribeToNamespaceDefaultPermissions(namespaceMetadataState);
}
}

/// <summary>
/// Handles value changes on <c>DefaultRolePermissions</c> or <c>DefaultUserRolePermissions</c>
/// and raises the <see cref="DefaultPermissionsChanged"/> event.
/// </summary>
private void OnNamespaceDefaultPermissionsChanged(
ISystemContext context,
NodeState node,
NodeStateChangeMasks changes)
{
if ((changes & NodeStateChangeMasks.Value) != 0)
{
DefaultPermissionsChanged?.Invoke(this, EventArgs.Empty);
}
}

/// <inheritdoc/>
public event EventHandler DefaultPermissionsChanged;

private class UpdateCertificateData
{
public NodeId SessionId { get; set; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,22 @@
* http://opcfoundation.org/License/MIT/1.00/
* ======================================================================*/

using System;

namespace Opc.Ua.Server
{
/// <summary>
/// The Server Configuration Node Manager.
/// </summary>
public interface IConfigurationNodeManager : INodeManager3
{
/// <summary>
/// Raised when the <c>DefaultRolePermissions</c> or <c>DefaultUserRolePermissions</c>
/// property of any <see cref="NamespaceMetadataState"/> node changes.
/// Subscribers should invalidate any cached role-permission validation results.
/// </summary>
event EventHandler DefaultPermissionsChanged;

/// <summary>
/// Gets or creates the <see cref="NamespaceMetadataState"/> node for the specified NamespaceUri.
/// </summary>
Expand Down
85 changes: 61 additions & 24 deletions Libraries/Opc.Ua.Server/NodeManager/MonitoredItem/MonitoredNode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -107,9 +107,16 @@ public bool HasMonitoredItems
/// <param name="datachangeItem">The monitored item.</param>
public void Add(IDataChangeMonitoredItem2 datachangeItem)
{
bool wasEmpty = DataChangeMonitoredItems.IsEmpty;
DataChangeMonitoredItems.TryAdd(datachangeItem.Id, datachangeItem);

Node.OnStateChanged = OnMonitoredNodeChanged;

// Subscribe to namespace default permission changes when the first item is added.
if (wasEmpty && m_server.ConfigurationNodeManager != null)
{
m_server.ConfigurationNodeManager.DefaultPermissionsChanged += OnDefaultPermissionsChanged;
}
}

/// <summary>
Expand All @@ -122,12 +129,18 @@ public void Remove(IDataChangeMonitoredItem2 datachangeItem)
{
// Remove the cached context for the monitored item
m_contextCache.TryRemove(datachangeItem.Id, out _);
m_operationContextCache.TryRemove(datachangeItem.Id, out _);
m_permissionCache.TryRemove(datachangeItem.Id, out _);
}

if (DataChangeMonitoredItems.IsEmpty)
{
Node.OnStateChanged = null;

// Unsubscribe from namespace default permission changes when the last item is removed.
if (m_server.ConfigurationNodeManager != null)
{
m_server.ConfigurationNodeManager.DefaultPermissionsChanged -= OnDefaultPermissionsChanged;
}
}
}

Expand Down Expand Up @@ -234,39 +247,60 @@ public void OnMonitoredNodeChanged(
return;
}

// If RolePermissions or UserRolePermissions have changed, invalidate the permission cache
// so it is revalidated on the next value change notification.
if ((changes & NodeStateChangeMasks.RolePermissions) != 0)
{
m_permissionCache.Clear();
}

foreach (KeyValuePair<uint, IDataChangeMonitoredItem2> kvp in DataChangeMonitoredItems)
{
IDataChangeMonitoredItem2 monitoredItem = kvp.Value;
OperationContext operationContext;
ISystemContext contextToUse;

if (context is ServerSystemContext serverContext)
{
ServerSystemContext serverSystemContextToUse = GetOrCreateContext(serverContext, monitoredItem);
operationContext = serverSystemContextToUse.OperationContext;
contextToUse = serverSystemContextToUse;
}
else
{
operationContext = new OperationContext(monitoredItem);
contextToUse = context;
}

if (monitoredItem.AttributeId == Attributes.Value &&
(changes & NodeStateChangeMasks.Value) != 0)
{
if (!m_operationContextCache.TryGetValue(monitoredItem.Id, out OperationContext operationContext))
// Use cached permission result to avoid validating on every value change.
// The cache is invalidated when RolePermissions/UserRolePermissions change
// or when the user identity of the monitored item changes.
if (!m_permissionCache.TryGetValue(monitoredItem.Id, out ServiceResult validationResult))
{
operationContext = new OperationContext(monitoredItem);
m_operationContextCache[monitoredItem.Id] = operationContext;
validationResult = NodeManager.ValidateRolePermissions(
operationContext,
node.NodeId,
PermissionType.Read);
m_permissionCache[monitoredItem.Id] = validationResult;
}

// validate if the monitored item has the required role permissions to read the value
ServiceResult validationResult = NodeManager.ValidateRolePermissions(
operationContext,
node.NodeId,
PermissionType.Read);

if (ServiceResult.IsBad(validationResult))
{
// skip if the monitored item does not have permission to read
continue;
}

QueueValue(context, node, monitoredItem);
QueueValue(contextToUse, node, monitoredItem);
continue;
}

if (monitoredItem.AttributeId != Attributes.Value &&
(changes & NodeStateChangeMasks.NonValue) != 0)
{
QueueValue(context, node, monitoredItem);
QueueValue(contextToUse, node, monitoredItem);
}
}
}
Expand All @@ -287,16 +321,8 @@ public void QueueValue(
SourceTimestamp = DateTime.MinValue,
StatusCode = StatusCodes.Good
};

ISystemContext contextToUse = context;

if (context is ServerSystemContext systemContext)
{
contextToUse = GetOrCreateContext(systemContext, monitoredItem);
}

ServiceResult error = node.ReadAttribute(
contextToUse,
context,
monitoredItem.AttributeId,
monitoredItem.IndexRange,
monitoredItem.DataEncoding,
Expand Down Expand Up @@ -336,12 +362,14 @@ private ServerSystemContext GetOrCreateContext(
(currentTicks - cachedEntry.CreatedAtTicks) > m_cacheLifetimeTicks)
{
operationContext = new OperationContext(monitoredItem);
m_operationContextCache[monitoredItemId] = operationContext;

ServerSystemContext updatedContext = context.Copy(
operationContext);
m_contextCache[monitoredItemId] = (updatedContext, currentTicks);

// Invalidate the permission cache since the user identity may have changed.
m_permissionCache.TryRemove(monitoredItemId, out _);

return updatedContext;
}

Expand All @@ -352,15 +380,24 @@ private ServerSystemContext GetOrCreateContext(
operationContext = new OperationContext(monitoredItem);
ServerSystemContext newContext = context.Copy(operationContext);
m_contextCache.TryAdd(monitoredItemId, (newContext, currentTicks));
m_operationContextCache[monitoredItemId] = operationContext;

return newContext;
}

/// <summary>
/// Called when the namespace default permissions (<c>DefaultRolePermissions</c> or
/// <c>DefaultUserRolePermissions</c>) change. Invalidates the entire permission cache
/// so that all entries are re-validated on the next value change.
/// </summary>
private void OnDefaultPermissionsChanged(object sender, EventArgs e)
{
m_permissionCache.Clear();
}

private readonly ConcurrentDictionary<uint, (ServerSystemContext Context, int CreatedAtTicks)> m_contextCache =
new();

private readonly ConcurrentDictionary<uint, OperationContext> m_operationContextCache =
private readonly ConcurrentDictionary<uint, ServiceResult> m_permissionCache =
new();

private readonly int m_cacheLifetimeTicks = (int)TimeSpan.FromMinutes(5).TotalMilliseconds;
Expand Down
13 changes: 10 additions & 3 deletions Stack/Opc.Ua.Types/State/NodeState.cs
Original file line number Diff line number Diff line change
Expand Up @@ -553,7 +553,7 @@ public RolePermissionTypeCollection RolePermissions
{
if (m_rolePermissions != value)
{
m_changeMasks |= NodeStateChangeMasks.NonValue;
m_changeMasks |= NodeStateChangeMasks.NonValue | NodeStateChangeMasks.RolePermissions;
}

m_rolePermissions = value;
Expand All @@ -571,7 +571,7 @@ public RolePermissionTypeCollection UserRolePermissions
{
if (m_userRolePermissions != value)
{
m_changeMasks |= NodeStateChangeMasks.NonValue;
m_changeMasks |= NodeStateChangeMasks.NonValue | NodeStateChangeMasks.RolePermissions;
}

m_userRolePermissions = value;
Expand Down Expand Up @@ -4207,6 +4207,7 @@ NodeAttributeEventHandler<AttributeWriteMask> onWriteUserWriteMask
if (ServiceResult.IsGood(result))
{
m_rolePermissions = rolePermissions;
m_changeMasks |= NodeStateChangeMasks.NonValue | NodeStateChangeMasks.RolePermissions;
}

return result;
Expand Down Expand Up @@ -5206,7 +5207,13 @@ public enum NodeStateChangeMasks
/// <summary>
/// The node has been deleted.
/// </summary>
Deleted = 0x10
Deleted = 0x10,

/// <summary>
/// The RolePermissions or UserRolePermissions attribute has changed.
/// This is a subset of <see cref="NonValue"/> and is set in addition to it.
/// </summary>
RolePermissions = 0x20
}

/// <summary>
Expand Down
Loading
Loading