Skip to content

Commit 1047cdf

Browse files
committed
Extract user token policy ID assignment
Keep OpcUaServer focused on endpoint construction while giving user token policy normalization a focused, documented boundary. Preserve existing advertisement behavior and add direct coverage for collision edge cases.
1 parent 9e80939 commit 1047cdf

4 files changed

Lines changed: 271 additions & 213 deletions

File tree

opc-ua-sdk/sdk-server/src/main/java/org/eclipse/milo/opcua/sdk/server/OpcUaServer.java

Lines changed: 3 additions & 134 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,10 @@
1919
import java.security.KeyPair;
2020
import java.security.cert.CertificateEncodingException;
2121
import java.security.cert.X509Certificate;
22-
import java.util.ArrayList;
2322
import java.util.Collections;
2423
import java.util.Comparator;
25-
import java.util.HashMap;
26-
import java.util.LinkedHashMap;
2724
import java.util.LinkedHashSet;
2825
import java.util.List;
29-
import java.util.Locale;
3026
import java.util.Map;
3127
import java.util.Objects;
3228
import java.util.Optional;
@@ -87,10 +83,8 @@
8783
import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.UInteger;
8884
import org.eclipse.milo.opcua.stack.core.types.enumerated.ApplicationType;
8985
import org.eclipse.milo.opcua.stack.core.types.enumerated.MessageSecurityMode;
90-
import org.eclipse.milo.opcua.stack.core.types.enumerated.UserTokenType;
9186
import org.eclipse.milo.opcua.stack.core.types.structured.ApplicationDescription;
9287
import org.eclipse.milo.opcua.stack.core.types.structured.EndpointDescription;
93-
import org.eclipse.milo.opcua.stack.core.types.structured.UserTokenPolicy;
9488
import org.eclipse.milo.opcua.stack.core.util.EndpointUtil;
9589
import org.eclipse.milo.opcua.stack.core.util.FutureUtils;
9690
import org.eclipse.milo.opcua.stack.core.util.Lazy;
@@ -850,149 +844,24 @@ private boolean isDiscoveryService(UaRequestMessageType requestMessage) {
850844
}
851845

852846
private List<EndpointDescription> transformEndpoints(Set<EndpointConfig> endpoints) {
853-
Map<UserTokenPolicyKey, String> userTokenPolicyIds = assignUserTokenPolicyIds(endpoints);
847+
UserTokenPolicyIds userTokenPolicyIds = UserTokenPolicyIds.assign(endpoints);
854848

855849
return endpoints.stream().map(e -> transformEndpoint(e, userTokenPolicyIds)).toList();
856850
}
857851

858852
private EndpointDescription transformEndpoint(
859-
EndpointConfig endpoint, Map<UserTokenPolicyKey, String> userTokenPolicyIds) {
853+
EndpointConfig endpoint, UserTokenPolicyIds userTokenPolicyIds) {
860854
return new EndpointDescription(
861855
endpoint.getEndpointUrl(),
862856
getApplicationDescription(),
863857
certificateByteString(endpoint.getCertificate()),
864858
endpoint.getSecurityMode(),
865859
endpoint.getSecurityPolicy().getUri(),
866-
transformUserTokenPolicies(endpoint, userTokenPolicyIds),
860+
userTokenPolicyIds.policiesFor(endpoint),
867861
endpoint.getTransportProfile().getUri(),
868862
ubyte(getSecurityLevel(endpoint.getSecurityPolicy(), endpoint.getSecurityMode())));
869863
}
870864

871-
private UserTokenPolicy[] transformUserTokenPolicies(
872-
EndpointConfig endpoint, Map<UserTokenPolicyKey, String> userTokenPolicyIds) {
873-
874-
return endpoint.getTokenPolicies().stream()
875-
.map(
876-
tokenPolicy -> {
877-
UserTokenPolicyKey key = UserTokenPolicyKey.from(endpoint, tokenPolicy);
878-
String assignedPolicyId = userTokenPolicyIds.get(key);
879-
880-
String policyId =
881-
policyIdChanged(tokenPolicy.getPolicyId(), assignedPolicyId)
882-
? assignedPolicyId
883-
: tokenPolicy.getPolicyId();
884-
885-
return new UserTokenPolicy(
886-
policyId,
887-
tokenPolicy.getTokenType(),
888-
tokenPolicy.getIssuedTokenType(),
889-
tokenPolicy.getIssuerEndpointUrl(),
890-
key.securityPolicyUri());
891-
})
892-
.toArray(UserTokenPolicy[]::new);
893-
}
894-
895-
private Map<UserTokenPolicyKey, String> assignUserTokenPolicyIds(
896-
Set<EndpointConfig> endpoints) {
897-
Map<String, List<UserTokenPolicyKey>> keysByPolicyId = new LinkedHashMap<>();
898-
899-
for (EndpointConfig endpoint : endpoints) {
900-
for (UserTokenPolicy tokenPolicy : endpoint.getTokenPolicies()) {
901-
UserTokenPolicyKey key = UserTokenPolicyKey.from(endpoint, tokenPolicy);
902-
List<UserTokenPolicyKey> keys =
903-
keysByPolicyId.computeIfAbsent(key.policyId(), ignored -> new ArrayList<>());
904-
905-
if (!keys.contains(key)) {
906-
keys.add(key);
907-
}
908-
}
909-
}
910-
911-
Set<String> reservedPolicyIds = new LinkedHashSet<>(keysByPolicyId.keySet());
912-
Map<UserTokenPolicyKey, String> assignedPolicyIds = new HashMap<>();
913-
914-
for (List<UserTokenPolicyKey> keys : keysByPolicyId.values()) {
915-
if (keys.size() == 1) {
916-
UserTokenPolicyKey key = keys.get(0);
917-
assignedPolicyIds.put(key, key.policyId());
918-
} else {
919-
UserTokenPolicyKey firstKey = keys.get(0);
920-
assignedPolicyIds.put(firstKey, firstKey.policyId());
921-
922-
for (int i = 1; i < keys.size(); i++) {
923-
UserTokenPolicyKey key = keys.get(i);
924-
assignedPolicyIds.put(key, uniquePolicyId(key, reservedPolicyIds));
925-
}
926-
}
927-
}
928-
929-
return assignedPolicyIds;
930-
}
931-
932-
private boolean policyIdChanged(@Nullable String configuredPolicyId, String assignedPolicyId) {
933-
if (Objects.equals(configuredPolicyId, assignedPolicyId)) {
934-
return false;
935-
} else {
936-
return !(isNullOrEmpty(configuredPolicyId) && assignedPolicyId.isEmpty());
937-
}
938-
}
939-
940-
private String uniquePolicyId(UserTokenPolicyKey key, Set<String> reservedPolicyIds) {
941-
String base =
942-
key.policyId().isEmpty()
943-
? key.tokenType().name().toLowerCase(Locale.ROOT)
944-
: key.policyId();
945-
946-
String securityPolicyName = securityPolicyName(key.securityPolicyUri());
947-
948-
String candidate = base + "-" + securityPolicyName;
949-
if (reservedPolicyIds.add(candidate)) {
950-
return candidate;
951-
}
952-
953-
candidate = base + "-" + key.tokenType().name() + "-" + securityPolicyName;
954-
if (reservedPolicyIds.add(candidate)) {
955-
return candidate;
956-
}
957-
958-
for (int i = 2; ; i++) {
959-
String indexedCandidate = candidate + "-" + i;
960-
if (reservedPolicyIds.add(indexedCandidate)) {
961-
return indexedCandidate;
962-
}
963-
}
964-
}
965-
966-
private String securityPolicyName(String securityPolicyUri) {
967-
int index = securityPolicyUri.lastIndexOf('#');
968-
String name = index >= 0 ? securityPolicyUri.substring(index + 1) : securityPolicyUri;
969-
970-
return name.replaceAll("[^A-Za-z0-9_.-]", "-");
971-
}
972-
973-
private boolean isNullOrEmpty(@Nullable String value) {
974-
return value == null || value.isEmpty();
975-
}
976-
977-
private record UserTokenPolicyKey(
978-
String policyId,
979-
UserTokenType tokenType,
980-
@Nullable String issuedTokenType,
981-
@Nullable String issuerEndpointUrl,
982-
String securityPolicyUri) {
983-
984-
static UserTokenPolicyKey from(EndpointConfig endpoint, UserTokenPolicy tokenPolicy) {
985-
String policyId = tokenPolicy.getPolicyId();
986-
987-
return new UserTokenPolicyKey(
988-
policyId == null ? "" : policyId,
989-
tokenPolicy.getTokenType(),
990-
tokenPolicy.getIssuedTokenType(),
991-
tokenPolicy.getIssuerEndpointUrl(),
992-
endpoint.getEffectiveTokenSecurityPolicyUri(tokenPolicy));
993-
}
994-
}
995-
996865
private ByteString certificateByteString(@Nullable X509Certificate certificate) {
997866
if (certificate != null) {
998867
try {
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
/*
2+
* Copyright (c) 2026 the Eclipse Milo Authors
3+
*
4+
* This program and the accompanying materials are made
5+
* available under the terms of the Eclipse Public License 2.0
6+
* which is available at https://www.eclipse.org/legal/epl-2.0/
7+
*
8+
* SPDX-License-Identifier: EPL-2.0
9+
*/
10+
11+
package org.eclipse.milo.opcua.sdk.server;
12+
13+
import java.util.HashMap;
14+
import java.util.LinkedHashMap;
15+
import java.util.LinkedHashSet;
16+
import java.util.Locale;
17+
import java.util.Map;
18+
import java.util.Objects;
19+
import java.util.Set;
20+
import org.eclipse.milo.opcua.stack.core.types.structured.EndpointDescription;
21+
import org.eclipse.milo.opcua.stack.core.types.structured.UserTokenPolicy;
22+
23+
/**
24+
* Resolves configured user token policies into the values advertised in {@link EndpointDescription
25+
* EndpointDescriptions}.
26+
*
27+
* <p>An {@link EndpointConfig} can reuse a configured policy across endpoint variants, and a policy
28+
* with a null or empty security policy URI inherits the security policy of its endpoint. The same
29+
* configured policy ID can therefore describe different effective policies. This type assigns IDs
30+
* across the complete endpoint set and creates endpoint-specific policy copies without modifying
31+
* the server configuration.
32+
*
33+
* <p>Each instance is scoped to the endpoint set supplied to {@link #assign(Set)}. It must only be
34+
* used to build descriptions for endpoints from that set.
35+
*
36+
* @see <a href="https://reference.opcfoundation.org/specs/OPC-10000-4/7.41">OPC UA Part 4 §7.41</a>
37+
*/
38+
final class UserTokenPolicyIds {
39+
40+
private final Map<UserTokenPolicy, String> assignedPolicyIds;
41+
42+
private UserTokenPolicyIds(Map<UserTokenPolicy, String> assignedPolicyIds) {
43+
this.assignedPolicyIds = assignedPolicyIds;
44+
}
45+
46+
/**
47+
* Computes the policy ID assignment shared by a set of endpoint descriptions.
48+
*
49+
* <p>When a configured ID represents more than one effective policy, the first policy encountered
50+
* retains that ID and subsequent policies receive unique IDs. The iteration order of {@code
51+
* endpoints} therefore determines which effective policy retains a conflicting configured ID.
52+
*
53+
* @param endpoints the complete set of endpoints that will be advertised together.
54+
* @return the policy ID assignment for the endpoint set.
55+
*/
56+
static UserTokenPolicyIds assign(Set<EndpointConfig> endpoints) {
57+
Map<String, Set<UserTokenPolicy>> policiesById = new LinkedHashMap<>();
58+
59+
for (EndpointConfig endpoint : endpoints) {
60+
for (UserTokenPolicy tokenPolicy : endpoint.getTokenPolicies()) {
61+
UserTokenPolicy effectivePolicy = effectivePolicy(endpoint, tokenPolicy);
62+
policiesById
63+
.computeIfAbsent(effectivePolicy.getPolicyId(), ignored -> new LinkedHashSet<>())
64+
.add(effectivePolicy);
65+
}
66+
}
67+
68+
Set<String> reservedPolicyIds = new LinkedHashSet<>(policiesById.keySet());
69+
Map<UserTokenPolicy, String> assignedPolicyIds = new HashMap<>();
70+
71+
for (Set<UserTokenPolicy> policies : policiesById.values()) {
72+
boolean preserveConfiguredId = true;
73+
74+
for (UserTokenPolicy policy : policies) {
75+
assignedPolicyIds.put(
76+
policy,
77+
preserveConfiguredId
78+
? policy.getPolicyId()
79+
: uniquePolicyId(policy, reservedPolicyIds));
80+
preserveConfiguredId = false;
81+
}
82+
}
83+
84+
return new UserTokenPolicyIds(assignedPolicyIds);
85+
}
86+
87+
/**
88+
* Builds the user token policies to advertise for an endpoint.
89+
*
90+
* <p>The returned policies contain effective security policy URIs and server-wide assigned IDs.
91+
* The configured policies in {@code endpoint} remain unchanged.
92+
*
93+
* @param endpoint an endpoint from the set supplied to {@link #assign(Set)}.
94+
* @return a new array of policies for the endpoint description.
95+
*/
96+
UserTokenPolicy[] policiesFor(EndpointConfig endpoint) {
97+
return endpoint.getTokenPolicies().stream()
98+
.map(tokenPolicy -> advertisedPolicy(endpoint, tokenPolicy))
99+
.toArray(UserTokenPolicy[]::new);
100+
}
101+
102+
private UserTokenPolicy advertisedPolicy(
103+
EndpointConfig endpoint, UserTokenPolicy configuredPolicy) {
104+
105+
UserTokenPolicy effectivePolicy = effectivePolicy(endpoint, configuredPolicy);
106+
String assignedPolicyId = assignedPolicyIds.get(effectivePolicy);
107+
108+
String policyId =
109+
assignedPolicyId.equals(effectivePolicy.getPolicyId())
110+
? configuredPolicy.getPolicyId()
111+
: assignedPolicyId;
112+
113+
return new UserTokenPolicy(
114+
policyId,
115+
effectivePolicy.getTokenType(),
116+
effectivePolicy.getIssuedTokenType(),
117+
effectivePolicy.getIssuerEndpointUrl(),
118+
effectivePolicy.getSecurityPolicyUri());
119+
}
120+
121+
private static UserTokenPolicy effectivePolicy(
122+
EndpointConfig endpoint, UserTokenPolicy tokenPolicy) {
123+
124+
return new UserTokenPolicy(
125+
Objects.requireNonNullElse(tokenPolicy.getPolicyId(), ""),
126+
tokenPolicy.getTokenType(),
127+
tokenPolicy.getIssuedTokenType(),
128+
tokenPolicy.getIssuerEndpointUrl(),
129+
endpoint.getEffectiveTokenSecurityPolicyUri(tokenPolicy));
130+
}
131+
132+
private static String uniquePolicyId(UserTokenPolicy policy, Set<String> reservedPolicyIds) {
133+
String base =
134+
policy.getPolicyId().isEmpty()
135+
? policy.getTokenType().name().toLowerCase(Locale.ROOT)
136+
: policy.getPolicyId();
137+
138+
String securityPolicyName = securityPolicyName(policy.getSecurityPolicyUri());
139+
140+
String candidate = base + "-" + securityPolicyName;
141+
if (reservedPolicyIds.add(candidate)) {
142+
return candidate;
143+
}
144+
145+
candidate = base + "-" + policy.getTokenType().name() + "-" + securityPolicyName;
146+
if (reservedPolicyIds.add(candidate)) {
147+
return candidate;
148+
}
149+
150+
for (int i = 2; ; i++) {
151+
String indexedCandidate = candidate + "-" + i;
152+
if (reservedPolicyIds.add(indexedCandidate)) {
153+
return indexedCandidate;
154+
}
155+
}
156+
}
157+
158+
private static String securityPolicyName(String securityPolicyUri) {
159+
int index = securityPolicyUri.lastIndexOf('#');
160+
String name = index >= 0 ? securityPolicyUri.substring(index + 1) : securityPolicyUri;
161+
162+
return name.replaceAll("[^A-Za-z0-9_.-]", "-");
163+
}
164+
}

0 commit comments

Comments
 (0)