This is an automated email from the ASF dual-hosted git repository.
oscerd pushed a commit to branch camel-kafka-connector-4.18.x
in repository https://gitbox.apache.org/repos/asf/camel-kafka-connector.git
The following commit(s) were added to refs/heads/camel-kafka-connector-4.18.x
by this push:
new 71b6dca1e2 [backport camel-kafka-connector-4.18.x] Fixes from the
security scan triage (#1810, #1811, #1812, #1814, #1815) (#1817)
71b6dca1e2 is described below
commit 71b6dca1e290c159efa62dbf330a1af1e1b30dba
Author: Andrea Cosentino <[email protected]>
AuthorDate: Mon Aug 24 18:46:05 2026 +0200
[backport camel-kafka-connector-4.18.x] Fixes from the security scan triage
(#1810, #1811, #1812, #1814, #1815) (#1817)
* Fix #1799: FileTransforms fails the record instead of emitting a null
value (#1810)
FileTransforms.apply() carried an Eclipse-generated catch stub: an
IOException
from readFileToString was printed with e.printStackTrace() and then
execution
continued with c still null, so a file that could not be read was silently
turned into a record with a null value and a schema derived from null.
Propagate it as a ConnectException naming the file instead, so Kafka
Connect's
error handling and DLQ apply and the failure is attributed to the file that
caused it.
Also fix the else branch, which dereferenced r.value() to log the unexpected
type and therefore threw NullPointerException for a record whose value is
null,
and reuse the already-read local instead of calling r.value() three times.
Adds the first test to a connector module. The junit-jupiter test
dependency is
placed before the START OF GENERATED CODE marker in the pom so the connector
generator does not overwrite it; surefire is already configured there.
Signed-off-by: Andrea Cosentino <[email protected]>
Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
(cherry picked from commit 47d918264cf786432fcf0d8861d58e35b4319c5e)
* Fix #1800: scope the SMT TypeConverter and CamelContext to the instance
(#1811)
CamelTypeConverterTransform declared its TypeConverter static but assigned
it
from the per-instance configure(), so the last SMT instance configured in a
plugin classloader repointed the converter used by every other connector's
transforms in that worker.
Make it an instance field, and release the CamelContext that
CamelTransformSupport creates per instance: close() was empty, so Kafka
Connect re-instantiating transforms on every connector config update left
those contexts accumulating for the lifetime of the worker. The core tasks
already stop their context in stop(); this brings the SMTs in line.
Also report the source and target type on a conversion failure rather than
formatting the record value into the exception message, which Kafka Connect
surfaces in the worker log and in the task status over the REST API.
The instance-scoping test registers a converter on one instance's context
only,
so it fails on a static field (the second configure() repoints it) and
passes on
an instance field - the existing tests could not tell the two apart because
each
default context resolves an equivalent converter.
Signed-off-by: Andrea Cosentino <[email protected]>
Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
(cherry picked from commit d74569861497f374d86829f9d45d2159d4433dbb)
* Fix #1801: reject unknown values for enum-like connector options (#1812)
camel.idempotency.expression.type, camel.idempotency.repository.type and
camel.error.handler are each matched with a switch whose default branch does
nothing, so a misspelled value was accepted at connector submission and only
surfaced later as unrelated behaviour:
- expression.type: an unknown value left the idempotent expression unset,
so
the route template fell back to its "dummyExpression" default. simple()
on a
constant means every record after the first is treated as a duplicate.
- repository.type: an unknown value left the repository null, which was
then
bound, failing at startup with an error unrelated to the typo.
- error.handler: an unknown value silently kept a
DefaultErrorHandlerBuilder
without the configured max.redeliveries and redelivery.delay, so the
retry
policy was ignored.
Declare a ConfigDef.ValidString for each so Kafka Connect rejects the value
when
the configuration is submitted, naming the option and the allowed values.
Signed-off-by: Andrea Cosentino <[email protected]>
Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
(cherry picked from commit 36e773b696df3b086dd48ced06ba8c8e05425e1f)
* Fix #1797: sanitize property values in the startup properties log (#1814)
CamelKafkaConnectMain.Builder.build() logs every initial property at INFO,
and
filterSensitive() decided what to mask from the property KEY alone. That
misses
the values that actually carry credentials:
- TaskHelper.buildUrl folds every camel.sink.endpoint.* /
camel.source.endpoint.*
option into one composed URI, stored under camel.kamelet.ckcSink.toUrl or
camel.kamelet.ckcSource.fromUrl. Neither key contains a token
SensitiveUtils
recognises, so an option that is correctly declared
ConfigDef.Type.PASSWORD
and dutifully masked under its own key was reproduced in clear text
inside
the composed URI on the same log line.
- camel.sink.url / camel.source.url may embed credentials as userinfo or as
query parameters.
Run String values through URISupport.sanitizeUri, which masks userinfo and
the
query parameters SensitiveUtils matches while leaving the endpoint itself
readable, so the log stays useful for diagnosis.
Signed-off-by: Andrea Cosentino <[email protected]>
Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
(cherry picked from commit 8a310e9380b2e166764e00ed746d62f75f7ba0d5)
* Fix #1806: apply camel.remove.headers.pattern before the sink route
stages (#1815)
The route was assembled with ckcRemoveHeader appended last for both
directions:
from(from) -> [ckcMarshal] -> [ckcUnMarshal] -> [ckcAggregator]
-> [ckcIdempotent] -> ckcRemoveHeader -> toD(to)
That is correct for the source direction, where headers arrive from the
Camel
consumer and are mapped onto the produced record after the route has run.
It is
wrong for the sink direction, where CamelSinkTask.put maps
CamelHeader.-prefixed
record headers onto the exchange before it enters the route, so every stage
runs
before the removal does. With camel.remove.headers.pattern set, the
idempotency
expression, a configured AggregationStrategy and the data formats all still
saw
the headers the operator asked to strip.
Give the builder the direction through withRemoveHeadersFirst and set it in
CamelSinkTask, so the stage runs first on the sink path and stays last on
the
source path. The builder default keeps the previous placement, so a caller
driving CamelKafkaConnectMain.Builder directly is unaffected.
Signed-off-by: Andrea Cosentino <[email protected]>
Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
(cherry picked from commit a859a86a0ced0e735d280271a2cbf2be1156dd29)
---------
Signed-off-by: Andrea Cosentino <[email protected]>
Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
---
connectors/camel-file-kafka-connector/pom.xml | 7 +
.../file/transformers/FileTransforms.java | 15 +-
.../file/transformers/FileTransformsTest.java | 88 ++++++++++++
.../camel/kafkaconnector/CamelConnectorConfig.java | 3 +
.../kafkaconnector/CamelSinkConnectorConfig.java | 8 +-
.../apache/camel/kafkaconnector/CamelSinkTask.java | 1 +
.../kafkaconnector/CamelSourceConnectorConfig.java | 8 +-
.../transforms/CamelTransformSupport.java | 17 +++
.../transforms/CamelTypeConverterTransform.java | 6 +-
.../utils/CamelKafkaConnectMain.java | 42 +++++-
.../camel/kafkaconnector/ConfigValidationTest.java | 120 ++++++++++++++++
.../CamelTypeConverterTransformTest.java | 89 +++++++++++-
.../utils/RemoveHeadersOrderTest.java | 95 +++++++++++++
.../utils/StartupPropertiesLoggingTest.java | 156 +++++++++++++++++++++
14 files changed, 635 insertions(+), 20 deletions(-)
diff --git a/connectors/camel-file-kafka-connector/pom.xml
b/connectors/camel-file-kafka-connector/pom.xml
index 1f9170d421..c4b6d4bbf2 100644
--- a/connectors/camel-file-kafka-connector/pom.xml
+++ b/connectors/camel-file-kafka-connector/pom.xml
@@ -49,6 +49,13 @@
<artifactId>commons-io</artifactId>
<version>${commons-io-version}</version>
</dependency>
+ <!-- Test scope; version managed by junit-bom via camel-parent. Kept
outside the generated
+ block below so the connector generator does not overwrite it. -->
+ <dependency>
+ <groupId>org.junit.jupiter</groupId>
+ <artifactId>junit-jupiter</artifactId>
+ <scope>test</scope>
+ </dependency>
<!--START OF GENERATED CODE-->
<dependency>
<groupId>org.apache.camel.kafkaconnector</groupId>
diff --git
a/connectors/camel-file-kafka-connector/src/main/java/org/apache/camel/kafkaconnector/file/transformers/FileTransforms.java
b/connectors/camel-file-kafka-connector/src/main/java/org/apache/camel/kafkaconnector/file/transformers/FileTransforms.java
index 7c8244119b..cf65a78e00 100644
---
a/connectors/camel-file-kafka-connector/src/main/java/org/apache/camel/kafkaconnector/file/transformers/FileTransforms.java
+++
b/connectors/camel-file-kafka-connector/src/main/java/org/apache/camel/kafkaconnector/file/transformers/FileTransforms.java
@@ -26,6 +26,7 @@ import org.apache.camel.kafkaconnector.utils.SchemaHelper;
import org.apache.commons.io.FileUtils;
import org.apache.kafka.common.config.ConfigDef;
import org.apache.kafka.connect.connector.ConnectRecord;
+import org.apache.kafka.connect.errors.ConnectException;
import org.apache.kafka.connect.transforms.Transformation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -40,21 +41,21 @@ public class FileTransforms<R extends ConnectRecord<R>>
implements Transformatio
public R apply(R r) {
Object value = r.value();
- if (r.value() instanceof GenericFile) {
+ if (value instanceof GenericFile) {
LOG.debug("Converting record from RemoteFile to text");
- GenericFile<File> message = (GenericFile<File>)r.value();
- String c = null;
+ GenericFile<File> message = (GenericFile<File>)value;
+ File file = message.getFile();
+ String c;
try {
- c = FileUtils.readFileToString(message.getFile(),
StandardCharsets.UTF_8);
+ c = FileUtils.readFileToString(file, StandardCharsets.UTF_8);
} catch (IOException e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
+ throw new ConnectException("Failed to read file " +
file.getAbsolutePath(), e);
}
return r.newRecord(r.topic(), r.kafkaPartition(), null, r.key(),
SchemaHelper.buildSchemaBuilderForType(c), c, r.timestamp());
} else {
- LOG.debug("Unexpected message type: {}", r.value().getClass());
+ LOG.debug("Unexpected message type: {}", value == null ? null :
value.getClass());
return r;
}
diff --git
a/connectors/camel-file-kafka-connector/src/test/java/org/apache/camel/kafkaconnector/file/transformers/FileTransformsTest.java
b/connectors/camel-file-kafka-connector/src/test/java/org/apache/camel/kafkaconnector/file/transformers/FileTransformsTest.java
new file mode 100644
index 0000000000..e9f8ba6044
--- /dev/null
+++
b/connectors/camel-file-kafka-connector/src/test/java/org/apache/camel/kafkaconnector/file/transformers/FileTransformsTest.java
@@ -0,0 +1,88 @@
+/*
+ * 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.kafkaconnector.file.transformers;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+import org.apache.camel.component.file.GenericFile;
+import org.apache.kafka.connect.errors.ConnectException;
+import org.apache.kafka.connect.source.SourceRecord;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+public class FileTransformsTest {
+
+ @TempDir
+ private Path tempDir;
+
+ private SourceRecord recordOf(Object value) {
+ return new SourceRecord(null, null, "mytopic", 0, null, null, null,
value, null);
+ }
+
+ private SourceRecord recordFor(File file) {
+ GenericFile<File> genericFile = new GenericFile<>();
+ genericFile.setFile(file);
+ return recordOf(genericFile);
+ }
+
+ @Test
+ public void testReadsFileContentAsString() throws IOException {
+ File file = tempDir.resolve("content.txt").toFile();
+ Files.write(file.toPath(), "hello".getBytes(StandardCharsets.UTF_8));
+
+ SourceRecord transformed = new
FileTransforms<SourceRecord>().apply(recordFor(file));
+
+ assertEquals("hello", transformed.value());
+ assertNotNull(transformed.valueSchema());
+ }
+
+ @Test
+ public void testUnreadableFileFailsTheRecordInsteadOfEmittingANullValue() {
+ File missing = tempDir.resolve("missing.txt").toFile();
+
+ FileTransforms<SourceRecord> transform = new FileTransforms<>();
+ SourceRecord record = recordFor(missing);
+
+ ConnectException e = assertThrows(ConnectException.class, () ->
transform.apply(record));
+
+ assertInstanceOf(IOException.class, e.getCause());
+ }
+
+ @Test
+ public void testRecordWithANullValueIsPassedThrough() {
+ SourceRecord record = recordOf(null);
+
+ assertSame(record, new FileTransforms<SourceRecord>().apply(record));
+ }
+
+ @Test
+ public void testRecordOfAnUnexpectedTypeIsPassedThrough() {
+ SourceRecord record = recordOf("not a file");
+
+ assertSame(record, new FileTransforms<SourceRecord>().apply(record));
+ }
+}
diff --git
a/core/src/main/java/org/apache/camel/kafkaconnector/CamelConnectorConfig.java
b/core/src/main/java/org/apache/camel/kafkaconnector/CamelConnectorConfig.java
index 11598a3b97..60b7f5b9b2 100644
---
a/core/src/main/java/org/apache/camel/kafkaconnector/CamelConnectorConfig.java
+++
b/core/src/main/java/org/apache/camel/kafkaconnector/CamelConnectorConfig.java
@@ -38,6 +38,7 @@ public abstract class CamelConnectorConfig extends
AbstractConfig {
public static final String CAMEL_CONNECTOR_ERROR_HANDLER_DEFAULT =
"default";
public static final String CAMEL_CONNECTOR_ERROR_HANDLER_CONF =
"camel.error.handler";
public static final String CAMEL_CONNECTOR_ERROR_HANDLER_DOC = "The error
handler to use: possible value are 'no' or 'default'";
+ public static final ConfigDef.Validator
CAMEL_CONNECTOR_ERROR_HANDLER_VALIDATOR = ConfigDef.ValidString.in("no",
"default");
public static final String CAMEL_CONNECTOR_REMOVE_HEADERS_PATTERN_DEFAULT
= "";
public static final String CAMEL_CONNECTOR_REMOVE_HEADERS_PATTERN_CONF =
"camel.remove.headers.pattern";
@@ -66,10 +67,12 @@ public abstract class CamelConnectorConfig extends
AbstractConfig {
public static final String
CAMEL_CONNECTOR_IDEMPOTENCY_REPOSITORY_TYPE_DEFAULT = "memory";
public static final String
CAMEL_CONNECTOR_IDEMPOTENCY_REPOSITORY_TYPE_CONF =
"camel.idempotency.repository.type";
public static final String CAMEL_CONNECTOR_IDEMPOTENCY_REPOSITORY_TYPE_DOC
= "The idempotent repository type to use, possible values are memory and kafka";
+ public static final ConfigDef.Validator
CAMEL_CONNECTOR_IDEMPOTENCY_REPOSITORY_TYPE_VALIDATOR =
ConfigDef.ValidString.in("memory", "kafka");
public static final String
CAMEL_CONNECTOR_IDEMPOTENCY_EXPRESSION_TYPE_DEFAULT = "body";
public static final String
CAMEL_CONNECTOR_IDEMPOTENCY_EXPRESSION_TYPE_CONF =
"camel.idempotency.expression.type";
public static final String CAMEL_CONNECTOR_IDEMPOTENCY_EXPRESSION_TYPE_DOC
= "How the idempotency will be evaluated: possible values are body and header";
+ public static final ConfigDef.Validator
CAMEL_CONNECTOR_IDEMPOTENCY_EXPRESSION_TYPE_VALIDATOR =
ConfigDef.ValidString.in("body", "header");
public static final String
CAMEL_CONNECTOR_IDEMPOTENCY_EXPRESSION_HEADER_DEFAULT = null;
public static final String
CAMEL_CONNECTOR_IDEMPOTENCY_EXPRESSION_HEADER_CONF =
"camel.idempotency.expression.header";
diff --git
a/core/src/main/java/org/apache/camel/kafkaconnector/CamelSinkConnectorConfig.java
b/core/src/main/java/org/apache/camel/kafkaconnector/CamelSinkConnectorConfig.java
index 1ef5a86276..11ebeb1e26 100644
---
a/core/src/main/java/org/apache/camel/kafkaconnector/CamelSinkConnectorConfig.java
+++
b/core/src/main/java/org/apache/camel/kafkaconnector/CamelSinkConnectorConfig.java
@@ -55,14 +55,16 @@ public class CamelSinkConnectorConfig extends
CamelConnectorConfig {
.define(CAMEL_CONNECTOR_AGGREGATE_CONF, Type.STRING,
CAMEL_CONNECTOR_AGGREGATE_DEFAULT, Importance.MEDIUM,
CAMEL_CONNECTOR_AGGREGATE_DOC)
.define(CAMEL_CONNECTOR_AGGREGATE_SIZE_CONF, Type.INT,
CAMEL_CONNECTOR_AGGREGATE_SIZE_DEFAULT, Importance.MEDIUM,
CAMEL_CONNECTOR_AGGREGATE_SIZE_DOC)
.define(CAMEL_CONNECTOR_AGGREGATE_TIMEOUT_CONF, Type.LONG,
CAMEL_CONNECTOR_AGGREGATE_TIMEOUT_DEFAULT, Importance.MEDIUM,
CAMEL_CONNECTOR_AGGREGATE_TIMEOUT_DOC)
- .define(CAMEL_CONNECTOR_ERROR_HANDLER_CONF, Type.STRING,
CAMEL_CONNECTOR_ERROR_HANDLER_DEFAULT, Importance.LOW,
CAMEL_CONNECTOR_ERROR_HANDLER_DOC)
+ .define(CAMEL_CONNECTOR_ERROR_HANDLER_CONF, Type.STRING,
CAMEL_CONNECTOR_ERROR_HANDLER_DEFAULT, CAMEL_CONNECTOR_ERROR_HANDLER_VALIDATOR,
Importance.LOW, CAMEL_CONNECTOR_ERROR_HANDLER_DOC)
.define(CAMEL_CONNECTOR_ERROR_HANDLER_MAXIMUM_REDELIVERIES_CONF,
Type.INT, CAMEL_CONNECTOR_ERROR_HANDLER_MAXIMUM_REDELIVERIES_DEFAULT,
Importance.MEDIUM, CAMEL_CONNECTOR_ERROR_HANDLER_MAXIMUM_REDELIVERIES_DOC)
.define(CAMEL_CONNECTOR_ERROR_HANDLER_REDELIVERY_DELAY_CONF,
Type.LONG, CAMEL_CONNECTOR_ERROR_HANDLER_REDELIVERY_DELAY_DEFAULT,
Importance.MEDIUM, CAMEL_CONNECTOR_ERROR_HANDLER_REDELIVERY_DELAY_DOC)
.define(CAMEL_CONNECTOR_IDEMPOTENCY_ENABLED_CONF, Type.BOOLEAN,
CAMEL_CONNECTOR_IDEMPOTENCY_ENABLED_DEFAULT, Importance.LOW,
CAMEL_CONNECTOR_IDEMPOTENCY_ENABLED_DOC)
- .define(CAMEL_CONNECTOR_IDEMPOTENCY_EXPRESSION_TYPE_CONF, Type.STRING,
CAMEL_CONNECTOR_IDEMPOTENCY_EXPRESSION_TYPE_DEFAULT, Importance.LOW,
CAMEL_CONNECTOR_IDEMPOTENCY_EXPRESSION_TYPE_DOC)
+ .define(CAMEL_CONNECTOR_IDEMPOTENCY_EXPRESSION_TYPE_CONF, Type.STRING,
CAMEL_CONNECTOR_IDEMPOTENCY_EXPRESSION_TYPE_DEFAULT,
+ CAMEL_CONNECTOR_IDEMPOTENCY_EXPRESSION_TYPE_VALIDATOR, Importance.LOW,
CAMEL_CONNECTOR_IDEMPOTENCY_EXPRESSION_TYPE_DOC)
.define(CAMEL_CONNECTOR_IDEMPOTENCY_EXPRESSION_HEADER_CONF,
Type.STRING, CAMEL_CONNECTOR_IDEMPOTENCY_EXPRESSION_HEADER_DEFAULT,
Importance.LOW, CAMEL_CONNECTOR_IDEMPOTENCY_EXPRESSION_HEADER_DOC)
.define(CAMEL_CONNECTOR_IDEMPOTENCY_MEMORY_DIMENSION_CONF, Type.INT,
CAMEL_CONNECTOR_IDEMPOTENCY_MEMORY_DIMENSION_DEFAULT, Importance.LOW,
CAMEL_CONNECTOR_IDEMPOTENCY_MEMORY_DIMENSION_DOC)
- .define(CAMEL_CONNECTOR_IDEMPOTENCY_REPOSITORY_TYPE_CONF, Type.STRING,
CAMEL_CONNECTOR_IDEMPOTENCY_REPOSITORY_TYPE_DEFAULT, Importance.LOW,
CAMEL_CONNECTOR_IDEMPOTENCY_REPOSITORY_TYPE_DOC)
+ .define(CAMEL_CONNECTOR_IDEMPOTENCY_REPOSITORY_TYPE_CONF, Type.STRING,
CAMEL_CONNECTOR_IDEMPOTENCY_REPOSITORY_TYPE_DEFAULT,
+ CAMEL_CONNECTOR_IDEMPOTENCY_REPOSITORY_TYPE_VALIDATOR, Importance.LOW,
CAMEL_CONNECTOR_IDEMPOTENCY_REPOSITORY_TYPE_DOC)
.define(CAMEL_CONNECTOR_IDEMPOTENCY_KAFKA_TOPIC_CONF, Type.STRING,
CAMEL_CONNECTOR_IDEMPOTENCY_KAFKA_TOPIC_DEFAULT, Importance.LOW,
CAMEL_CONNECTOR_IDEMPOTENCY_KAFKA_TOPIC_DOC)
.define(CAMEL_CONNECTOR_IDEMPOTENCY_KAFKA_BOOTSTRAP_SERVERS_CONF,
Type.STRING, CAMEL_CONNECTOR_IDEMPOTENCY_KAFKA_BOOTSTRAP_SERVERS_DEFAULT,
Importance.LOW, CAMEL_CONNECTOR_IDEMPOTENCY_KAFKA_BOOTSTRAP_SERVERS_DOC)
.define(CAMEL_CONNECTOR_IDEMPOTENCY_KAFKA_MAX_CACHE_SIZE_CONF,
Type.INT, CAMEL_CONNECTOR_IDEMPOTENCY_KAFKA_MAX_CACHE_SIZE_DEFAULT,
Importance.LOW, CAMEL_CONNECTOR_IDEMPOTENCY_KAFKA_MAX_CACHE_SIZE_DOC)
diff --git
a/core/src/main/java/org/apache/camel/kafkaconnector/CamelSinkTask.java
b/core/src/main/java/org/apache/camel/kafkaconnector/CamelSinkTask.java
index b66bce262e..67635d489d 100644
--- a/core/src/main/java/org/apache/camel/kafkaconnector/CamelSinkTask.java
+++ b/core/src/main/java/org/apache/camel/kafkaconnector/CamelSinkTask.java
@@ -129,6 +129,7 @@ public class CamelSinkTask extends SinkTask {
}
cms = CamelKafkaConnectMain.builder(LOCAL_URL, getSinkKamelet())
+ .withRemoveHeadersFirst(true)
.withProperties(actualProps)
.withUnmarshallDataFormat(unmarshaller)
.withMarshallDataFormat(marshaller)
diff --git
a/core/src/main/java/org/apache/camel/kafkaconnector/CamelSourceConnectorConfig.java
b/core/src/main/java/org/apache/camel/kafkaconnector/CamelSourceConnectorConfig.java
index 4acfa62a42..6de77ca0c7 100644
---
a/core/src/main/java/org/apache/camel/kafkaconnector/CamelSourceConnectorConfig.java
+++
b/core/src/main/java/org/apache/camel/kafkaconnector/CamelSourceConnectorConfig.java
@@ -96,14 +96,16 @@ public class CamelSourceConnectorConfig extends
CamelConnectorConfig {
.define(CAMEL_CONNECTOR_AGGREGATE_CONF, Type.STRING,
CAMEL_CONNECTOR_AGGREGATE_DEFAULT, Importance.MEDIUM,
CAMEL_CONNECTOR_AGGREGATE_DOC)
.define(CAMEL_CONNECTOR_AGGREGATE_SIZE_CONF, Type.INT,
CAMEL_CONNECTOR_AGGREGATE_SIZE_DEFAULT, Importance.MEDIUM,
CAMEL_CONNECTOR_AGGREGATE_SIZE_DOC)
.define(CAMEL_CONNECTOR_AGGREGATE_TIMEOUT_CONF, Type.LONG,
CAMEL_CONNECTOR_AGGREGATE_TIMEOUT_DEFAULT, Importance.MEDIUM,
CAMEL_CONNECTOR_AGGREGATE_TIMEOUT_DOC)
- .define(CAMEL_CONNECTOR_ERROR_HANDLER_CONF, Type.STRING,
CAMEL_CONNECTOR_ERROR_HANDLER_DEFAULT, Importance.LOW,
CAMEL_CONNECTOR_ERROR_HANDLER_DOC)
+ .define(CAMEL_CONNECTOR_ERROR_HANDLER_CONF, Type.STRING,
CAMEL_CONNECTOR_ERROR_HANDLER_DEFAULT, CAMEL_CONNECTOR_ERROR_HANDLER_VALIDATOR,
Importance.LOW, CAMEL_CONNECTOR_ERROR_HANDLER_DOC)
.define(CAMEL_CONNECTOR_ERROR_HANDLER_MAXIMUM_REDELIVERIES_CONF,
Type.INT, CAMEL_CONNECTOR_ERROR_HANDLER_MAXIMUM_REDELIVERIES_DEFAULT,
Importance.MEDIUM, CAMEL_CONNECTOR_ERROR_HANDLER_MAXIMUM_REDELIVERIES_DOC)
.define(CAMEL_CONNECTOR_ERROR_HANDLER_REDELIVERY_DELAY_CONF,
Type.LONG, CAMEL_CONNECTOR_ERROR_HANDLER_REDELIVERY_DELAY_DEFAULT,
Importance.MEDIUM, CAMEL_CONNECTOR_ERROR_HANDLER_REDELIVERY_DELAY_DOC)
.define(CAMEL_CONNECTOR_IDEMPOTENCY_ENABLED_CONF, Type.BOOLEAN,
CAMEL_CONNECTOR_IDEMPOTENCY_ENABLED_DEFAULT, Importance.LOW,
CAMEL_CONNECTOR_IDEMPOTENCY_ENABLED_DOC)
- .define(CAMEL_CONNECTOR_IDEMPOTENCY_EXPRESSION_TYPE_CONF, Type.STRING,
CAMEL_CONNECTOR_IDEMPOTENCY_EXPRESSION_TYPE_DEFAULT, Importance.LOW,
CAMEL_CONNECTOR_IDEMPOTENCY_EXPRESSION_TYPE_DOC)
+ .define(CAMEL_CONNECTOR_IDEMPOTENCY_EXPRESSION_TYPE_CONF, Type.STRING,
CAMEL_CONNECTOR_IDEMPOTENCY_EXPRESSION_TYPE_DEFAULT,
+ CAMEL_CONNECTOR_IDEMPOTENCY_EXPRESSION_TYPE_VALIDATOR, Importance.LOW,
CAMEL_CONNECTOR_IDEMPOTENCY_EXPRESSION_TYPE_DOC)
.define(CAMEL_CONNECTOR_IDEMPOTENCY_EXPRESSION_HEADER_CONF,
Type.STRING, CAMEL_CONNECTOR_IDEMPOTENCY_EXPRESSION_HEADER_DEFAULT,
Importance.LOW, CAMEL_CONNECTOR_IDEMPOTENCY_EXPRESSION_HEADER_DOC)
.define(CAMEL_CONNECTOR_IDEMPOTENCY_MEMORY_DIMENSION_CONF, Type.INT,
CAMEL_CONNECTOR_IDEMPOTENCY_MEMORY_DIMENSION_DEFAULT, Importance.LOW,
CAMEL_CONNECTOR_IDEMPOTENCY_MEMORY_DIMENSION_DOC)
- .define(CAMEL_CONNECTOR_IDEMPOTENCY_REPOSITORY_TYPE_CONF, Type.STRING,
CAMEL_CONNECTOR_IDEMPOTENCY_REPOSITORY_TYPE_DEFAULT, Importance.LOW,
CAMEL_CONNECTOR_IDEMPOTENCY_REPOSITORY_TYPE_DOC)
+ .define(CAMEL_CONNECTOR_IDEMPOTENCY_REPOSITORY_TYPE_CONF, Type.STRING,
CAMEL_CONNECTOR_IDEMPOTENCY_REPOSITORY_TYPE_DEFAULT,
+ CAMEL_CONNECTOR_IDEMPOTENCY_REPOSITORY_TYPE_VALIDATOR, Importance.LOW,
CAMEL_CONNECTOR_IDEMPOTENCY_REPOSITORY_TYPE_DOC)
.define(CAMEL_CONNECTOR_IDEMPOTENCY_KAFKA_TOPIC_CONF, Type.STRING,
CAMEL_CONNECTOR_IDEMPOTENCY_KAFKA_TOPIC_DEFAULT, Importance.LOW,
CAMEL_CONNECTOR_IDEMPOTENCY_KAFKA_TOPIC_DOC)
.define(CAMEL_CONNECTOR_IDEMPOTENCY_KAFKA_BOOTSTRAP_SERVERS_CONF,
Type.STRING, CAMEL_CONNECTOR_IDEMPOTENCY_KAFKA_BOOTSTRAP_SERVERS_DEFAULT,
Importance.LOW, CAMEL_CONNECTOR_IDEMPOTENCY_KAFKA_BOOTSTRAP_SERVERS_DOC)
.define(CAMEL_CONNECTOR_IDEMPOTENCY_KAFKA_MAX_CACHE_SIZE_CONF,
Type.INT, CAMEL_CONNECTOR_IDEMPOTENCY_KAFKA_MAX_CACHE_SIZE_DEFAULT,
Importance.LOW, CAMEL_CONNECTOR_IDEMPOTENCY_KAFKA_MAX_CACHE_SIZE_DOC)
diff --git
a/core/src/main/java/org/apache/camel/kafkaconnector/transforms/CamelTransformSupport.java
b/core/src/main/java/org/apache/camel/kafkaconnector/transforms/CamelTransformSupport.java
index 4f318b0ca7..64be4ace0e 100644
---
a/core/src/main/java/org/apache/camel/kafkaconnector/transforms/CamelTransformSupport.java
+++
b/core/src/main/java/org/apache/camel/kafkaconnector/transforms/CamelTransformSupport.java
@@ -20,12 +20,29 @@ import org.apache.camel.CamelContext;
import org.apache.camel.impl.DefaultCamelContext;
import org.apache.kafka.connect.connector.ConnectRecord;
import org.apache.kafka.connect.transforms.Transformation;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
public abstract class CamelTransformSupport<R extends ConnectRecord<R>>
implements Transformation<R> {
+ private static final Logger LOG =
LoggerFactory.getLogger(CamelTransformSupport.class);
+
private final CamelContext camelContext = new DefaultCamelContext();
protected CamelContext getCamelContext() {
return camelContext;
}
+
+ /**
+ * Stops the {@link CamelContext} created for this transform instance.
Kafka Connect re-instantiates transforms on
+ * every connector configuration update, so a subclass must release it
from {@link Transformation#close()} rather
+ * than let it accumulate for the lifetime of the worker.
+ */
+ protected void stopCamelContext() {
+ try {
+ camelContext.stop();
+ } catch (Exception e) {
+ LOG.warn("Failed to stop the Camel context of {}: {}",
getClass().getSimpleName(), e.getMessage(), e);
+ }
+ }
}
diff --git
a/core/src/main/java/org/apache/camel/kafkaconnector/transforms/CamelTypeConverterTransform.java
b/core/src/main/java/org/apache/camel/kafkaconnector/transforms/CamelTypeConverterTransform.java
index 0ca1162c74..2078ea21fa 100644
---
a/core/src/main/java/org/apache/camel/kafkaconnector/transforms/CamelTypeConverterTransform.java
+++
b/core/src/main/java/org/apache/camel/kafkaconnector/transforms/CamelTypeConverterTransform.java
@@ -36,7 +36,7 @@ public abstract class CamelTypeConverterTransform<R extends
ConnectRecord<R>> ex
.define(FIELD_TARGET_TYPE_CONFIG, ConfigDef.Type.CLASS, null,
ConfigDef.Importance.HIGH,
"The target field type to convert the value from, this is
full qualified Java class, e.g: java.util.Map");
- private static TypeConverter typeConverter;
+ private TypeConverter typeConverter;
private Class<?> fieldTargetType;
@Override
@@ -54,7 +54,8 @@ public abstract class CamelTypeConverterTransform<R extends
ConnectRecord<R>> ex
final Object convertedValue =
typeConverter.tryConvertTo(fieldTargetType, originalValue);
if (convertedValue == null) {
- throw new DataException(String.format("CamelTypeConverter was not
able to convert value `%s` to target type of `%s`", originalValue,
fieldTargetType.getSimpleName()));
+ throw new DataException(String.format("CamelTypeConverter was not
able to convert a value of type `%s` to target type of `%s`",
+ originalValue == null ? "null" :
originalValue.getClass().getName(), fieldTargetType.getSimpleName()));
}
return convertedValue;
@@ -80,6 +81,7 @@ public abstract class CamelTypeConverterTransform<R extends
ConnectRecord<R>> ex
@Override
public void close() {
+ stopCamelContext();
}
@Override
diff --git
a/core/src/main/java/org/apache/camel/kafkaconnector/utils/CamelKafkaConnectMain.java
b/core/src/main/java/org/apache/camel/kafkaconnector/utils/CamelKafkaConnectMain.java
index 78bbeefd8b..d2c9ec4f2e 100644
---
a/core/src/main/java/org/apache/camel/kafkaconnector/utils/CamelKafkaConnectMain.java
+++
b/core/src/main/java/org/apache/camel/kafkaconnector/utils/CamelKafkaConnectMain.java
@@ -37,6 +37,7 @@ import
org.apache.camel.support.processor.idempotent.MemoryIdempotentRepository;
import org.apache.camel.support.service.ServiceHelper;
import org.apache.camel.util.ObjectHelper;
import org.apache.camel.util.SensitiveUtils;
+import org.apache.camel.util.URISupport;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -116,6 +117,7 @@ public class CamelKafkaConnectMain extends SimpleMain {
private int idempotentRepositoryKafkaMaxCacheSize;
private int idempotentRepositoryKafkaPollDuration;
private String headersExcludePattern;
+ private boolean removeHeadersFirst;
public Builder(String from, String to) {
this.from = from;
@@ -212,12 +214,39 @@ public class CamelKafkaConnectMain extends SimpleMain {
return this;
}
+ /**
+ * Controls where the header-removal stage sits in the route.
+ *
+ * On the sink path record headers are mapped onto the exchange by
{@code CamelSinkTask.put} *before* the
+ * exchange enters the route, so the stage must run first for {@code
camel.remove.headers.pattern} to keep
+ * those headers away from the marshalling, aggregation and
idempotency stages.
+ *
+ * On the source path headers arrive from the Camel consumer and are
mapped onto the produced record after
+ * the route has run, so the stage stays last and the intermediate
stages keep seeing them.
+ *
+ * @param removeHeadersFirst true for the sink direction, false (the
default) for the source direction.
+ */
+ public Builder withRemoveHeadersFirst(boolean removeHeadersFirst) {
+ this.removeHeadersFirst = removeHeadersFirst;
+ return this;
+ }
+
+
private String filterSensitive(Map.Entry<Object, Object> entry) {
+ final String key = (String) entry.getKey();
- if (SensitiveUtils.containsSensitive((String) entry.getKey())) {
- return entry.getKey() + "=xxxxxxx";
+ if (SensitiveUtils.containsSensitive(key)) {
+ return key + "=xxxxxxx";
}
- return entry.getKey() + "=" + entry.getValue();
+ final Object value = entry.getValue();
+ if (value instanceof String) {
+ // The key alone is not enough: TaskHelper.buildUrl folds
every endpoint option into a single
+ // composed URI stored under a key that carries no sensitive
token (ckcSink.toUrl /
+ // ckcSource.fromUrl), and camel.sink.url / camel.source.url
may embed credentials directly.
+ // Sanitize the value as well so userinfo and sensitive query
parameters never reach the log.
+ return key + "=" + URISupport.sanitizeUri((String) value);
+ }
+ return key + "=" + value;
}
public CamelKafkaConnectMain build(CamelContext camelContext) {
@@ -361,6 +390,9 @@ public class CamelKafkaConnectMain extends SimpleMain {
//creating the actual route
ProcessorDefinition<?> rd = from(from);
+ if (removeHeadersFirst) {
+ rd = rd.kamelet("ckcRemoveHeader");
+ }
if (!ObjectHelper.isEmpty(marshallDataFormat)) {
rd = rd.kamelet("ckcMarshal");
}
@@ -373,7 +405,9 @@ public class CamelKafkaConnectMain extends SimpleMain {
if (idempotencyEnabled) {
rd = rd.kamelet("ckcIdempotent");
}
- rd = rd.kamelet("ckcRemoveHeader");
+ if (!removeHeadersFirst) {
+ rd = rd.kamelet("ckcRemoveHeader");
+ }
rd.toD(to);
}
});
diff --git
a/core/src/test/java/org/apache/camel/kafkaconnector/ConfigValidationTest.java
b/core/src/test/java/org/apache/camel/kafkaconnector/ConfigValidationTest.java
new file mode 100644
index 0000000000..135fa8219b
--- /dev/null
+++
b/core/src/test/java/org/apache/camel/kafkaconnector/ConfigValidationTest.java
@@ -0,0 +1,120 @@
+/*
+ * 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.kafkaconnector;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.kafka.common.config.ConfigException;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * A behaviour-selecting option that is misspelled must be rejected when the
connector configuration is submitted,
+ * rather than silently starting a connector that behaves differently from
what the configuration asked for.
+ */
+public class ConfigValidationTest {
+
+ private Map<String, String> sinkProps() {
+ Map<String, String> props = new HashMap<>();
+ props.put("camel.sink.url", "direct://test");
+ props.put("camel.sink.kafka.topic", "mytopic");
+ return props;
+ }
+
+ private Map<String, String> sourceProps() {
+ Map<String, String> props = new HashMap<>();
+ props.put("camel.source.url", "direct://test");
+ props.put("topics", "mytopic");
+ return props;
+ }
+
+ private void assertRejected(Map<String, String> props, String option,
boolean sink) {
+ ConfigException e = assertThrows(ConfigException.class,
+ () -> {
+ if (sink) {
+ new CamelSinkConnectorConfig(props);
+ } else {
+ new CamelSourceConnectorConfig(props);
+ }
+ });
+ assertTrue(e.getMessage().contains(option), "the message should name "
+ option + " but was: " + e.getMessage());
+ }
+
+ @Test
+ public void testSinkRejectsUnknownIdempotencyExpressionType() {
+ Map<String, String> props = sinkProps();
+
props.put(CamelConnectorConfig.CAMEL_CONNECTOR_IDEMPOTENCY_EXPRESSION_TYPE_CONF,
"headers");
+ assertRejected(props,
CamelConnectorConfig.CAMEL_CONNECTOR_IDEMPOTENCY_EXPRESSION_TYPE_CONF, true);
+ }
+
+ @Test
+ public void testSinkRejectsUnknownIdempotencyRepositoryType() {
+ Map<String, String> props = sinkProps();
+
props.put(CamelConnectorConfig.CAMEL_CONNECTOR_IDEMPOTENCY_REPOSITORY_TYPE_CONF,
"in-memory");
+ assertRejected(props,
CamelConnectorConfig.CAMEL_CONNECTOR_IDEMPOTENCY_REPOSITORY_TYPE_CONF, true);
+ }
+
+ @Test
+ public void testSinkRejectsUnknownErrorHandler() {
+ Map<String, String> props = sinkProps();
+ props.put(CamelConnectorConfig.CAMEL_CONNECTOR_ERROR_HANDLER_CONF,
"none");
+ assertRejected(props,
CamelConnectorConfig.CAMEL_CONNECTOR_ERROR_HANDLER_CONF, true);
+ }
+
+ @Test
+ public void testSourceRejectsUnknownIdempotencyExpressionType() {
+ Map<String, String> props = sourceProps();
+
props.put(CamelConnectorConfig.CAMEL_CONNECTOR_IDEMPOTENCY_EXPRESSION_TYPE_CONF,
"Body");
+ assertRejected(props,
CamelConnectorConfig.CAMEL_CONNECTOR_IDEMPOTENCY_EXPRESSION_TYPE_CONF, false);
+ }
+
+ @Test
+ public void testSourceRejectsUnknownErrorHandler() {
+ Map<String, String> props = sourceProps();
+ props.put(CamelConnectorConfig.CAMEL_CONNECTOR_ERROR_HANDLER_CONF,
"retry");
+ assertRejected(props,
CamelConnectorConfig.CAMEL_CONNECTOR_ERROR_HANDLER_CONF, false);
+ }
+
+ @Test
+ public void testDocumentedValuesAreAccepted() {
+ for (String expressionType : new String[] {"body", "header"}) {
+ Map<String, String> props = sinkProps();
+
props.put(CamelConnectorConfig.CAMEL_CONNECTOR_IDEMPOTENCY_EXPRESSION_TYPE_CONF,
expressionType);
+ assertDoesNotThrow(() -> new CamelSinkConnectorConfig(props));
+ }
+ for (String repositoryType : new String[] {"memory", "kafka"}) {
+ Map<String, String> props = sinkProps();
+
props.put(CamelConnectorConfig.CAMEL_CONNECTOR_IDEMPOTENCY_REPOSITORY_TYPE_CONF,
repositoryType);
+ assertDoesNotThrow(() -> new CamelSinkConnectorConfig(props));
+ }
+ for (String errorHandler : new String[] {"no", "default"}) {
+ Map<String, String> props = sourceProps();
+ props.put(CamelConnectorConfig.CAMEL_CONNECTOR_ERROR_HANDLER_CONF,
errorHandler);
+ assertDoesNotThrow(() -> new CamelSourceConnectorConfig(props));
+ }
+ }
+
+ @Test
+ public void testDefaultsAreAccepted() {
+ assertDoesNotThrow(() -> new CamelSinkConnectorConfig(sinkProps()));
+ assertDoesNotThrow(() -> new
CamelSourceConnectorConfig(sourceProps()));
+ }
+}
diff --git
a/core/src/test/java/org/apache/camel/kafkaconnector/transforms/CamelTypeConverterTransformTest.java
b/core/src/test/java/org/apache/camel/kafkaconnector/transforms/CamelTypeConverterTransformTest.java
index 6da72c20ab..4cc93ff110 100644
---
a/core/src/test/java/org/apache/camel/kafkaconnector/transforms/CamelTypeConverterTransformTest.java
+++
b/core/src/test/java/org/apache/camel/kafkaconnector/transforms/CamelTypeConverterTransformTest.java
@@ -22,7 +22,10 @@ import java.util.HashMap;
import java.util.Map;
import io.netty.buffer.Unpooled;
+import org.apache.camel.CamelContext;
+import org.apache.camel.Exchange;
import org.apache.camel.component.netty.http.NettyChannelBufferStreamCache;
+import org.apache.camel.support.TypeConverterSupport;
import org.apache.kafka.common.config.ConfigException;
import org.apache.kafka.connect.data.Schema;
import org.apache.kafka.connect.data.SchemaBuilder;
@@ -31,8 +34,12 @@ import org.apache.kafka.connect.source.SourceRecord;
import org.apache.kafka.connect.transforms.Transformation;
import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -134,4 +141,84 @@ public class CamelTypeConverterTransformTest {
assertThrows(ConfigException.class, () ->
transformationKey.configure(Collections.emptyMap()));
}
-}
\ No newline at end of file
+ @Test
+ public void testEachInstanceKeepsItsOwnCamelContext() {
+ final CamelTypeConverterTransform.Value<SourceRecord> first = new
CamelTypeConverterTransform.Value<>();
+ final CamelTypeConverterTransform.Value<SourceRecord> second = new
CamelTypeConverterTransform.Value<>();
+
+ assertNotSame(first.getCamelContext(), second.getCamelContext());
+ }
+
+ @Test
+ public void testConverterIsScopedToTheInstanceThatConfiguredIt() {
+ final CamelTypeConverterTransform.Value<SourceRecord> first = new
CamelTypeConverterTransform.Value<>();
+
+ // teach ONLY this instance's context how to produce a Marker
+
first.getCamelContext().getTypeConverterRegistry().addTypeConverter(Marker.class,
String.class,
+ new TypeConverterSupport() {
+ @Override
+ public <T> T convertTo(Class<T> type, Exchange exchange,
Object value) {
+ return type.cast(new Marker(String.valueOf(value)));
+ }
+ });
+
+ final Map<String, Object> toMarker = new HashMap<>();
+ toMarker.put(CamelTypeConverterTransform.FIELD_TARGET_TYPE_CONFIG,
Marker.class.getName());
+ first.configure(toMarker);
+
+ // configuring a second instance afterwards must not repoint the
converter the first one uses
+ final Map<String, Object> toString = new HashMap<>();
+ toString.put(CamelTypeConverterTransform.FIELD_TARGET_TYPE_CONFIG,
String.class.getName());
+ new
CamelTypeConverterTransform.Value<SourceRecord>().configure(toString);
+
+ final SourceRecord record = new SourceRecord(Collections.emptyMap(),
Collections.emptyMap(), "topic",
+ Schema.STRING_SCHEMA, "1234", Schema.STRING_SCHEMA, "abc");
+
+ assertInstanceOf(Marker.class, first.apply(record).value());
+ }
+
+ /** Target type known only to the converter registered on one instance's
context. */
+ public static final class Marker {
+ private final String value;
+
+ Marker(String value) {
+ this.value = value;
+ }
+
+ @Override
+ public String toString() {
+ return "Marker[" + value + "]";
+ }
+ }
+
+ @Test
+ public void testCloseStopsTheCamelContext() {
+ final CamelTypeConverterTransform.Value<SourceRecord> transform = new
CamelTypeConverterTransform.Value<>();
+ final Map<String, Object> props = new HashMap<>();
+ props.put(CamelTypeConverterTransform.FIELD_TARGET_TYPE_CONFIG,
String.class.getName());
+ transform.configure(props);
+
+ final CamelContext context = transform.getCamelContext();
+ assertDoesNotThrow(transform::close);
+ assertFalse(context.getStatus().isStarted());
+ }
+
+ @Test
+ public void testConversionFailureDoesNotEchoTheRecordValue() {
+ final Map<String, Object> props = new HashMap<>();
+ props.put(CamelTypeConverterTransform.FIELD_TARGET_TYPE_CONFIG,
java.time.LocalDate.class.getName());
+
+ final Transformation<SourceRecord> transform = new
CamelTypeConverterTransform.Value<>();
+ transform.configure(props);
+
+ final String secret = "s3cr3t-record-content";
+ final SourceRecord record = new SourceRecord(Collections.emptyMap(),
Collections.emptyMap(), "topic",
+ Schema.STRING_SCHEMA, "1234", Schema.STRING_SCHEMA, secret);
+
+ final org.apache.kafka.connect.errors.DataException e =
+
assertThrows(org.apache.kafka.connect.errors.DataException.class, () ->
transform.apply(record));
+
+ assertFalse(e.getMessage().contains(secret), "the record value must
not be echoed into the exception message");
+ assertTrue(e.getMessage().contains(String.class.getName()), "the
source type should be reported instead");
+ }
+}
diff --git
a/core/src/test/java/org/apache/camel/kafkaconnector/utils/RemoveHeadersOrderTest.java
b/core/src/test/java/org/apache/camel/kafkaconnector/utils/RemoveHeadersOrderTest.java
new file mode 100644
index 0000000000..0ce66ae284
--- /dev/null
+++
b/core/src/test/java/org/apache/camel/kafkaconnector/utils/RemoveHeadersOrderTest.java
@@ -0,0 +1,95 @@
+/*
+ * 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.kafkaconnector.utils;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.camel.AggregationStrategy;
+import org.apache.camel.Exchange;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.apache.camel.kafkaconnector.CamelConnectorConfig;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+/**
+ * camel.remove.headers.pattern is the documented mitigation for untrusted
headers reaching the route. On the sink
+ * path the headers are mapped onto the exchange before it enters the route,
so the removal stage has to run before
+ * the stages that consume them; on the source path they arrive from the
consumer and are read off the exchange after
+ * the route, so it stays last.
+ */
+public class RemoveHeadersOrderTest {
+
+ private static final String STRIPPED_HEADER = "CamelExecCommandExecutable";
+
+ private RecordingAggregationStrategy runRouteWith(boolean
removeHeadersFirst) throws Exception {
+ Map<String, String> props = new HashMap<>();
+
props.put(CamelConnectorConfig.CAMEL_CONNECTOR_REMOVE_HEADERS_PATTERN_CONF,
STRIPPED_HEADER);
+
+ DefaultCamelContext context = new DefaultCamelContext();
+ RecordingAggregationStrategy strategy = new
RecordingAggregationStrategy();
+
context.getRegistry().bind(CamelConnectorConfig.CAMEL_CONNECTOR_AGGREGATE_NAME,
strategy);
+
+ CamelKafkaConnectMain cms =
CamelKafkaConnectMain.builder("direct://start", "log://end")
+ .withProperties(props)
+ .withHeadersExcludePattern(STRIPPED_HEADER)
+ .withAggregationSize(1)
+ .withAggregationTimeout(1000L)
+ .withRemoveHeadersFirst(removeHeadersFirst)
+ .build(context);
+
+ cms.start();
+ try {
+ cms.getProducerTemplate().sendBodyAndHeader("direct://start",
"payload", STRIPPED_HEADER, "/bin/sh");
+ } finally {
+ cms.stop();
+ }
+ return strategy;
+ }
+
+ @Test
+ public void testSinkDirectionStripsHeadersBeforeTheAggregationStage()
throws Exception {
+ RecordingAggregationStrategy strategy = runRouteWith(true);
+
+ assertNull(strategy.seenHeaderValue,
+ "camel.remove.headers.pattern must strip the header before the
aggregation stage sees it on the "
+ + "sink path, but the strategy saw: " +
strategy.seenHeaderValue);
+ }
+
+ @Test
+ public void testSourceDirectionKeepsHeadersUntilTheEndOfTheRoute() throws
Exception {
+ RecordingAggregationStrategy strategy = runRouteWith(false);
+
+ assertEquals("/bin/sh", strategy.seenHeaderValue,
+ "on the source path the stripping stage stays last, so
intermediate stages still see the header");
+ }
+
+ private static final class RecordingAggregationStrategy implements
AggregationStrategy {
+
+ private volatile Object seenHeaderValue;
+
+ @Override
+ public Exchange aggregate(Exchange oldExchange, Exchange newExchange) {
+ if (newExchange != null) {
+ seenHeaderValue =
newExchange.getMessage().getHeader(STRIPPED_HEADER);
+ }
+ return newExchange;
+ }
+ }
+}
diff --git
a/core/src/test/java/org/apache/camel/kafkaconnector/utils/StartupPropertiesLoggingTest.java
b/core/src/test/java/org/apache/camel/kafkaconnector/utils/StartupPropertiesLoggingTest.java
new file mode 100644
index 0000000000..b4de6f7d97
--- /dev/null
+++
b/core/src/test/java/org/apache/camel/kafkaconnector/utils/StartupPropertiesLoggingTest.java
@@ -0,0 +1,156 @@
+/*
+ * 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.kafkaconnector.utils;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.camel.impl.DefaultCamelContext;
+import org.apache.camel.kafkaconnector.CamelSinkTask;
+import org.apache.camel.kafkaconnector.CamelSourceTask;
+import org.apache.logging.log4j.Level;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.core.LogEvent;
+import org.apache.logging.log4j.core.LoggerContext;
+import org.apache.logging.log4j.core.appender.AbstractAppender;
+import org.apache.logging.log4j.core.config.Configuration;
+import org.apache.logging.log4j.core.config.LoggerConfig;
+import org.apache.logging.log4j.core.config.Property;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * The connector must not write configuration secrets to the worker log at
default levels, whichever property key
+ * they reach the log under. Endpoint options are folded into a single
composed URI stored under a key that carries
+ * no sensitive token, so masking by key name alone is not enough.
+ */
+public class StartupPropertiesLoggingTest {
+
+ private static final String SECRET_KEY_VALUE = "sUp3rS3cr3tAccessValue";
+ private static final String USERINFO_PASSWORD = "hunter2PlainPassword";
+
+ private CapturingAppender appender;
+ private LoggerConfig loggerConfig;
+
+ @BeforeEach
+ public void attachAppender() {
+ appender = new CapturingAppender();
+ appender.start();
+
+ LoggerContext context = (LoggerContext) LogManager.getContext(false);
+ Configuration configuration = context.getConfiguration();
+ configuration.addAppender(appender);
+ loggerConfig =
configuration.getLoggerConfig(CamelKafkaConnectMain.class.getName());
+ loggerConfig.addAppender(appender, Level.INFO, null);
+ context.updateLoggers();
+ }
+
+ @AfterEach
+ public void detachAppender() {
+ loggerConfig.removeAppender(appender.getName());
+ appender.stop();
+ ((LoggerContext) LogManager.getContext(false)).updateLoggers();
+ }
+
+ private String startupLogLine(Map<String, String> props) {
+ CamelKafkaConnectMain.builder("direct://start", "log://test")
+ .withProperties(props)
+ .build(new DefaultCamelContext());
+
+ return appender.messages.stream()
+ .filter(m -> m.startsWith("Setting initial properties"))
+ .findFirst()
+ .orElseThrow(() -> new AssertionError("the startup properties line
was not logged: " + appender.messages));
+ }
+
+ @Test
+ public void testComposedSinkEndpointUriIsNotLoggedInClearText() {
+ Map<String, String> props = new HashMap<>();
+ props.put(CamelSinkTask.KAMELET_SINK_TEMPLATE_PARAMETERS_PREFIX +
"toUrl",
+ "aws2-kms://label?accessKey=AKIAEXAMPLEKEY&secretKey=" +
SECRET_KEY_VALUE + "®ion=eu-west-1");
+
+ String logged = startupLogLine(props);
+
+ assertFalse(logged.contains(SECRET_KEY_VALUE),
+ "the composed endpoint URI must not carry the secret into the
log: " + logged);
+ assertTrue(logged.contains("aws2-kms"), "the endpoint itself should
stay readable: " + logged);
+ }
+
+ @Test
+ public void testComposedSourceEndpointUriIsNotLoggedInClearText() {
+ Map<String, String> props = new HashMap<>();
+ props.put(CamelSourceTask.KAMELET_SOURCE_TEMPLATE_PARAMETERS_PREFIX +
"fromUrl",
+ "aws2-kms://label?accessKey=AKIAEXAMPLEKEY&secretKey=" +
SECRET_KEY_VALUE + "®ion=eu-west-1");
+
+ String logged = startupLogLine(props);
+
+ assertFalse(logged.contains(SECRET_KEY_VALUE),
+ "the composed endpoint URI must not carry the secret into the
log: " + logged);
+ }
+
+ @Test
+ public void
testCredentialsEmbeddedInTheConfiguredUrlAreNotLoggedInClearText() {
+ Map<String, String> props = new HashMap<>();
+ props.put("camel.sink.url", "netty:tcp://someuser:" +
USERINFO_PASSWORD + "@somehost:5555");
+
+ String logged = startupLogLine(props);
+
+ assertFalse(logged.contains(USERINFO_PASSWORD),
+ "userinfo credentials must not reach the log: " + logged);
+ }
+
+ @Test
+ public void testKeyBasedMaskingStillApplies() {
+ Map<String, String> props = new HashMap<>();
+ props.put("camel.sink.endpoint.secretKey", SECRET_KEY_VALUE);
+
+ String logged = startupLogLine(props);
+
+ assertFalse(logged.contains(SECRET_KEY_VALUE), "a sensitively named
key must stay masked: " + logged);
+ }
+
+ @Test
+ public void testNonSensitiveValuesAreStillLogged() {
+ Map<String, String> props = new HashMap<>();
+ props.put("camel.sink.endpoint.region", "eu-west-1");
+
+ String logged = startupLogLine(props);
+
+ assertTrue(logged.contains("eu-west-1"), "ordinary configuration
should remain visible: " + logged);
+ }
+
+ private static final class CapturingAppender extends AbstractAppender {
+
+ private final List<String> messages = Collections.synchronizedList(new
ArrayList<>());
+
+ CapturingAppender() {
+ super("ckcCapture", null, null, true, Property.EMPTY_ARRAY);
+ }
+
+ @Override
+ public void append(LogEvent event) {
+ messages.add(event.getMessage().getFormattedMessage());
+ }
+ }
+}