Skip to content

Commit 55ddcdc

Browse files
authored
Make sure nullable attributes security and auth bi-directional adapters are considered null when no structure is defined (#1206)
* Make sure nullable attributes security and auth bi-directional adapters are considered null when no structure is defined
1 parent 9c47a1c commit 55ddcdc

File tree

4 files changed

+293
-0
lines changed

4 files changed

+293
-0
lines changed

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

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,19 @@
1717

1818
import com.fasterxml.jackson.annotation.JsonCreator;
1919
import com.fasterxml.jackson.annotation.JsonProperty;
20+
import com.fasterxml.jackson.core.JsonParser;
21+
import com.fasterxml.jackson.databind.DeserializationContext;
22+
import com.fasterxml.jackson.databind.JsonDeserializer;
23+
import com.fasterxml.jackson.databind.ObjectMapper;
24+
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
2025
import com.hivemq.adapter.sdk.api.annotations.ModuleConfigField;
26+
import org.jetbrains.annotations.NotNull;
2127
import org.jetbrains.annotations.Nullable;
2228

29+
import java.io.IOException;
30+
import java.util.Map;
31+
32+
@JsonDeserialize(using = Auth.AuthDeserializer.class)
2333
public record Auth(@JsonProperty("basic") @ModuleConfigField(title = "Basic Authentication",
2434
description = "Username / password based authentication") @Nullable BasicAuth basicAuth,
2535
@JsonProperty("x509") @ModuleConfigField(title = "X509 Authentication",
@@ -29,6 +39,14 @@ public record Auth(@JsonProperty("basic") @ModuleConfigField(title = "Basic Auth
2939
public Auth {
3040
}
3141

42+
private static <T> @Nullable T fetch(
43+
final @NotNull Map<String, Object> map,
44+
final @NotNull String key,
45+
final @NotNull Class<T> clazz,
46+
final @NotNull ObjectMapper mapper) {
47+
return map.containsKey(key) ? mapper.convertValue(map.get(key), clazz) : null;
48+
}
49+
3250
@Override
3351
public @Nullable BasicAuth basicAuth() {
3452
return basicAuth;
@@ -38,4 +56,29 @@ public record Auth(@JsonProperty("basic") @ModuleConfigField(title = "Basic Auth
3856
public @Nullable X509Auth x509Auth() {
3957
return x509Auth;
4058
}
59+
60+
static class AuthDeserializer extends JsonDeserializer<Auth> {
61+
@Override
62+
public @NotNull Auth deserialize(final @NotNull JsonParser parser, final @NotNull DeserializationContext context)
63+
throws IOException {
64+
final String text = parser.getText();
65+
if (text != null && text.isEmpty()) {
66+
return new Auth(null, null);
67+
}
68+
69+
try {
70+
final Map<String, Object> map = parser.readValueAs(Map.class);
71+
if (map == null || map.isEmpty()) {
72+
return new Auth(null, null);
73+
}
74+
75+
final ObjectMapper mapper = (ObjectMapper) parser.getCodec();
76+
final BasicAuth basicAuth = fetch(map, "basic", BasicAuth.class, mapper);
77+
final X509Auth x509Auth = fetch(map, "x509", X509Auth.class, mapper);
78+
return new Auth(basicAuth, x509Auth);
79+
} catch (final IOException e) {
80+
return new Auth(null, null);
81+
}
82+
}
83+
}
4184
}

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

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,20 @@
1616
package com.hivemq.edge.adapters.opcua.config;
1717

1818
import com.fasterxml.jackson.annotation.JsonProperty;
19+
import com.fasterxml.jackson.core.JsonParser;
20+
import com.fasterxml.jackson.databind.DeserializationContext;
21+
import com.fasterxml.jackson.databind.JsonDeserializer;
22+
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
1923
import com.hivemq.adapter.sdk.api.annotations.ModuleConfigField;
2024
import com.hivemq.edge.adapters.opcua.Constants;
2125
import org.jetbrains.annotations.NotNull;
2226
import org.jetbrains.annotations.Nullable;
2327

28+
import java.io.IOException;
29+
import java.util.Map;
2430
import java.util.Objects;
2531

32+
@JsonDeserialize(using = Security.SecurityDeserializer.class)
2633
public record Security(@JsonProperty("policy") @ModuleConfigField(title = "OPC UA security policy",
2734
description = "Security policy to use for communication with the server.",
2835
defaultValue = "NONE") @NotNull SecPolicy policy) {
@@ -35,4 +42,34 @@ public Security(@JsonProperty("policy") final @Nullable SecPolicy policy) {
3542
public @NotNull SecPolicy policy() {
3643
return policy;
3744
}
45+
46+
static class SecurityDeserializer extends JsonDeserializer<Security> {
47+
@Override
48+
public @NotNull Security deserialize(
49+
final @NotNull JsonParser parser,
50+
final @NotNull DeserializationContext context) throws IOException {
51+
final String text = parser.getText();
52+
if (text != null && text.isEmpty()) {
53+
return new Security(Constants.DEFAULT_SECURITY_POLICY);
54+
}
55+
56+
try {
57+
final Map<String, Object> map = parser.readValueAs(Map.class);
58+
if (map == null || map.isEmpty()) {
59+
return new Security(Constants.DEFAULT_SECURITY_POLICY);
60+
}
61+
62+
final Object policyValue = map.get("policy");
63+
final SecPolicy policy;
64+
if (policyValue instanceof String) {
65+
policy = SecPolicy.valueOf((String) policyValue);
66+
} else {
67+
policy = Constants.DEFAULT_SECURITY_POLICY;
68+
}
69+
return new Security(policy);
70+
} catch (final IOException e) {
71+
return new Security(Constants.DEFAULT_SECURITY_POLICY);
72+
}
73+
}
74+
}
3875
}
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
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.databind.ObjectMapper;
19+
import org.jetbrains.annotations.NotNull;
20+
import org.junit.jupiter.api.Test;
21+
22+
import java.util.HashMap;
23+
import java.util.Map;
24+
25+
import static org.assertj.core.api.Assertions.assertThat;
26+
27+
class AuthDeserializerTest {
28+
29+
private final @NotNull ObjectMapper mapper = new ObjectMapper();
30+
31+
@Test
32+
void deserialize_emptyString_returnsNullAuth() throws Exception {
33+
final String json = "\"\"";
34+
final Auth auth = mapper.readValue(json, Auth.class);
35+
assertThat(auth).isNotNull();
36+
assertThat(auth.basicAuth()).isNull();
37+
assertThat(auth.x509Auth()).isNull();
38+
}
39+
40+
@Test
41+
void deserialize_emptyMap_returnsNullAuth() throws Exception {
42+
final String json = "{}";
43+
final Auth auth = mapper.readValue(json, Auth.class);
44+
assertThat(auth).isNotNull();
45+
assertThat(auth.basicAuth()).isNull();
46+
assertThat(auth.x509Auth()).isNull();
47+
}
48+
49+
@Test
50+
void deserialize_basicAuthOnly_parsesCorrectly() throws Exception {
51+
final String json = "{\"basic\":{\"username\":\"testuser\",\"password\":\"testpass\"}}";
52+
final Auth auth = mapper.readValue(json, Auth.class);
53+
assertThat(auth).isNotNull();
54+
assertThat(auth.basicAuth()).isNotNull();
55+
assertThat(auth.basicAuth().username()).isEqualTo("testuser");
56+
assertThat(auth.basicAuth().password()).isEqualTo("testpass");
57+
assertThat(auth.x509Auth()).isNull();
58+
}
59+
60+
@Test
61+
void deserialize_x509AuthOnly_parsesCorrectly() throws Exception {
62+
final String json = "{\"x509\":{\"enabled\":true}}";
63+
final Auth auth = mapper.readValue(json, Auth.class);
64+
assertThat(auth).isNotNull();
65+
assertThat(auth.basicAuth()).isNull();
66+
assertThat(auth.x509Auth()).isNotNull();
67+
assertThat(auth.x509Auth().enabled()).isTrue();
68+
}
69+
70+
@Test
71+
void deserialize_bothAuthTypes_parsesCorrectly() throws Exception {
72+
final String json = "{\"basic\":{\"username\":\"user\",\"password\":\"pass\"},\"x509\":{\"enabled\":true}}";
73+
final Auth auth = mapper.readValue(json, Auth.class);
74+
assertThat(auth).isNotNull();
75+
assertThat(auth.basicAuth()).isNotNull();
76+
assertThat(auth.basicAuth().username()).isEqualTo("user");
77+
assertThat(auth.basicAuth().password()).isEqualTo("pass");
78+
assertThat(auth.x509Auth()).isNotNull();
79+
assertThat(auth.x509Auth().enabled()).isTrue();
80+
}
81+
82+
@Test
83+
void deserialize_mapWithUnrelatedFields_ignoresThemAndReturnsNullAuth() throws Exception {
84+
final String json = "{\"someOtherField\":\"value\"}";
85+
final Auth auth = mapper.readValue(json, Auth.class);
86+
assertThat(auth).isNotNull();
87+
assertThat(auth.basicAuth()).isNull();
88+
assertThat(auth.x509Auth()).isNull();
89+
}
90+
91+
@Test
92+
void convertValue_emptyString_returnsNullAuth() {
93+
final Map<String, Object> configMap = new HashMap<>();
94+
configMap.put("auth", "");
95+
final Auth auth = mapper.convertValue(configMap.get("auth"), Auth.class);
96+
assertThat(auth).isNotNull();
97+
assertThat(auth.basicAuth()).isNull();
98+
assertThat(auth.x509Auth()).isNull();
99+
}
100+
101+
@Test
102+
void deserialize_nullBasicAuth_parsesCorrectly() throws Exception {
103+
final String json = "{\"basic\":null,\"x509\":{\"enabled\":true}}";
104+
final Auth auth = mapper.readValue(json, Auth.class);
105+
assertThat(auth).isNotNull();
106+
assertThat(auth.basicAuth()).isNull();
107+
assertThat(auth.x509Auth()).isNotNull();
108+
assertThat(auth.x509Auth().enabled()).isTrue();
109+
}
110+
111+
@Test
112+
void deserialize_nullX509Auth_parsesCorrectly() throws Exception {
113+
final String json = "{\"basic\":{\"username\":\"user\",\"password\":\"pass\"},\"x509\":null}";
114+
final Auth auth = mapper.readValue(json, Auth.class);
115+
assertThat(auth).isNotNull();
116+
assertThat(auth.basicAuth()).isNotNull();
117+
assertThat(auth.basicAuth().username()).isEqualTo("user");
118+
assertThat(auth.x509Auth()).isNull();
119+
}
120+
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
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.databind.ObjectMapper;
19+
import com.hivemq.edge.adapters.opcua.Constants;
20+
import org.jetbrains.annotations.NotNull;
21+
import org.junit.jupiter.api.Test;
22+
23+
import java.util.HashMap;
24+
import java.util.Map;
25+
26+
import static org.assertj.core.api.Assertions.assertThat;
27+
28+
class SecurityDeserializerTest {
29+
30+
private final @NotNull ObjectMapper mapper = new ObjectMapper();
31+
32+
@Test
33+
void deserialize_emptyString_usesDefaultPolicy() throws Exception {
34+
final String json = "\"\"";
35+
final Security security = mapper.readValue(json, Security.class);
36+
assertThat(security).isNotNull();
37+
assertThat(security.policy()).isEqualTo(Constants.DEFAULT_SECURITY_POLICY);
38+
}
39+
40+
@Test
41+
void deserialize_emptyMap_usesDefaultPolicy() throws Exception {
42+
final String json = "{}";
43+
final Security security = mapper.readValue(json, Security.class);
44+
assertThat(security).isNotNull();
45+
assertThat(security.policy()).isEqualTo(Constants.DEFAULT_SECURITY_POLICY);
46+
}
47+
48+
@Test
49+
void deserialize_nullValue_usesDefaultPolicy() throws Exception {
50+
final Map<String, Object> map = new HashMap<>();
51+
map.put("security", null);
52+
final String json = mapper.writeValueAsString(map);
53+
final Map<String, Object> result = mapper.readValue(json, Map.class);
54+
final Security security = result.get("security") == null ?
55+
new Security(null) :
56+
mapper.convertValue(result.get("security"), Security.class);
57+
assertThat(security).isNotNull();
58+
assertThat(security.policy()).isEqualTo(Constants.DEFAULT_SECURITY_POLICY);
59+
}
60+
61+
@Test
62+
void deserialize_validPolicy_parsesCorrectly() throws Exception {
63+
final String json = "{\"policy\":\"BASIC128RSA15\"}";
64+
final Security security = mapper.readValue(json, Security.class);
65+
assertThat(security).isNotNull();
66+
assertThat(security.policy()).isEqualTo(SecPolicy.BASIC128RSA15);
67+
}
68+
69+
@Test
70+
void deserialize_nonePolicy_parsesCorrectly() throws Exception {
71+
final String json = "{\"policy\":\"NONE\"}";
72+
final Security security = mapper.readValue(json, Security.class);
73+
assertThat(security).isNotNull();
74+
assertThat(security.policy()).isEqualTo(SecPolicy.NONE);
75+
}
76+
77+
@Test
78+
void deserialize_mapWithoutPolicy_usesDefaultPolicy() throws Exception {
79+
final String json = "{\"someOtherField\":\"value\"}";
80+
final Security security = mapper.readValue(json, Security.class);
81+
assertThat(security).isNotNull();
82+
assertThat(security.policy()).isEqualTo(Constants.DEFAULT_SECURITY_POLICY);
83+
}
84+
85+
@Test
86+
void convertValue_emptyString_usesDefaultPolicy() {
87+
final Map<String, Object> configMap = new HashMap<>();
88+
configMap.put("security", "");
89+
final Security security = mapper.convertValue(configMap.get("security"), Security.class);
90+
assertThat(security).isNotNull();
91+
assertThat(security.policy()).isEqualTo(Constants.DEFAULT_SECURITY_POLICY);
92+
}
93+
}

0 commit comments

Comments
 (0)