This is an automated email from the ASF dual-hosted git repository.
davsclaus 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 64ab57e4dc34 CAMEL-26254: camel-ibm-secrets-manager - getSecret gives
the Secret for every other secret type, createKVSecret takes a JSON object, and
a Map or a POJO is stored as JSON in an arbitrary secret (#28973)
64ab57e4dc34 is described below
commit 64ab57e4dc34b96d49a676a4724650753e049115
Author: Andrea Cosentino <[email protected]>
AuthorDate: Mon Nov 2 23:21:48 2026 +0100
CAMEL-26254: camel-ibm-secrets-manager - getSecret gives the Secret for
every other secret type, createKVSecret takes a JSON object, and a Map or a
POJO is stored as JSON in an arbitrary secret (#28973)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
.../catalog/components/ibm-secrets-manager.json | 4 +-
.../docs/ibm-secrets-manager-component.adoc | 70 ++-
.../ibm/secrets/manager/ibm-secrets-manager.json | 4 +-
.../main/docs/ibm-secrets-manager-component.adoc | 70 ++-
.../secrets/manager/IBMSecretsManagerEndpoint.java | 27 +-
.../secrets/manager/IBMSecretsManagerProducer.java | 118 ++++-
.../manager/IBMSecretsManagerProducerBodyTest.java | 504 +++++++++++++++++++++
.../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc | 8 +
8 files changed, 767 insertions(+), 38 deletions(-)
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/ibm-secrets-manager.json
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/ibm-secrets-manager.json
index fa2ea7fd7a2c..4dc4884604fb 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/ibm-secrets-manager.json
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/ibm-secrets-manager.json
@@ -36,8 +36,8 @@
"CamelIbmSecretsManagerSecretData": { "index": 5, "kind": "header",
"displayName": "", "group": "producer", "label": "producer", "required": false,
"javaType": "java.util.Map", "deprecated": false, "deprecationNote": "",
"autowired": false, "secret": false, "description": "Set the secret data (Map)
for KV secret update operation.", "constantName":
"org.apache.camel.component.ibm.secrets.manager.IBMSecretsManagerConstants#SECRET_DATA"
}
},
"bodyTypes": {
- "producerIn": { "carriers": [ "text", "bytes", "stream", "file" ],
"byOption": [ { "option": "operation", "value": "createKVSecret", "carriers": [
"map" ] }, { "option": "operation", "value": "updateSecret", "carriers": [
"text", "bytes", "stream", "file", "map" ] }, { "option": "operation", "value":
"getSecret", "carriers": [ "any" ] }, { "option": "operation", "value":
"deleteSecret", "carriers": [ "any" ] }, { "option": "operation", "value":
"listSecrets", "carriers": [ "any" ] }, [...]
- "producerOut": { "carriers": [ "text" ], "javaType": "java.lang.String",
"byOption": [ { "option": "operation", "value": "getSecret", "carriers": [
"text", "map", "pojo" ] }, { "option": "operation", "value": "deleteSecret",
"carriers": [ "unchanged" ] }, { "option": "operation", "value": "listSecrets",
"carriers": [ "pojo" ], "javaType":
"com.ibm.cloud.secrets_manager_sdk.secrets_manager.v2.model.SecretMetadataPaginatedCollection"
}, { "option": "operation", "value": "updateSecret", [...]
+ "producerIn": { "carriers": [ "text", "bytes", "stream", "file", "map",
"list", "pojo" ], "byOption": [ { "option": "operation", "value":
"createKVSecret", "carriers": [ "map", "text", "bytes", "stream", "file" ] }, {
"option": "operation", "value": "updateSecret", "carriers": [ "text", "bytes",
"stream", "file", "map", "list", "pojo" ] }, { "option": "operation", "value":
"getSecret", "carriers": [ "any" ] }, { "option": "operation", "value":
"deleteSecret", "carriers": [ "any" ] }, [...]
+ "producerOut": { "carriers": [ "text" ], "javaType": "java.lang.String",
"byOption": [ { "option": "operation", "value": "getSecret", "carriers": [
"text", "map", "pojo" ] }, { "option": "operation", "value": "deleteSecret",
"carriers": [ "unchanged" ] }, { "option": "operation", "value": "listSecrets",
"carriers": [ "pojo" ], "javaType":
"com.ibm.cloud.secrets_manager_sdk.secrets_manager.v2.model.SecretMetadataPaginatedCollection"
}, { "option": "operation", "value": "updateSecret", [...]
},
"properties": {
"label": { "index": 0, "kind": "path", "displayName": "Label", "group":
"producer", "label": "", "required": true, "type": "string", "javaType":
"java.lang.String", "deprecated": false, "deprecationNote": "", "autowired":
false, "secret": false, "configurationClass":
"org.apache.camel.component.ibm.secrets.manager.IBMSecretsManagerConfiguration",
"configurationField": "configuration", "description": "Logical name" },
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/ibm-secrets-manager-component.adoc
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/ibm-secrets-manager-component.adoc
index 43a9e69fa908..e4231c850eee 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/ibm-secrets-manager-component.adoc
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/ibm-secrets-manager-component.adoc
@@ -48,6 +48,22 @@ The component supports operations at the producer level.
Specifically, it provid
* `updateSecret` - Updates an existing secret (creates a new version)
* `listSecretVersions` - Lists all versions of a specific secret
+The payload of an arbitrary secret is the message body as text: bytes, a
stream, a file, XML (a DOM node or a
+`Source`) or a `ByteBuffer` are read as text. A `Map`, a `List` or another
object (POJO) is stored as JSON.
+
+The data of a key-value secret is a `Map` of the keys and values (or a body
that converts to a `Map`), or a JSON
+object, which can be text, bytes, a stream or a file.
+
+`getSecret` gives the payload text of an arbitrary secret and the `Map` of a
key-value secret. For the other secret
+types, such as `service_credentials`, `username_password`, `iam_credentials`,
`imported_cert`, `public_cert` or
+`private_cert`, it gives the `Secret` of the IBM SDK, whose getters return the
fields of the secret type, for example
+`getUsername()` and `getPassword()`.
+
+`updateSecret` supports arbitrary and key-value secrets: it creates a new
version with the payload of the
+`CamelIbmSecretsManagerSecretPayload` header or the message body for an
arbitrary secret, and with the data of the
+`CamelIbmSecretsManagerSecretData` header or the message body for a key-value
secret. It fails for the other secret
+types.
+
== Examples
=== Producer Operations
@@ -106,7 +122,59 @@ YAML::
==== Create a Key-Value Secret
-._Java-only: requires programmatic Map construction_
+The keys and values can be given as a JSON object:
+
+[tabs]
+====
+Java::
++
+[source,java]
+----
+from("direct:createKVSecret")
+ .setHeader("CamelIbmSecretsManagerSecretName", constant("database-creds"))
+ .setBody(constant("{\"username\": \"admin\", \"password\":
\"secret123\"}"))
+
.to("ibm-secrets-manager://label?operation=createKVSecret&token={{token}}&serviceUrl={{serviceUrl}}");
+----
+
+XML::
++
+[source,xml]
+----
+<route>
+ <from uri="direct:createKVSecret"/>
+ <setHeader name="CamelIbmSecretsManagerSecretName">
+ <constant>database-creds</constant>
+ </setHeader>
+ <setBody>
+ <constant>{"username": "admin", "password": "secret123"}</constant>
+ </setBody>
+ <to
uri="ibm-secrets-manager://label?operation=createKVSecret&token={{token}}&serviceUrl={{serviceUrl}}"/>
+</route>
+----
+
+YAML::
++
+[source,yaml]
+----
+- route:
+ from:
+ uri: direct:createKVSecret
+ steps:
+ - setHeader:
+ name: CamelIbmSecretsManagerSecretName
+ constant: database-creds
+ - setBody:
+ constant: '{"username": "admin", "password": "secret123"}'
+ - to:
+ uri: ibm-secrets-manager://label
+ parameters:
+ operation: createKVSecret
+ token: "{{token}}"
+ serviceUrl: "{{serviceUrl}}"
+----
+====
+
+or as a `Map`:
[source,java]
----
diff --git
a/components/camel-ibm/camel-ibm-secrets-manager/src/generated/resources/META-INF/org/apache/camel/component/ibm/secrets/manager/ibm-secrets-manager.json
b/components/camel-ibm/camel-ibm-secrets-manager/src/generated/resources/META-INF/org/apache/camel/component/ibm/secrets/manager/ibm-secrets-manager.json
index fa2ea7fd7a2c..4dc4884604fb 100644
---
a/components/camel-ibm/camel-ibm-secrets-manager/src/generated/resources/META-INF/org/apache/camel/component/ibm/secrets/manager/ibm-secrets-manager.json
+++
b/components/camel-ibm/camel-ibm-secrets-manager/src/generated/resources/META-INF/org/apache/camel/component/ibm/secrets/manager/ibm-secrets-manager.json
@@ -36,8 +36,8 @@
"CamelIbmSecretsManagerSecretData": { "index": 5, "kind": "header",
"displayName": "", "group": "producer", "label": "producer", "required": false,
"javaType": "java.util.Map", "deprecated": false, "deprecationNote": "",
"autowired": false, "secret": false, "description": "Set the secret data (Map)
for KV secret update operation.", "constantName":
"org.apache.camel.component.ibm.secrets.manager.IBMSecretsManagerConstants#SECRET_DATA"
}
},
"bodyTypes": {
- "producerIn": { "carriers": [ "text", "bytes", "stream", "file" ],
"byOption": [ { "option": "operation", "value": "createKVSecret", "carriers": [
"map" ] }, { "option": "operation", "value": "updateSecret", "carriers": [
"text", "bytes", "stream", "file", "map" ] }, { "option": "operation", "value":
"getSecret", "carriers": [ "any" ] }, { "option": "operation", "value":
"deleteSecret", "carriers": [ "any" ] }, { "option": "operation", "value":
"listSecrets", "carriers": [ "any" ] }, [...]
- "producerOut": { "carriers": [ "text" ], "javaType": "java.lang.String",
"byOption": [ { "option": "operation", "value": "getSecret", "carriers": [
"text", "map", "pojo" ] }, { "option": "operation", "value": "deleteSecret",
"carriers": [ "unchanged" ] }, { "option": "operation", "value": "listSecrets",
"carriers": [ "pojo" ], "javaType":
"com.ibm.cloud.secrets_manager_sdk.secrets_manager.v2.model.SecretMetadataPaginatedCollection"
}, { "option": "operation", "value": "updateSecret", [...]
+ "producerIn": { "carriers": [ "text", "bytes", "stream", "file", "map",
"list", "pojo" ], "byOption": [ { "option": "operation", "value":
"createKVSecret", "carriers": [ "map", "text", "bytes", "stream", "file" ] }, {
"option": "operation", "value": "updateSecret", "carriers": [ "text", "bytes",
"stream", "file", "map", "list", "pojo" ] }, { "option": "operation", "value":
"getSecret", "carriers": [ "any" ] }, { "option": "operation", "value":
"deleteSecret", "carriers": [ "any" ] }, [...]
+ "producerOut": { "carriers": [ "text" ], "javaType": "java.lang.String",
"byOption": [ { "option": "operation", "value": "getSecret", "carriers": [
"text", "map", "pojo" ] }, { "option": "operation", "value": "deleteSecret",
"carriers": [ "unchanged" ] }, { "option": "operation", "value": "listSecrets",
"carriers": [ "pojo" ], "javaType":
"com.ibm.cloud.secrets_manager_sdk.secrets_manager.v2.model.SecretMetadataPaginatedCollection"
}, { "option": "operation", "value": "updateSecret", [...]
},
"properties": {
"label": { "index": 0, "kind": "path", "displayName": "Label", "group":
"producer", "label": "", "required": true, "type": "string", "javaType":
"java.lang.String", "deprecated": false, "deprecationNote": "", "autowired":
false, "secret": false, "configurationClass":
"org.apache.camel.component.ibm.secrets.manager.IBMSecretsManagerConfiguration",
"configurationField": "configuration", "description": "Logical name" },
diff --git
a/components/camel-ibm/camel-ibm-secrets-manager/src/main/docs/ibm-secrets-manager-component.adoc
b/components/camel-ibm/camel-ibm-secrets-manager/src/main/docs/ibm-secrets-manager-component.adoc
index 43a9e69fa908..e4231c850eee 100644
---
a/components/camel-ibm/camel-ibm-secrets-manager/src/main/docs/ibm-secrets-manager-component.adoc
+++
b/components/camel-ibm/camel-ibm-secrets-manager/src/main/docs/ibm-secrets-manager-component.adoc
@@ -48,6 +48,22 @@ The component supports operations at the producer level.
Specifically, it provid
* `updateSecret` - Updates an existing secret (creates a new version)
* `listSecretVersions` - Lists all versions of a specific secret
+The payload of an arbitrary secret is the message body as text: bytes, a
stream, a file, XML (a DOM node or a
+`Source`) or a `ByteBuffer` are read as text. A `Map`, a `List` or another
object (POJO) is stored as JSON.
+
+The data of a key-value secret is a `Map` of the keys and values (or a body
that converts to a `Map`), or a JSON
+object, which can be text, bytes, a stream or a file.
+
+`getSecret` gives the payload text of an arbitrary secret and the `Map` of a
key-value secret. For the other secret
+types, such as `service_credentials`, `username_password`, `iam_credentials`,
`imported_cert`, `public_cert` or
+`private_cert`, it gives the `Secret` of the IBM SDK, whose getters return the
fields of the secret type, for example
+`getUsername()` and `getPassword()`.
+
+`updateSecret` supports arbitrary and key-value secrets: it creates a new
version with the payload of the
+`CamelIbmSecretsManagerSecretPayload` header or the message body for an
arbitrary secret, and with the data of the
+`CamelIbmSecretsManagerSecretData` header or the message body for a key-value
secret. It fails for the other secret
+types.
+
== Examples
=== Producer Operations
@@ -106,7 +122,59 @@ YAML::
==== Create a Key-Value Secret
-._Java-only: requires programmatic Map construction_
+The keys and values can be given as a JSON object:
+
+[tabs]
+====
+Java::
++
+[source,java]
+----
+from("direct:createKVSecret")
+ .setHeader("CamelIbmSecretsManagerSecretName", constant("database-creds"))
+ .setBody(constant("{\"username\": \"admin\", \"password\":
\"secret123\"}"))
+
.to("ibm-secrets-manager://label?operation=createKVSecret&token={{token}}&serviceUrl={{serviceUrl}}");
+----
+
+XML::
++
+[source,xml]
+----
+<route>
+ <from uri="direct:createKVSecret"/>
+ <setHeader name="CamelIbmSecretsManagerSecretName">
+ <constant>database-creds</constant>
+ </setHeader>
+ <setBody>
+ <constant>{"username": "admin", "password": "secret123"}</constant>
+ </setBody>
+ <to
uri="ibm-secrets-manager://label?operation=createKVSecret&token={{token}}&serviceUrl={{serviceUrl}}"/>
+</route>
+----
+
+YAML::
++
+[source,yaml]
+----
+- route:
+ from:
+ uri: direct:createKVSecret
+ steps:
+ - setHeader:
+ name: CamelIbmSecretsManagerSecretName
+ constant: database-creds
+ - setBody:
+ constant: '{"username": "admin", "password": "secret123"}'
+ - to:
+ uri: ibm-secrets-manager://label
+ parameters:
+ operation: createKVSecret
+ token: "{{token}}"
+ serviceUrl: "{{serviceUrl}}"
+----
+====
+
+or as a `Map`:
[source,java]
----
diff --git
a/components/camel-ibm/camel-ibm-secrets-manager/src/main/java/org/apache/camel/component/ibm/secrets/manager/IBMSecretsManagerEndpoint.java
b/components/camel-ibm/camel-ibm-secrets-manager/src/main/java/org/apache/camel/component/ibm/secrets/manager/IBMSecretsManagerEndpoint.java
index fbaada0b20a0..3bfa113cbbec 100644
---
a/components/camel-ibm/camel-ibm-secrets-manager/src/main/java/org/apache/camel/component/ibm/secrets/manager/IBMSecretsManagerEndpoint.java
+++
b/components/camel-ibm/camel-ibm-secrets-manager/src/main/java/org/apache/camel/component/ibm/secrets/manager/IBMSecretsManagerEndpoint.java
@@ -38,26 +38,30 @@ import org.apache.camel.support.DefaultEndpoint;
Category.CLOUD, Category.CLOUD },
producerOnly = true,
headersClass = IBMSecretsManagerConstants.class)
-@BodyTypes(producerIn = @BodyType(value = { Carrier.TEXT, Carrier.BYTES,
Carrier.STREAM, Carrier.FILE },
+@BodyTypes(producerIn = @BodyType(value = {
+ Carrier.TEXT, Carrier.BYTES, Carrier.STREAM, Carrier.FILE,
Carrier.MAP, Carrier.LIST, Carrier.POJO },
byOption = {
@BodyType.When(option = "operation",
value = "createKVSecret",
- carriers =
Carrier.MAP),
+ carriers = {
+ Carrier.MAP,
Carrier.TEXT, Carrier.BYTES, Carrier.STREAM,
+ Carrier.FILE
}),
@BodyType.When(option = "operation",
value = "updateSecret",
carriers = {
Carrier.TEXT,
Carrier.BYTES, Carrier.STREAM, Carrier.FILE,
- Carrier.MAP
}),
+ Carrier.MAP,
Carrier.LIST, Carrier.POJO }),
@BodyType.When(option = "operation",
value = "getSecret", carriers = Carrier.ANY),
@BodyType.When(option = "operation",
value = "deleteSecret", carriers = Carrier.ANY),
@BodyType.When(option = "operation",
value = "listSecrets", carriers = Carrier.ANY),
@BodyType.When(option = "operation",
value = "listSecretVersions",
carriers =
Carrier.ANY) },
description = "The payload of the secret for
createArbitrarySecret, the default operation, as"
- + " text: bytes, a stream or a
file are read as text, a Map or a POJO is stored as"
- + " its toString(), and no
body fails. createKVSecret needs a Map of the keys and"
- + " values: a JSON text fails,
so unmarshal it first. updateSecret takes text for"
- + " an arbitrary secret and a
Map for a kv secret, unless the"
+ + " text: bytes, a stream, a
file or XML are read as text, a Map, a List or a POJO"
+ + " is stored as JSON, and no
body fails. createKVSecret needs the keys and values"
+ + " as a Map or as a JSON
object, which can be text, bytes, a stream or a file."
+ + " updateSecret takes the
same bodies for an arbitrary and a kv secret, unless the"
+ "
CamelIbmSecretsManagerSecretPayload or CamelIbmSecretsManagerSecretData header"
- + " is set. The other
operations take the secret id from a header"),
+ + " is set, and fails for the
other secret types. The other operations take the"
+ + " secret id from a header"),
producerOut = @BodyType(value = Carrier.TEXT, javaType =
"java.lang.String",
byOption = {
@BodyType.When(option =
"operation", value = "getSecret",
@@ -73,9 +77,10 @@ import org.apache.camel.support.DefaultEndpoint;
carriers =
Carrier.POJO,
javaType =
"com.ibm.cloud.secrets_manager_sdk.secrets_manager.v2.model.SecretVersionMetadataCollection")
},
description = "The id of the new secret for
createArbitrarySecret and createKVSecret. getSecret"
- + " gives the payload text of
an arbitrary secret, the Map of a kv secret, or the"
- + " Secret of the IBM SDK for
service credentials, and fails for other secret"
- + " types. updateSecret gives
the SecretVersion of the IBM SDK, the list"
+ + " gives the payload text of
an arbitrary secret, the Map of a kv secret, and the"
+ + " Secret of the IBM SDK for
the other secret types, such as service credentials,"
+ + " username and password,
IAM credentials or certificates. updateSecret gives the"
+ + " SecretVersion of the IBM
SDK, the list"
+ " operations the collection
of the IBM SDK, and deleteSecret leaves the body"
+ " unchanged"))
public class IBMSecretsManagerEndpoint extends DefaultEndpoint {
diff --git
a/components/camel-ibm/camel-ibm-secrets-manager/src/main/java/org/apache/camel/component/ibm/secrets/manager/IBMSecretsManagerProducer.java
b/components/camel-ibm/camel-ibm-secrets-manager/src/main/java/org/apache/camel/component/ibm/secrets/manager/IBMSecretsManagerProducer.java
index 44d777aea3b0..e99d5302d091 100644
---
a/components/camel-ibm/camel-ibm-secrets-manager/src/main/java/org/apache/camel/component/ibm/secrets/manager/IBMSecretsManagerProducer.java
+++
b/components/camel-ibm/camel-ibm-secrets-manager/src/main/java/org/apache/camel/component/ibm/secrets/manager/IBMSecretsManagerProducer.java
@@ -16,16 +16,37 @@
*/
package org.apache.camel.component.ibm.secrets.manager;
+import java.io.File;
+import java.io.InputStream;
+import java.io.Reader;
+import java.nio.ByteBuffer;
+import java.nio.file.Path;
import java.util.Map;
+import java.util.UUID;
+import javax.xml.transform.Source;
+
+import org.w3c.dom.Node;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
import com.ibm.cloud.sdk.core.http.Response;
import com.ibm.cloud.secrets_manager_sdk.secrets_manager.v2.model.*;
import org.apache.camel.Endpoint;
import org.apache.camel.Exchange;
+import org.apache.camel.Message;
+import org.apache.camel.StreamCache;
+import org.apache.camel.WrappedFile;
import org.apache.camel.support.DefaultProducer;
import org.apache.camel.util.ObjectHelper;
public class IBMSecretsManagerProducer extends DefaultProducer {
+
+ private static final ObjectMapper MAPPER = new ObjectMapper();
+ private static final TypeReference<Map<String, Object>> MAP_TYPE = new
TypeReference<>() {
+ };
+
public IBMSecretsManagerProducer(final Endpoint endpoint) {
super(endpoint);
}
@@ -70,7 +91,7 @@ public class IBMSecretsManagerProducer extends
DefaultProducer {
"Secret Name must be specified. Set the header '" +
IBMSecretsManagerConstants.SECRET_NAME
+ "' with the desired secret
name.");
}
- String payload = exchange.getMessage().getBody(String.class);
+ String payload = arbitraryPayload(exchange.getMessage());
if (ObjectHelper.isEmpty(payload)) {
throw new IllegalArgumentException(
"Secret payload must be provided in the message body for
createArbitrarySecret operation.");
@@ -97,10 +118,10 @@ public class IBMSecretsManagerProducer extends
DefaultProducer {
"Secret Name must be specified. Set the header '" +
IBMSecretsManagerConstants.SECRET_NAME
+ "' with the desired secret
name.");
}
- Map<String, Object> data = exchange.getMessage().getBody(Map.class);
+ Map<String, Object> data = kvData(exchange.getMessage());
if (ObjectHelper.isEmpty(data)) {
throw new IllegalArgumentException(
- "Secret data must be provided as a Map in the message body
for createKVSecret operation.");
+ "Secret data must be provided as a Map or a JSON object in
the message body for createKVSecret operation.");
}
kvSecretResourceBuilder.data(data);
kvSecretResourceBuilder.secretType(KVSecretPrototype.SecretType.KV);
@@ -125,21 +146,16 @@ public class IBMSecretsManagerProducer extends
DefaultProducer {
}
Response<Secret> getResp =
getEndpoint().getSecretManager().getSecret(getSecretOptionsBuilder.build()).execute();
- String secretType = getResp.getResult().getSecretType();
- switch (secretType) {
- case "arbitrary":
-
exchange.getMessage().setBody(getResp.getResult().getPayload());
- break;
- case "kv":
- exchange.getMessage().setBody(getResp.getResult().getData());
- break;
- case "service_credentials":
- exchange.getMessage().setBody(getResp.getResult());
- break;
- default:
- throw new IllegalArgumentException(
- "Unsupported secret type '" + secretType
- + "'. Supported types are:
arbitrary, kv, service_credentials.");
+ Secret secret = getResp.getResult();
+ String secretType = secret.getSecretType();
+ if (Secret.SecretType.ARBITRARY.equals(secretType)) {
+ exchange.getMessage().setBody(secret.getPayload());
+ } else if (Secret.SecretType.KV.equals(secretType)) {
+ exchange.getMessage().setBody(secret.getData());
+ } else {
+ // service_credentials, username_password, iam_credentials,
imported_cert, public_cert, private_cert and
+ // custom_credentials: the Secret of the IBM SDK, with the fields
of its type
+ exchange.getMessage().setBody(secret);
}
}
@@ -172,13 +188,17 @@ public class IBMSecretsManagerProducer extends
DefaultProducer {
.build();
Response<Secret> getResp =
getEndpoint().getSecretManager().getSecret(getSecretOptions).execute();
String secretType = getResp.getResult().getSecretType();
+ if (secretType == null) {
+ throw new IllegalArgumentException(
+ "The secret has no secret type. The updateSecret operation
supports arbitrary and kv secrets.");
+ }
SecretVersionPrototype secretVersionPrototype;
switch (secretType) {
case "arbitrary":
String payload =
exchange.getMessage().getHeader(IBMSecretsManagerConstants.SECRET_PAYLOAD,
String.class);
if (ObjectHelper.isEmpty(payload)) {
- payload = exchange.getMessage().getBody(String.class);
+ payload = arbitraryPayload(exchange.getMessage());
}
if (ObjectHelper.isEmpty(payload)) {
throw new IllegalArgumentException("Secret payload must be
specified for arbitrary secret update");
@@ -190,7 +210,7 @@ public class IBMSecretsManagerProducer extends
DefaultProducer {
case "kv":
Map<String, Object> data =
exchange.getMessage().getHeader(IBMSecretsManagerConstants.SECRET_DATA,
Map.class);
if (ObjectHelper.isEmpty(data)) {
- data = exchange.getMessage().getBody(Map.class);
+ data = kvData(exchange.getMessage());
}
if (ObjectHelper.isEmpty(data)) {
throw new IllegalArgumentException("Secret data must be
specified for KV secret update");
@@ -200,7 +220,9 @@ public class IBMSecretsManagerProducer extends
DefaultProducer {
secretVersionPrototype = kvBuilder.build();
break;
default:
- throw new IllegalArgumentException("Unsupported secret type
for update: " + secretType);
+ throw new IllegalArgumentException(
+ "Unsupported secret type for update: " + secretType
+ + ". The updateSecret
operation supports arbitrary and kv secrets.");
}
CreateSecretVersionOptions createSecretVersionOptions = new
CreateSecretVersionOptions.Builder()
@@ -228,6 +250,60 @@ public class IBMSecretsManagerProducer extends
DefaultProducer {
exchange.getMessage().setBody(result.getResult());
}
+ /**
+ * The payload of an arbitrary secret. Text, bytes, a stream, a file, XML
(a DOM node or a Source) or a ByteBuffer
+ * are read as text with the type converter, and a number, a boolean, a
character, an enum or a UUID is its text. A
+ * Map, a List or another object is stored as JSON, rather than as its
toString().
+ */
+ private static String arbitraryPayload(Message message) {
+ Object body = message.getBody();
+ if (body == null || isText(body)) {
+ return message.getBody(String.class);
+ }
+ try {
+ return MAPPER.writeValueAsString(body);
+ } catch (JsonProcessingException e) {
+ throw new IllegalArgumentException(
+ "The message body of type " + body.getClass().getName()
+ + " cannot be stored as JSON in
an arbitrary secret. Convert it to text first.",
+ e);
+ }
+ }
+
+ /**
+ * The keys and values of a kv secret: a Map (also a body that the type
converter turns into a Map), or a JSON
+ * object as text, bytes, a stream or a file.
+ */
+ @SuppressWarnings("unchecked")
+ private static Map<String, Object> kvData(Message message) {
+ Map<String, Object> data = message.getBody(Map.class);
+ Object body = message.getBody();
+ if (data != null || body == null) {
+ return data;
+ }
+ String json = message.getBody(String.class);
+ if (ObjectHelper.isEmpty(json)) {
+ return null;
+ }
+ try {
+ return MAPPER.readValue(json, MAP_TYPE);
+ } catch (JsonProcessingException e) {
+ // the cause is not kept, as the message of the JSON parser can
quote the secret
+ throw new IllegalArgumentException(
+ "Secret data must be provided as a Map or a JSON object in
the message body for a kv secret,"
+ + " but the body of type " +
body.getClass().getName()
+ + " is not a JSON object");
+ }
+ }
+
+ private static boolean isText(Object body) {
+ return body instanceof CharSequence || body instanceof byte[] || body
instanceof InputStream
+ || body instanceof Reader || body instanceof StreamCache ||
body instanceof File || body instanceof Path
+ || body instanceof WrappedFile || body instanceof Node || body
instanceof Source
+ || body instanceof ByteBuffer || body instanceof Number ||
body instanceof Boolean
+ || body instanceof Character || body instanceof Enum<?> ||
body instanceof UUID;
+ }
+
@Override
public IBMSecretsManagerEndpoint getEndpoint() {
return (IBMSecretsManagerEndpoint) super.getEndpoint();
diff --git
a/components/camel-ibm/camel-ibm-secrets-manager/src/test/java/org/apache/camel/component/ibm/secrets/manager/IBMSecretsManagerProducerBodyTest.java
b/components/camel-ibm/camel-ibm-secrets-manager/src/test/java/org/apache/camel/component/ibm/secrets/manager/IBMSecretsManagerProducerBodyTest.java
new file mode 100644
index 000000000000..8e93e5dd2346
--- /dev/null
+++
b/components/camel-ibm/camel-ibm-secrets-manager/src/test/java/org/apache/camel/component/ibm/secrets/manager/IBMSecretsManagerProducerBodyTest.java
@@ -0,0 +1,504 @@
+/*
+ * 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.component.ibm.secrets.manager;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.net.InetSocketAddress;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
+
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.transform.dom.DOMSource;
+
+import org.w3c.dom.Document;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.ibm.cloud.sdk.core.security.NoAuthAuthenticator;
+import com.ibm.cloud.secrets_manager_sdk.secrets_manager.v2.SecretsManager;
+import com.ibm.cloud.secrets_manager_sdk.secrets_manager.v2.model.Secret;
+import com.sun.net.httpserver.HttpExchange;
+import com.sun.net.httpserver.HttpServer;
+import org.apache.camel.CamelContext;
+import org.apache.camel.Exchange;
+import org.apache.camel.TypeConversionException;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.support.TypeConverterSupport;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+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.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * The bodies the producer takes and gives, with the IBM Secrets Manager SDK
against a JDK HttpServer that plays the
+ * service and records the requests.
+ */
+class IBMSecretsManagerProducerBodyTest extends CamelTestSupport {
+
+ private static final ObjectMapper MAPPER = new ObjectMapper();
+
+ private static final String SECRETS_PATH = "/api/v2/secrets";
+
+ // the secrets the server knows, by id, which is also the secret type
+ private static final Map<String, String> SECRETS = Map.of(
+ "arbitrary", "{\"payload\":\"s3cr3t\"}",
+ "kv",
"{\"data\":{\"username\":\"admin\",\"password\":\"s3cr3t\"}}",
+ "username_password",
"{\"username\":\"admin\",\"password\":\"s3cr3t\"}",
+ "iam_credentials",
"{\"api_key\":\"my-api-key\",\"service_id\":\"my-service-id\"}",
+ "imported_cert", "{\"certificate\":\"-----BEGIN
CERTIFICATE-----\",\"common_name\":\"example.com\"}",
+ "public_cert", "{\"certificate\":\"-----BEGIN
CERTIFICATE-----\",\"common_name\":\"example.com\"}",
+ "private_cert", "{\"certificate\":\"-----BEGIN
CERTIFICATE-----\",\"common_name\":\"example.com\"}",
+ "service_credentials",
"{\"credentials\":{\"apikey\":\"my-api-key\"}}",
+ "custom_credentials",
"{\"credentials_content\":{\"token\":\"my-token\"}}");
+
+ private static final Map<String, String> REQUESTS = new
ConcurrentHashMap<>();
+ private static HttpServer server;
+
+ @BeforeAll
+ static void startServer() throws IOException {
+ server = HttpServer.create(new InetSocketAddress("localhost", 0), 0);
+ server.createContext("/", IBMSecretsManagerProducerBodyTest::handle);
+ server.start();
+ }
+
+ @AfterAll
+ static void stopServer() {
+ if (server != null) {
+ server.stop(0);
+ }
+ }
+
+ private static void handle(HttpExchange exchange) throws IOException {
+ String method = exchange.getRequestMethod();
+ String path = exchange.getRequestURI().getPath();
+ String request = new String(exchange.getRequestBody().readAllBytes(),
StandardCharsets.UTF_8);
+ REQUESTS.put(method + " " + path, request);
+
+ String response = null;
+ int status = 200;
+ if ("GET".equals(method) && path.startsWith(SECRETS_PATH + "/")) {
+ // get a secret: the id is the type
+ String id = path.substring(SECRETS_PATH.length() + 1);
+ String fields = SECRETS.get(id);
+ if (fields != null) {
+ response = secret(id, id, fields);
+ }
+ } else if ("POST".equals(method) && path.equals(SECRETS_PATH)) {
+ // create a secret
+ String type =
MAPPER.readTree(request).path("secret_type").asText();
+ response = secret("new-secret-id", type, "{}");
+ status = 201;
+ } else if ("POST".equals(method) && path.startsWith(SECRETS_PATH +
"/") && path.endsWith("/versions")) {
+ // create a version of a secret
+ String id = path.substring(SECRETS_PATH.length() + 1,
path.length() - "/versions".length());
+ ObjectNode version = MAPPER.createObjectNode();
+ version.put("id", "new-version-id");
+ version.put("secret_id", id);
+ version.put("secret_type", id);
+ response = version.toString();
+ status = 201;
+ }
+ if (response == null) {
+ exchange.sendResponseHeaders(404, -1);
+ exchange.close();
+ return;
+ }
+ byte[] bytes = response.getBytes(StandardCharsets.UTF_8);
+ exchange.getResponseHeaders().add("Content-Type", "application/json");
+ exchange.sendResponseHeaders(status, bytes.length);
+ try (OutputStream out = exchange.getResponseBody()) {
+ out.write(bytes);
+ }
+ }
+
+ private static String secret(String id, String type, String fields) throws
IOException {
+ ObjectNode secret = (ObjectNode) MAPPER.readTree(fields);
+ secret.put("id", id);
+ secret.put("name", "my-secret");
+ secret.put("secret_type", type);
+ return secret.toString();
+ }
+
+ @BeforeEach
+ void useTestServer() {
+ REQUESTS.clear();
+ // the endpoint is started with an IAM client: use a client without
authentication that calls the test server
+ SecretsManager client = new SecretsManager("test", new
NoAuthAuthenticator());
+ client.setServiceUrl("http://localhost:" +
server.getAddress().getPort());
+ context.getEndpoint(endpointUri(),
IBMSecretsManagerEndpoint.class).setSecretManager(client);
+ }
+
+ @Override
+ protected CamelContext createCamelContext() throws Exception {
+ CamelContext context = super.createCamelContext();
+ // a body that becomes a Map only through a type converter
+ context.getTypeConverterRegistry().addTypeConverter(Map.class,
KeyValue.class, new TypeConverterSupport() {
+ @Override
+ public <T> T convertTo(Class<T> type, Exchange exchange, Object
value) throws TypeConversionException {
+ KeyValue keyValue = (KeyValue) value;
+ return type.cast(Map.of(keyValue.key(), keyValue.value()));
+ }
+ });
+ return context;
+ }
+
+ private static String endpointUri() {
+ return
"ibm-secrets-manager:test?token=test-token&serviceUrl=http://localhost:" +
server.getAddress().getPort();
+ }
+
+ @Override
+ protected RouteBuilder createRouteBuilder() {
+ return new RouteBuilder() {
+ @Override
+ public void configure() {
+ from("direct:secrets").to(endpointUri());
+ }
+ };
+ }
+
+ // 1. getSecret and updateSecret for the other secret types
+
+ @ParameterizedTest
+ @ValueSource(strings = {
+ "username_password", "iam_credentials", "imported_cert",
"public_cert", "private_cert", "service_credentials",
+ "custom_credentials" })
+ void getSecretGivesTheSecretOfTheOtherTypes(String type) {
+ Exchange result = getSecret(type);
+
+ Secret secret = assertInstanceOf(Secret.class,
result.getMessage().getBody());
+ assertEquals(type, secret.getSecretType());
+ assertEquals(type, secret.getId());
+ }
+
+ @Test
+ void getSecretGivesTheUsernameAndPasswordOfAUsernamePasswordSecret() {
+ Secret secret = assertInstanceOf(Secret.class,
getSecret("username_password").getMessage().getBody());
+
+ assertEquals("admin", secret.getUsername());
+ assertEquals("s3cr3t", secret.getPassword());
+ }
+
+ @Test
+ void getSecretGivesTheApiKeyOfAnIamCredentialsSecret() {
+ Secret secret = assertInstanceOf(Secret.class,
getSecret("iam_credentials").getMessage().getBody());
+
+ assertEquals("my-api-key", secret.getApiKey());
+ }
+
+ @Test
+ void getSecretGivesTheCertificateOfAnImportedCertificate() {
+ Secret secret = assertInstanceOf(Secret.class,
getSecret("imported_cert").getMessage().getBody());
+
+ assertEquals("-----BEGIN CERTIFICATE-----", secret.getCertificate());
+ }
+
+ @Test
+ void getSecretGivesThePayloadOfAnArbitrarySecretAndTheMapOfAKvSecret() {
+ assertEquals("s3cr3t", getSecret("arbitrary").getMessage().getBody());
+ assertEquals(Map.of("username", "admin", "password", "s3cr3t"),
getSecret("kv").getMessage().getBody());
+ }
+
+ @Test
+ void updateSecretFailsClearlyForTheOtherTypes() {
+ Exchange result = updateSecret("username_password", "new-password");
+
+ IllegalArgumentException e =
assertInstanceOf(IllegalArgumentException.class, result.getException());
+ assertTrue(e.getMessage().contains("username_password"),
e.getMessage());
+ assertTrue(e.getMessage().contains("supports arbitrary and kv
secrets"), e.getMessage());
+ assertNull(REQUESTS.get("POST " + SECRETS_PATH +
"/username_password/versions"), "no version is created");
+ }
+
+ // 2. createKVSecret and a JSON object
+
+ @Test
+ void createKVSecretTakesAJsonObjectText() throws Exception {
+ Exchange result = createKVSecret("{\"username\": \"admin\",
\"password\": \"s3cr3t\", \"port\": 5432}");
+
+ assertEquals("new-secret-id", result.getMessage().getBody());
+ JsonNode request = request("POST " + SECRETS_PATH);
+ assertEquals("kv", request.get("secret_type").asText());
+ assertEquals(MAPPER.readTree("{\"username\": \"admin\", \"password\":
\"s3cr3t\", \"port\": 5432}"),
+ request.get("data"));
+ }
+
+ @Test
+ void createKVSecretTakesAJsonObjectAsBytes() throws Exception {
+ createKVSecret("{\"username\":
\"admin\"}".getBytes(StandardCharsets.UTF_8));
+
+ assertEquals(MAPPER.readTree("{\"username\": \"admin\"}"),
request("POST " + SECRETS_PATH).get("data"));
+ }
+
+ @Test
+ void createKVSecretTakesAJsonObjectAsAStream() throws Exception {
+ createKVSecret(new ByteArrayInputStream("{\"username\":
\"admin\"}".getBytes(StandardCharsets.UTF_8)));
+
+ assertEquals(MAPPER.readTree("{\"username\": \"admin\"}"),
request("POST " + SECRETS_PATH).get("data"));
+ }
+
+ @Test
+ void createKVSecretTakesAMap() throws Exception {
+ createKVSecret(Map.of("username", "admin"));
+
+ assertEquals(MAPPER.readTree("{\"username\": \"admin\"}"),
request("POST " + SECRETS_PATH).get("data"));
+ }
+
+ @Test
+ void createKVSecretTakesABodyThatConvertsToAMap() throws Exception {
+ createKVSecret(new KeyValue("username", "admin"));
+
+ assertEquals(MAPPER.readTree("{\"username\": \"admin\"}"),
request("POST " + SECRETS_PATH).get("data"));
+ }
+
+ @Test
+ void createKVSecretFailsForJsonThatIsNotAnObject() {
+ Exchange result = template.send("direct:secrets", exchange -> {
+ exchange.getIn().setHeader(IBMSecretsManagerConstants.OPERATION,
IBMSecretsManagerOperation.createKVSecret);
+ exchange.getIn().setHeader(IBMSecretsManagerConstants.SECRET_NAME,
"my-secret");
+ exchange.getIn().setBody("[\"admin\", \"s3cr3t\"]");
+ });
+
+ IllegalArgumentException e =
assertInstanceOf(IllegalArgumentException.class, result.getException());
+ assertTrue(e.getMessage().contains("is not a JSON object"),
e.getMessage());
+ assertNull(REQUESTS.get("POST " + SECRETS_PATH), "no secret is
created");
+ }
+
+ @Test
+ void createKVSecretDoesNotQuoteATextThatIsNotJson() {
+ Exchange result = template.send("direct:secrets", exchange -> {
+ exchange.getIn().setHeader(IBMSecretsManagerConstants.OPERATION,
IBMSecretsManagerOperation.createKVSecret);
+ exchange.getIn().setHeader(IBMSecretsManagerConstants.SECRET_NAME,
"my-secret");
+ exchange.getIn().setBody("s3cr3t-password");
+ });
+
+ IllegalArgumentException e =
assertInstanceOf(IllegalArgumentException.class, result.getException());
+ assertTrue(e.getMessage().contains("is not a JSON object"),
e.getMessage());
+ // the error is logged: it must not quote the secret, as the message
of the JSON parser does
+ StringWriter trace = new StringWriter();
+ e.printStackTrace(new PrintWriter(trace));
+ assertFalse(trace.toString().contains("s3cr3t"), trace.toString());
+ }
+
+ @Test
+ void updateSecretOfAKvSecretTakesAJsonObjectText() throws Exception {
+ Exchange result = updateSecret("kv", "{\"password\":
\"new-password\"}");
+
+ assertNoException(result);
+ assertEquals(MAPPER.readTree("{\"password\": \"new-password\"}"),
+ request("POST " + SECRETS_PATH + "/kv/versions").get("data"));
+ }
+
+ // 3. createArbitrarySecret and updateSecret store a Map or a POJO as JSON
+
+ @Test
+ void createArbitrarySecretStoresAMapAsJson() throws Exception {
+ Map<String, Object> body = new LinkedHashMap<>();
+ body.put("username", "admin");
+ body.put("port", 5432);
+
+ createArbitrarySecret(body);
+
+ assertEquals(MAPPER.readTree("{\"username\": \"admin\", \"port\":
5432}"), payloadOf("POST " + SECRETS_PATH));
+ }
+
+ @Test
+ void createArbitrarySecretStoresAPojoAsJson() throws Exception {
+ createArbitrarySecret(new DatabaseCredentials("admin", "s3cr3t"));
+
+ assertEquals(MAPPER.readTree("{\"username\": \"admin\", \"password\":
\"s3cr3t\"}"),
+ payloadOf("POST " + SECRETS_PATH));
+ }
+
+ @Test
+ void createArbitrarySecretStoresAListAsJson() throws Exception {
+ createArbitrarySecret(List.of("admin", "s3cr3t"));
+
+ assertEquals(MAPPER.readTree("[\"admin\", \"s3cr3t\"]"),
payloadOf("POST " + SECRETS_PATH));
+ }
+
+ @Test
+ void createArbitrarySecretReadsTextBytesAndNumbersAsText() throws
Exception {
+ createArbitrarySecret("s3cr3t");
+ assertEquals("s3cr3t", request("POST " +
SECRETS_PATH).get("payload").asText());
+
+ createArbitrarySecret("s3cr3t-bytes".getBytes(StandardCharsets.UTF_8));
+ assertEquals("s3cr3t-bytes", request("POST " +
SECRETS_PATH).get("payload").asText());
+
+ createArbitrarySecret(new
ByteArrayInputStream("s3cr3t".getBytes(StandardCharsets.UTF_8)));
+ assertEquals("s3cr3t", request("POST " +
SECRETS_PATH).get("payload").asText());
+
+ createArbitrarySecret(1234);
+ assertEquals("1234", request("POST " +
SECRETS_PATH).get("payload").asText());
+ }
+
+ @Test
+ void createArbitrarySecretReadsADomDocumentAndASourceAsXml() throws
Exception {
+ Document document =
DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();
+
document.appendChild(document.createElement("user")).setTextContent("admin");
+ String xml = context.getTypeConverter().convertTo(String.class,
document);
+ assertTrue(xml.contains("<user>admin</user>"), xml);
+
+ createArbitrarySecret(document);
+ assertEquals(xml, request("POST " +
SECRETS_PATH).get("payload").asText(), "the XML, not a JSON string");
+
+ createArbitrarySecret(new DOMSource(document));
+ assertEquals(xml, request("POST " +
SECRETS_PATH).get("payload").asText(), "the XML, not a JSON string");
+ }
+
+ @Test
+ void createArbitrarySecretReadsAByteBufferAsText() throws Exception {
+
createArbitrarySecret(ByteBuffer.wrap("s3cr3t".getBytes(StandardCharsets.UTF_8)));
+
+ assertEquals("s3cr3t", request("POST " +
SECRETS_PATH).get("payload").asText(), "the text, not Base64");
+ }
+
+ @Test
+ void createArbitrarySecretReadsAUuidAsText() throws Exception {
+ UUID uuid = UUID.randomUUID();
+
+ createArbitrarySecret(uuid);
+
+ assertEquals(uuid.toString(), request("POST " +
SECRETS_PATH).get("payload").asText(), "the text, not quoted");
+ }
+
+ @Test
+ void createArbitrarySecretFailsWithoutABody() {
+ Exchange result = template.send("direct:secrets", exchange -> {
+ exchange.getIn().setHeader(IBMSecretsManagerConstants.SECRET_NAME,
"my-secret");
+ });
+
+ IllegalArgumentException e =
assertInstanceOf(IllegalArgumentException.class, result.getException());
+ assertTrue(e.getMessage().startsWith("Secret payload must be
provided"), e.getMessage());
+ }
+
+ @Test
+ void updateSecretOfAnArbitrarySecretStoresAMapAsJson() throws Exception {
+ Exchange result = updateSecret("arbitrary", Map.of("username",
"admin"));
+
+ assertNoException(result);
+ assertEquals(MAPPER.readTree("{\"username\": \"admin\"}"),
payloadOf("POST " + SECRETS_PATH + "/arbitrary/versions"));
+ }
+
+ private Exchange getSecret(String id) {
+ Exchange result = template.send("direct:secrets", exchange -> {
+ exchange.getIn().setHeader(IBMSecretsManagerConstants.OPERATION,
IBMSecretsManagerOperation.getSecret);
+ exchange.getIn().setHeader(IBMSecretsManagerConstants.SECRET_ID,
id);
+ });
+ assertNoException(result);
+ return result;
+ }
+
+ private Exchange updateSecret(String id, Object body) {
+ return template.send("direct:secrets", exchange -> {
+ exchange.getIn().setHeader(IBMSecretsManagerConstants.OPERATION,
IBMSecretsManagerOperation.updateSecret);
+ exchange.getIn().setHeader(IBMSecretsManagerConstants.SECRET_ID,
id);
+ exchange.getIn().setBody(body);
+ });
+ }
+
+ private Exchange createKVSecret(Object body) {
+ Exchange result = template.send("direct:secrets", exchange -> {
+ exchange.getIn().setHeader(IBMSecretsManagerConstants.OPERATION,
IBMSecretsManagerOperation.createKVSecret);
+ exchange.getIn().setHeader(IBMSecretsManagerConstants.SECRET_NAME,
"my-secret");
+ exchange.getIn().setBody(body);
+ });
+ assertNoException(result);
+ return result;
+ }
+
+ private Exchange createArbitrarySecret(Object body) {
+ Exchange result = template.send("direct:secrets", exchange -> {
+ exchange.getIn().setHeader(IBMSecretsManagerConstants.SECRET_NAME,
"my-secret");
+ exchange.getIn().setBody(body);
+ });
+ assertNoException(result);
+ assertEquals("new-secret-id", result.getMessage().getBody());
+ return result;
+ }
+
+ private static JsonNode request(String key) throws IOException {
+ String body = REQUESTS.get(key);
+ if (body == null) {
+ throw new AssertionError("no request " + key + " in " +
REQUESTS.keySet());
+ }
+ return MAPPER.readTree(body);
+ }
+
+ /**
+ * The payload of the secret or version the request creates, read as JSON.
+ */
+ private static JsonNode payloadOf(String key) throws IOException {
+ String payload = request(key).get("payload").asText();
+ try {
+ return MAPPER.readTree(payload);
+ } catch (JsonProcessingException e) {
+ throw new AssertionError("the payload is not JSON: " + payload, e);
+ }
+ }
+
+ private static void assertNoException(Exchange result) {
+ if (result.getException() != null) {
+ throw new AssertionError("the exchange failed: " +
result.getException(), result.getException());
+ }
+ }
+
+ record KeyValue(String key, String value) {
+ }
+
+ static final class DatabaseCredentials {
+
+ private final String username;
+ private final String password;
+
+ DatabaseCredentials(String username, String password) {
+ this.username = username;
+ this.password = password;
+ }
+
+ public String getUsername() {
+ return username;
+ }
+
+ public String getPassword() {
+ return password;
+ }
+
+ @Override
+ public String toString() {
+ return "DatabaseCredentials[username=" + username + "]";
+ }
+ }
+}
diff --git
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
index d01e5531afdc..d140e03fee39 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
@@ -2808,6 +2808,14 @@ nothing was stored and the exchange completed without an
error. The requests are
the answer like the other state operations: the states are stored, and a
request that Dapr rejects (or a sidecar that
cannot be reached) fails the exchange.
+=== camel-ibm-secrets-manager - a Map or a POJO body is stored as JSON in an
arbitrary secret
+
+The `createArbitrarySecret` operation, and the `updateSecret` operation for an
arbitrary secret, now store a `Map`, a
+`List` or another object (POJO) in the message body as JSON. Before, the
payload was the `toString()` of the body, such
+as `{username=admin}`. Text, bytes, a stream, a file, XML (a DOM node or a
`Source`), a `ByteBuffer`, a `UUID`, a number,
+a boolean, a character or an enum is stored as text, as before. A route that
relied on the `toString()` form must convert the body to text
+before the call.
+
=== camel-pubnub - the producer uses its configured channel
The pubnub producer no longer reads the `CamelPubNubChannel` header. This
header is set by the pubnub consumer to the