This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch fix/CAMEL-24576 in repository https://gitbox.apache.org/repos/asf/camel.git
commit 1ebf135ae198459bcf4ce393fcc5f2e5ad2b7a99 Author: Andrea Cosentino <[email protected]> AuthorDate: Tue Sep 1 16:51:05 2026 +0200 CAMEL-24576: dynamic-router - gate control message predicate Adds allowPredicateFromMessage on the dynamic-router-control endpoint, default false. Previously a control message could freely supply a predicate/expressionLanguage that got compiled into the channel's rule base -- untrusted input driving expression evaluation, unlike every other Camel opt-in-gated template/script source (allowTemplateFromHeader). When the gate is closed, control messages fall back to the endpoint's configured predicate instead of being rejected. Also fixes DynamicRouterRecipientListHelper to wire allowedSchemes into the RecipientList it builds, and makes subscribeFromHeaders fall back to endpoint configuration for subscription parameters the message omits. This changes a default: deployments relying on control messages supplying their own predicate must now set allowPredicateFromMessage=true or switch to predicateBean. Documented in the 4.23 upgrade guide. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Closes #25992 (cherry picked from commit 1d5d3e983c3a3fb64b6bfe238d614726191ee977) --- .../catalog/components/dynamic-router-control.json | 3 +- .../camel/catalog/components/dynamic-router.json | 3 +- .../catalog/docs/dynamic-router-component.adoc | 19 +++- .../docs/dynamic-router-control-component.adoc | 31 +++++ .../DynamicRouterControlEndpointConfigurer.java | 6 + .../DynamicRouterControlEndpointUriFactory.java | 3 +- .../routing/DynamicRouterEndpointConfigurer.java | 6 + .../routing/DynamicRouterEndpointUriFactory.java | 3 +- .../control/dynamic-router-control.json | 3 +- .../dynamicrouter/routing/dynamic-router.json | 3 +- .../src/main/docs/dynamic-router-component.adoc | 19 +++- .../docs/dynamic-router-control-component.adoc | 31 +++++ ...ynamicRouterControlChannelSendDynamicAware.java | 9 ++ .../control/DynamicRouterControlConfiguration.java | 33 ++++++ .../control/DynamicRouterControlConstants.java | 16 +++ .../control/DynamicRouterControlProducer.java | 70 +++++++++--- .../routing/DynamicRouterConfiguration.java | 28 +++++ .../routing/DynamicRouterRecipientListHelper.java | 1 + ...icRouterControlChannelSendDynamicAwareTest.java | 11 ++ .../control/DynamicRouterControlProducerTest.java | 126 +++++++++++++++++++++ .../DynamicRouterBasicSynchronousIT.java | 8 +- .../DynamicRouterDefaultAggregationIT.java | 2 +- .../DynamicRouterSendDynamicAwareIT.java | 3 +- .../DynamicRouterRecipientListHelperTest.java | 2 + .../java/org/apache/camel/util/SecurityUtils.java | 1 + ...DynamicRouterControlEndpointBuilderFactory.java | 42 +++++++ .../dsl/DynamicRouterEndpointBuilderFactory.java | 17 +++ 27 files changed, 465 insertions(+), 34 deletions(-) diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/dynamic-router-control.json b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/dynamic-router-control.json index c1e2d0b0372f..e94aa95905bc 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/dynamic-router-control.json +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/dynamic-router-control.json @@ -46,6 +46,7 @@ "predicateBean": { "index": 5, "kind": "parameter", "displayName": "Predicate Bean", "group": "control", "label": "control", "required": false, "type": "object", "javaType": "org.apache.camel.Predicate", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.dynamicrouter.control.DynamicRouterControlConfiguration", "configurationField": "configuration", "description": "A Predicate instance in the registry." }, "priority": { "index": 6, "kind": "parameter", "displayName": "Priority", "group": "control", "label": "control", "required": false, "type": "integer", "javaType": "java.lang.Integer", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.dynamicrouter.control.DynamicRouterControlConfiguration", "configurationField": "configuration", "description": "The subscription priority." }, "subscribeChannel": { "index": 7, "kind": "parameter", "displayName": "Subscribe Channel", "group": "control", "label": "control", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.dynamicrouter.control.DynamicRouterControlConfiguration", "configurationField": "configuration", "description": "The channel to subscribe to" }, - "subscriptionId": { "index": 8, "kind": "parameter", "displayName": "Subscription Id", "group": "control", "label": "control", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.dynamicrouter.control.DynamicRouterControlConfiguration", "configurationField": "configuration", "description": "The subscription ID; if unspecified, one will be assigned and returned." } + "subscriptionId": { "index": 8, "kind": "parameter", "displayName": "Subscription Id", "group": "control", "label": "control", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.dynamicrouter.control.DynamicRouterControlConfiguration", "configurationField": "configuration", "description": "The subscription ID; if unspecified, one will be assigned and returned." }, + "allowPredicateFromMessage": { "index": 9, "kind": "parameter", "displayName": "Allow Predicate From Message", "group": "security", "label": "control,security", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "security": "insecure:dev", "insecureValue": "true", "defaultValue": false, "configurationClass": "org.apache.camel.component.dynamicrouter.control.DynamicRouterControlConfiguration", "configurationField": "c [...] } } diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/dynamic-router.json b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/dynamic-router.json index 088f2b008feb..3e992a75968a 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/dynamic-router.json +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/dynamic-router.json @@ -48,6 +48,7 @@ "synchronous": { "index": 17, "kind": "parameter", "displayName": "Synchronous", "group": "common", "label": "common", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "configurationClass": "org.apache.camel.component.dynamicrouter.routing.DynamicRouterConfiguration", "configurationField": "configuration", "description": "Sets whether synchronous processing should be strictly used. When enabl [...] "timeout": { "index": 18, "kind": "parameter", "displayName": "Timeout", "group": "common", "label": "common", "required": false, "type": "integer", "javaType": "long", "deprecated": false, "autowired": false, "secret": false, "defaultValue": -1, "configurationClass": "org.apache.camel.component.dynamicrouter.routing.DynamicRouterConfiguration", "configurationField": "configuration", "description": "Sets a total timeout specified in millis, when using parallel processing. If the Dyna [...] "warnDroppedMessage": { "index": 19, "kind": "parameter", "displayName": "Warn Dropped Message", "group": "common", "label": "common", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "configurationClass": "org.apache.camel.component.dynamicrouter.routing.DynamicRouterConfiguration", "configurationField": "configuration", "description": "Flag to log a warning if no predicates match for an exc [...] - "lazyStartProducer": { "index": 20, "kind": "parameter", "displayName": "Lazy Start Producer", "group": "producer (advanced)", "label": "producer,advanced", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "Whether the producer should be started lazy (on the first message). By starting lazy you can use this to allow CamelContext and routes to startup in situations where a produ [...] + "lazyStartProducer": { "index": 20, "kind": "parameter", "displayName": "Lazy Start Producer", "group": "producer (advanced)", "label": "producer,advanced", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "Whether the producer should be started lazy (on the first message). By starting lazy you can use this to allow CamelContext and routes to startup in situations where a produ [...] + "allowedSchemes": { "index": 21, "kind": "parameter", "displayName": "Allowed Schemes", "group": "security", "label": "advanced,security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.dynamicrouter.routing.DynamicRouterConfiguration", "configurationField": "configuration", "description": "Sets an optional comma-separated allow-list of component schemes t [...] } } diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/dynamic-router-component.adoc b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/dynamic-router-component.adoc index 826097176c52..1aa7f2b2d187 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/dynamic-router-component.adoc +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/dynamic-router-component.adoc @@ -318,7 +318,18 @@ producerTemplate.sendBody("kafka://subscriptions", mapper.writeValueAsString(con ---- In another module, additional routing will serve as a bridge to get the message from Kafka to the control channel of the -Dynamic Router: +Dynamic Router. The subscriptions above carry their predicate in the control message, so the bridge has to opt in with +`allowPredicateFromMessage=true`. + +[WARNING] +==== +`allowPredicateFromMessage` lets whoever puts a message on the bridged transport choose both the expression language and +the expression that the Dynamic Router compiles and evaluates, which means it decides what runs inside the Camel +process. Only enable it when the transport that feeds the control channel is trusted. When it is not, have the +participants subscribe with `predicateBean`, which selects a `Predicate` that the route author bound in the registry, +and leave the flag at its default of `false`. See +xref:dynamic-router-control-component.adoc[the control component documentation] for the alternatives. +==== .Bridge from Kafka to the Dynamic Router control channel @@ -330,7 +341,7 @@ Java:: ---- from("kafka:subscriptions") .unmarshal().json(DynamicRouterControlMessage.class) - .to("dynamic-router-control:subscribe"); + .to("dynamic-router-control:subscribe?allowPredicateFromMessage=true"); ---- XML:: @@ -342,7 +353,7 @@ XML:: <unmarshal> <json library="Jackson" unmarshalType="org.apache.camel.component.dynamicrouter.control.DynamicRouterControlMessage"/> </unmarshal> - <to uri="dynamic-router-control:subscribe"/> + <to uri="dynamic-router-control:subscribe?allowPredicateFromMessage=true"/> </route> ---- @@ -360,6 +371,8 @@ YAML:: unmarshalType: org.apache.camel.component.dynamicrouter.control.DynamicRouterControlMessage - to: uri: dynamic-router-control:subscribe + parameters: + allowPredicateFromMessage: true ---- ==== diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/dynamic-router-control-component.adoc b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/dynamic-router-control-component.adoc index 6f7ee90aae83..ec0eeb535aa8 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/dynamic-router-control-component.adoc +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/dynamic-router-control-component.adoc @@ -30,6 +30,37 @@ include::partial$component-endpoint-options.adoc[] include::partial$component-endpoint-headers.adoc[] // component options: END +== Supplying the subscription predicate + +A subscription needs a predicate that decides which exchanges the participant receives. There are three ways to supply +one, and they differ in who chooses it: + +* `predicateBean` -- the name of a `Predicate` bound in the registry. The route author decides which predicates exist, +and the control message only selects one of them by name. +* `predicate` and `expressionLanguage` as control endpoint URI parameters -- the route author writes the expression, and +every subscription made through that endpoint uses it. +* `predicate` and `expressionLanguage` carried in the control message, in its body or its headers -- the sender of the +control message chooses both the expression language and the expression. + +The third form is disabled by default. The predicate is compiled and then evaluated against every exchange on the +channel, so letting the control message choose both the language and the expression means the sender of that message +decides what runs inside the Camel process. Set `allowPredicateFromMessage=true` on the control endpoint to enable it, +and only do so when control messages can only come from a trusted source: + +[source,java] +---- +from("kafka:subscriptions") + .unmarshal().json(DynamicRouterControlMessage.class) + .to("dynamic-router-control:subscribe?allowPredicateFromMessage=true"); +---- + +When `allowPredicateFromMessage` is `false`, which is the default, a control message that supplies a `predicate` or an +`expressionLanguage` is rejected with an `IllegalArgumentException`. Prefer `predicateBean`, or configure the predicate +on the endpoint, when control messages arrive from outside the application. + +Subscription parameters that the control message does not carry fall back to the values configured on the control +endpoint, so a participant can send only the parameters that identify it and let the endpoint supply the rest. + == Subscribing Subscribing can be achieved by using query parameters in the control endpoint URI, or by sending a diff --git a/components/camel-dynamic-router/src/generated/java/org/apache/camel/component/dynamicrouter/control/DynamicRouterControlEndpointConfigurer.java b/components/camel-dynamic-router/src/generated/java/org/apache/camel/component/dynamicrouter/control/DynamicRouterControlEndpointConfigurer.java index 65978aadf7f5..6eaaebe11bff 100644 --- a/components/camel-dynamic-router/src/generated/java/org/apache/camel/component/dynamicrouter/control/DynamicRouterControlEndpointConfigurer.java +++ b/components/camel-dynamic-router/src/generated/java/org/apache/camel/component/dynamicrouter/control/DynamicRouterControlEndpointConfigurer.java @@ -23,6 +23,8 @@ public class DynamicRouterControlEndpointConfigurer extends PropertyConfigurerSu public boolean configure(CamelContext camelContext, Object obj, String name, Object value, boolean ignoreCase) { DynamicRouterControlEndpoint target = (DynamicRouterControlEndpoint) obj; switch (ignoreCase ? name.toLowerCase() : name) { + case "allowpredicatefrommessage": + case "allowPredicateFromMessage": target.getConfiguration().setAllowPredicateFromMessage(property(camelContext, boolean.class, value)); return true; case "destinationuri": case "destinationUri": target.getConfiguration().setDestinationUri(property(camelContext, java.lang.String.class, value)); return true; case "expressionlanguage": @@ -44,6 +46,8 @@ public class DynamicRouterControlEndpointConfigurer extends PropertyConfigurerSu @Override public Class<?> getOptionType(String name, boolean ignoreCase) { switch (ignoreCase ? name.toLowerCase() : name) { + case "allowpredicatefrommessage": + case "allowPredicateFromMessage": return boolean.class; case "destinationuri": case "destinationUri": return java.lang.String.class; case "expressionlanguage": @@ -66,6 +70,8 @@ public class DynamicRouterControlEndpointConfigurer extends PropertyConfigurerSu public Object getOptionValue(Object obj, String name, boolean ignoreCase) { DynamicRouterControlEndpoint target = (DynamicRouterControlEndpoint) obj; switch (ignoreCase ? name.toLowerCase() : name) { + case "allowpredicatefrommessage": + case "allowPredicateFromMessage": return target.getConfiguration().isAllowPredicateFromMessage(); case "destinationuri": case "destinationUri": return target.getConfiguration().getDestinationUri(); case "expressionlanguage": diff --git a/components/camel-dynamic-router/src/generated/java/org/apache/camel/component/dynamicrouter/control/DynamicRouterControlEndpointUriFactory.java b/components/camel-dynamic-router/src/generated/java/org/apache/camel/component/dynamicrouter/control/DynamicRouterControlEndpointUriFactory.java index 98313723392b..42772d430286 100644 --- a/components/camel-dynamic-router/src/generated/java/org/apache/camel/component/dynamicrouter/control/DynamicRouterControlEndpointUriFactory.java +++ b/components/camel-dynamic-router/src/generated/java/org/apache/camel/component/dynamicrouter/control/DynamicRouterControlEndpointUriFactory.java @@ -24,7 +24,8 @@ public class DynamicRouterControlEndpointUriFactory extends org.apache.camel.sup private static final Set<String> ENDPOINT_IDENTITY_PROPERTY_NAMES; private static final Map<String, String> MULTI_VALUE_PREFIXES; static { - Set<String> props = new HashSet<>(9); + Set<String> props = new HashSet<>(10); + props.add("allowPredicateFromMessage"); props.add("controlAction"); props.add("destinationUri"); props.add("expressionLanguage"); diff --git a/components/camel-dynamic-router/src/generated/java/org/apache/camel/component/dynamicrouter/routing/DynamicRouterEndpointConfigurer.java b/components/camel-dynamic-router/src/generated/java/org/apache/camel/component/dynamicrouter/routing/DynamicRouterEndpointConfigurer.java index d1a144748b73..968bbd8dd52d 100644 --- a/components/camel-dynamic-router/src/generated/java/org/apache/camel/component/dynamicrouter/routing/DynamicRouterEndpointConfigurer.java +++ b/components/camel-dynamic-router/src/generated/java/org/apache/camel/component/dynamicrouter/routing/DynamicRouterEndpointConfigurer.java @@ -31,6 +31,8 @@ public class DynamicRouterEndpointConfigurer extends PropertyConfigurerSupport i case "aggregationStrategyMethodAllowNull": target.getConfiguration().setAggregationStrategyMethodAllowNull(property(camelContext, boolean.class, value)); return true; case "aggregationstrategymethodname": case "aggregationStrategyMethodName": target.getConfiguration().setAggregationStrategyMethodName(property(camelContext, java.lang.String.class, value)); return true; + case "allowedschemes": + case "allowedSchemes": target.getConfiguration().setAllowedSchemes(property(camelContext, java.lang.String.class, value)); return true; case "cachesize": case "cacheSize": target.getConfiguration().setCacheSize(property(camelContext, int.class, value)); return true; case "executorservice": @@ -75,6 +77,8 @@ public class DynamicRouterEndpointConfigurer extends PropertyConfigurerSupport i case "aggregationStrategyMethodAllowNull": return boolean.class; case "aggregationstrategymethodname": case "aggregationStrategyMethodName": return java.lang.String.class; + case "allowedschemes": + case "allowedSchemes": return java.lang.String.class; case "cachesize": case "cacheSize": return int.class; case "executorservice": @@ -120,6 +124,8 @@ public class DynamicRouterEndpointConfigurer extends PropertyConfigurerSupport i case "aggregationStrategyMethodAllowNull": return target.getConfiguration().isAggregationStrategyMethodAllowNull(); case "aggregationstrategymethodname": case "aggregationStrategyMethodName": return target.getConfiguration().getAggregationStrategyMethodName(); + case "allowedschemes": + case "allowedSchemes": return target.getConfiguration().getAllowedSchemes(); case "cachesize": case "cacheSize": return target.getConfiguration().getCacheSize(); case "executorservice": diff --git a/components/camel-dynamic-router/src/generated/java/org/apache/camel/component/dynamicrouter/routing/DynamicRouterEndpointUriFactory.java b/components/camel-dynamic-router/src/generated/java/org/apache/camel/component/dynamicrouter/routing/DynamicRouterEndpointUriFactory.java index a8542b69adce..908eea5e9efd 100644 --- a/components/camel-dynamic-router/src/generated/java/org/apache/camel/component/dynamicrouter/routing/DynamicRouterEndpointUriFactory.java +++ b/components/camel-dynamic-router/src/generated/java/org/apache/camel/component/dynamicrouter/routing/DynamicRouterEndpointUriFactory.java @@ -24,11 +24,12 @@ public class DynamicRouterEndpointUriFactory extends org.apache.camel.support.co private static final Set<String> ENDPOINT_IDENTITY_PROPERTY_NAMES; private static final Map<String, String> MULTI_VALUE_PREFIXES; static { - Set<String> props = new HashSet<>(21); + Set<String> props = new HashSet<>(22); props.add("aggregationStrategy"); props.add("aggregationStrategyBean"); props.add("aggregationStrategyMethodAllowNull"); props.add("aggregationStrategyMethodName"); + props.add("allowedSchemes"); props.add("cacheSize"); props.add("channel"); props.add("executorService"); diff --git a/components/camel-dynamic-router/src/generated/resources/META-INF/org/apache/camel/component/dynamicrouter/control/dynamic-router-control.json b/components/camel-dynamic-router/src/generated/resources/META-INF/org/apache/camel/component/dynamicrouter/control/dynamic-router-control.json index c1e2d0b0372f..e94aa95905bc 100644 --- a/components/camel-dynamic-router/src/generated/resources/META-INF/org/apache/camel/component/dynamicrouter/control/dynamic-router-control.json +++ b/components/camel-dynamic-router/src/generated/resources/META-INF/org/apache/camel/component/dynamicrouter/control/dynamic-router-control.json @@ -46,6 +46,7 @@ "predicateBean": { "index": 5, "kind": "parameter", "displayName": "Predicate Bean", "group": "control", "label": "control", "required": false, "type": "object", "javaType": "org.apache.camel.Predicate", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.dynamicrouter.control.DynamicRouterControlConfiguration", "configurationField": "configuration", "description": "A Predicate instance in the registry." }, "priority": { "index": 6, "kind": "parameter", "displayName": "Priority", "group": "control", "label": "control", "required": false, "type": "integer", "javaType": "java.lang.Integer", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.dynamicrouter.control.DynamicRouterControlConfiguration", "configurationField": "configuration", "description": "The subscription priority." }, "subscribeChannel": { "index": 7, "kind": "parameter", "displayName": "Subscribe Channel", "group": "control", "label": "control", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.dynamicrouter.control.DynamicRouterControlConfiguration", "configurationField": "configuration", "description": "The channel to subscribe to" }, - "subscriptionId": { "index": 8, "kind": "parameter", "displayName": "Subscription Id", "group": "control", "label": "control", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.dynamicrouter.control.DynamicRouterControlConfiguration", "configurationField": "configuration", "description": "The subscription ID; if unspecified, one will be assigned and returned." } + "subscriptionId": { "index": 8, "kind": "parameter", "displayName": "Subscription Id", "group": "control", "label": "control", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.dynamicrouter.control.DynamicRouterControlConfiguration", "configurationField": "configuration", "description": "The subscription ID; if unspecified, one will be assigned and returned." }, + "allowPredicateFromMessage": { "index": 9, "kind": "parameter", "displayName": "Allow Predicate From Message", "group": "security", "label": "control,security", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "security": "insecure:dev", "insecureValue": "true", "defaultValue": false, "configurationClass": "org.apache.camel.component.dynamicrouter.control.DynamicRouterControlConfiguration", "configurationField": "c [...] } } diff --git a/components/camel-dynamic-router/src/generated/resources/META-INF/org/apache/camel/component/dynamicrouter/routing/dynamic-router.json b/components/camel-dynamic-router/src/generated/resources/META-INF/org/apache/camel/component/dynamicrouter/routing/dynamic-router.json index 088f2b008feb..3e992a75968a 100644 --- a/components/camel-dynamic-router/src/generated/resources/META-INF/org/apache/camel/component/dynamicrouter/routing/dynamic-router.json +++ b/components/camel-dynamic-router/src/generated/resources/META-INF/org/apache/camel/component/dynamicrouter/routing/dynamic-router.json @@ -48,6 +48,7 @@ "synchronous": { "index": 17, "kind": "parameter", "displayName": "Synchronous", "group": "common", "label": "common", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "configurationClass": "org.apache.camel.component.dynamicrouter.routing.DynamicRouterConfiguration", "configurationField": "configuration", "description": "Sets whether synchronous processing should be strictly used. When enabl [...] "timeout": { "index": 18, "kind": "parameter", "displayName": "Timeout", "group": "common", "label": "common", "required": false, "type": "integer", "javaType": "long", "deprecated": false, "autowired": false, "secret": false, "defaultValue": -1, "configurationClass": "org.apache.camel.component.dynamicrouter.routing.DynamicRouterConfiguration", "configurationField": "configuration", "description": "Sets a total timeout specified in millis, when using parallel processing. If the Dyna [...] "warnDroppedMessage": { "index": 19, "kind": "parameter", "displayName": "Warn Dropped Message", "group": "common", "label": "common", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "configurationClass": "org.apache.camel.component.dynamicrouter.routing.DynamicRouterConfiguration", "configurationField": "configuration", "description": "Flag to log a warning if no predicates match for an exc [...] - "lazyStartProducer": { "index": 20, "kind": "parameter", "displayName": "Lazy Start Producer", "group": "producer (advanced)", "label": "producer,advanced", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "Whether the producer should be started lazy (on the first message). By starting lazy you can use this to allow CamelContext and routes to startup in situations where a produ [...] + "lazyStartProducer": { "index": 20, "kind": "parameter", "displayName": "Lazy Start Producer", "group": "producer (advanced)", "label": "producer,advanced", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "Whether the producer should be started lazy (on the first message). By starting lazy you can use this to allow CamelContext and routes to startup in situations where a produ [...] + "allowedSchemes": { "index": 21, "kind": "parameter", "displayName": "Allowed Schemes", "group": "security", "label": "advanced,security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.dynamicrouter.routing.DynamicRouterConfiguration", "configurationField": "configuration", "description": "Sets an optional comma-separated allow-list of component schemes t [...] } } diff --git a/components/camel-dynamic-router/src/main/docs/dynamic-router-component.adoc b/components/camel-dynamic-router/src/main/docs/dynamic-router-component.adoc index 826097176c52..1aa7f2b2d187 100644 --- a/components/camel-dynamic-router/src/main/docs/dynamic-router-component.adoc +++ b/components/camel-dynamic-router/src/main/docs/dynamic-router-component.adoc @@ -318,7 +318,18 @@ producerTemplate.sendBody("kafka://subscriptions", mapper.writeValueAsString(con ---- In another module, additional routing will serve as a bridge to get the message from Kafka to the control channel of the -Dynamic Router: +Dynamic Router. The subscriptions above carry their predicate in the control message, so the bridge has to opt in with +`allowPredicateFromMessage=true`. + +[WARNING] +==== +`allowPredicateFromMessage` lets whoever puts a message on the bridged transport choose both the expression language and +the expression that the Dynamic Router compiles and evaluates, which means it decides what runs inside the Camel +process. Only enable it when the transport that feeds the control channel is trusted. When it is not, have the +participants subscribe with `predicateBean`, which selects a `Predicate` that the route author bound in the registry, +and leave the flag at its default of `false`. See +xref:dynamic-router-control-component.adoc[the control component documentation] for the alternatives. +==== .Bridge from Kafka to the Dynamic Router control channel @@ -330,7 +341,7 @@ Java:: ---- from("kafka:subscriptions") .unmarshal().json(DynamicRouterControlMessage.class) - .to("dynamic-router-control:subscribe"); + .to("dynamic-router-control:subscribe?allowPredicateFromMessage=true"); ---- XML:: @@ -342,7 +353,7 @@ XML:: <unmarshal> <json library="Jackson" unmarshalType="org.apache.camel.component.dynamicrouter.control.DynamicRouterControlMessage"/> </unmarshal> - <to uri="dynamic-router-control:subscribe"/> + <to uri="dynamic-router-control:subscribe?allowPredicateFromMessage=true"/> </route> ---- @@ -360,6 +371,8 @@ YAML:: unmarshalType: org.apache.camel.component.dynamicrouter.control.DynamicRouterControlMessage - to: uri: dynamic-router-control:subscribe + parameters: + allowPredicateFromMessage: true ---- ==== diff --git a/components/camel-dynamic-router/src/main/docs/dynamic-router-control-component.adoc b/components/camel-dynamic-router/src/main/docs/dynamic-router-control-component.adoc index 6f7ee90aae83..ec0eeb535aa8 100644 --- a/components/camel-dynamic-router/src/main/docs/dynamic-router-control-component.adoc +++ b/components/camel-dynamic-router/src/main/docs/dynamic-router-control-component.adoc @@ -30,6 +30,37 @@ include::partial$component-endpoint-options.adoc[] include::partial$component-endpoint-headers.adoc[] // component options: END +== Supplying the subscription predicate + +A subscription needs a predicate that decides which exchanges the participant receives. There are three ways to supply +one, and they differ in who chooses it: + +* `predicateBean` -- the name of a `Predicate` bound in the registry. The route author decides which predicates exist, +and the control message only selects one of them by name. +* `predicate` and `expressionLanguage` as control endpoint URI parameters -- the route author writes the expression, and +every subscription made through that endpoint uses it. +* `predicate` and `expressionLanguage` carried in the control message, in its body or its headers -- the sender of the +control message chooses both the expression language and the expression. + +The third form is disabled by default. The predicate is compiled and then evaluated against every exchange on the +channel, so letting the control message choose both the language and the expression means the sender of that message +decides what runs inside the Camel process. Set `allowPredicateFromMessage=true` on the control endpoint to enable it, +and only do so when control messages can only come from a trusted source: + +[source,java] +---- +from("kafka:subscriptions") + .unmarshal().json(DynamicRouterControlMessage.class) + .to("dynamic-router-control:subscribe?allowPredicateFromMessage=true"); +---- + +When `allowPredicateFromMessage` is `false`, which is the default, a control message that supplies a `predicate` or an +`expressionLanguage` is rejected with an `IllegalArgumentException`. Prefer `predicateBean`, or configure the predicate +on the endpoint, when control messages arrive from outside the application. + +Subscription parameters that the control message does not carry fall back to the values configured on the control +endpoint, so a participant can send only the parameters that identify it and let the endpoint supply the rest. + == Subscribing Subscribing can be achieved by using query parameters in the control endpoint URI, or by sending a diff --git a/components/camel-dynamic-router/src/main/java/org/apache/camel/component/dynamicrouter/control/DynamicRouterControlChannelSendDynamicAware.java b/components/camel-dynamic-router/src/main/java/org/apache/camel/component/dynamicrouter/control/DynamicRouterControlChannelSendDynamicAware.java index 13662241b271..3e5d0a79731d 100644 --- a/components/camel-dynamic-router/src/main/java/org/apache/camel/component/dynamicrouter/control/DynamicRouterControlChannelSendDynamicAware.java +++ b/components/camel-dynamic-router/src/main/java/org/apache/camel/component/dynamicrouter/control/DynamicRouterControlChannelSendDynamicAware.java @@ -28,6 +28,7 @@ import org.apache.camel.spi.annotations.SendDynamic; import org.apache.camel.support.component.SendDynamicAwareSupport; import org.apache.camel.util.URISupport; +import static org.apache.camel.component.dynamicrouter.control.DynamicRouterControlConstants.ALLOW_PREDICATE_FROM_MESSAGE_PROPERTY; import static org.apache.camel.component.dynamicrouter.control.DynamicRouterControlConstants.COMPONENT_SCHEME_CONTROL; import static org.apache.camel.component.dynamicrouter.control.DynamicRouterControlConstants.CONTROL_ACTION_PROPERTY; @@ -95,6 +96,14 @@ public class DynamicRouterControlChannelSendDynamicAware extends SendDynamicAwar String uri = entry.getUri(); if (DynamicRouterControlConstants.SHOULD_OPTIMIZE.test(uri)) { optimizedUri = URISupport.stripQuery(uri); + // The subscription parameters are carried as headers, but "allowPredicateFromMessage" decides whether a + // control message may supply the predicate at all, so it has to stay on the endpoint, where only the + // route author can set it. Stripping it here would make the endpoint fall back to the default of false. + Object allowPredicateFromMessage = entry.getProperties().get(ALLOW_PREDICATE_FROM_MESSAGE_PROPERTY); + if (allowPredicateFromMessage != null) { + optimizedUri = optimizedUri + "?" + ALLOW_PREDICATE_FROM_MESSAGE_PROPERTY + "=" + + allowPredicateFromMessage; + } } return optimizedUri; } diff --git a/components/camel-dynamic-router/src/main/java/org/apache/camel/component/dynamicrouter/control/DynamicRouterControlConfiguration.java b/components/camel-dynamic-router/src/main/java/org/apache/camel/component/dynamicrouter/control/DynamicRouterControlConfiguration.java index 27a02fbe1684..875820f8d7be 100644 --- a/components/camel-dynamic-router/src/main/java/org/apache/camel/component/dynamicrouter/control/DynamicRouterControlConfiguration.java +++ b/components/camel-dynamic-router/src/main/java/org/apache/camel/component/dynamicrouter/control/DynamicRouterControlConfiguration.java @@ -80,6 +80,19 @@ public class DynamicRouterControlConfiguration { @UriParam(label = "control", defaultValue = "simple", description = "The subscription predicate language.") private String expressionLanguage = "simple"; + /** + * Whether the subscription predicate, and the language used to compile it, may be taken from the incoming control + * message. + */ + @UriParam(label = "control,security", defaultValue = "false", security = "insecure:dev", insecureValue = "true", + description = "Whether the subscription predicate, and the language used to compile it, may be taken" + + " from the incoming control message. When disabled, the predicate and expressionLanguage" + + " configured on this endpoint are used instead. Enabling this lets the sender of a" + + " control message choose both the expression language and the expression that the" + + " Dynamic Router compiles into a live predicate, so only enable it when control messages" + + " come from a trusted source.") + private boolean allowPredicateFromMessage; + /** * The control action (subscribe or unsubscribe). * @@ -237,6 +250,26 @@ public class DynamicRouterControlConfiguration { this.expressionLanguage = expressionLanguage; } + /** + * Whether the subscription predicate, and the language used to compile it, may be taken from the incoming control + * message. + * + * @return true if the control message may supply the predicate + */ + public boolean isAllowPredicateFromMessage() { + return allowPredicateFromMessage; + } + + /** + * Whether the subscription predicate, and the language used to compile it, may be taken from the incoming control + * message. + * + * @param allowPredicateFromMessage true if the control message may supply the predicate + */ + public void setAllowPredicateFromMessage(final boolean allowPredicateFromMessage) { + this.allowPredicateFromMessage = allowPredicateFromMessage; + } + public DynamicRouterControlConfiguration() { // Default constructor is empty because we need an instance, and // then things can be populated through mutator methods. diff --git a/components/camel-dynamic-router/src/main/java/org/apache/camel/component/dynamicrouter/control/DynamicRouterControlConstants.java b/components/camel-dynamic-router/src/main/java/org/apache/camel/component/dynamicrouter/control/DynamicRouterControlConstants.java index c5516973453d..196cb80f8174 100644 --- a/components/camel-dynamic-router/src/main/java/org/apache/camel/component/dynamicrouter/control/DynamicRouterControlConstants.java +++ b/components/camel-dynamic-router/src/main/java/org/apache/camel/component/dynamicrouter/control/DynamicRouterControlConstants.java @@ -124,6 +124,15 @@ public final class DynamicRouterControlConstants { */ public static final String ERROR_NO_PREDICATE_BEAN_FOUND = "Predicate bean could not be found"; + /** + * Error when a control message supplies a predicate expression, or the language to compile it with, but the control + * endpoint does not allow those to be taken from a control message. + */ + public static final String ERROR_PREDICATE_FROM_MESSAGE_NOT_ALLOWED + = "The subscription predicate expression, and the language used to compile it, cannot be taken from a " + + "control message. Configure 'predicate' and 'expressionLanguage' on the dynamic-router-control " + + "endpoint, or set 'allowPredicateFromMessage=true' to let the control message supply them."; + /** * The configuration property for the control channel action. */ @@ -164,6 +173,13 @@ public final class DynamicRouterControlConstants { */ public static final String EXPRESSION_LANGUAGE_PROPERTY = "expressionLanguage"; + /** + * The configuration property that controls whether the predicate may be taken from a control message. Unlike the + * other properties, this one is deliberately absent from {@link #URI_PARAMS_TO_HEADER_NAMES}: it must stay on the + * endpoint, where only the route author can set it, and must never be settable from a message. + */ + public static final String ALLOW_PREDICATE_FROM_MESSAGE_PROPERTY = "allowPredicateFromMessage"; + /** * Header name for the control action. */ diff --git a/components/camel-dynamic-router/src/main/java/org/apache/camel/component/dynamicrouter/control/DynamicRouterControlProducer.java b/components/camel-dynamic-router/src/main/java/org/apache/camel/component/dynamicrouter/control/DynamicRouterControlProducer.java index 178cd0903390..658969e6af3d 100644 --- a/components/camel-dynamic-router/src/main/java/org/apache/camel/component/dynamicrouter/control/DynamicRouterControlProducer.java +++ b/components/camel-dynamic-router/src/main/java/org/apache/camel/component/dynamicrouter/control/DynamicRouterControlProducer.java @@ -42,6 +42,7 @@ import static org.apache.camel.component.dynamicrouter.control.DynamicRouterCont import static org.apache.camel.component.dynamicrouter.control.DynamicRouterControlConstants.CONTROL_SUBSCRIBE_CHANNEL; import static org.apache.camel.component.dynamicrouter.control.DynamicRouterControlConstants.CONTROL_SUBSCRIPTION_ID; import static org.apache.camel.component.dynamicrouter.control.DynamicRouterControlConstants.ERROR_NO_PREDICATE_BEAN_FOUND; +import static org.apache.camel.component.dynamicrouter.control.DynamicRouterControlConstants.ERROR_PREDICATE_FROM_MESSAGE_NOT_ALLOWED; import static org.apache.camel.component.dynamicrouter.control.DynamicRouterControlConstants.SIMPLE_LANGUAGE; /** @@ -74,29 +75,57 @@ public class DynamicRouterControlProducer extends HeaderSelectorProducer { this.configuration = configuration; } + /** + * Returns the value to use for a predicate parameter that a control message may try to supply. Unless the endpoint + * explicitly allows the predicate to come from the message, a message-supplied value is rejected, and the value + * configured on the endpoint is used instead. Without this, the sender of a control message would choose both the + * expression language and the expression that the Dynamic Router compiles into a live predicate. + * + * @param allowFromMessage whether the endpoint allows the predicate to come from the control message + * @param messageValue the value found in the control message, if any + * @param configValue the value configured on the endpoint + * @return the value to use + */ + static String predicateValueToUse( + final boolean allowFromMessage, final String messageValue, final String configValue) { + if (allowFromMessage) { + return messageValue; + } + if (ObjectHelper.isNotEmpty(messageValue)) { + throw new IllegalArgumentException(ERROR_PREDICATE_FROM_MESSAGE_NOT_ALLOWED); + } + return configValue; + } + /** * Create a filter from parameters in the message body. * * @param dynamicRouterControlService the {@link DynamicRouterControlService} + * @param configuration the configuration for the Dynamic Router Control * @param message the message, where the body contains a control message * @param update whether to update an existing filter (true) or add a new one (false) * @return the ID of the added filter */ static String subscribeFromMessage( final DynamicRouterControlService dynamicRouterControlService, + final DynamicRouterControlConfiguration configuration, final Message message, final boolean update) { DynamicRouterControlMessage messageBody = message.getBody(DynamicRouterControlMessage.class); String subscriptionId = messageBody.getSubscriptionId(); String subscribeChannel = messageBody.getSubscribeChannel(); String destinationUri = messageBody.getDestinationUri(); String priority = String.valueOf(messageBody.getPriority()); - String predicate = messageBody.getPredicate(); String predicateBean = messageBody.getPredicateBean(); - String expressionLanguage = messageBody.getExpressionLanguage(); if (ObjectHelper.isNotEmpty(predicateBean)) { return dynamicRouterControlService.subscribeWithPredicateBean(subscribeChannel, subscriptionId, destinationUri, Integer.parseInt(priority), predicateBean, update); - } else if (ObjectHelper.isNotEmpty(predicate) && ObjectHelper.isNotEmpty(expressionLanguage)) { + } + boolean allowFromMessage = configuration.isAllowPredicateFromMessage(); + String predicate = predicateValueToUse(allowFromMessage, messageBody.getPredicate(), + configuration.getPredicate()); + String expressionLanguage = predicateValueToUse(allowFromMessage, messageBody.getExpressionLanguage(), + configuration.getExpressionLanguage()); + if (ObjectHelper.isNotEmpty(predicate) && ObjectHelper.isNotEmpty(expressionLanguage)) { return dynamicRouterControlService.subscribeWithPredicateExpression(subscribeChannel, subscriptionId, destinationUri, Integer.parseInt(priority), predicate, expressionLanguage, update); } else { @@ -105,29 +134,38 @@ public class DynamicRouterControlProducer extends HeaderSelectorProducer { } /** - * Create a filter from parameters in message headers. + * Create a filter from parameters in message headers, falling back to the values configured on the endpoint for any + * parameter that the message does not carry. * * @param dynamicRouterControlService the {@link DynamicRouterControlService} + * @param configuration the configuration for the Dynamic Router Control * @param message the message, where the headers contain subscription params * @param update whether to update an existing filter (true) or add a new one (false) * @return the ID of the added filter */ static String subscribeFromHeaders( final DynamicRouterControlService dynamicRouterControlService, + final DynamicRouterControlConfiguration configuration, final Message message, final boolean update) { Map<String, Object> headers = message.getHeaders(); - String subscriptionId = (String) headers.get(CONTROL_SUBSCRIPTION_ID); - String subscribeChannel = (String) headers.get(CONTROL_SUBSCRIBE_CHANNEL); - String destinationUri = (String) headers.get(CONTROL_DESTINATION_URI); - String priority = String.valueOf(headers.get(CONTROL_PRIORITY)); - String predicate = (String) headers.get(CONTROL_PREDICATE); + String subscriptionId = (String) headers.getOrDefault(CONTROL_SUBSCRIPTION_ID, configuration.getSubscriptionId()); + String subscribeChannel + = (String) headers.getOrDefault(CONTROL_SUBSCRIBE_CHANNEL, configuration.getSubscribeChannel()); + String destinationUri = (String) headers.getOrDefault(CONTROL_DESTINATION_URI, configuration.getDestinationUri()); + String priority = String.valueOf(headers.getOrDefault(CONTROL_PRIORITY, configuration.getPriority())); String predicateBean = (String) headers.get(CONTROL_PREDICATE_BEAN); - String expressionLanguage = Optional.ofNullable((String) headers.get(CONTROL_EXPRESSION_LANGUAGE)) - .orElse(SIMPLE_LANGUAGE); if (ObjectHelper.isNotEmpty(predicateBean)) { return dynamicRouterControlService.subscribeWithPredicateBean(subscribeChannel, subscriptionId, destinationUri, Integer.parseInt(priority), predicateBean, update); - } else if (ObjectHelper.isNotEmpty(predicate) && ObjectHelper.isNotEmpty(expressionLanguage)) { + } + boolean allowFromMessage = configuration.isAllowPredicateFromMessage(); + String predicate = predicateValueToUse(allowFromMessage, (String) headers.get(CONTROL_PREDICATE), + configuration.getPredicate()); + String expressionLanguage = Optional + .ofNullable(predicateValueToUse(allowFromMessage, (String) headers.get(CONTROL_EXPRESSION_LANGUAGE), + configuration.getExpressionLanguage())) + .orElse(SIMPLE_LANGUAGE); + if (ObjectHelper.isNotEmpty(predicate) && ObjectHelper.isNotEmpty(expressionLanguage)) { return dynamicRouterControlService.subscribeWithPredicateExpression(subscribeChannel, subscriptionId, destinationUri, Integer.parseInt(priority), predicate, expressionLanguage, update); } else { @@ -147,9 +185,9 @@ public class DynamicRouterControlProducer extends HeaderSelectorProducer { public void performSubscribe(final Message message, AsyncCallback callback) { String filterId; if (message.getBody() instanceof DynamicRouterControlMessage) { - filterId = subscribeFromMessage(dynamicRouterControlService, message, false); + filterId = subscribeFromMessage(dynamicRouterControlService, configuration, message, false); } else { - filterId = subscribeFromHeaders(dynamicRouterControlService, message, false); + filterId = subscribeFromHeaders(dynamicRouterControlService, configuration, message, false); } message.setBody(filterId); callback.done(false); @@ -190,9 +228,9 @@ public class DynamicRouterControlProducer extends HeaderSelectorProducer { public void performUpdate(final Message message, AsyncCallback callback) { String filterId; if (message.getBody() instanceof DynamicRouterControlMessage) { - filterId = subscribeFromMessage(dynamicRouterControlService, message, true); + filterId = subscribeFromMessage(dynamicRouterControlService, configuration, message, true); } else { - filterId = subscribeFromHeaders(dynamicRouterControlService, message, true); + filterId = subscribeFromHeaders(dynamicRouterControlService, configuration, message, true); } message.setBody(filterId); callback.done(false); diff --git a/components/camel-dynamic-router/src/main/java/org/apache/camel/component/dynamicrouter/routing/DynamicRouterConfiguration.java b/components/camel-dynamic-router/src/main/java/org/apache/camel/component/dynamicrouter/routing/DynamicRouterConfiguration.java index e8d67b232cb8..a2b0bd203d29 100644 --- a/components/camel-dynamic-router/src/main/java/org/apache/camel/component/dynamicrouter/routing/DynamicRouterConfiguration.java +++ b/components/camel-dynamic-router/src/main/java/org/apache/camel/component/dynamicrouter/routing/DynamicRouterConfiguration.java @@ -103,6 +103,16 @@ public class DynamicRouterConfiguration { @UriParam(label = "common", defaultValue = "false") private boolean ignoreInvalidEndpoints; + /** + * An optional comma-separated allow-list of component schemes that a subscription destination may resolve to. + */ + @UriParam(label = "advanced,security", + description = "Sets an optional comma-separated allow-list of component schemes that a subscription" + + " destination may resolve to (e.g. http,https). When set, a subscription whose" + + " destination URI uses a scheme that is not in the list is rejected. By default (unset)" + + " any scheme is allowed.") + private String allowedSchemes; + /** * If enabled, then Camel will process replies out-of-order (e.g., in the order they come back). If disabled, Camel * will process replies in the same order as defined by the multicast. @@ -315,6 +325,24 @@ public class DynamicRouterConfiguration { this.ignoreInvalidEndpoints = ignoreInvalidEndpoints; } + /** + * An optional comma-separated allow-list of component schemes that a subscription destination may resolve to. + * + * @return the allowed schemes, or null if any scheme is allowed + */ + public String getAllowedSchemes() { + return allowedSchemes; + } + + /** + * An optional comma-separated allow-list of component schemes that a subscription destination may resolve to. + * + * @param allowedSchemes the allowed schemes + */ + public void setAllowedSchemes(String allowedSchemes) { + this.allowedSchemes = allowedSchemes; + } + /** * Gets whether parallel processing is enabled. * diff --git a/components/camel-dynamic-router/src/main/java/org/apache/camel/component/dynamicrouter/routing/DynamicRouterRecipientListHelper.java b/components/camel-dynamic-router/src/main/java/org/apache/camel/component/dynamicrouter/routing/DynamicRouterRecipientListHelper.java index 414a16814e07..31bb6e2de5a1 100644 --- a/components/camel-dynamic-router/src/main/java/org/apache/camel/component/dynamicrouter/routing/DynamicRouterRecipientListHelper.java +++ b/components/camel-dynamic-router/src/main/java/org/apache/camel/component/dynamicrouter/routing/DynamicRouterRecipientListHelper.java @@ -124,6 +124,7 @@ public final class DynamicRouterRecipientListHelper { recipientList.setShareUnitOfWork(cfg.isShareUnitOfWork()); recipientList.setStopOnException(cfg.isStopOnException()); recipientList.setIgnoreInvalidEndpoints(cfg.isIgnoreInvalidEndpoints()); + recipientList.setAllowedSchemes(cfg.getAllowedSchemes()); recipientList.setCacheSize(cfg.getCacheSize()); if (cfg.getOnPrepare() != null) { recipientList.setOnPrepare(mandatoryLookup(camelContext, cfg.getOnPrepare(), Processor.class)); diff --git a/components/camel-dynamic-router/src/test/java/org/apache/camel/component/dynamicrouter/control/DynamicRouterControlChannelSendDynamicAwareTest.java b/components/camel-dynamic-router/src/test/java/org/apache/camel/component/dynamicrouter/control/DynamicRouterControlChannelSendDynamicAwareTest.java index 634d942297b5..deb7e48b812d 100644 --- a/components/camel-dynamic-router/src/test/java/org/apache/camel/component/dynamicrouter/control/DynamicRouterControlChannelSendDynamicAwareTest.java +++ b/components/camel-dynamic-router/src/test/java/org/apache/camel/component/dynamicrouter/control/DynamicRouterControlChannelSendDynamicAwareTest.java @@ -68,6 +68,17 @@ class DynamicRouterControlChannelSendDynamicAwareTest { } } + @Test + void resolveStaticUriRetainsAllowPredicateFromMessage() throws Exception { + String originalUri = "dynamic-router-control:subscribe?subscriptionId=testSub1&allowPredicateFromMessage=true"; + String uri = "dynamic-router-control://subscribe?subscriptionId=testSub1&allowPredicateFromMessage=true"; + try (DynamicRouterControlChannelSendDynamicAware testSubject = new DynamicRouterControlChannelSendDynamicAware()) { + SendDynamicAware.DynamicAwareEntry entry = testSubject.prepare(exchange, uri, originalUri); + String result = testSubject.resolveStaticUri(exchange, entry); + assertEquals("dynamic-router-control://subscribe?allowPredicateFromMessage=true", result); + } + } + @Test void resolveStaticUriShouldNotOptimize() throws Exception { String originalUri = "dynamic-router-ctrl:subscribe?subscriptionId=testSub1"; diff --git a/components/camel-dynamic-router/src/test/java/org/apache/camel/component/dynamicrouter/control/DynamicRouterControlProducerTest.java b/components/camel-dynamic-router/src/test/java/org/apache/camel/component/dynamicrouter/control/DynamicRouterControlProducerTest.java index f86a48d55575..fad05c24c670 100644 --- a/components/camel-dynamic-router/src/test/java/org/apache/camel/component/dynamicrouter/control/DynamicRouterControlProducerTest.java +++ b/components/camel-dynamic-router/src/test/java/org/apache/camel/component/dynamicrouter/control/DynamicRouterControlProducerTest.java @@ -48,6 +48,7 @@ import static org.apache.camel.component.dynamicrouter.control.DynamicRouterCont import static org.apache.camel.component.dynamicrouter.control.DynamicRouterControlConstants.CONTROL_PRIORITY; import static org.apache.camel.component.dynamicrouter.control.DynamicRouterControlConstants.CONTROL_SUBSCRIBE_CHANNEL; import static org.apache.camel.component.dynamicrouter.control.DynamicRouterControlConstants.CONTROL_SUBSCRIPTION_ID; +import static org.apache.camel.component.dynamicrouter.control.DynamicRouterControlConstants.ERROR_PREDICATE_FROM_MESSAGE_NOT_ALLOWED; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Mockito.when; @@ -89,6 +90,7 @@ class DynamicRouterControlProducerTest { @Test void performSubscribeAction() { + when(configuration.isAllowPredicateFromMessage()).thenReturn(true); String subscribeChannel = "testChannel"; Map<String, Object> headers = Map.of( CONTROL_ACTION_HEADER, CONTROL_ACTION_SUBSCRIBE, @@ -107,6 +109,7 @@ class DynamicRouterControlProducerTest { @Test void performSubscribeActionWithEmptyExpressionLanguage() { + when(configuration.isAllowPredicateFromMessage()).thenReturn(true); String subscribeChannel = "testChannel"; Map<String, Object> headers = Map.of( CONTROL_ACTION_HEADER, CONTROL_ACTION_SUBSCRIBE, @@ -155,6 +158,7 @@ class DynamicRouterControlProducerTest { @Test void performUpdateAction() { + when(configuration.isAllowPredicateFromMessage()).thenReturn(true); // First, perform initial subscription String subscribeChannel = "testChannel"; Map<String, Object> headers = Map.of( @@ -229,6 +233,7 @@ class DynamicRouterControlProducerTest { @Test void performSubscribeActionWithMessageInBody() { + when(configuration.isAllowPredicateFromMessage()).thenReturn(true); String subscribeChannel = "testChannel"; DynamicRouterControlMessage subMsg = DynamicRouterControlMessage.Builder.newBuilder() .subscribeChannel(subscribeChannel) @@ -249,6 +254,7 @@ class DynamicRouterControlProducerTest { @Test void performSubscribeActionWithMessageInBodyWithEmptyExpressionLanguage() { + when(configuration.isAllowPredicateFromMessage()).thenReturn(true); String subscribeChannel = "testChannel"; DynamicRouterControlMessage subMsg = DynamicRouterControlMessage.Builder.newBuilder() .subscribeChannel(subscribeChannel) @@ -266,6 +272,7 @@ class DynamicRouterControlProducerTest { @Test void performSubscribeActionWithMessageInBodyWithEmptyExpression() { + when(configuration.isAllowPredicateFromMessage()).thenReturn(true); String subscribeChannel = "testChannel"; DynamicRouterControlMessage subMsg = DynamicRouterControlMessage.Builder.newBuilder() .subscribeChannel(subscribeChannel) @@ -302,6 +309,7 @@ class DynamicRouterControlProducerTest { @Test void performUpdateActionWithMessageInBody() { + when(configuration.isAllowPredicateFromMessage()).thenReturn(true); String subscribeChannel = "testChannel"; DynamicRouterControlMessage subMsg = DynamicRouterControlMessage.Builder.newBuilder() .subscribeChannel(subscribeChannel) @@ -320,6 +328,124 @@ class DynamicRouterControlProducerTest { subscribeChannel, "testId", "mock://test", 10, "true", "simple", true); } + @Test + void performSubscribeActionRejectsPredicateFromHeadersByDefault() { + Map<String, Object> headers = Map.of( + CONTROL_ACTION_HEADER, CONTROL_ACTION_SUBSCRIBE, + CONTROL_SUBSCRIBE_CHANNEL, "testChannel", + CONTROL_SUBSCRIPTION_ID, "testId", + CONTROL_DESTINATION_URI, "mock://test", + CONTROL_PREDICATE, "true", + CONTROL_EXPRESSION_LANGUAGE, "simple", + CONTROL_PRIORITY, 10); + when(message.getHeaders()).thenReturn(headers); + Exception ex = assertThrows(IllegalArgumentException.class, () -> producer.performSubscribe(message, callback)); + assertEquals(ERROR_PREDICATE_FROM_MESSAGE_NOT_ALLOWED, ex.getMessage()); + Mockito.verifyNoInteractions(controlService); + } + + @Test + void performSubscribeActionRejectsExpressionLanguageFromHeadersByDefault() { + Map<String, Object> headers = Map.of( + CONTROL_ACTION_HEADER, CONTROL_ACTION_SUBSCRIBE, + CONTROL_SUBSCRIBE_CHANNEL, "testChannel", + CONTROL_SUBSCRIPTION_ID, "testId", + CONTROL_DESTINATION_URI, "mock://test", + CONTROL_EXPRESSION_LANGUAGE, "simple", + CONTROL_PRIORITY, 10); + when(message.getHeaders()).thenReturn(headers); + Exception ex = assertThrows(IllegalArgumentException.class, () -> producer.performSubscribe(message, callback)); + assertEquals(ERROR_PREDICATE_FROM_MESSAGE_NOT_ALLOWED, ex.getMessage()); + Mockito.verifyNoInteractions(controlService); + } + + @Test + void performSubscribeActionRejectsPredicateFromMessageBodyByDefault() { + DynamicRouterControlMessage subMsg = DynamicRouterControlMessage.Builder.newBuilder() + .subscribeChannel("testChannel") + .subscriptionId("testId") + .destinationUri("mock://test") + .priority(10) + .predicate("true") + .expressionLanguage("simple") + .build(); + when(message.getBody()).thenReturn(subMsg); + when(message.getBody(DynamicRouterControlMessage.class)).thenReturn(subMsg); + Exception ex = assertThrows(IllegalArgumentException.class, () -> producer.performSubscribe(message, callback)); + assertEquals(ERROR_PREDICATE_FROM_MESSAGE_NOT_ALLOWED, ex.getMessage()); + Mockito.verifyNoInteractions(controlService); + } + + @Test + void performUpdateActionRejectsPredicateFromMessageBodyByDefault() { + DynamicRouterControlMessage subMsg = DynamicRouterControlMessage.Builder.newBuilder() + .subscribeChannel("testChannel") + .subscriptionId("testId") + .destinationUri("mock://test") + .priority(10) + .predicate("true") + .expressionLanguage("simple") + .build(); + when(message.getBody()).thenReturn(subMsg); + when(message.getBody(DynamicRouterControlMessage.class)).thenReturn(subMsg); + Exception ex = assertThrows(IllegalArgumentException.class, () -> producer.performUpdate(message, callback)); + assertEquals(ERROR_PREDICATE_FROM_MESSAGE_NOT_ALLOWED, ex.getMessage()); + Mockito.verifyNoInteractions(controlService); + } + + @Test + void performSubscribeActionUsesEndpointPredicateWhenMessageSuppliesNone() { + String subscribeChannel = "testChannel"; + Map<String, Object> headers = Map.of( + CONTROL_ACTION_HEADER, CONTROL_ACTION_SUBSCRIBE, + CONTROL_SUBSCRIBE_CHANNEL, subscribeChannel, + CONTROL_SUBSCRIPTION_ID, "testId", + CONTROL_DESTINATION_URI, "mock://test", + CONTROL_PRIORITY, 10); + when(message.getHeaders()).thenReturn(headers); + when(configuration.getPredicate()).thenReturn("true"); + when(configuration.getExpressionLanguage()).thenReturn("simple"); + Mockito.doNothing().when(callback).done(false); + producer.performSubscribe(message, callback); + Mockito.verify(controlService, Mockito.times(1)).subscribeWithPredicateExpression( + subscribeChannel, "testId", "mock://test", 10, "true", "simple", false); + } + + @Test + void performSubscribeActionFallsBackToEndpointParametersWhenHeadersAreAbsent() { + Map<String, Object> headers = Map.of(CONTROL_ACTION_HEADER, CONTROL_ACTION_SUBSCRIBE); + when(message.getHeaders()).thenReturn(headers); + when(configuration.getSubscribeChannel()).thenReturn("testChannel"); + when(configuration.getSubscriptionId()).thenReturn("testId"); + when(configuration.getDestinationUri()).thenReturn("mock://test"); + when(configuration.getPriority()).thenReturn(10); + when(configuration.getPredicate()).thenReturn("true"); + when(configuration.getExpressionLanguage()).thenReturn("simple"); + Mockito.doNothing().when(callback).done(false); + producer.performSubscribe(message, callback); + Mockito.verify(controlService, Mockito.times(1)).subscribeWithPredicateExpression( + "testChannel", "testId", "mock://test", 10, "true", "simple", false); + } + + @Test + void performSubscribeActionWithPredicateBeanIsUnaffectedByTheGate() { + String subscribeChannel = "testChannel"; + Map<String, Object> headers = Map.of( + CONTROL_ACTION_HEADER, CONTROL_ACTION_SUBSCRIBE, + CONTROL_SUBSCRIBE_CHANNEL, subscribeChannel, + CONTROL_SUBSCRIPTION_ID, "testId", + CONTROL_DESTINATION_URI, "mock://test", + CONTROL_PREDICATE, "true", + CONTROL_EXPRESSION_LANGUAGE, "simple", + CONTROL_PRIORITY, 10, + CONTROL_PREDICATE_BEAN, "testPredicate"); + when(message.getHeaders()).thenReturn(headers); + Mockito.doNothing().when(callback).done(false); + producer.performSubscribe(message, callback); + Mockito.verify(controlService, Mockito.times(1)).subscribeWithPredicateBean( + subscribeChannel, "testId", "mock://test", 10, "testPredicate", false); + } + @Test void testPerformListAction() { String subscribeChannel = "testChannel"; diff --git a/components/camel-dynamic-router/src/test/java/org/apache/camel/component/dynamicrouter/integration/DynamicRouterBasicSynchronousIT.java b/components/camel-dynamic-router/src/test/java/org/apache/camel/component/dynamicrouter/integration/DynamicRouterBasicSynchronousIT.java index da957ccdc46c..8b21c21b624d 100644 --- a/components/camel-dynamic-router/src/test/java/org/apache/camel/component/dynamicrouter/integration/DynamicRouterBasicSynchronousIT.java +++ b/components/camel-dynamic-router/src/test/java/org/apache/camel/component/dynamicrouter/integration/DynamicRouterBasicSynchronousIT.java @@ -176,7 +176,7 @@ public class DynamicRouterBasicSynchronousIT { .expressionLanguage("spel") .build(); - template.sendBody("dynamic-router-control:subscribe", controlMessage); + template.sendBody("dynamic-router-control:subscribe?allowPredicateFromMessage=true", controlMessage); // Trigger events to subscribers template.sendBodyAndHeader("direct:start", "testMessage", "test", "testValue"); @@ -239,7 +239,7 @@ public class DynamicRouterBasicSynchronousIT { .predicate("#{headers.test == 'testValue1'}") .expressionLanguage("spel") .build(); - template.sendBody("dynamic-router-control:subscribe", controlMessage1); + template.sendBody("dynamic-router-control:subscribe?allowPredicateFromMessage=true", controlMessage1); DynamicRouterControlMessage controlMessage2 = DynamicRouterControlMessage.Builder.newBuilder() .subscribeChannel("test") @@ -249,7 +249,7 @@ public class DynamicRouterBasicSynchronousIT { .predicate("#{headers.test == 'testValue2'}") .expressionLanguage("spel") .build(); - template.sendBody("dynamic-router-control:subscribe", controlMessage2); + template.sendBody("dynamic-router-control:subscribe?allowPredicateFromMessage=true", controlMessage2); DynamicRouterControlMessage controlMessage3 = DynamicRouterControlMessage.Builder.newBuilder() .subscribeChannel("test") @@ -259,7 +259,7 @@ public class DynamicRouterBasicSynchronousIT { .predicate("#{headers.test == 'testValue3'}") .expressionLanguage("spel") .build(); - template.sendBody("dynamic-router-control:subscribe", controlMessage3); + template.sendBody("dynamic-router-control:subscribe?allowPredicateFromMessage=true", controlMessage3); String filtersJson = template.requestBodyAndHeader("direct:list", "", "subscribeChannel", "test", String.class); Assertions.assertEquals( diff --git a/components/camel-dynamic-router/src/test/java/org/apache/camel/component/dynamicrouter/integration/DynamicRouterDefaultAggregationIT.java b/components/camel-dynamic-router/src/test/java/org/apache/camel/component/dynamicrouter/integration/DynamicRouterDefaultAggregationIT.java index bdf67dfc9ff5..47e08e1fed60 100644 --- a/components/camel-dynamic-router/src/test/java/org/apache/camel/component/dynamicrouter/integration/DynamicRouterDefaultAggregationIT.java +++ b/components/camel-dynamic-router/src/test/java/org/apache/camel/component/dynamicrouter/integration/DynamicRouterDefaultAggregationIT.java @@ -116,6 +116,6 @@ public class DynamicRouterDefaultAggregationIT { .predicate("${body} != null") .expressionLanguage("simple") .build(); - template.sendBody("dynamic-router-control:subscribe", controlMessage); + template.sendBody("dynamic-router-control:subscribe?allowPredicateFromMessage=true", controlMessage); } } diff --git a/components/camel-dynamic-router/src/test/java/org/apache/camel/component/dynamicrouter/integration/DynamicRouterSendDynamicAwareIT.java b/components/camel-dynamic-router/src/test/java/org/apache/camel/component/dynamicrouter/integration/DynamicRouterSendDynamicAwareIT.java index dfcc58352080..aae5c2e1d694 100644 --- a/components/camel-dynamic-router/src/test/java/org/apache/camel/component/dynamicrouter/integration/DynamicRouterSendDynamicAwareIT.java +++ b/components/camel-dynamic-router/src/test/java/org/apache/camel/component/dynamicrouter/integration/DynamicRouterSendDynamicAwareIT.java @@ -62,7 +62,8 @@ public class DynamicRouterSendDynamicAwareIT { "&subscriptionId=${header.subscriptionId}" + "&destinationUri=${header.destinationUri}" + "&priority=${header.priority}" + - "&predicate=${header.predicate}"); + "&predicate=${header.predicate}" + + "&allowPredicateFromMessage=true"); } }); } diff --git a/components/camel-dynamic-router/src/test/java/org/apache/camel/component/dynamicrouter/routing/DynamicRouterRecipientListHelperTest.java b/components/camel-dynamic-router/src/test/java/org/apache/camel/component/dynamicrouter/routing/DynamicRouterRecipientListHelperTest.java index 7f9171d5f797..ea765b79e3f9 100644 --- a/components/camel-dynamic-router/src/test/java/org/apache/camel/component/dynamicrouter/routing/DynamicRouterRecipientListHelperTest.java +++ b/components/camel-dynamic-router/src/test/java/org/apache/camel/component/dynamicrouter/routing/DynamicRouterRecipientListHelperTest.java @@ -160,9 +160,11 @@ class DynamicRouterRecipientListHelperTest { when(mockConfig.isStopOnException()).thenReturn(true); when(mockConfig.isIgnoreInvalidEndpoints()).thenReturn(true); when(mockConfig.getCacheSize()).thenReturn(10); + when(mockConfig.getAllowedSchemes()).thenReturn("direct,mock"); // Invoke the method under test DynamicRouterRecipientListHelper.setPropertiesForRecipientList(recipientList, camelContext, mockConfig); // Verify results + verify(recipientList, times(1)).setAllowedSchemes("direct,mock"); verify(recipientList, times(1)).setParallelProcessing(true); verify(recipientList, times(1)).setParallelAggregate(true); verify(recipientList, times(1)).setSynchronous(true); diff --git a/core/camel-util/src/main/java/org/apache/camel/util/SecurityUtils.java b/core/camel-util/src/main/java/org/apache/camel/util/SecurityUtils.java index 22da8e66497e..a6584469baad 100644 --- a/core/camel-util/src/main/java/org/apache/camel/util/SecurityUtils.java +++ b/core/camel-util/src/main/java/org/apache/camel/util/SecurityUtils.java @@ -61,6 +61,7 @@ public final class SecurityUtils { map.put("allowfilepathsource", new SecurityOption(INSECURE_DEV, "true")); map.put("allowjavaserializedobject", new SecurityOption(INSECURE_SERIALIZATION, "true")); map.put("allowlocalwebhookurls", new SecurityOption(INSECURE_DEV, "true")); + map.put("allowpredicatefrommessage", new SecurityOption(INSECURE_DEV, "true")); map.put("allowserializedheaders", new SecurityOption(INSECURE_SERIALIZATION, "true")); map.put("allowurlbody", new SecurityOption(INSECURE_DEV, "true")); map.put("allowurlsource", new SecurityOption(INSECURE_DEV, "true")); diff --git a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/DynamicRouterControlEndpointBuilderFactory.java b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/DynamicRouterControlEndpointBuilderFactory.java index 0e45af063a02..aab0f8b357fb 100644 --- a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/DynamicRouterControlEndpointBuilderFactory.java +++ b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/DynamicRouterControlEndpointBuilderFactory.java @@ -176,6 +176,48 @@ public interface DynamicRouterControlEndpointBuilderFactory { doSetProperty("subscriptionId", subscriptionId); return this; } + /** + * Whether the subscription predicate, and the language used to compile + * it, may be taken from the incoming control message. When disabled, + * the predicate and expressionLanguage configured on this endpoint are + * used instead. Enabling this lets the sender of a control message + * choose both the expression language and the expression that the + * Dynamic Router compiles into a live predicate, so only enable it when + * control messages come from a trusted source. + * + * The option is a: <code>boolean</code> type. + * + * Default: false + * Group: security + * + * @param allowPredicateFromMessage the value to set + * @return the dsl builder + */ + default DynamicRouterControlEndpointBuilder allowPredicateFromMessage(boolean allowPredicateFromMessage) { + doSetProperty("allowPredicateFromMessage", allowPredicateFromMessage); + return this; + } + /** + * Whether the subscription predicate, and the language used to compile + * it, may be taken from the incoming control message. When disabled, + * the predicate and expressionLanguage configured on this endpoint are + * used instead. Enabling this lets the sender of a control message + * choose both the expression language and the expression that the + * Dynamic Router compiles into a live predicate, so only enable it when + * control messages come from a trusted source. + * + * The option will be converted to a <code>boolean</code> type. + * + * Default: false + * Group: security + * + * @param allowPredicateFromMessage the value to set + * @return the dsl builder + */ + default DynamicRouterControlEndpointBuilder allowPredicateFromMessage(String allowPredicateFromMessage) { + doSetProperty("allowPredicateFromMessage", allowPredicateFromMessage); + return this; + } } /** diff --git a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/DynamicRouterEndpointBuilderFactory.java b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/DynamicRouterEndpointBuilderFactory.java index 090629ff004e..a1969f993a80 100644 --- a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/DynamicRouterEndpointBuilderFactory.java +++ b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/DynamicRouterEndpointBuilderFactory.java @@ -701,6 +701,23 @@ public interface DynamicRouterEndpointBuilderFactory { doSetProperty("lazyStartProducer", lazyStartProducer); return this; } + /** + * Sets an optional comma-separated allow-list of component schemes that + * a subscription destination may resolve to (e.g. http,https). When + * set, a subscription whose destination URI uses a scheme that is not + * in the list is rejected. By default (unset) any scheme is allowed. + * + * The option is a: <code>java.lang.String</code> type. + * + * Group: security + * + * @param allowedSchemes the value to set + * @return the dsl builder + */ + default AdvancedDynamicRouterEndpointBuilder allowedSchemes(String allowedSchemes) { + doSetProperty("allowedSchemes", allowedSchemes); + return this; + } } public interface DynamicRouterBuilders {
