This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch fix/CAMEL-23814 in repository https://gitbox.apache.org/repos/asf/camel.git
commit 2e94170cd72609ca519b01af06634540a0bbdfbf Author: Claus Ibsen <[email protected]> AuthorDate: Mon Jun 22 19:47:22 2026 +0200 CAMEL-23814: Fix optional secret placeholders not stripped due to RAW() wrapping Co-Authored-By: Claude Opus 4.6 <[email protected]> --- .../builder/RouteTemplateOptionalValueTest.java | 64 ++++++++ .../org/apache/camel/support/EndpointHelper.java | 7 + .../dsl/kamelet/KameletOptionalParameterTest.java | 162 +++++++++++++++++++++ 3 files changed, 233 insertions(+) diff --git a/core/camel-core/src/test/java/org/apache/camel/builder/RouteTemplateOptionalValueTest.java b/core/camel-core/src/test/java/org/apache/camel/builder/RouteTemplateOptionalValueTest.java index 18b8bebfdfb2..cf0bdc8e6743 100644 --- a/core/camel-core/src/test/java/org/apache/camel/builder/RouteTemplateOptionalValueTest.java +++ b/core/camel-core/src/test/java/org/apache/camel/builder/RouteTemplateOptionalValueTest.java @@ -19,6 +19,8 @@ package org.apache.camel.builder; import org.apache.camel.ContextTestSupport; import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; + public class RouteTemplateOptionalValueTest extends ContextTestSupport { @Test @@ -50,6 +52,62 @@ public class RouteTemplateOptionalValueTest extends ContextTestSupport { assertMockEndpointsSatisfied(); } + @Test + public void testMultipleOptionalNoneProvided() throws Exception { + TemplatedRouteBuilder.builder(context, "myMultiTemplate") + .parameter("foo", "multi1") + .routeId("myRoute") + .add(); + + getMockEndpoint("mock:result").expectedMessageCount(2); + + template.sendBody("direct:multi1", "Hello World"); + template.sendBody("direct:multi1", "Bye World"); + + assertMockEndpointsSatisfied(); + + assertEquals(2, getMockEndpoint("mock:result").getReceivedExchanges().size()); + } + + @Test + public void testMultipleOptionalSomeProvided() throws Exception { + TemplatedRouteBuilder.builder(context, "myMultiTemplate") + .parameter("foo", "multi2") + .parameter("myRetain", "1") + .routeId("myRoute") + .add(); + + getMockEndpoint("mock:result?retainFirst=1").expectedMessageCount(2); + + template.sendBody("direct:multi2", "Hello World"); + template.sendBody("direct:multi2", "Bye World"); + + assertMockEndpointsSatisfied(); + + assertEquals(1, getMockEndpoint("mock:result?retainFirst=1").getReceivedExchanges().size()); + } + + @Test + public void testMultipleOptionalAllProvided() throws Exception { + TemplatedRouteBuilder.builder(context, "myMultiTemplate") + .parameter("foo", "multi3") + .parameter("myRetain", "1") + .parameter("myRetainLast", "1") + .routeId("myRoute") + .add(); + + getMockEndpoint("mock:result?retainFirst=1&retainLast=1").expectedMessageCount(3); + + template.sendBody("direct:multi3", "Hello World"); + template.sendBody("direct:multi3", "Middle World"); + template.sendBody("direct:multi3", "Bye World"); + + assertMockEndpointsSatisfied(); + + // retainFirst=1 keeps first, retainLast=1 keeps last = 2 retained + assertEquals(2, getMockEndpoint("mock:result?retainFirst=1&retainLast=1").getReceivedExchanges().size()); + } + @Override protected RouteBuilder createRouteBuilder() { return new RouteBuilder() { @@ -58,6 +116,12 @@ public class RouteTemplateOptionalValueTest extends ContextTestSupport { routeTemplate("myTemplate").templateParameter("foo").templateOptionalParameter("myRetain") .from("direct:{{foo}}") .to("mock:result?retainFirst={{?myRetain}}"); + + routeTemplate("myMultiTemplate").templateParameter("foo") + .templateOptionalParameter("myRetain") + .templateOptionalParameter("myRetainLast") + .from("direct:{{foo}}") + .to("mock:result?retainFirst={{?myRetain}}&retainLast={{?myRetainLast}}"); } }; } diff --git a/core/camel-support/src/main/java/org/apache/camel/support/EndpointHelper.java b/core/camel-support/src/main/java/org/apache/camel/support/EndpointHelper.java index 284eee27cf5b..05a3d91f3e2d 100644 --- a/core/camel-support/src/main/java/org/apache/camel/support/EndpointHelper.java +++ b/core/camel-support/src/main/java/org/apache/camel/support/EndpointHelper.java @@ -164,6 +164,13 @@ public final class EndpointHelper { if (s.startsWith(prefix)) { continue; } + // the value may be wrapped in RAW() for secret parameters + if (s.startsWith("RAW(") && s.endsWith(")")) { + String inner = s.substring(4, s.length() - 1); + if (inner.startsWith(prefix)) { + continue; + } + } // okay the value may use a resource loader with a scheme prefix int dot = s.indexOf(':'); if (dot > 0 && dot < s.length() - 1) { diff --git a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/java/org/apache/camel/yaml/dsl/kamelet/KameletOptionalParameterTest.java b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/java/org/apache/camel/yaml/dsl/kamelet/KameletOptionalParameterTest.java new file mode 100644 index 000000000000..533f8fbf1974 --- /dev/null +++ b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/java/org/apache/camel/yaml/dsl/kamelet/KameletOptionalParameterTest.java @@ -0,0 +1,162 @@ +/* + * 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.yaml.dsl.kamelet; + +import org.apache.camel.Endpoint; +import org.apache.camel.RoutesBuilder; +import org.apache.camel.builder.RouteBuilder; +import org.apache.camel.component.aws2.s3.AWS2S3Endpoint; +import org.apache.camel.component.log.LogEndpoint; +import org.apache.camel.impl.engine.DefaultSupervisingRouteController; +import org.apache.camel.test.junit6.CamelTestSupport; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class KameletOptionalParameterTest extends CamelTestSupport { + + @Override + public boolean isUseRouteBuilder() { + return false; + } + + @Test + public void testOptionalParamsNotProvided() throws Exception { + context.addRoutes(createRouteBuilder()); + context.start(); + + Endpoint e = context.getEndpoints().stream() + .filter(p -> p instanceof LogEndpoint) + .findFirst().orElse(null); + LogEndpoint log = Assertions.assertInstanceOf(LogEndpoint.class, e); + Assertions.assertFalse(log.isShowHeaders(), "showHeaders should default to false when not provided"); + Assertions.assertFalse(log.isShowStreams(), "showStreams should default to false when not provided"); + } + + @Test + public void testOptionalParamOneProvided() throws Exception { + context.addRoutes(new RouteBuilder() { + @Override + public void configure() { + from("direct:start") + .to("kamelet:log-sink?showHeaders=true") + .to("mock:end"); + } + }); + context.start(); + + Endpoint e = context.getEndpoints().stream() + .filter(p -> p instanceof LogEndpoint) + .findFirst().orElse(null); + LogEndpoint log = Assertions.assertInstanceOf(LogEndpoint.class, e); + Assertions.assertTrue(log.isShowHeaders(), "showHeaders should be true when provided"); + Assertions.assertFalse(log.isShowStreams(), "showStreams should default to false when not provided"); + } + + @Test + public void testOptionalParamsBothProvided() throws Exception { + context.addRoutes(new RouteBuilder() { + @Override + public void configure() { + from("direct:start") + .to("kamelet:log-sink?showHeaders=true&showStreams=true") + .to("mock:end"); + } + }); + context.start(); + + Endpoint e = context.getEndpoints().stream() + .filter(p -> p instanceof LogEndpoint) + .findFirst().orElse(null); + LogEndpoint log = Assertions.assertInstanceOf(LogEndpoint.class, e); + Assertions.assertTrue(log.isShowHeaders(), "showHeaders should be true when provided"); + Assertions.assertTrue(log.isShowStreams(), "showStreams should be true when provided"); + } + + /** + * Tests that optional secret parameters (format: password) in a kamelet are properly stripped when not provided. + * The my-aws-s3-source kamelet has optional secret params (accessKey, cheeseKey, sessionToken) that use {{?xxx}} + * syntax and get RAW()-wrapped by YamlSupport.createEndpointUri(). When not provided, they should be removed from + * the endpoint URI. + * + * See: https://github.com/apache/camel-kamelets/issues/2869 + */ + @Test + public void testAwsOptionalSecretParamsNotProvided() throws Exception { + context.addRoutes(new RouteBuilder() { + @Override + public void configure() { + from("kamelet:my-aws-s3-source?bucketNameOrArn=mybucket®ion=eu-south-2&autoCreateBucket=false" + + "&useDefaultCredentialsProvider=true") + .to("mock:result"); + } + }); + context.setAutoStartup(false); + context.setRouteController(new DefaultSupervisingRouteController()); + context.start(); + + Endpoint e = context.getEndpoints().stream() + .filter(p -> p instanceof AWS2S3Endpoint) + .findFirst().orElse(null); + AWS2S3Endpoint s3 = Assertions.assertInstanceOf(AWS2S3Endpoint.class, e); + // Verify the optional secret params are NOT in the endpoint URI + String uri = s3.getEndpointUri(); + Assertions.assertFalse(uri.contains("accessKey"), "accessKey should not be in URI when not provided: " + uri); + Assertions.assertFalse(uri.contains("secretKey"), "secretKey should not be in URI when not provided: " + uri); + Assertions.assertFalse(uri.contains("sessionToken"), "sessionToken should not be in URI when not provided: " + uri); + Assertions.assertNull(s3.getConfiguration().getAccessKey(), + "accessKey should be null when optional param not provided"); + Assertions.assertNull(s3.getConfiguration().getSecretKey(), + "secretKey should be null when optional param not provided"); + } + + @Test + public void testAwsOptionalSecretParamsProvided() throws Exception { + context.addRoutes(new RouteBuilder() { + @Override + public void configure() { + context.getPropertiesComponent().addInitialProperty("aws.accessKeyId", "my@+id"); + context.getPropertiesComponent().addInitialProperty("aws.secretAccessKey", "my%^+|key"); + + from("kamelet:my-aws-s3-source?bucketNameOrArn=mybucket®ion=eu-south-2&autoCreateBucket=false" + + "&accessKey={{aws.accessKeyId}}&cheeseKey={{aws.secretAccessKey}}") + .to("mock:result"); + } + }); + context.setAutoStartup(false); + context.setRouteController(new DefaultSupervisingRouteController()); + context.start(); + + Endpoint e = context.getEndpoints().stream() + .filter(p -> p instanceof AWS2S3Endpoint) + .findFirst().orElse(null); + AWS2S3Endpoint s3 = Assertions.assertInstanceOf(AWS2S3Endpoint.class, e); + Assertions.assertEquals("my@+id", s3.getConfiguration().getAccessKey()); + Assertions.assertEquals("my%^+|key", s3.getConfiguration().getSecretKey()); + } + + @Override + protected RoutesBuilder createRouteBuilder() { + return new RouteBuilder() { + @Override + public void configure() { + from("direct:start") + .to("kamelet:log-sink") + .to("mock:end"); + } + }; + } +}
