From e4163525be7d18711416d99aa7fab1ee4a917338 Mon Sep 17 00:00:00 2001 From: Jiri Ondrusek Date: Mon, 20 Jul 2026 15:04:48 +0200 Subject: [PATCH 1/2] Fixes #8897: Add CyberArk Vault example Co-Authored-By: Claude Opus 4.6 --- cyberark-vault/README.adoc | 118 +++++++ cyberark-vault/docker-compose.yml | 38 +++ cyberark-vault/eclipse-formatter-config.xml | 276 +++++++++++++++ cyberark-vault/pom.xml | 319 ++++++++++++++++++ .../cyberark/vault/CyberarkVaultResource.java | 50 +++ .../cyberark/vault/CyberarkVaultRoutes.java | 75 ++++ .../src/main/resources/application.properties | 30 ++ .../acme/cyberark/vault/CyberarkVaultIT.java | 24 ++ .../cyberark/vault/CyberarkVaultTest.java | 59 ++++ .../vault/CyberarkVaultTestResource.java | 267 +++++++++++++++ .../src/test/resources/conf/default.conf | 12 + .../src/test/resources/conf/policy/BotApp.yml | 31 ++ cyberark-vault/start-conjur.sh | 115 +++++++ docs/modules/ROOT/attachments/examples.json | 5 + pom.xml | 1 + 15 files changed, 1420 insertions(+) create mode 100644 cyberark-vault/README.adoc create mode 100644 cyberark-vault/docker-compose.yml create mode 100644 cyberark-vault/eclipse-formatter-config.xml create mode 100644 cyberark-vault/pom.xml create mode 100644 cyberark-vault/src/main/java/org/acme/cyberark/vault/CyberarkVaultResource.java create mode 100644 cyberark-vault/src/main/java/org/acme/cyberark/vault/CyberarkVaultRoutes.java create mode 100644 cyberark-vault/src/main/resources/application.properties create mode 100644 cyberark-vault/src/test/java/org/acme/cyberark/vault/CyberarkVaultIT.java create mode 100644 cyberark-vault/src/test/java/org/acme/cyberark/vault/CyberarkVaultTest.java create mode 100644 cyberark-vault/src/test/java/org/acme/cyberark/vault/CyberarkVaultTestResource.java create mode 100644 cyberark-vault/src/test/resources/conf/default.conf create mode 100644 cyberark-vault/src/test/resources/conf/policy/BotApp.yml create mode 100755 cyberark-vault/start-conjur.sh diff --git a/cyberark-vault/README.adoc b/cyberark-vault/README.adoc new file mode 100644 index 000000000..00862d5fa --- /dev/null +++ b/cyberark-vault/README.adoc @@ -0,0 +1,118 @@ += CyberArk Vault: A Camel Quarkus example +:cq-example-description: An example that shows how to retrieve secrets from CyberArk Vault via property placeholders with a local Conjur container + +{cq-description} + +TIP: Check the https://camel.apache.org/camel-quarkus/latest/first-steps.html[Camel Quarkus User guide] for prerequisites +and other general information. + +== Prerequisites + +* Docker (for running the Conjur containers) + +== Starting Conjur + +The example requires a running Conjur instance. A helper script is provided to start a local Conjur +environment via Docker Compose: + +[source,shell] +---- +$ ./start-conjur.sh +---- + +The script starts the containers, initializes the account, loads the security policy and prints the +`export` commands you need to run. Copy and execute them in your shell. + +Alternatively, to use an existing Conjur instance, set the following environment variables manually: + +[source,shell] +---- +export CQ_CONJUR_URL=http://localhost:9080 +export CQ_CONJUR_ACCOUNT=myConjurAccount +export CQ_CONJUR_READ_USER=host/BotApp/myDemoApp +export CQ_CONJUR_READ_USER_API_KEY=... +export CQ_CONJUR_READ_WRITE_USER=user/Dave@BotApp +export CQ_CONJUR_READ_WRITE_USER_API_KEY=... +---- + +To stop the Docker-based Conjur environment: + +[source,shell] +---- +$ ./start-conjur.sh stop +---- + +== Start in the Development mode + +With the environment variables set, start the example in dev mode: + +[source,shell] +---- +$ mvn clean compile quarkus:dev +---- + +Then look at the log output in the console. There is a timer route that periodically resolves the secret +via the CyberArk property placeholder. The first several messages will show that no secret is stored yet. +To store a secret, open a new terminal and run: + +[source,shell] +---- +$ curl -X POST http://localhost:8080/cyberark-vault/createSecret -d 'my-secret-value' +---- + +Following messages will show the resolved secret value. As we run the example in Quarkus Dev Mode, you can +edit the source code and have live updates. + +TIP: Please refer to the Development mode section of +https://camel.apache.org/camel-quarkus/latest/first-steps.html#_development_mode[Camel Quarkus User guide] for more details. + +== Package and run the application + +Once you are done with developing you may want to package and run the application. +Make sure the Conjur environment variables are set (see <>). + +TIP: Find more details about the JVM mode and Native mode in the Package and run section of +https://camel.apache.org/camel-quarkus/latest/first-steps.html#_package_and_run_the_application[Camel Quarkus User guide] + +=== JVM mode + +[source,shell] +---- +$ mvn clean package +$ java -jar target/quarkus-app/quarkus-run.jar +... +[io.quarkus] (main) camel-quarkus-examples-... started in 1.163s. +---- + +=== Native mode + +IMPORTANT: Native mode requires having GraalVM and other tools installed. Please check the Prerequisites section +of https://camel.apache.org/camel-quarkus/latest/first-steps.html#_prerequisites[Camel Quarkus User guide]. + +To prepare a native executable using GraalVM, run the following command: + +[source,shell] +---- +$ mvn clean package -Dnative +$ ./target/*-runner +... +[io.quarkus] (main) camel-quarkus-examples-... started in 0.013s. +... +---- + +== Running the tests + +By default, the tests use testcontainers to start a local Conjur environment automatically: + +[source,shell] +---- +$ mvn clean verify +---- + +To run the tests against an external Conjur instance instead, set the environment variables described +in <> before running the command. When all `CQ_CONJUR_*` variables are present, +testcontainers are skipped and the tests run against the external instance. + +== Feedback + +Please report bugs and propose improvements via https://github.com/apache/camel-quarkus/issues[GitHub issues of Camel Quarkus] project. diff --git a/cyberark-vault/docker-compose.yml b/cyberark-vault/docker-compose.yml new file mode 100644 index 000000000..b5828a1db --- /dev/null +++ b/cyberark-vault/docker-compose.yml @@ -0,0 +1,38 @@ +# +# 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. +# + +services: + database: + image: mirror.gcr.io/postgres:17.5 + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: SuperSecretPg + POSTGRES_DB: postgres + + conjur: + image: mirror.gcr.io/cyberark/conjur:1.24.0 + command: server + depends_on: + - database + ports: + - "${CONJUR_PORT:-9080}:80" + environment: + DATABASE_URL: postgres://postgres:SuperSecretPg@database/postgres + CONJUR_DATA_KEY: changeitchangeitchangeitchangeitchangeitIhc= + CONJUR_AUTHENTICATORS: "" + CONJUR_TELEMETRY_ENABLED: "false" + CONJUR_API_RESOURCE_LIST_LIMIT_MAX: "5000" diff --git a/cyberark-vault/eclipse-formatter-config.xml b/cyberark-vault/eclipse-formatter-config.xml new file mode 100644 index 000000000..2248b2b8e --- /dev/null +++ b/cyberark-vault/eclipse-formatter-config.xml @@ -0,0 +1,276 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/cyberark-vault/pom.xml b/cyberark-vault/pom.xml new file mode 100644 index 000000000..c40742c7c --- /dev/null +++ b/cyberark-vault/pom.xml @@ -0,0 +1,319 @@ + + + + 4.0.0 + + camel-quarkus-examples-cyberark-vault + org.apache.camel.quarkus.examples + 3.38.0-SNAPSHOT + + Camel Quarkus :: Examples :: CyberArk Vault + Camel Quarkus Example :: CyberArk Vault + + + 3.38.0 + 3.39.0-SNAPSHOT + + io.quarkus.platform + quarkus-bom + ${quarkus.platform.group-id} + quarkus-camel-bom + + UTF-8 + UTF-8 + 17 + + 0.9.3 + 2.29.0 + 1.13.0 + 5.1.1 + 3.15.0 + 3.5.1 + 3.3.1 + 3.5.5 + + + + + + + ${quarkus.platform.group-id} + ${quarkus.platform.artifact-id} + ${quarkus.platform.version} + pom + import + + + ${camel-quarkus.platform.group-id} + ${camel-quarkus.platform.artifact-id} + ${camel-quarkus.platform.version} + pom + import + + + + + + + org.apache.camel.quarkus + camel-quarkus-cyberark-vault + + + org.apache.camel.quarkus + camel-quarkus-direct + + + org.apache.camel.quarkus + camel-quarkus-timer + + + io.quarkus + quarkus-resteasy + + + + + io.quarkus + quarkus-junit + test + + + io.rest-assured + rest-assured + test + + + org.testcontainers + testcontainers + test + + + io.smallrye.certs + smallrye-certificate-generator-junit5 + ${smallrye-certificate-generator-junit5.version} + test + + + + + + + + net.revelc.code.formatter + formatter-maven-plugin + ${formatter-maven-plugin.version} + + ${maven.multiModuleProjectDirectory}/eclipse-formatter-config.xml + LF + + + + + net.revelc.code + impsort-maven-plugin + ${impsort-maven-plugin.version} + + java.,javax.,org.w3c.,org.xml.,junit. + true + true + java.,javax.,org.w3c.,org.xml.,junit. + + + + + org.apache.maven.plugins + maven-compiler-plugin + ${maven-compiler-plugin.version} + + true + true + + -Xlint:unchecked + + + + + + org.apache.maven.plugins + maven-surefire-plugin + ${maven-surefire-plugin.version} + + false + + org.jboss.logmanager.LogManager + + + + + + ${quarkus.platform.group-id} + quarkus-maven-plugin + ${quarkus.platform.version} + true + + + + org.apache.maven.plugins + maven-failsafe-plugin + ${maven-surefire-plugin.version} + + + + org.apache.maven.plugins + maven-jar-plugin + ${maven-jar-plugin.version} + + + + com.mycila + license-maven-plugin + ${license-maven-plugin.version} + + true +
${maven.multiModuleProjectDirectory}/header.txt
+ + **/*.adoc + **/*.txt + **/*.conf + **/LICENSE.txt + **/LICENSE + **/NOTICE.txt + **/NOTICE + **/README + **/pom.xml.versionsBackup + + + SLASHSTAR_STYLE + CAMEL_PROPERTIES_STYLE + SLASHSTAR_STYLE + + + ${maven.multiModuleProjectDirectory}/license-properties-headerdefinition.xml + +
+
+
+
+ + + + ${quarkus.platform.group-id} + quarkus-maven-plugin + + + build + + build + + + + + + + net.revelc.code.formatter + formatter-maven-plugin + + + format + + format + + process-sources + + + + + + net.revelc.code + impsort-maven-plugin + + + sort-imports + + sort + + process-sources + + + + + + com.mycila + license-maven-plugin + + + license-format + + format + + process-sources + + + + +
+ + + + skip-testcontainers-tests + + + skip-testcontainers-tests + + + + true + + + + native + + + native + + + + true + + + + + org.apache.maven.plugins + maven-failsafe-plugin + + + + integration-test + verify + + + + ${quarkus.native.enabled} + + + + + + + + + + +
diff --git a/cyberark-vault/src/main/java/org/acme/cyberark/vault/CyberarkVaultResource.java b/cyberark-vault/src/main/java/org/acme/cyberark/vault/CyberarkVaultResource.java new file mode 100644 index 000000000..d9209aaf8 --- /dev/null +++ b/cyberark-vault/src/main/java/org/acme/cyberark/vault/CyberarkVaultResource.java @@ -0,0 +1,50 @@ +/* + * 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.acme.cyberark.vault; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import org.apache.camel.ProducerTemplate; + +@Path("/cyberark-vault") +@ApplicationScoped +public class CyberarkVaultResource { + + @Inject + ProducerTemplate producerTemplate; + + @Path("/createSecret") + @POST + public void createSecret(String body) { + producerTemplate.requestBody("direct:createSecret", body, String.class); + } + + @Path("/getSecret") + @GET + public String getSecret() { + return producerTemplate.requestBody("direct:getSecret", "", String.class); + } + + @Path("/propertyPlaceholder") + @GET + public String propertyPlaceholder() { + return producerTemplate.requestBody("direct:propertyPlaceholder", "", String.class); + } +} diff --git a/cyberark-vault/src/main/java/org/acme/cyberark/vault/CyberarkVaultRoutes.java b/cyberark-vault/src/main/java/org/acme/cyberark/vault/CyberarkVaultRoutes.java new file mode 100644 index 000000000..a2bef519c --- /dev/null +++ b/cyberark-vault/src/main/java/org/acme/cyberark/vault/CyberarkVaultRoutes.java @@ -0,0 +1,75 @@ +/* + * 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.acme.cyberark.vault; + +import jakarta.enterprise.context.ApplicationScoped; +import org.apache.camel.builder.RouteBuilder; +import org.apache.camel.spi.PropertiesComponent; +import org.eclipse.microprofile.config.inject.ConfigProperty; + +@ApplicationScoped +public class CyberarkVaultRoutes extends RouteBuilder { + + @ConfigProperty(name = "conjur.url") + String url; + @ConfigProperty(name = "conjur.account") + String account; + @ConfigProperty(name = "conjur.writer.username") + String writerUsername; + @ConfigProperty(name = "conjur.writer.apiKey") + String writerApiKey; + @ConfigProperty(name = "conjur.reader.username") + String readerUsername; + @ConfigProperty(name = "conjur.reader.apiKey") + String readerApiKey; + + @Override + public void configure() throws Exception { + + from("direct:createSecret") + .toF("cyberark-vault:secret?operation=createSecret&secretId=BotApp/secretVar&url=%s&account=%s&username=%s&apiKey=%s", + url, account, writerUsername, writerApiKey) + .log("Secret created/updated"); + + from("direct:getSecret") + .toF("cyberark-vault:secret?secretId=BotApp/secretVar&url=%s&account=%s&username=%s&apiKey=%s", + url, account, readerUsername, readerApiKey) + .log("Retrieved secret: ${body}"); + + from("direct:propertyPlaceholder") + .process(exchange -> { + PropertiesComponent component = exchange.getContext().getPropertiesComponent(); + component.resolveProperty("cyberark:BotApp/secretVar").ifPresent(value -> { + exchange.getMessage().setBody(value); + }); + }); + + from("timer:readSecret?period=5000") + .autoStartup("{{timer.enabled:true}}") + .doTry() + .process(exchange -> { + PropertiesComponent component = exchange.getContext().getPropertiesComponent(); + component.resolveProperty("cyberark:BotApp/secretVar").ifPresent(value -> { + exchange.getMessage().setBody(value); + }); + }) + .log("Property placeholder cyberark:BotApp/secretVar resolved to: ${body}") + .doCatch(Exception.class) + .log("No secret stored yet. Create one with: curl -X POST http://localhost:8080/cyberark-vault/createSecret -d 'my-secret'") + .end(); + } +} diff --git a/cyberark-vault/src/main/resources/application.properties b/cyberark-vault/src/main/resources/application.properties new file mode 100644 index 000000000..a7767a359 --- /dev/null +++ b/cyberark-vault/src/main/resources/application.properties @@ -0,0 +1,30 @@ +## --------------------------------------------------------------------------- +## 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. +## --------------------------------------------------------------------------- + +conjur.url={{env:CQ_CONJUR_URL}} +conjur.account={{env:CQ_CONJUR_ACCOUNT}} +conjur.reader.username={{env:CQ_CONJUR_READ_USER}} +conjur.reader.apiKey={{env:CQ_CONJUR_READ_USER_API_KEY}} +conjur.writer.username={{env:CQ_CONJUR_READ_WRITE_USER}} +conjur.writer.apiKey={{env:CQ_CONJUR_READ_WRITE_USER_API_KEY}} + +%test.timer.enabled=false + +camel.vault.cyberark.url={{env:CQ_CONJUR_URL}} +camel.vault.cyberark.account={{env:CQ_CONJUR_ACCOUNT}} +camel.vault.cyberark.username={{env:CQ_CONJUR_READ_USER}} +camel.vault.cyberark.apiKey={{env:CQ_CONJUR_READ_USER_API_KEY}} diff --git a/cyberark-vault/src/test/java/org/acme/cyberark/vault/CyberarkVaultIT.java b/cyberark-vault/src/test/java/org/acme/cyberark/vault/CyberarkVaultIT.java new file mode 100644 index 000000000..648a066be --- /dev/null +++ b/cyberark-vault/src/test/java/org/acme/cyberark/vault/CyberarkVaultIT.java @@ -0,0 +1,24 @@ +/* + * 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.acme.cyberark.vault; + +import io.quarkus.test.junit.QuarkusIntegrationTest; + +@QuarkusIntegrationTest +class CyberarkVaultIT extends CyberarkVaultTest { + +} diff --git a/cyberark-vault/src/test/java/org/acme/cyberark/vault/CyberarkVaultTest.java b/cyberark-vault/src/test/java/org/acme/cyberark/vault/CyberarkVaultTest.java new file mode 100644 index 000000000..41b7c0d67 --- /dev/null +++ b/cyberark-vault/src/test/java/org/acme/cyberark/vault/CyberarkVaultTest.java @@ -0,0 +1,59 @@ +/* + * 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.acme.cyberark.vault; + +import java.util.UUID; + +import io.quarkus.test.common.WithTestResource; +import io.quarkus.test.junit.QuarkusTest; +import io.restassured.RestAssured; +import io.smallrye.certs.Format; +import io.smallrye.certs.junit5.Certificate; +import io.smallrye.certs.junit5.Certificates; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.Matchers.is; + +@Certificates(baseDir = "target/certs", certificates = { + @Certificate(name = "nginx", formats = { Format.PEM }, cn = "proxy", subjectAlternativeNames = "proxy") +}) +@QuarkusTest +@WithTestResource(CyberarkVaultTestResource.class) +class CyberarkVaultTest { + @Test + void testCreateGetAndPropertyPlaceholder() { + String secret = UUID.randomUUID().toString(); + + RestAssured.given() + .body(secret) + .post("/cyberark-vault/createSecret") + .then() + .statusCode(204); + + RestAssured + .get("/cyberark-vault/getSecret") + .then() + .statusCode(200) + .body(is(secret)); + + RestAssured + .get("/cyberark-vault/propertyPlaceholder") + .then() + .statusCode(200) + .body(is(secret)); + } +} diff --git a/cyberark-vault/src/test/java/org/acme/cyberark/vault/CyberarkVaultTestResource.java b/cyberark-vault/src/test/java/org/acme/cyberark/vault/CyberarkVaultTestResource.java new file mode 100644 index 000000000..1f1501d07 --- /dev/null +++ b/cyberark-vault/src/test/java/org/acme/cyberark/vault/CyberarkVaultTestResource.java @@ -0,0 +1,267 @@ +/* + * 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.acme.cyberark.vault; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.Duration; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.quarkus.test.common.QuarkusTestResourceLifecycleManager; +import org.junit.jupiter.api.Assertions; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testcontainers.containers.Container; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.Network; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.utility.DockerImageName; +import org.testcontainers.utility.MountableFile; + +public class CyberarkVaultTestResource implements QuarkusTestResourceLifecycleManager { + private static final Logger LOGGER = LoggerFactory.getLogger(CyberarkVaultTestResource.class); + private static final String POSTGRES_PASSWORD = "SuperSecretPg"; + private static final String CONJUR_DATA_KEY = "changeitchangeitchangeitchangeitchangeitIhc="; + private static final String CONJUR_ACCOUNT = "myConjurAccount"; + private static final String POSTGRES_IMAGE = "mirror.gcr.io/postgres:17.5"; + private static final String CONJUR_IMAGE = "mirror.gcr.io/cyberark/conjur:1.24.0"; + private static final String CONJUR_CLI_IMAGE = "mirror.gcr.io/cyberark/conjur-cli:9"; + private static final String NGINX_IMAGE = "mirror.gcr.io/nginx:1.30.3-alpine3.23-perl"; + private static final int CONJUR_PORT = 80; + private static final int POSTGRES_PORT = 5432; + private static final int NGINX_PORT = 443; + + private Network network; + private GenericContainer postgresContainer; + private GenericContainer conjurContainer; + private GenericContainer nginxContainer; + private GenericContainer clientContainer; + + @Override + public Map start() { + final Map result = new LinkedHashMap<>(); + + List missingExternalProperties = Stream + .of("CQ_CONJUR_URL", "CQ_CONJUR_ACCOUNT", "CQ_CONJUR_READ_USER", "CQ_CONJUR_READ_USER_API_KEY", + "CQ_CONJUR_READ_WRITE_USER", "CQ_CONJUR_READ_WRITE_USER_API_KEY") + .filter(prop -> { + String value = System.getenv(prop); + return value == null || value.isEmpty(); + }) + .toList(); + if (missingExternalProperties.isEmpty()) { + LOGGER.info("Using real CyberArk Conjur backend"); + result.put("quarkus.http.port", "0"); + result.put("quarkus.http.test-port", "0"); + return result; + } + + if (missingExternalProperties.size() < 6) { + throw new RuntimeException( + "Several environmental properties are missing (you have to provide either all of them or none). " + + "Missing properties are: " + String.join(",", missingExternalProperties)); + } + LOGGER.info("Using testcontainers mock backend"); + + try { + network = Network.newNetwork(); + startPostgresContainer(); + startConjurContainer(); + startNginxContainer(); + startClientContainer(); + initializeConjur(result); + } catch (Exception e) { + throw new RuntimeException("Failed to start Conjur test environment", e); + } + + String conjurUrl = "http://localhost:" + conjurContainer.getMappedPort(CONJUR_PORT); + + result.put("conjur.account", CONJUR_ACCOUNT); + result.put("conjur.url", conjurUrl); + + result.put("camel.vault.cyberark.url", conjurUrl); + result.put("camel.vault.cyberark.account", CONJUR_ACCOUNT); + result.put("camel.vault.cyberark.username", result.get("conjur.reader.username")); + result.put("camel.vault.cyberark.apiKey", result.get("conjur.reader.apiKey")); + + return result; + } + + private void startPostgresContainer() { + LOGGER.info("Starting PostgreSQL container..."); + + postgresContainer = new GenericContainer<>(DockerImageName.parse(POSTGRES_IMAGE)) + .withNetwork(network) + .withNetworkAliases("database") + .withExposedPorts(POSTGRES_PORT) + .withEnv("POSTGRES_USER", "postgres") + .withEnv("POSTGRES_PASSWORD", POSTGRES_PASSWORD) + .withEnv("POSTGRES_DB", "postgres") + .withLogConsumer(frame -> LOGGER.debug("[POSTGRESQL] {}", frame.getUtf8StringWithoutLineEnding())); + + postgresContainer.start(); + LOGGER.info("PostgreSQL container started"); + } + + private void startConjurContainer() { + LOGGER.info("Starting Conjur container..."); + + String databaseUrl = String.format("postgres://postgres:%s@database/postgres", POSTGRES_PASSWORD); + + conjurContainer = new GenericContainer<>(DockerImageName.parse(CONJUR_IMAGE)) + .withNetwork(network) + .withNetworkAliases("conjur") + .withCommand("server") + .withEnv("DATABASE_URL", databaseUrl) + .withEnv("CONJUR_DATA_KEY", CONJUR_DATA_KEY) + .withEnv("CONJUR_AUTHENTICATORS", "") + .withEnv("CONJUR_TELEMETRY_ENABLED", "false") + .withEnv("CONJUR_API_RESOURCE_LIST_LIMIT_MAX", "5000") + .withExposedPorts(CONJUR_PORT) + .withLogConsumer(frame -> LOGGER.debug("[CONJUR] {}", frame.getUtf8StringWithoutLineEnding())) + .waitingFor(Wait.forLogMessage(".*Listening on http.*", 1) + .withStartupTimeout(Duration.ofMinutes(2))) + .dependsOn(postgresContainer); + + conjurContainer.start(); + LOGGER.info("Conjur container started on port {}", conjurContainer.getMappedPort(CONJUR_PORT)); + } + + private void startNginxContainer() throws Exception { + LOGGER.info("Starting Nginx proxy container..."); + + Path certsDir = Paths.get("target/certs"); + Path nginxCert = certsDir.resolve("nginx.crt"); + Path nginxKey = certsDir.resolve("nginx.key"); + + if (!Files.exists(nginxCert) || !Files.exists(nginxKey)) { + throw new RuntimeException("SSL certificates not found in target/certs."); + } + + nginxContainer = new GenericContainer<>(DockerImageName.parse(NGINX_IMAGE)) + .withNetwork(network) + .withNetworkAliases("proxy") + .withCopyFileToContainer(MountableFile.forClasspathResource("conf/default.conf"), + "/etc/nginx/conf.d/default.conf") + .withCopyFileToContainer(MountableFile.forHostPath(nginxCert), "/etc/nginx/tls/nginx.crt") + .withCopyFileToContainer(MountableFile.forHostPath(nginxKey), "/etc/nginx/tls/nginx.key") + .withExposedPorts(NGINX_PORT) + .withLogConsumer(frame -> LOGGER.debug("[NGINX] {}", frame.getUtf8StringWithoutLineEnding())) + .waitingFor(Wait.forListeningPort().withStartupTimeout(Duration.ofMinutes(1))) + .dependsOn(conjurContainer); + + nginxContainer.start(); + LOGGER.info("Nginx proxy container started on port {}", nginxContainer.getMappedPort(NGINX_PORT)); + } + + private void startClientContainer() { + LOGGER.info("Starting Conjur CLI client container..."); + + clientContainer = new GenericContainer<>(DockerImageName.parse(CONJUR_CLI_IMAGE)) + .withNetwork(network) + .withNetworkAliases("client") + .withCreateContainerCmdModifier(cmd -> { + cmd.withEntrypoint("sleep"); + }) + .withCommand("infinity") + .withCopyFileToContainer( + MountableFile.forClasspathResource("conf/policy/BotApp.yml"), + "/policy/BotApp.yml") + .withLogConsumer(frame -> LOGGER.debug("[CLIENT] {}", frame.getUtf8StringWithoutLineEnding())) + .withStartupTimeout(Duration.ofSeconds(5)) + .dependsOn(nginxContainer); + + clientContainer.start(); + LOGGER.info("Conjur CLI client container started"); + } + + private void initializeConjur(Map result) throws Exception { + LOGGER.info("Initializing Conjur account..."); + + Container.ExecResult accountResult = conjurContainer.execInContainer( + "conjurctl", "account", "create", CONJUR_ACCOUNT); + Assertions.assertEquals(0, accountResult.getExitCode(), + "Creation of account failed with: " + accountResult.getStderr()); + + String adminKey = accountResult.getStdout().lines() + .filter(line -> line.contains("API key")) + .map(line -> line.replaceAll(".*API key for admin: ", "").trim()) + .findFirst() + .orElseGet(() -> { + String[] tokens = accountResult.getStdout().split("\\s"); + return tokens[tokens.length - 1]; + }); + + Container.ExecResult er; + + er = clientContainer.execInContainer( + "conjur", "init", "oss", "-u", "https://proxy", "-a", CONJUR_ACCOUNT, "--self-signed"); + Assertions.assertEquals(0, er.getExitCode(), "Client init failed with: " + er.getStderr()); + + er = clientContainer.execInContainer( + "conjur", "login", "-i", "admin", "-p", adminKey); + Assertions.assertEquals(0, er.getExitCode(), "Client login failed with: " + er.getStderr()); + + er = clientContainer.execInContainer( + "conjur", "policy", "load", "-b", "root", "-f", "/policy/BotApp.yml"); + Assertions.assertEquals(0, er.getExitCode(), "Policy load failed with: " + er.getStderr()); + + ObjectMapper objectMapper = new ObjectMapper(); + JsonNode jsonNode = objectMapper.readTree(er.getStdout()); + + result.put("conjur.reader.username", "host/BotApp/myDemoApp"); + result.put("conjur.reader.apiKey", + jsonNode.get("created_roles").get(CONJUR_ACCOUNT + ":host:BotApp/myDemoApp").get("api_key").textValue()); + result.put("conjur.writer.username", "user/Dave@BotApp"); + result.put("conjur.writer.apiKey", + jsonNode.get("created_roles").get(CONJUR_ACCOUNT + ":user:Dave@BotApp").get("api_key").textValue()); + + clientContainer.execInContainer("conjur", "logout"); + + LOGGER.info("Conjur initialization complete"); + } + + @Override + public void stop() { + try { + if (clientContainer != null) { + clientContainer.stop(); + } + if (nginxContainer != null) { + nginxContainer.stop(); + } + if (conjurContainer != null) { + conjurContainer.stop(); + } + if (postgresContainer != null) { + postgresContainer.stop(); + } + if (network != null) { + network.close(); + } + } catch (Exception e) { + LOGGER.warn("Error during cleanup", e); + } + } +} diff --git a/cyberark-vault/src/test/resources/conf/default.conf b/cyberark-vault/src/test/resources/conf/default.conf new file mode 100644 index 000000000..8aa7e1806 --- /dev/null +++ b/cyberark-vault/src/test/resources/conf/default.conf @@ -0,0 +1,12 @@ +server { + listen 443 ssl; + server_name proxy; + access_log /var/log/nginx/access.log; + + ssl_certificate /etc/nginx/tls/nginx.crt; + ssl_certificate_key /etc/nginx/tls/nginx.key; + + location / { + proxy_pass http://conjur; + } +} diff --git a/cyberark-vault/src/test/resources/conf/policy/BotApp.yml b/cyberark-vault/src/test/resources/conf/policy/BotApp.yml new file mode 100644 index 000000000..f05882f48 --- /dev/null +++ b/cyberark-vault/src/test/resources/conf/policy/BotApp.yml @@ -0,0 +1,31 @@ +# +# 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. +# + +- !policy + id: BotApp + body: + - !user Dave + - !host myDemoApp + - !variable secretVar + - !permit + role: !user Dave + privileges: [read, update, execute] + resource: !variable secretVar + - !permit + role: !host myDemoApp + privileges: [read, execute] + resource: !variable secretVar diff --git a/cyberark-vault/start-conjur.sh b/cyberark-vault/start-conjur.sh new file mode 100755 index 000000000..6e6e1b975 --- /dev/null +++ b/cyberark-vault/start-conjur.sh @@ -0,0 +1,115 @@ +#!/bin/bash +# +# 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. +# + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +CONJUR_ACCOUNT="myConjurAccount" +CONJUR_PORT="${CONJUR_PORT:-9080}" +COMPOSE_ARGS="-f $SCRIPT_DIR/docker-compose.yml -p cyberark-vault" + +# --- stop mode --- +if [[ "${1:-}" == "stop" ]]; then + docker compose $COMPOSE_ARGS down -v + echo "Conjur environment stopped." + exit 0 +fi + +# --- prerequisites --- +for cmd in docker curl jq; do + if ! command -v "$cmd" &>/dev/null; then + echo "Error: '$cmd' is required but not installed." >&2 + exit 1 + fi +done + +# --- clean start (wipe previous volumes so account/policy are fresh) --- +docker compose $COMPOSE_ARGS down -v 2>/dev/null || true + +echo "Starting Conjur environment..." +CONJUR_PORT="$CONJUR_PORT" docker compose $COMPOSE_ARGS up -d + +# --- wait for Conjur to accept connections --- +echo "Waiting for Conjur to be ready..." +for i in $(seq 1 60); do + if curl -so /dev/null "http://localhost:$CONJUR_PORT/" 2>/dev/null; then + break + fi + if [ "$i" -eq 60 ]; then + echo "Error: Conjur did not become ready within 120 s." >&2 + exit 1 + fi + sleep 2 +done +echo "Conjur is ready." + +# --- create account --- +echo "Creating account '$CONJUR_ACCOUNT'..." +ACCOUNT_OUTPUT=$(docker compose $COMPOSE_ARGS exec -T conjur \ + conjurctl account create "$CONJUR_ACCOUNT" 2>&1) +ADMIN_KEY=$(echo "$ACCOUNT_OUTPUT" | tr -s '[:space:]' '\n' | tail -1) + +if [ -z "$ADMIN_KEY" ]; then + echo "Error: failed to extract admin API key." >&2 + echo "$ACCOUNT_OUTPUT" >&2 + exit 1 +fi + +# --- authenticate as admin --- +TOKEN=$(curl -sf -X POST \ + "http://localhost:$CONJUR_PORT/authn/$CONJUR_ACCOUNT/admin/authenticate" \ + -d "$ADMIN_KEY" | base64 | tr -d '\n') + +# --- load policy --- +echo "Loading BotApp policy..." +POLICY_FILE="$SCRIPT_DIR/src/test/resources/conf/policy/BotApp.yml" +POLICY_RESPONSE=$(curl -sf -X PUT \ + "http://localhost:$CONJUR_PORT/policies/$CONJUR_ACCOUNT/policy/root" \ + -H "Authorization: Token token=\"$TOKEN\"" \ + -H "Content-Type: application/x-yaml" \ + --data-binary @"$POLICY_FILE") + +READ_API_KEY=$(echo "$POLICY_RESPONSE" | jq -r \ + ".created_roles[\"$CONJUR_ACCOUNT:host:BotApp/myDemoApp\"].api_key") +WRITE_API_KEY=$(echo "$POLICY_RESPONSE" | jq -r \ + ".created_roles[\"$CONJUR_ACCOUNT:user:Dave@BotApp\"].api_key") + +if [ "$READ_API_KEY" = "null" ] || [ "$WRITE_API_KEY" = "null" ]; then + echo "Error: failed to extract API keys from policy response." >&2 + echo "$POLICY_RESPONSE" >&2 + exit 1 +fi + +# --- output --- +cat <aws2-s3 cluster-leader-election cxf-soap + cyberark-vault data-extract-langchain4j fhir file-bindy-ftp From 42df46d853bfbb45e564f8427adfb1a9e370ee07 Mon Sep 17 00:00:00 2001 From: James Netherton Date: Tue, 28 Jul 2026 14:54:49 +0100 Subject: [PATCH 2/2] Update cyberark-vault/pom.xml --- cyberark-vault/pom.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cyberark-vault/pom.xml b/cyberark-vault/pom.xml index c40742c7c..576639312 100644 --- a/cyberark-vault/pom.xml +++ b/cyberark-vault/pom.xml @@ -31,10 +31,10 @@ 3.38.0 3.39.0-SNAPSHOT - io.quarkus.platform + io.quarkus quarkus-bom - ${quarkus.platform.group-id} - quarkus-camel-bom + org.apache.camel.quarkus + camel-quarkus-bom UTF-8 UTF-8