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
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ScheduledExecutorService;
import java.util.function.Consumer;
import org.eclipse.milo.opcua.sdk.server.diagnostics.SessionSecurityDiagnosticsAccessMode;
import org.eclipse.milo.opcua.sdk.server.identity.AnonymousIdentityValidator;
import org.eclipse.milo.opcua.sdk.server.identity.CompositeValidator;
import org.eclipse.milo.opcua.sdk.server.identity.IdentityValidator;
Expand Down Expand Up @@ -117,6 +118,15 @@ public interface OpcUaServerConfig {

Optional<RoleMapper> getRoleMapper();

/**
* Get the authorization mode for Session security diagnostics and the diagnostics enabled flag.
*
* @return the configured access mode.
*/
default SessionSecurityDiagnosticsAccessMode getSessionSecurityDiagnosticsAccessMode() {
return SessionSecurityDiagnosticsAccessMode.RESTRICTED;
}

/**
* Get the {@link SecurityKeysListener} to be notified when symmetric security keys are derived
* during OpenSecureChannel handshakes.
Expand Down Expand Up @@ -165,6 +175,8 @@ static OpcUaServerConfigBuilder copy(OpcUaServerConfig config) {
builder.setIdentityValidator(config.getIdentityValidator());
builder.setCertificateManager(config.getCertificateManager());
config.getRoleMapper().ifPresent(builder::setRoleMapper);
builder.setSessionSecurityDiagnosticsAccessMode(
config.getSessionSecurityDiagnosticsAccessMode());
builder.setExecutor(config.getExecutor());
builder.setScheduledExecutor(config.getScheduledExecutorService());
config.getSecurityKeysListener().ifPresent(builder::setSecurityKeysListener);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,14 @@

package org.eclipse.milo.opcua.sdk.server;

import static java.util.Objects.requireNonNull;

import java.util.HashSet;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ScheduledExecutorService;
import org.eclipse.milo.opcua.sdk.server.diagnostics.SessionSecurityDiagnosticsAccessMode;
import org.eclipse.milo.opcua.sdk.server.identity.AnonymousIdentityValidator;
import org.eclipse.milo.opcua.sdk.server.identity.IdentityValidator;
import org.eclipse.milo.opcua.stack.core.Stack;
Expand Down Expand Up @@ -49,6 +52,9 @@ public class OpcUaServerConfigBuilder {

private RoleMapper roleMapper;

private SessionSecurityDiagnosticsAccessMode sessionSecurityDiagnosticsAccessMode =
SessionSecurityDiagnosticsAccessMode.RESTRICTED;

private @Nullable SecurityKeysListener securityKeysListener;

private ExecutorService executor;
Expand Down Expand Up @@ -104,6 +110,19 @@ public OpcUaServerConfigBuilder setRoleMapper(RoleMapper roleMapper) {
return this;
}

/**
* Set the authorization mode for Session security diagnostics and the diagnostics enabled flag.
*
* @param accessMode the authorization mode.
* @return this builder.
*/
public OpcUaServerConfigBuilder setSessionSecurityDiagnosticsAccessMode(
SessionSecurityDiagnosticsAccessMode accessMode) {

this.sessionSecurityDiagnosticsAccessMode = requireNonNull(accessMode);
return this;
}

public OpcUaServerConfigBuilder setSecurityKeysListener(
@Nullable SecurityKeysListener securityKeysListener) {
this.securityKeysListener = securityKeysListener;
Expand Down Expand Up @@ -139,6 +158,7 @@ public OpcUaServerConfig build() {
limits,
certificateManager,
roleMapper,
sessionSecurityDiagnosticsAccessMode,
securityKeysListener,
executor,
scheduledExecutor);
Expand All @@ -156,6 +176,7 @@ public static final class OpcUaServerConfigImpl implements OpcUaServerConfig {
private final OpcUaServerConfigLimits limits;
private final CertificateManager certificateManager;
private final RoleMapper roleMapper;
private final SessionSecurityDiagnosticsAccessMode sessionSecurityDiagnosticsAccessMode;
private final @Nullable SecurityKeysListener securityKeysListener;
private final ExecutorService executor;
private final ScheduledExecutorService scheduledExecutorService;
Expand All @@ -175,6 +196,39 @@ public OpcUaServerConfigImpl(
ExecutorService executor,
ScheduledExecutorService scheduledExecutorService) {

this(
endpoints,
applicationName,
applicationUri,
productUri,
buildInfo,
identityValidator,
encodingLimits,
limits,
certificateManager,
roleMapper,
SessionSecurityDiagnosticsAccessMode.RESTRICTED,
securityKeysListener,
executor,
scheduledExecutorService);
}

public OpcUaServerConfigImpl(
Set<EndpointConfig> endpoints,
LocalizedText applicationName,
String applicationUri,
String productUri,
BuildInfo buildInfo,
IdentityValidator identityValidator,
EncodingLimits encodingLimits,
OpcUaServerConfigLimits limits,
CertificateManager certificateManager,
RoleMapper roleMapper,
SessionSecurityDiagnosticsAccessMode sessionSecurityDiagnosticsAccessMode,
@Nullable SecurityKeysListener securityKeysListener,
ExecutorService executor,
ScheduledExecutorService scheduledExecutorService) {

this.endpoints = endpoints;
this.applicationName = applicationName;
this.applicationUri = applicationUri;
Expand All @@ -185,6 +239,8 @@ public OpcUaServerConfigImpl(
this.limits = limits;
this.certificateManager = certificateManager;
this.roleMapper = roleMapper;
this.sessionSecurityDiagnosticsAccessMode =
requireNonNull(sessionSecurityDiagnosticsAccessMode);
this.securityKeysListener = securityKeysListener;
this.executor = executor;
this.scheduledExecutorService = scheduledExecutorService;
Expand Down Expand Up @@ -240,6 +296,11 @@ public Optional<RoleMapper> getRoleMapper() {
return Optional.ofNullable(roleMapper);
}

@Override
public SessionSecurityDiagnosticsAccessMode getSessionSecurityDiagnosticsAccessMode() {
return sessionSecurityDiagnosticsAccessMode;
}

@Override
public Optional<SecurityKeysListener> getSecurityKeysListener() {
return Optional.ofNullable(securityKeysListener);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* Copyright (c) 2026 the Eclipse Milo Authors
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/

package org.eclipse.milo.opcua.sdk.server.diagnostics;

/** Controls authorization for Session security diagnostics and the diagnostics enabled flag. */
public enum SessionSecurityDiagnosticsAccessMode {

/**
* Derive access from the standard diagnostics nodes' RolePermissions. Session security
* diagnostics are readable only by SecurityAdmin or an explicitly configured equivalent role,
* while diagnostics may be enabled or disabled only by ConfigureAdmin, SecurityAdmin, or an
* explicitly configured equivalent role.
*/
RESTRICTED,

/**
* Preserve the previous authorization behavior without requiring role mapping. All Sessions may
* read Session security diagnostics and enable or disable diagnostics.
*
* <p>AccessRestrictions, including signing and encryption requirements, remain enforced. This
* mode is provided for compatibility and is less secure than {@link #RESTRICTED}.
*/
LEGACY
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,29 @@
import org.eclipse.milo.opcua.sdk.server.diagnostics.variables.SessionSecurityDiagnosticsVariableArray;
import org.eclipse.milo.opcua.sdk.server.model.objects.SessionDiagnosticsObjectTypeNode;
import org.eclipse.milo.opcua.sdk.server.model.objects.SessionsDiagnosticsSummaryTypeNode;
import org.eclipse.milo.opcua.sdk.server.model.variables.SessionSecurityDiagnosticsArrayTypeNode;
import org.eclipse.milo.opcua.sdk.server.model.variables.SessionSecurityDiagnosticsTypeNode;
import org.eclipse.milo.opcua.sdk.server.nodes.UaNode;
import org.eclipse.milo.opcua.sdk.server.nodes.UaNodeContext;
import org.eclipse.milo.opcua.sdk.server.nodes.UaVariableNode;
import org.eclipse.milo.opcua.sdk.server.nodes.factories.NodeFactory;
import org.eclipse.milo.opcua.stack.core.NodeIds;
import org.eclipse.milo.opcua.stack.core.UaException;
import org.eclipse.milo.opcua.stack.core.types.builtin.LocalizedText;
import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId;
import org.eclipse.milo.opcua.stack.core.types.builtin.QualifiedName;
import org.jspecify.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* Manages the standard diagnostics arrays and the per-Session diagnostics Objects beneath the
* server's SessionsDiagnosticsSummary node.
*
* <p>Ordinary session diagnostics and security diagnostics share a lifecycle but retain distinct
* authorization policies. Security metadata is propagated only to each dynamically instantiated
* security diagnostics subtree.
*/
public class SessionsDiagnosticsSummaryObject extends AbstractLifecycle {

static final int MAX_BROWSE_NAME_LENGTH = 512;
Expand Down Expand Up @@ -125,7 +137,9 @@ private void createSessionDiagnosticsObject(Session session) {
SessionDiagnosticsObjectTypeNode sdoNode =
(SessionDiagnosticsObjectTypeNode)
nodeFactory.createNode(
new NodeId(1, UUID.randomUUID()), NodeIds.SessionDiagnosticsObjectType);
new NodeId(1, UUID.randomUUID()),
NodeIds.SessionDiagnosticsObjectType,
securityDiagnosticsAccessControl(node));
sdoNode.setBrowseName(browseName);
sdoNode.setDisplayName(LocalizedText.english(sessionName));

Expand All @@ -147,6 +161,36 @@ private void createSessionDiagnosticsObject(Session session) {
}
}

/**
* Creates a callback that applies the standard security diagnostics array's access policy only to
* the security diagnostics subtree of a dynamically instantiated Session diagnostics Object.
*
* @param summaryNode the standard summary node containing the security diagnostics array.
* @return a callback that applies security attributes to security diagnostics Variables.
*/
static NodeFactory.InstantiationCallback securityDiagnosticsAccessControl(
SessionsDiagnosticsSummaryTypeNode summaryNode) {

return new NodeFactory.InstantiationCallback() {
@Override
public void onVariableAdded(
@Nullable UaNode parent, UaVariableNode instance, NodeId typeDefinitionId) {

if (instance instanceof SessionSecurityDiagnosticsTypeNode
|| parent instanceof SessionSecurityDiagnosticsTypeNode) {

// Ordinary SessionDiagnostics intentionally retain their separate standard permissions.
SessionSecurityDiagnosticsArrayTypeNode securityArray =
summaryNode.getSessionSecurityDiagnosticsArrayNode();

instance.setRolePermissions(securityArray.getRolePermissions());
instance.setUserRolePermissions(securityArray.getUserRolePermissions());
instance.setAccessRestrictions(securityArray.getAccessRestrictions());
}
}
};
}

static String sessionNameBrowseName(String sessionName) {
if (sessionName != null && sessionName.length() > MAX_BROWSE_NAME_LENGTH) {
return sessionName.substring(0, MAX_BROWSE_NAME_LENGTH);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,16 @@
package org.eclipse.milo.opcua.sdk.server.diagnostics.variables;

import static org.eclipse.milo.opcua.sdk.server.diagnostics.variables.Util.diagnosticValueFilter;
import static org.eclipse.milo.opcua.sdk.server.diagnostics.variables.Util.roleBasedUserAccessLevelFilter;
import static org.eclipse.milo.opcua.sdk.server.diagnostics.variables.Util.roleBasedUserRolePermissionsFilter;

import java.util.List;
import java.util.NoSuchElementException;
import java.util.concurrent.atomic.AtomicBoolean;
import org.eclipse.milo.opcua.sdk.server.AbstractLifecycle;
import org.eclipse.milo.opcua.sdk.server.OpcUaServer;
import org.eclipse.milo.opcua.sdk.server.Session;
import org.eclipse.milo.opcua.sdk.server.diagnostics.SessionSecurityDiagnosticsAccessMode;
import org.eclipse.milo.opcua.sdk.server.model.objects.ServerDiagnosticsTypeNode;
import org.eclipse.milo.opcua.sdk.server.model.variables.SessionSecurityDiagnosticsTypeNode;
import org.eclipse.milo.opcua.sdk.server.nodes.AttributeObserver;
Expand All @@ -29,6 +33,14 @@
import org.eclipse.milo.opcua.stack.core.types.builtin.Variant;
import org.eclipse.milo.opcua.stack.core.types.enumerated.MessageSecurityMode;

/**
* Publishes the security diagnostics for one Session through a dynamically created diagnostics
* Variable.
*
* <p>The Variable and each of its fields follow the server-wide diagnostics enabled flag. In
* restricted access mode, Session-specific access attributes are derived from the role metadata
* copied onto the nodes when they are instantiated.
*/
public class SessionSecurityDiagnosticsVariable extends AbstractLifecycle {

private final AtomicBoolean diagnosticsEnabled = new AtomicBoolean(false);
Expand Down Expand Up @@ -69,6 +81,30 @@ protected void onStartup() {

diagnosticsEnabled.set(diagnosticsNode.getEnabledFlag());

if (server.getConfig().getSessionSecurityDiagnosticsAccessMode()
== SessionSecurityDiagnosticsAccessMode.RESTRICTED) {

// Value access is reflected in UserAccessLevel; UserRolePermissions supplies the same
// session-specific policy to authorization checks for other node operations such as Browse.
List.of(
node,
node.getSessionIdNode(),
node.getClientUserIdOfSessionNode(),
node.getClientUserIdHistoryNode(),
node.getAuthenticationMechanismNode(),
node.getEncodingNode(),
node.getTransportProtocolNode(),
node.getSecurityModeNode(),
node.getSecurityPolicyUriNode(),
node.getClientCertificateNode())
.forEach(
securityNode ->
securityNode
.getFilterChain()
.addLast(
roleBasedUserAccessLevelFilter(), roleBasedUserRolePermissionsFilter()));
}

attributeObserver =
(node, attributeId, value) -> {
if (attributeId == AttributeId.Value) {
Expand Down
Loading
Loading