This is an automated email from the ASF dual-hosted git repository.
gnodet pushed a commit to branch camel-4.18.x
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/camel-4.18.x by this push:
new 04edc01c9dcb [backport camel-4.18.x] CAMEL-25584: camel-lra - only
join LRAs hosted by the configured LRA coordinator (#27772)
04edc01c9dcb is described below
commit 04edc01c9dcbd87ca4ed8b6fd76c97a089ad2fb9
Author: Andrea Cosentino <[email protected]>
AuthorDate: Tue Oct 13 16:27:51 2026 +0200
[backport camel-4.18.x] CAMEL-25584: camel-lra - only join LRAs hosted by
the configured LRA coordinator (#27772)
CAMEL-25584: camel-lra - only join LRAs hosted by the configured LRA
coordinator (#27725)
The LRA saga service now joins an LRA only when its URL is hosted by the
configured
LRA coordinator (or one of the allowedCoordinatorUrls), instead of joining
whatever
LRA URL arrives in the Long-Running-Action header. The URL check rejects
dot segments
(including ';' and percent-encoded forms), reads host, port and userinfo so
host names
with an underscore work, and validates the allowed coordinator URLs at
startup.
Adapted to camel-4.18.x: LRASagaService is ported by hand, as the branch
predates CAMEL-24150 and
CAMEL-24192, and LRAPropagatedSagaTest uses camel-test-junit5. The
upgrade-guide notes stay on main.
(cherry picked from commit 86224cd178c36d06fcf76ca8d139d9d68fd22b4e)
Signed-off-by: Andrea Cosentino <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
---
.../main/camel-main-configuration-metadata.json | 1 +
.../service/lra/LRASagaServiceConfigurer.java | 6 +
.../apache/camel/service/lra/LRASagaService.java | 124 +++++++++++++++++-
.../camel/service/lra/LRAPropagatedSagaTest.java | 129 +++++++++++++++++++
.../lra/LRASagaServiceAllowedCoordinatorTest.java | 141 +++++++++++++++++++++
.../src/main/docs/modules/eips/pages/saga-eip.adoc | 5 +
.../main/LraConfigurationPropertiesConfigurer.java | 7 +
.../camel-main-configuration-metadata.json | 1 +
core/camel-main/src/main/docs/main.adoc | 3 +-
.../camel/main/LraConfigurationProperties.java | 24 ++++
10 files changed, 439 insertions(+), 2 deletions(-)
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/main/camel-main-configuration-metadata.json
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/main/camel-main-configuration-metadata.json
index 13ce22342152..2f8df635ce4e 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/main/camel-main-configuration-metadata.json
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/main/camel-main-configuration-metadata.json
@@ -189,6 +189,7 @@
{ "name": "camel.health.producersEnabled", "required": false,
"description": "Whether producers health check is enabled", "sourceType":
"org.apache.camel.main.HealthConfigurationProperties", "type": "boolean",
"javaType": "java.lang.Boolean", "defaultValue": false, "secret": false },
{ "name": "camel.health.registryEnabled", "required": false,
"description": "Whether registry health check is enabled", "sourceType":
"org.apache.camel.main.HealthConfigurationProperties", "type": "boolean",
"javaType": "java.lang.Boolean", "defaultValue": true, "secret": false },
{ "name": "camel.health.routesEnabled", "required": false, "description":
"Whether routes health check is enabled", "sourceType":
"org.apache.camel.main.HealthConfigurationProperties", "type": "boolean",
"javaType": "java.lang.Boolean", "defaultValue": true, "secret": false },
+ { "name": "camel.lra.allowedCoordinatorUrls", "required": false,
"description": "Comma-separated URLs, including the context path, of further
LRA coordinators whose LRAs may be joined, completed or compensated. By
default, only an LRA hosted by the configured coordinator is accepted. Use it
when participants reach the same coordinator under different URLs, for example
with different host names.", "sourceType":
"org.apache.camel.main.LraConfigurationProperties", "type": "string", "jav [...]
{ "name": "camel.lra.coordinatorContextPath", "required": false,
"description": "The context-path for the LRA coordinator. Is default
\/lra-coordinator", "sourceType":
"org.apache.camel.main.LraConfigurationProperties", "type": "string",
"javaType": "java.lang.String", "defaultValue": "\/lra-coordinator", "secret":
false },
{ "name": "camel.lra.coordinatorUrl", "required": false, "description":
"The URL for the LRA coordinator service that orchestrates the transactions",
"sourceType": "org.apache.camel.main.LraConfigurationProperties", "type":
"string", "javaType": "java.lang.String", "secret": false },
{ "name": "camel.lra.enabled", "required": false, "description": "To
enable Saga LRA", "sourceType":
"org.apache.camel.main.LraConfigurationProperties", "type": "boolean",
"javaType": "boolean", "defaultValue": false, "secret": false },
diff --git
a/components/camel-lra/src/generated/java/org/apache/camel/service/lra/LRASagaServiceConfigurer.java
b/components/camel-lra/src/generated/java/org/apache/camel/service/lra/LRASagaServiceConfigurer.java
index 53d34137e6c8..cba17a6be24b 100644
---
a/components/camel-lra/src/generated/java/org/apache/camel/service/lra/LRASagaServiceConfigurer.java
+++
b/components/camel-lra/src/generated/java/org/apache/camel/service/lra/LRASagaServiceConfigurer.java
@@ -23,6 +23,8 @@ public class LRASagaServiceConfigurer extends
org.apache.camel.support.component
public boolean configure(CamelContext camelContext, Object obj, String
name, Object value, boolean ignoreCase) {
org.apache.camel.service.lra.LRASagaService target =
(org.apache.camel.service.lra.LRASagaService) obj;
switch (ignoreCase ? name.toLowerCase() : name) {
+ case "allowedcoordinatorurls":
+ case "allowedCoordinatorUrls":
target.setAllowedCoordinatorUrls(property(camelContext, java.lang.String.class,
value)); return true;
case "camelcontext":
case "camelContext": target.setCamelContext(property(camelContext,
org.apache.camel.CamelContext.class, value)); return true;
case "coordinatorcontextpath":
@@ -40,6 +42,8 @@ public class LRASagaServiceConfigurer extends
org.apache.camel.support.component
@Override
public Class<?> getOptionType(String name, boolean ignoreCase) {
switch (ignoreCase ? name.toLowerCase() : name) {
+ case "allowedcoordinatorurls":
+ case "allowedCoordinatorUrls": return java.lang.String.class;
case "camelcontext":
case "camelContext": return org.apache.camel.CamelContext.class;
case "coordinatorcontextpath":
@@ -58,6 +62,8 @@ public class LRASagaServiceConfigurer extends
org.apache.camel.support.component
public Object getOptionValue(Object obj, String name, boolean ignoreCase) {
org.apache.camel.service.lra.LRASagaService target =
(org.apache.camel.service.lra.LRASagaService) obj;
switch (ignoreCase ? name.toLowerCase() : name) {
+ case "allowedcoordinatorurls":
+ case "allowedCoordinatorUrls": return
target.getAllowedCoordinatorUrls();
case "camelcontext":
case "camelContext": return target.getCamelContext();
case "coordinatorcontextpath":
diff --git
a/components/camel-lra/src/main/java/org/apache/camel/service/lra/LRASagaService.java
b/components/camel-lra/src/main/java/org/apache/camel/service/lra/LRASagaService.java
index 0d3acb834eb5..6a6359d22fca 100644
---
a/components/camel-lra/src/main/java/org/apache/camel/service/lra/LRASagaService.java
+++
b/components/camel-lra/src/main/java/org/apache/camel/service/lra/LRASagaService.java
@@ -16,7 +16,13 @@
*/
package org.apache.camel.service.lra;
+import java.net.MalformedURLException;
import java.net.URI;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import java.util.Objects;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
@@ -32,6 +38,7 @@ import org.apache.camel.spi.Configurer;
import org.apache.camel.spi.Metadata;
import org.apache.camel.spi.annotations.JdkService;
import org.apache.camel.support.service.ServiceSupport;
+import org.apache.camel.util.ObjectHelper;
/**
* A Camel saga service based on Microprofile LRA
(https://github.com/eclipse/microprofile-lra).
@@ -56,6 +63,8 @@ public class LRASagaService extends ServiceSupport implements
StaticService, Cam
private String localParticipantUrl;
@Metadata
private String localParticipantContextPath =
LRAConstants.DEFAULT_LOCAL_PARTICIPANT_CONTEXT_PATH;
+ @Metadata(label = "security")
+ private String allowedCoordinatorUrls;
public LRASagaService() {
}
@@ -75,17 +84,107 @@ public class LRASagaService extends ServiceSupport
implements StaticService, Cam
return true;
}
+ /**
+ * Returns the coordinator of an LRA, which is joined, completed or
compensated by calling its URL. Only an LRA
+ * hosted by the configured coordinator, or by one of the allowed
coordinator URLs, is accepted.
+ */
@Override
public CompletableFuture<CamelSagaCoordinator> getSaga(String id) {
CompletableFuture<CamelSagaCoordinator> coordinator;
try {
- coordinator = CompletableFuture.completedFuture(new
LRASagaCoordinator(this, URI.create(id).toURL()));
+ URI lra = URI.create(id);
+ if (!isHostedByAllowedCoordinator(lra)) {
+ throw new IllegalArgumentException(
+ "The Long-Running-Action " + id + " is not hosted by
the LRA coordinator configured on the LRA"
+ + " saga service. If it is
a trusted coordinator, add its URL to"
+ + "
allowedCoordinatorUrls.");
+ }
+ coordinator = CompletableFuture.completedFuture(new
LRASagaCoordinator(this, lra.toURL()));
} catch (Exception ex) {
coordinator = CompletableFuture.failedFuture(ex);
}
return coordinator;
}
+ private boolean isHostedByAllowedCoordinator(URI lra) {
+ for (URI coordinator : allowedCoordinators()) {
+ if (isHostedBy(lra, coordinator)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * The URLs of the coordinators whose LRAs are accepted: the configured
coordinator, and the allowed coordinator
+ * URLs.
+ */
+ private List<URI> allowedCoordinators() {
+ List<URI> coordinators = new ArrayList<>();
+ coordinators.add(URI.create(new
LRAUrlBuilder().host(coordinatorUrl).path(coordinatorContextPath).build()));
+ coordinators.addAll(additionalCoordinators());
+ return coordinators;
+ }
+
+ private List<URI> additionalCoordinators() {
+ List<URI> coordinators = new ArrayList<>();
+ if (ObjectHelper.isNotEmpty(allowedCoordinatorUrls)) {
+ for (String url : allowedCoordinatorUrls.split(",")) {
+ if (!url.isBlank()) {
+ coordinators.add(URI.create(url.trim()));
+ }
+ }
+ }
+ return coordinators;
+ }
+
+ /**
+ * Whether the LRA URL is under the coordinator URL: same scheme, host and
port, no user info, and a path under the
+ * path of the coordinator.
+ */
+ private static boolean isHostedBy(URI lra, URI coordinator) {
+ // host and port are read through URL, which also takes host names
that URI does not, such as lra_coordinator
+ URL lraUrl = toUrl(lra);
+ URL coordinatorUrl = toUrl(coordinator);
+ if (lraUrl == null || coordinatorUrl == null || lraUrl.getUserInfo()
!= null || lraUrl.getHost().isEmpty()) {
+ return false;
+ }
+ String rawPath = lra.getRawPath();
+ if (rawPath == null || hasEncodedOrParameterSegment(rawPath)) {
+ return false;
+ }
+ String lraPath = lra.normalize().getRawPath();
+ String coordinatorPath =
Objects.toString(coordinator.normalize().getRawPath(), "");
+ if (!coordinatorPath.endsWith("/")) {
+ coordinatorPath += "/";
+ }
+ return
lraUrl.getProtocol().equalsIgnoreCase(coordinatorUrl.getProtocol())
+ && lraUrl.getHost().equalsIgnoreCase(coordinatorUrl.getHost())
+ && port(lraUrl) == port(coordinatorUrl)
+ && lraPath.startsWith(coordinatorPath);
+ }
+
+ /**
+ * Whether the path holds a path parameter, or an encoded dot, slash or
backslash. A server may resolve them as dot
+ * segments after this check, so such a path could leave the path of the
coordinator.
+ */
+ private static boolean hasEncodedOrParameterSegment(String rawPath) {
+ String path = rawPath.toLowerCase(Locale.ROOT);
+ return path.contains(";") || path.contains("%2e") ||
path.contains("%2f") || path.contains("%5c");
+ }
+
+ private static URL toUrl(URI uri) {
+ try {
+ return uri.isAbsolute() ? uri.toURL() : null;
+ } catch (MalformedURLException | IllegalArgumentException e) {
+ return null;
+ }
+ }
+
+ private static int port(URL url) {
+ return url.getPort() != -1 ? url.getPort() : url.getDefaultPort();
+ }
+
@Override
public void registerStep(CamelSagaStep step) {
// Register which uris should be exposed
@@ -95,6 +194,15 @@ public class LRASagaService extends ServiceSupport
implements StaticService, Cam
@Override
protected void doStart() throws Exception {
+ for (URI coordinator : additionalCoordinators()) {
+ URL url = toUrl(coordinator);
+ if (url == null || url.getHost().isEmpty()
+ || !("http".equals(url.getProtocol()) ||
"https".equals(url.getProtocol()))) {
+ throw new IllegalArgumentException(
+ "allowedCoordinatorUrls must hold http or https
coordinator URLs with a host name, but no such"
+ + " URL could be read from:
" + coordinator);
+ }
+ }
if (this.executorService == null) {
this.executorService = camelContext.getExecutorServiceManager()
.newDefaultScheduledThreadPool(this, "saga-lra");
@@ -188,6 +296,20 @@ public class LRASagaService extends ServiceSupport
implements StaticService, Cam
this.localParticipantContextPath = localParticipantContextPath;
}
+ @ManagedAttribute(description = "Allowed coordinator URLs")
+ public String getAllowedCoordinatorUrls() {
+ return allowedCoordinatorUrls;
+ }
+
+ /**
+ * Comma-separated URLs, including the context path, of further LRA
coordinators whose LRAs may be joined, completed
+ * or compensated. By default, only an LRA hosted by the configured
coordinator is accepted. Use it when
+ * participants reach the same coordinator under different URLs, for
example with different host names.
+ */
+ public void setAllowedCoordinatorUrls(String allowedCoordinatorUrls) {
+ this.allowedCoordinatorUrls = allowedCoordinatorUrls;
+ }
+
public Set<String> getRegisteredURIs() {
return sagaURIs;
}
diff --git
a/components/camel-lra/src/test/java/org/apache/camel/service/lra/LRAPropagatedSagaTest.java
b/components/camel-lra/src/test/java/org/apache/camel/service/lra/LRAPropagatedSagaTest.java
new file mode 100644
index 000000000000..8c33bdaca361
--- /dev/null
+++
b/components/camel-lra/src/test/java/org/apache/camel/service/lra/LRAPropagatedSagaTest.java
@@ -0,0 +1,129 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.service.lra;
+
+import java.net.URL;
+import java.util.concurrent.CompletableFuture;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.Exchange;
+import org.apache.camel.RoutesBuilder;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.mock.MockEndpoint;
+import org.apache.camel.model.SagaPropagation;
+import org.apache.camel.test.AvailablePortFinder;
+import org.apache.camel.test.junit5.CamelTestSupport;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mockito;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+/**
+ * Tests that a saga step joins an LRA propagated in the Long-Running-Action
header only when the LRA is hosted by the
+ * configured coordinator.
+ */
+class LRAPropagatedSagaTest extends CamelTestSupport {
+
+ private static final String COORDINATOR_URL =
"http://lra-coordinator:8080";
+
+ @RegisterExtension
+ AvailablePortFinder.Port port = AvailablePortFinder.find();
+
+ private final LRAClient client = Mockito.mock(LRAClient.class);
+
+ @Override
+ protected CamelContext createCamelContext() throws Exception {
+ CamelContext context = super.createCamelContext();
+
+ LRASagaService sagaService = new MockClientLRASagaService(client);
+ sagaService.setCoordinatorUrl(COORDINATOR_URL);
+ sagaService.setLocalParticipantUrl("http://localhost:" +
port.getPort());
+ context.addService(sagaService);
+
+ context.addRoutes(new RouteBuilder() {
+ @Override
+ public void configure() {
+ restConfiguration().port(port.getPort());
+ }
+ });
+ return context;
+ }
+
+ @Override
+ protected RoutesBuilder createRouteBuilder() {
+ return new RouteBuilder() {
+ @Override
+ public void configure() {
+ from("direct:step")
+ .saga().propagation(SagaPropagation.SUPPORTS)
+ .compensation("mock:compensate")
+ .to("mock:step");
+ }
+ };
+ }
+
+ @Test
+ void joinsAnLraOfTheConfiguredCoordinator() throws Exception {
+ Mockito.when(client.join(Mockito.any(), Mockito.any(), Mockito.any()))
+ .thenReturn(CompletableFuture.completedFuture(null));
+ MockEndpoint step = getMockEndpoint("mock:step");
+ step.expectedMessageCount(1);
+
+ Exchange exchange = send(COORDINATOR_URL + "/lra-coordinator/0_1");
+
+ assertNull(exchange.getException());
+ step.assertIsSatisfied();
+ ArgumentCaptor<URL> joined = ArgumentCaptor.forClass(URL.class);
+ Mockito.verify(client).join(joined.capture(), Mockito.any(),
Mockito.any());
+ assertEquals(COORDINATOR_URL + "/lra-coordinator/0_1",
joined.getValue().toString());
+ }
+
+ @Test
+ void doesNotJoinAnLraOfAnotherCoordinator() throws Exception {
+ MockEndpoint step = getMockEndpoint("mock:step");
+ step.expectedMessageCount(0);
+
+ Exchange exchange = send("http://elsewhere:8080/lra-coordinator/0_1");
+
+ assertInstanceOf(IllegalArgumentException.class,
exchange.getException());
+ step.assertIsSatisfied();
+ Mockito.verifyNoInteractions(client);
+ }
+
+ private Exchange send(String lra) {
+ return template.send("direct:step", e ->
e.getIn().setHeader(Exchange.SAGA_LONG_RUNNING_ACTION, lra));
+ }
+
+ // a named class, as the thread pool of the service is named after its
class
+ private static class MockClientLRASagaService extends LRASagaService {
+
+ private final LRAClient client;
+
+ MockClientLRASagaService(LRAClient client) {
+ this.client = client;
+ }
+
+ @Override
+ protected LRAClient createLRAClient() {
+ return client;
+ }
+ }
+}
diff --git
a/components/camel-lra/src/test/java/org/apache/camel/service/lra/LRASagaServiceAllowedCoordinatorTest.java
b/components/camel-lra/src/test/java/org/apache/camel/service/lra/LRASagaServiceAllowedCoordinatorTest.java
new file mode 100644
index 000000000000..f8a1dbea5a78
--- /dev/null
+++
b/components/camel-lra/src/test/java/org/apache/camel/service/lra/LRASagaServiceAllowedCoordinatorTest.java
@@ -0,0 +1,141 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.service.lra;
+
+import java.util.concurrent.CompletionException;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+/**
+ * Tests which LRAs the LRA saga service accepts to join, complete or
compensate.
+ */
+class LRASagaServiceAllowedCoordinatorTest {
+
+ private final LRASagaService sagaService = new LRASagaService();
+
+ @BeforeEach
+ void configure() {
+ sagaService.setCoordinatorUrl("http://lra-coordinator:8080");
+ sagaService.setLocalParticipantUrl("http://participant:8080");
+ }
+
+ @Test
+ void acceptsAnLraOfTheConfiguredCoordinator() {
+
assertAccepted("http://lra-coordinator:8080/lra-coordinator/0_ffff0a1e1f05_1");
+ }
+
+ @Test
+ void comparesSchemeAndHostIgnoringCase() {
+ assertAccepted("HTTP://LRA-Coordinator:8080/lra-coordinator/0_1");
+ }
+
+ @Test
+ void treatsTheDefaultPortLikeAnExplicitOne() {
+ sagaService.setCoordinatorUrl("http://lra-coordinator");
+
+ assertAccepted("http://lra-coordinator:80/lra-coordinator/0_1");
+ }
+
+ @Test
+ void honoursTheCoordinatorContextPath() {
+ sagaService.setCoordinatorContextPath("/api/v1/lra-coordinator");
+
+
assertAccepted("http://lra-coordinator:8080/api/v1/lra-coordinator/0_1");
+ assertRejected("http://lra-coordinator:8080/lra-coordinator/0_1");
+ }
+
+ @Test
+ void rejectsAnLraOfAnotherHost() {
+ assertRejected("http://elsewhere:8080/lra-coordinator/0_1");
+ }
+
+ @Test
+ void rejectsAnLraOfAnotherPortOrScheme() {
+ assertRejected("http://lra-coordinator:9090/lra-coordinator/0_1");
+ assertRejected("https://lra-coordinator:8080/lra-coordinator/0_1");
+ }
+
+ @Test
+ void rejectsAPathOutsideTheCoordinatorContextPath() {
+ assertRejected("http://lra-coordinator:8080/other/0_1");
+
assertRejected("http://lra-coordinator:8080/lra-coordinator-other/0_1");
+
assertRejected("http://lra-coordinator:8080/lra-coordinator/../other/0_1");
+ }
+
+ @Test
+ void rejectsEncodedOrParameterDotSegments() {
+
assertRejected("http://lra-coordinator:8080/lra-coordinator/%2e%2e/other/0_1");
+
assertRejected("http://lra-coordinator:8080/lra-coordinator/%2E%2E/other/0_1");
+
assertRejected("http://lra-coordinator:8080/lra-coordinator/..;/other/0_1");
+
assertRejected("http://lra-coordinator:8080/lra-coordinator/..%2fother/0_1");
+
assertRejected("http://lra-coordinator:8080/lra-coordinator/..%5Cother/0_1");
+ }
+
+ @Test
+ void acceptsAnLraOfACoordinatorWithAnUnderscoreInItsHostName() {
+ sagaService.setCoordinatorUrl("http://lra_coordinator:8080");
+
+ assertAccepted("http://lra_coordinator:8080/lra-coordinator/0_1");
+ assertRejected("http://other_coordinator:8080/lra-coordinator/0_1");
+ }
+
+ @Test
+ void rejectsAnLraWithUserInfo() {
+ assertRejected("http://user@lra-coordinator:8080/lra-coordinator/0_1");
+ }
+
+ @Test
+ void acceptsAnLraOfAnAllowedCoordinatorUrl() {
+ sagaService.setAllowedCoordinatorUrls(
+ "http://lra-coordinator.lra.svc:8080/lra-coordinator,
https://lra.example.com/coordinator,"
+ + "
http://lra_coordinator:8080/lra-coordinator");
+
+
assertAccepted("http://lra-coordinator.lra.svc:8080/lra-coordinator/0_1");
+ assertAccepted("https://lra.example.com/coordinator/0_1");
+ assertAccepted("http://lra_coordinator:8080/lra-coordinator/0_1");
+ assertRejected("http://elsewhere:8080/lra-coordinator/0_1");
+ }
+
+ @Test
+ void failsToStartWithARelativeAllowedCoordinatorUrl() {
+
sagaService.setAllowedCoordinatorUrls("lra-coordinator/lra-coordinator");
+
+ assertThrows(IllegalArgumentException.class, sagaService::doStart);
+ }
+
+ @Test
+ void failsToStartWithAnAllowedCoordinatorUrlThatIsNotHttp() {
+
sagaService.setAllowedCoordinatorUrls("ftp://lra-coordinator/lra-coordinator");
+
+ assertThrows(IllegalArgumentException.class, sagaService::doStart);
+ }
+
+ private void assertAccepted(String lra) {
+ assertNotNull(sagaService.getSaga(lra).join(), lra + " must be
accepted");
+ }
+
+ private void assertRejected(String lra) {
+ CompletionException e = assertThrows(CompletionException.class, () ->
sagaService.getSaga(lra).join(),
+ lra + " must be rejected");
+ assertInstanceOf(IllegalArgumentException.class, e.getCause());
+ }
+}
diff --git
a/core/camel-core-engine/src/main/docs/modules/eips/pages/saga-eip.adoc
b/core/camel-core-engine/src/main/docs/modules/eips/pages/saga-eip.adoc
index 27eb8668d216..164b7c433e10 100644
--- a/core/camel-core-engine/src/main/docs/modules/eips/pages/saga-eip.adoc
+++ b/core/camel-core-engine/src/main/docs/modules/eips/pages/saga-eip.adoc
@@ -93,6 +93,11 @@ Users can follow instructions present on the Narayana
website to *start up a rem
The URL of the LRA coordinator is a required parameter of the Camel LRA
service. The Camel application and the LRA service communicate using the HTTP
protocol.
+A saga started by another participant is joined when the message carries its
LRA in the `Long-Running-Action` header.
+The LRA Saga Service only joins, completes or compensates an LRA hosted by the
configured coordinator: a `Long-Running-Action` with any other URL fails the
exchange.
+When the participants reach the coordinator under different URLs, for example
with different host names, list the other coordinator URLs, including the
context path, in the `allowedCoordinatorUrls` option
(`camel.lra.allowedCoordinatorUrls` with Camel Main).
+This also applies to the LRAs the service starts itself: when the coordinator
returns LRA URLs under another base URL than `coordinatorUrl`, for example
behind a proxy, list that base URL as well.
+
To use the LRA Saga service, maven users will need to add the following
dependency to their `pom.xml`:
[source,xml]
diff --git
a/core/camel-main/src/generated/java/org/apache/camel/main/LraConfigurationPropertiesConfigurer.java
b/core/camel-main/src/generated/java/org/apache/camel/main/LraConfigurationPropertiesConfigurer.java
index badb2b23ea60..3bdb1349e7c1 100644
---
a/core/camel-main/src/generated/java/org/apache/camel/main/LraConfigurationPropertiesConfigurer.java
+++
b/core/camel-main/src/generated/java/org/apache/camel/main/LraConfigurationPropertiesConfigurer.java
@@ -22,6 +22,7 @@ public class LraConfigurationPropertiesConfigurer extends
org.apache.camel.suppo
private static final Map<String, Object> ALL_OPTIONS;
static {
Map<String, Object> map = new CaseInsensitiveMap();
+ map.put("AllowedCoordinatorUrls", java.lang.String.class);
map.put("CoordinatorContextPath", java.lang.String.class);
map.put("CoordinatorUrl", java.lang.String.class);
map.put("Enabled", boolean.class);
@@ -34,6 +35,8 @@ public class LraConfigurationPropertiesConfigurer extends
org.apache.camel.suppo
public boolean configure(CamelContext camelContext, Object obj, String
name, Object value, boolean ignoreCase) {
org.apache.camel.main.LraConfigurationProperties target =
(org.apache.camel.main.LraConfigurationProperties) obj;
switch (ignoreCase ? name.toLowerCase() : name) {
+ case "allowedcoordinatorurls":
+ case "allowedCoordinatorUrls":
target.setAllowedCoordinatorUrls(property(camelContext, java.lang.String.class,
value)); return true;
case "coordinatorcontextpath":
case "coordinatorContextPath":
target.setCoordinatorContextPath(property(camelContext, java.lang.String.class,
value)); return true;
case "coordinatorurl":
@@ -55,6 +58,8 @@ public class LraConfigurationPropertiesConfigurer extends
org.apache.camel.suppo
@Override
public Class<?> getOptionType(String name, boolean ignoreCase) {
switch (ignoreCase ? name.toLowerCase() : name) {
+ case "allowedcoordinatorurls":
+ case "allowedCoordinatorUrls": return java.lang.String.class;
case "coordinatorcontextpath":
case "coordinatorContextPath": return java.lang.String.class;
case "coordinatorurl":
@@ -72,6 +77,8 @@ public class LraConfigurationPropertiesConfigurer extends
org.apache.camel.suppo
public Object getOptionValue(Object obj, String name, boolean ignoreCase) {
org.apache.camel.main.LraConfigurationProperties target =
(org.apache.camel.main.LraConfigurationProperties) obj;
switch (ignoreCase ? name.toLowerCase() : name) {
+ case "allowedcoordinatorurls":
+ case "allowedCoordinatorUrls": return
target.getAllowedCoordinatorUrls();
case "coordinatorcontextpath":
case "coordinatorContextPath": return
target.getCoordinatorContextPath();
case "coordinatorurl":
diff --git
a/core/camel-main/src/generated/resources/META-INF/camel-main-configuration-metadata.json
b/core/camel-main/src/generated/resources/META-INF/camel-main-configuration-metadata.json
index 13ce22342152..2f8df635ce4e 100644
---
a/core/camel-main/src/generated/resources/META-INF/camel-main-configuration-metadata.json
+++
b/core/camel-main/src/generated/resources/META-INF/camel-main-configuration-metadata.json
@@ -189,6 +189,7 @@
{ "name": "camel.health.producersEnabled", "required": false,
"description": "Whether producers health check is enabled", "sourceType":
"org.apache.camel.main.HealthConfigurationProperties", "type": "boolean",
"javaType": "java.lang.Boolean", "defaultValue": false, "secret": false },
{ "name": "camel.health.registryEnabled", "required": false,
"description": "Whether registry health check is enabled", "sourceType":
"org.apache.camel.main.HealthConfigurationProperties", "type": "boolean",
"javaType": "java.lang.Boolean", "defaultValue": true, "secret": false },
{ "name": "camel.health.routesEnabled", "required": false, "description":
"Whether routes health check is enabled", "sourceType":
"org.apache.camel.main.HealthConfigurationProperties", "type": "boolean",
"javaType": "java.lang.Boolean", "defaultValue": true, "secret": false },
+ { "name": "camel.lra.allowedCoordinatorUrls", "required": false,
"description": "Comma-separated URLs, including the context path, of further
LRA coordinators whose LRAs may be joined, completed or compensated. By
default, only an LRA hosted by the configured coordinator is accepted. Use it
when participants reach the same coordinator under different URLs, for example
with different host names.", "sourceType":
"org.apache.camel.main.LraConfigurationProperties", "type": "string", "jav [...]
{ "name": "camel.lra.coordinatorContextPath", "required": false,
"description": "The context-path for the LRA coordinator. Is default
\/lra-coordinator", "sourceType":
"org.apache.camel.main.LraConfigurationProperties", "type": "string",
"javaType": "java.lang.String", "defaultValue": "\/lra-coordinator", "secret":
false },
{ "name": "camel.lra.coordinatorUrl", "required": false, "description":
"The URL for the LRA coordinator service that orchestrates the transactions",
"sourceType": "org.apache.camel.main.LraConfigurationProperties", "type":
"string", "javaType": "java.lang.String", "secret": false },
{ "name": "camel.lra.enabled", "required": false, "description": "To
enable Saga LRA", "sourceType":
"org.apache.camel.main.LraConfigurationProperties", "type": "boolean",
"javaType": "boolean", "defaultValue": false, "secret": false },
diff --git a/core/camel-main/src/main/docs/main.adoc
b/core/camel-main/src/main/docs/main.adoc
index a800a1b8acaa..0aa7f14d4836 100644
--- a/core/camel-main/src/main/docs/main.adoc
+++ b/core/camel-main/src/main/docs/main.adoc
@@ -674,11 +674,12 @@ The camel.resilience4j supports 21 options, which are
listed below.
=== Camel Saga EIP (Long Running Actions) configurations
-The camel.lra supports 5 options, which are listed below.
+The camel.lra supports 6 options, which are listed below.
[width="100%",cols="2,5,^1,2",options="header"]
|===
| Name | Description | Default | Type
+| *camel.lra.allowedCoordinator{zwsp}Urls* | Comma-separated URLs, including
the context path, of further LRA coordinators whose LRAs may be joined,
completed or compensated. By default, only an LRA hosted by the configured
coordinator is accepted. Use it when participants reach the same coordinator
under different URLs, for example with different host names. | | String
| *camel.lra.coordinatorContext{zwsp}Path* | The context-path for the LRA
coordinator. Is default /lra-coordinator | /lra-coordinator | String
| *camel.lra.coordinatorUrl* | The URL for the LRA coordinator service that
orchestrates the transactions | | String
| *camel.lra.enabled* | To enable Saga LRA | false | boolean
diff --git
a/core/camel-main/src/main/java/org/apache/camel/main/LraConfigurationProperties.java
b/core/camel-main/src/main/java/org/apache/camel/main/LraConfigurationProperties.java
index 3c91843d31c7..6ae967f1a0a8 100644
---
a/core/camel-main/src/main/java/org/apache/camel/main/LraConfigurationProperties.java
+++
b/core/camel-main/src/main/java/org/apache/camel/main/LraConfigurationProperties.java
@@ -36,6 +36,8 @@ public class LraConfigurationProperties implements
BootstrapCloseable {
private String localParticipantUrl;
@Metadata(defaultValue = "/lra-participant")
private String localParticipantContextPath = "/lra-participant";
+ @Metadata(label = "security")
+ private String allowedCoordinatorUrls;
public LraConfigurationProperties(MainConfigurationProperties parent) {
this.parent = parent;
@@ -109,6 +111,19 @@ public class LraConfigurationProperties implements
BootstrapCloseable {
this.localParticipantContextPath = localParticipantContextPath;
}
+ public String getAllowedCoordinatorUrls() {
+ return allowedCoordinatorUrls;
+ }
+
+ /**
+ * Comma-separated URLs, including the context path, of further LRA
coordinators whose LRAs may be joined, completed
+ * or compensated. By default, only an LRA hosted by the configured
coordinator is accepted. Use it when
+ * participants reach the same coordinator under different URLs, for
example with different host names.
+ */
+ public void setAllowedCoordinatorUrls(String allowedCoordinatorUrls) {
+ this.allowedCoordinatorUrls = allowedCoordinatorUrls;
+ }
+
/**
* The URL for the LRA coordinator service that orchestrates the
transactions
*/
@@ -149,4 +164,13 @@ public class LraConfigurationProperties implements
BootstrapCloseable {
return this;
}
+ /**
+ * Comma-separated URLs, including the context path, of further LRA
coordinators whose LRAs may be joined, completed
+ * or compensated.
+ */
+ public LraConfigurationProperties withAllowedCoordinatorUrls(String
allowedCoordinatorUrls) {
+ this.allowedCoordinatorUrls = allowedCoordinatorUrls;
+ return this;
+ }
+
}