This is an automated email from the ASF dual-hosted git repository.

oscerd pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git


The following commit(s) were added to refs/heads/main by this push:
     new 1fd7a667913a CAMEL-24901: camel-aws2-s3 - restrict Simple expression 
evaluation of key/bucket to configured values (#26746)
1fd7a667913a is described below

commit 1fd7a667913ad068c8571b39c96b0a9c2dd4f3ef
Author: Andrea Cosentino <[email protected]>
AuthorDate: Thu Sep 24 11:45:36 2026 +0200

    CAMEL-24901: camel-aws2-s3 - restrict Simple expression evaluation of 
key/bucket to configured values (#26746)
    
    The camel-aws2-s3 producer now evaluates Simple expressions only for the 
configured keyName/bucketName; a key or bucket supplied through the 
CamelAwsS3Key / CamelAwsS3OverrideBucketName header is used literally.
    
    Co-authored-by: Claude Opus 4.8 <[email protected]>
---
 .../camel/component/aws2/s3/utils/AWS2S3Utils.java |  26 +++---
 .../S3GetObjectDynamicKeyOperationIT.java          |   6 +-
 .../component/aws2/s3/utils/AWS2S3UtilsTest.java   | 102 +++++++++++++++++++++
 .../ROOT/pages/camel-4x-upgrade-guide-4_23.adoc    |  32 +++++++
 4 files changed, 151 insertions(+), 15 deletions(-)

diff --git 
a/components/camel-aws/camel-aws2-s3/src/main/java/org/apache/camel/component/aws2/s3/utils/AWS2S3Utils.java
 
b/components/camel-aws/camel-aws2-s3/src/main/java/org/apache/camel/component/aws2/s3/utils/AWS2S3Utils.java
index 2fe1e86e2c32..638b7e180655 100644
--- 
a/components/camel-aws/camel-aws2-s3/src/main/java/org/apache/camel/component/aws2/s3/utils/AWS2S3Utils.java
+++ 
b/components/camel-aws/camel-aws2-s3/src/main/java/org/apache/camel/component/aws2/s3/utils/AWS2S3Utils.java
@@ -50,15 +50,16 @@ public final class AWS2S3Utils {
     public static String determineBucketName(final Exchange exchange, 
AWS2S3Configuration configuration) {
         String bucketName = 
exchange.getIn().getHeader(AWS2S3Constants.OVERRIDE_BUCKET_NAME, String.class);
         if (ObjectHelper.isEmpty(bucketName)) {
+            // only the configured bucket name (supplied by the route) may be 
a dynamic simple expression;
+            // a bucket name provided through the header is used literally and 
never evaluated
             bucketName = configuration.getBucketName();
+            if (bucketName != null && hasSimpleFunction(bucketName)) {
+                Language simple = 
exchange.getContext().resolveLanguage("simple");
+                bucketName = 
simple.createExpression(bucketName).evaluate(exchange, String.class);
+            }
         }
         if (bucketName == null) {
-            throw new IllegalArgumentException("AWS S3 Bucket name header is 
missing or not configured.");
-        }
-        // dynamic keys using built-in simple language
-        if (hasSimpleFunction(bucketName)) {
-            Language simple = exchange.getContext().resolveLanguage("simple");
-            bucketName = 
simple.createExpression(bucketName).evaluate(exchange, String.class);
+            throw new IllegalArgumentException("AWS S3 Bucket name is not set, 
or resolved to null.");
         }
         return bucketName;
     }
@@ -137,15 +138,16 @@ public final class AWS2S3Utils {
     public static String determineKey(final Exchange exchange, 
AWS2S3Configuration configuration) {
         String key = exchange.getIn().getHeader(AWS2S3Constants.KEY, 
String.class);
         if (ObjectHelper.isEmpty(key)) {
+            // only the configured key (supplied by the route) may be a 
dynamic simple expression;
+            // a key provided through the header is used literally and never 
evaluated
             key = configuration.getKeyName();
+            if (key != null && hasSimpleFunction(key)) {
+                Language simple = 
exchange.getContext().resolveLanguage("simple");
+                key = simple.createExpression(key).evaluate(exchange, 
String.class);
+            }
         }
         if (key == null) {
-            throw new IllegalArgumentException("AWS S3 Key header missing.");
-        }
-        // dynamic keys using built-in simple language
-        if (hasSimpleFunction(key)) {
-            Language simple = exchange.getContext().resolveLanguage("simple");
-            key = simple.createExpression(key).evaluate(exchange, 
String.class);
+            throw new IllegalArgumentException("AWS S3 Key is not set, or 
resolved to null.");
         }
         return key;
     }
diff --git 
a/components/camel-aws/camel-aws2-s3/src/test/java/org/apache/camel/component/aws2/s3/integration/S3GetObjectDynamicKeyOperationIT.java
 
b/components/camel-aws/camel-aws2-s3/src/test/java/org/apache/camel/component/aws2/s3/integration/S3GetObjectDynamicKeyOperationIT.java
index 76041c38b88e..0711e4ad0252 100644
--- 
a/components/camel-aws/camel-aws2-s3/src/test/java/org/apache/camel/component/aws2/s3/integration/S3GetObjectDynamicKeyOperationIT.java
+++ 
b/components/camel-aws/camel-aws2-s3/src/test/java/org/apache/camel/component/aws2/s3/integration/S3GetObjectDynamicKeyOperationIT.java
@@ -49,7 +49,6 @@ public class S3GetObjectDynamicKeyOperationIT extends 
Aws2S3Base {
 
             @Override
             public void process(Exchange exchange) {
-                exchange.getIn().setHeader(AWS2S3Constants.KEY, 
"${variable.global:myVar}.txt");
                 exchange.getIn().setHeader(AWS2S3Constants.CONTENT_TYPE, 
"application/text");
                 exchange.getIn().setBody("Camel rocks again!");
             }
@@ -60,7 +59,6 @@ public class S3GetObjectDynamicKeyOperationIT extends 
Aws2S3Base {
             @Override
             public void process(Exchange exchange) {
                 exchange.getIn().setHeader(AWS2S3Constants.BUCKET_NAME, 
name.get());
-                exchange.getIn().setHeader(AWS2S3Constants.KEY, 
"${variable.global:myVar}.txt");
                 exchange.getIn().setHeader(AWS2S3Constants.S3_OPERATION, 
AWS2S3Operations.getObject);
             }
         });
@@ -81,7 +79,9 @@ public class S3GetObjectDynamicKeyOperationIT extends 
Aws2S3Base {
             public void configure() {
                 context.setVariable("myVar", "myCamel");
 
-                String awsEndpoint = "aws2-s3://" + name.get() + 
"?autoCreateBucket=true";
+                // the dynamic key is a simple expression supplied through the 
endpoint configuration (keyName)
+                String awsEndpoint
+                        = "aws2-s3://" + name.get() + 
"?autoCreateBucket=true&keyName=RAW(${variable.global:myVar}.txt)";
 
                 from("direct:putObject").to(awsEndpoint);
 
diff --git 
a/components/camel-aws/camel-aws2-s3/src/test/java/org/apache/camel/component/aws2/s3/utils/AWS2S3UtilsTest.java
 
b/components/camel-aws/camel-aws2-s3/src/test/java/org/apache/camel/component/aws2/s3/utils/AWS2S3UtilsTest.java
index daf396e0b52c..6239b30a6206 100644
--- 
a/components/camel-aws/camel-aws2-s3/src/test/java/org/apache/camel/component/aws2/s3/utils/AWS2S3UtilsTest.java
+++ 
b/components/camel-aws/camel-aws2-s3/src/test/java/org/apache/camel/component/aws2/s3/utils/AWS2S3UtilsTest.java
@@ -18,10 +18,12 @@ package org.apache.camel.component.aws2.s3.utils;
 
 import org.apache.camel.Exchange;
 import org.apache.camel.component.aws2.s3.AWS2S3Configuration;
+import org.apache.camel.component.aws2.s3.AWS2S3Constants;
 import org.apache.camel.test.junit6.CamelTestSupport;
 import org.junit.jupiter.api.Test;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
 
 public class AWS2S3UtilsTest extends CamelTestSupport {
 
@@ -84,4 +86,104 @@ public class AWS2S3UtilsTest extends CamelTestSupport {
 
         assertEquals("", AWS2S3Utils.evaluateDestinationBucketSuffix(exchange, 
config));
     }
+
+    // ---- determineKey ----
+
+    @Test
+    void keyFromHeaderIsUsedLiterallyAndNotEvaluated() {
+        Exchange exchange = createExchangeWithBody("body");
+        // a key coming from the header (e.g. inherited from a consumed object 
name) must be used as-is
+        exchange.getIn().setHeader(AWS2S3Constants.KEY, 
"${sys.user.name}.txt");
+        AWS2S3Configuration config = new AWS2S3Configuration();
+
+        assertEquals("${sys.user.name}.txt", 
AWS2S3Utils.determineKey(exchange, config));
+    }
+
+    @Test
+    void keyFromConfigurationIsEvaluatedAsSimpleExpression() {
+        Exchange exchange = createExchangeWithBody("body");
+        exchange.getIn().setHeader("name", "report");
+        AWS2S3Configuration config = new AWS2S3Configuration();
+        config.setKeyName("${header.name}.txt");
+
+        assertEquals("report.txt", AWS2S3Utils.determineKey(exchange, config));
+    }
+
+    @Test
+    void keyFromHeaderTakesPrecedenceOverConfiguration() {
+        Exchange exchange = createExchangeWithBody("body");
+        exchange.getIn().setHeader(AWS2S3Constants.KEY, "literal-key");
+        AWS2S3Configuration config = new AWS2S3Configuration();
+        config.setKeyName("${header.name}.txt");
+
+        assertEquals("literal-key", AWS2S3Utils.determineKey(exchange, 
config));
+    }
+
+    @Test
+    void keyMissingFromHeaderAndConfigurationThrows() {
+        Exchange exchange = createExchangeWithBody("body");
+        AWS2S3Configuration config = new AWS2S3Configuration();
+
+        assertThrows(IllegalArgumentException.class, () -> 
AWS2S3Utils.determineKey(exchange, config));
+    }
+
+    @Test
+    void keyFromConfigurationResolvingToNullThrows() {
+        Exchange exchange = createExchangeWithBody("body");
+        // a configured keyName whose simple expression resolves to null fails 
fast at the producer
+        AWS2S3Configuration config = new AWS2S3Configuration();
+        config.setKeyName("${header.missing}");
+
+        assertThrows(IllegalArgumentException.class, () -> 
AWS2S3Utils.determineKey(exchange, config));
+    }
+
+    // ---- determineBucketName ----
+
+    @Test
+    void bucketFromOverrideHeaderIsUsedLiterallyAndNotEvaluated() {
+        Exchange exchange = createExchangeWithBody("body");
+        exchange.getIn().setHeader(AWS2S3Constants.OVERRIDE_BUCKET_NAME, 
"${sys.user.name}-bucket");
+        AWS2S3Configuration config = new AWS2S3Configuration();
+        config.setBucketName("configured-bucket");
+
+        assertEquals("${sys.user.name}-bucket", 
AWS2S3Utils.determineBucketName(exchange, config));
+    }
+
+    @Test
+    void bucketFromConfigurationIsEvaluatedAsSimpleExpression() {
+        Exchange exchange = createExchangeWithBody("body");
+        exchange.getIn().setHeader("env", "prod");
+        AWS2S3Configuration config = new AWS2S3Configuration();
+        config.setBucketName("bucket-${header.env}");
+
+        assertEquals("bucket-prod", AWS2S3Utils.determineBucketName(exchange, 
config));
+    }
+
+    @Test
+    void bucketOverrideHeaderTakesPrecedenceOverConfiguration() {
+        Exchange exchange = createExchangeWithBody("body");
+        exchange.getIn().setHeader(AWS2S3Constants.OVERRIDE_BUCKET_NAME, 
"header-bucket");
+        AWS2S3Configuration config = new AWS2S3Configuration();
+        config.setBucketName("configured-bucket");
+
+        assertEquals("header-bucket", 
AWS2S3Utils.determineBucketName(exchange, config));
+    }
+
+    @Test
+    void bucketMissingFromHeaderAndConfigurationThrows() {
+        Exchange exchange = createExchangeWithBody("body");
+        AWS2S3Configuration config = new AWS2S3Configuration();
+
+        assertThrows(IllegalArgumentException.class, () -> 
AWS2S3Utils.determineBucketName(exchange, config));
+    }
+
+    @Test
+    void bucketFromConfigurationResolvingToNullThrows() {
+        Exchange exchange = createExchangeWithBody("body");
+        // a configured bucketName whose simple expression resolves to null 
fails fast at the producer
+        AWS2S3Configuration config = new AWS2S3Configuration();
+        config.setBucketName("${header.missing}");
+
+        assertThrows(IllegalArgumentException.class, () -> 
AWS2S3Utils.determineBucketName(exchange, config));
+    }
 }
diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
index d2e5f405484b..f5e04dcb608e 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
@@ -177,6 +177,38 @@ through the transformer. The defaults are unchanged when 
the header is absent.
 If a route sets `CamelAwsDdbReturnValues` before the transformer runs and 
relies on it being discarded, remove the
 header instead.
 
+=== camel-aws2-s3
+
+The producer no longer evaluates the S3 object key or bucket name taken from a 
message header as a Simple
+expression. `AWS2S3Utils.determineKey()` and `determineBucketName()` evaluate 
the Simple language only for the
+values configured on the endpoint (`keyName` and `bucketName`); a key supplied 
through the `CamelAwsS3Key` header,
+or a bucket name supplied through the `CamelAwsS3OverrideBucketName` header, 
is now used literally. The
+`CamelAwsS3Key` header carries message content — for example the name of a 
consumed object, which the consumer
+sets on the exchange — whereas a dynamic key or bucket is a route authoring 
feature.
+
+Configured dynamic keys and buckets are unchanged:
+
+[source,java]
+----
+from("direct:start")
+    .to("aws2-s3://mybucket?keyName=RAW(${date:now:yyyyMMdd}/file.txt)");
+----
+
+A route that needs a dynamic value on the header must evaluate it in the 
route, so the header already holds the
+resolved value when it reaches the producer:
+
+[source,java]
+----
+from("direct:start")
+    .setHeader(AWS2S3Constants.KEY, simple("${date:now:yyyyMMdd}/file.txt"))
+    .to("aws2-s3://mybucket");
+----
+
+A header that carries a literal `${...}` string is now used as the object key 
verbatim instead of being evaluated.
+
+A configured `keyName` or `bucketName` whose Simple expression resolves to 
`null` now fails with an
+`IllegalArgumentException` at the producer, instead of passing a `null` 
key/bucket on to the AWS SDK.
+
 === camel-console
 
 The `context` developer console no longer counts routes created by Kamelets in 
its `routesTotal` and `routesStarted`

Reply via email to