Skip to content

Commit 5ee0c26

Browse files
authored
Support by filtering endpoints against both, MessageSecurityMode and SecurityPolicyUri (#1247)
* Support by filtering endpoints against both, MessageSecurityMode and SecurityPolicyUri * Small cleanup * Nicer check
1 parent 47fd3fc commit 5ee0c26

File tree

5 files changed

+268
-17
lines changed

5 files changed

+268
-17
lines changed

modules/hivemq-edge-module-opcua/src/main/java/com/hivemq/edge/adapters/opcua/OpcUaClientConnection.java

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@
2424
import com.hivemq.edge.adapters.opcua.client.OpcUaClientConfigurator;
2525
import com.hivemq.edge.adapters.opcua.client.OpcUaEndpointFilter;
2626
import com.hivemq.edge.adapters.opcua.client.ParsedConfig;
27+
import com.hivemq.edge.adapters.opcua.config.MsgSecurityMode;
2728
import com.hivemq.edge.adapters.opcua.config.OpcUaSpecificAdapterConfig;
29+
import com.hivemq.edge.adapters.opcua.config.SecPolicy;
2830
import com.hivemq.edge.adapters.opcua.config.tag.OpcuaTag;
2931
import com.hivemq.edge.adapters.opcua.listeners.OpcUaServiceFaultListener;
3032
import com.hivemq.edge.adapters.opcua.listeners.OpcUaSessionActivityListener;
@@ -35,6 +37,7 @@
3537
import org.eclipse.milo.opcua.sdk.client.subscriptions.OpcUaSubscription;
3638
import org.eclipse.milo.opcua.stack.core.UaException;
3739
import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.UInteger;
40+
import org.eclipse.milo.opcua.stack.core.types.enumerated.MessageSecurityMode;
3841
import org.jetbrains.annotations.NotNull;
3942
import org.jetbrains.annotations.Nullable;
4043
import org.slf4j.Logger;
@@ -86,7 +89,31 @@ synchronized boolean start(final ParsedConfig parsedConfig) {
8689
final OpcUaClient client;
8790
final var faultListener = new OpcUaServiceFaultListener(protocolAdapterMetricsService, eventService, adapterId);
8891
final var activityListener = new OpcUaSessionActivityListener(protocolAdapterMetricsService, eventService, adapterId, protocolAdapterState);
89-
final var endpointFilter = new OpcUaEndpointFilter(adapterId, config.getSecurity().policy().getSecurityPolicy().getUri(), config);
92+
93+
// Determine preferred MessageSecurityMode with intelligent defaults
94+
final MessageSecurityMode preferredMode;
95+
final MsgSecurityMode configuredMode = config.getSecurity().messageSecurityMode();
96+
if (configuredMode != null) {
97+
// Explicitly configured mode
98+
preferredMode = configuredMode.getMiloMode();
99+
if (log.isDebugEnabled()) {
100+
log.debug("Using configured message security mode: {}", preferredMode);
101+
}
102+
} else {
103+
// Intelligent default based on security policy
104+
final SecPolicy policy = config.getSecurity().policy();
105+
if (policy == SecPolicy.NONE) {
106+
preferredMode = MessageSecurityMode.None;
107+
} else {
108+
// For all secure policies, prefer SignAndEncrypt (most secure)
109+
preferredMode = MessageSecurityMode.SignAndEncrypt;
110+
if (log.isDebugEnabled()) {
111+
log.debug("No message security mode configured, defaulting to SignAndEncrypt for policy {}", policy);
112+
}
113+
}
114+
}
115+
116+
final var endpointFilter = new OpcUaEndpointFilter(adapterId, config.getSecurity().policy().getSecurityPolicy().getUri(), preferredMode, config);
90117
try {
91118
client = OpcUaClient
92119
.create(

modules/hivemq-edge-module-opcua/src/main/java/com/hivemq/edge/adapters/opcua/client/OpcUaEndpointFilter.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,11 @@
1818
import com.hivemq.edge.adapters.opcua.config.OpcUaSpecificAdapterConfig;
1919
import com.hivemq.edge.adapters.opcua.config.SecPolicy;
2020
import org.eclipse.milo.opcua.stack.core.security.SecurityPolicy;
21+
import org.eclipse.milo.opcua.stack.core.types.enumerated.MessageSecurityMode;
2122
import org.eclipse.milo.opcua.stack.core.types.structured.EndpointDescription;
2223
import org.eclipse.milo.opcua.stack.core.util.EndpointUtil;
2324
import org.jetbrains.annotations.NotNull;
25+
import org.jetbrains.annotations.Nullable;
2426
import org.slf4j.Logger;
2527
import org.slf4j.LoggerFactory;
2628

@@ -33,24 +35,42 @@ public class OpcUaEndpointFilter implements Function<List<EndpointDescription>,
3335

3436
private final @NotNull String adapterId;
3537
private final @NotNull String configPolicyUri;
38+
private final @Nullable MessageSecurityMode preferredMode;
3639
private final @NotNull OpcUaSpecificAdapterConfig adapterConfig;
3740

3841
public OpcUaEndpointFilter(
3942
final @NotNull String adapterId,
4043
final @NotNull String configPolicyUri,
44+
final @Nullable MessageSecurityMode preferredMode,
4145
final @NotNull OpcUaSpecificAdapterConfig adapterConfig) {
4246
this.adapterId = adapterId;
4347
this.configPolicyUri = configPolicyUri;
48+
this.preferredMode = preferredMode;
4449
this.adapterConfig = adapterConfig;
4550
}
4651

4752
@Override
4853
public @NotNull Optional<EndpointDescription> apply(final List<EndpointDescription> endpointDescriptions) {
4954
return endpointDescriptions.stream().filter(endpointDescription -> {
5055
final String policyUri = endpointDescription.getSecurityPolicyUri();
56+
57+
// Filter by SecurityPolicyUri
5158
if (!configPolicyUri.equals(policyUri)) {
5259
return false;
5360
}
61+
62+
// Filter by MessageSecurityMode if specified
63+
if (preferredMode != null) {
64+
final MessageSecurityMode endpointMode = endpointDescription.getSecurityMode();
65+
if (!preferredMode.equals(endpointMode)) {
66+
if (log.isDebugEnabled()) {
67+
log.debug("Endpoint {} has mode {} but preferred mode is {}, skipping",
68+
endpointDescription.getEndpointUrl(), endpointMode, preferredMode);
69+
}
70+
return false;
71+
}
72+
}
73+
5474
if (policyUri.equals(SecurityPolicy.None.getUri())) {
5575
return true;
5676
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/*
2+
* Copyright 2023-present HiveMQ GmbH
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.hivemq.edge.adapters.opcua.config;
17+
18+
import com.fasterxml.jackson.annotation.JsonCreator;
19+
import com.fasterxml.jackson.annotation.JsonProperty;
20+
import org.eclipse.milo.opcua.stack.core.types.enumerated.MessageSecurityMode;
21+
import org.jetbrains.annotations.NotNull;
22+
import org.jetbrains.annotations.Nullable;
23+
24+
/**
25+
* OPC UA Message Security Mode configuration enum.
26+
* Maps to Eclipse Milo's MessageSecurityMode.
27+
*/
28+
public enum MsgSecurityMode {
29+
30+
@JsonProperty("None")
31+
NONE(MessageSecurityMode.None),
32+
33+
@JsonProperty("Sign")
34+
SIGN(MessageSecurityMode.Sign),
35+
36+
@JsonProperty("SignAndEncrypt")
37+
SIGN_AND_ENCRYPT(MessageSecurityMode.SignAndEncrypt);
38+
39+
private final @NotNull MessageSecurityMode miloMode;
40+
41+
MsgSecurityMode(final @NotNull MessageSecurityMode miloMode) {
42+
this.miloMode = miloMode;
43+
}
44+
45+
/**
46+
* @return the corresponding Eclipse Milo MessageSecurityMode
47+
*/
48+
public @NotNull MessageSecurityMode getMiloMode() {
49+
return miloMode;
50+
}
51+
52+
/**
53+
* Find the MsgSecurityMode enum for a given Milo MessageSecurityMode.
54+
*
55+
* @param mode the Milo MessageSecurityMode
56+
* @return the corresponding MsgSecurityMode, or null if not found
57+
*/
58+
public static @Nullable MsgSecurityMode forMiloMode(final @NotNull MessageSecurityMode mode) {
59+
for (final var value : values()) {
60+
if (value.miloMode == mode) {
61+
return value;
62+
}
63+
}
64+
return null;
65+
}
66+
67+
/**
68+
* Jackson creator method for deserialization.
69+
*
70+
* @param value the string value from JSON
71+
* @return the corresponding MsgSecurityMode
72+
*/
73+
@JsonCreator
74+
public static @Nullable MsgSecurityMode fromString(final @Nullable String value) {
75+
if (value == null || value.isBlank()) {
76+
return null;
77+
}
78+
for (final var mode : values()) {
79+
if (mode.name().equalsIgnoreCase(value) ||
80+
mode.name().replace("_", "").equalsIgnoreCase(value)) {
81+
return mode;
82+
}
83+
}
84+
return null;
85+
}
86+
}

modules/hivemq-edge-module-opcua/src/main/java/com/hivemq/edge/adapters/opcua/config/Security.java

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616
package com.hivemq.edge.adapters.opcua.config;
1717

18+
import com.fasterxml.jackson.annotation.JsonInclude;
1819
import com.fasterxml.jackson.annotation.JsonProperty;
1920
import com.fasterxml.jackson.core.JsonParser;
2021
import com.fasterxml.jackson.databind.DeserializationContext;
@@ -32,10 +33,22 @@
3233
@JsonDeserialize(using = Security.SecurityDeserializer.class)
3334
public record Security(@JsonProperty("policy") @ModuleConfigField(title = "OPC UA security policy",
3435
description = "Security policy to use for communication with the server.",
35-
defaultValue = "NONE") @NotNull SecPolicy policy) {
36+
defaultValue = "NONE") @NotNull SecPolicy policy,
37+
@JsonProperty("messageSecurityMode")
38+
@JsonInclude(JsonInclude.Include.NON_NULL)
39+
@ModuleConfigField(title = "Message Security Mode",
40+
description = "Message security mode (None, Sign, SignAndEncrypt). If not specified, defaults based on policy: None→None, others→SignAndEncrypt.",
41+
required = false) @Nullable MsgSecurityMode messageSecurityMode) {
3642

37-
public Security(@JsonProperty("policy") final @Nullable SecPolicy policy) {
43+
public Security(@JsonProperty("policy") final @Nullable SecPolicy policy,
44+
@JsonProperty("messageSecurityMode") final @Nullable MsgSecurityMode messageSecurityMode) {
3845
this.policy = Objects.requireNonNullElse(policy, Constants.DEFAULT_SECURITY_POLICY);
46+
this.messageSecurityMode = messageSecurityMode;
47+
}
48+
49+
// Backwards compatibility constructor
50+
public Security(final @Nullable SecPolicy policy) {
51+
this(policy, null);
3952
}
4053

4154
@Override
@@ -50,13 +63,13 @@ static class SecurityDeserializer extends JsonDeserializer<Security> {
5063
final @NotNull DeserializationContext context) throws IOException {
5164
final String text = parser.getText();
5265
if (text != null && text.isEmpty()) {
53-
return new Security(Constants.DEFAULT_SECURITY_POLICY);
66+
return new Security(Constants.DEFAULT_SECURITY_POLICY, null);
5467
}
5568

5669
try {
5770
final Map<String, Object> map = parser.readValueAs(Map.class);
5871
if (map == null || map.isEmpty()) {
59-
return new Security(Constants.DEFAULT_SECURITY_POLICY);
72+
return new Security(Constants.DEFAULT_SECURITY_POLICY, null);
6073
}
6174

6275
final Object policyValue = map.get("policy");
@@ -66,9 +79,18 @@ static class SecurityDeserializer extends JsonDeserializer<Security> {
6679
} else {
6780
policy = Constants.DEFAULT_SECURITY_POLICY;
6881
}
69-
return new Security(policy);
82+
83+
final Object modeValue = map.get("messageSecurityMode");
84+
final MsgSecurityMode messageSecurityMode;
85+
if (modeValue instanceof String) {
86+
messageSecurityMode = MsgSecurityMode.fromString((String) modeValue);
87+
} else {
88+
messageSecurityMode = null;
89+
}
90+
91+
return new Security(policy, messageSecurityMode);
7092
} catch (final IOException e) {
71-
return new Security(Constants.DEFAULT_SECURITY_POLICY);
93+
return new Security(Constants.DEFAULT_SECURITY_POLICY, null);
7294
}
7395
}
7496
}

0 commit comments

Comments
 (0)