Skip to content

Commit

Permalink
fixed #6979: azure-key-vault better coverage for identity credentials
Browse files Browse the repository at this point in the history
  • Loading branch information
JiriOndrusek committed Feb 11, 2025
1 parent 3de09bf commit f88b1ea
Show file tree
Hide file tree
Showing 17 changed files with 699 additions and 181 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import org.apache.camel.CamelContext;
import org.apache.camel.ProducerTemplate;
import org.apache.camel.ResolveEndpointFailedException;
import org.apache.camel.component.azure.key.vault.KeyVaultConstants;
import org.apache.camel.impl.event.CamelContextReloadedEvent;

Expand All @@ -41,47 +43,86 @@ public class AzureKeyVaultResource {
@Inject
ProducerTemplate producerTemplate;

@Inject
CamelContext camelContext;

static final AtomicBoolean contextReloaded = new AtomicBoolean(false);

void onReload(@Observes CamelContextReloadedEvent event) {
contextReloaded.set(true);
}

@Path("/secret/{secretName}")
@Path("/secret/routes/{command}")
@POST
public void startRoutes(@PathParam("command") String cmd) throws Exception {
if ("start".equals(cmd)) {
camelContext.getRouteController().startRoute("createSecret");
camelContext.getRouteController().startRoute("getSecret");
camelContext.getRouteController().startRoute("deleteSecret");
camelContext.getRouteController().startRoute("purgeDeletedSecret");
}
if ("stop".equals(cmd)) {
camelContext.getRouteController().stopRoute("createSecret");
camelContext.getRouteController().stopRoute("getSecret");
camelContext.getRouteController().stopRoute("deleteSecret");
camelContext.getRouteController().stopRoute("purgeDeletedSecret");
}
}

@Path("/secret/{identity}/{secretName}")
@POST
@Consumes(MediaType.TEXT_PLAIN)
@Produces(MediaType.TEXT_PLAIN)
public Response createSecret(@PathParam("secretName") String secretName, String secret) {
KeyVaultSecret result = producerTemplate.requestBodyAndHeader("direct:createSecret", secret,
public Response createSecret(@PathParam("secretName") String secretName, @PathParam("identity") boolean identity,
String secret) {
KeyVaultSecret result = producerTemplate.requestBodyAndHeader("direct:createSecret" + (identity ? "Identity" : ""),
secret,
KeyVaultConstants.SECRET_NAME, secretName, KeyVaultSecret.class);
return Response.ok(result.getName()).build();
}

@Path("/secret/{secretName}")
@Path("/secret/wrongClient/{secretName}")
@POST
@Consumes(MediaType.TEXT_PLAIN)
@Produces(MediaType.TEXT_PLAIN)
public Response createSecretWithWrongClient(@PathParam("secretName") String secretName,
String secret) {
try {
KeyVaultSecret result = producerTemplate.requestBodyAndHeader("azure-key-vault://{{camel.vault.azure.vaultName}}" +
"?operation=createSecret",
secret,
KeyVaultConstants.SECRET_NAME, secretName, KeyVaultSecret.class);
return Response.ok(result.getName()).build();
} catch (ResolveEndpointFailedException e) {
return Response.status(500).entity("ResolveEndpointFailedException").build();
}
}

@Path("/secret/{identity}/{secretName}")
@GET
@Produces(MediaType.TEXT_PLAIN)
public String getSecret(@PathParam("secretName") String secretName) {
return producerTemplate.requestBodyAndHeader("direct:getSecret", null,
public String getSecret(@PathParam("secretName") String secretName, @PathParam("identity") boolean identity) {
return producerTemplate.requestBodyAndHeader("direct:getSecret" + (identity ? "Identity" : ""), null,
KeyVaultConstants.SECRET_NAME, secretName, String.class);
}

@Path("/secret/{secretName}")
@Path("/secret/{identity}/{secretName}")
@DELETE
public Response deleteSecret(@PathParam("secretName") String secretName) {
producerTemplate.requestBodyAndHeader("direct:deleteSecret", null,
public Response deleteSecret(@PathParam("secretName") String secretName, @PathParam("identity") boolean identity) {
producerTemplate.requestBodyAndHeader("direct:deleteSecret" + (identity ? "Identity" : ""), null,
KeyVaultConstants.SECRET_NAME, secretName, Void.class);
return Response.ok().build();
}

@Path("/secret/{secretName}/purge")
@Path("/secret/{identity}/{secretName}/purge")
@DELETE
public Response purgeSecret(@PathParam("secretName") String secretName) {
producerTemplate.requestBodyAndHeader("direct:purgeDeletedSecret", null,
public Response purgeSecret(@PathParam("secretName") String secretName, @PathParam("identity") boolean identity) {
producerTemplate.requestBodyAndHeader("direct:purgeDeletedSecret" + (identity ? "Identity" : ""), null,
KeyVaultConstants.SECRET_NAME, secretName, Void.class);
return Response.ok().build();
}

@Path("/secret/from/placeholder")
@Path("/secret/fromPlaceholder")
@GET
public String getSecretFromPropertyPlaceholder() {
return producerTemplate.requestBody("direct:propertyPlaceholder", null, String.class);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,17 +24,37 @@ public class AzureKeyVaultRoutes extends RouteBuilder {
@Override
public void configure() throws Exception {
from("direct:createSecret")
.to(azureKeyVault("createSecret", true));
.autoStartup(false)
.id("createSecret")
.to(azureKeyVault("createSecret", false));

from("direct:getSecret")
.autoStartup(false)
.id("getSecret")
.to(azureKeyVault("getSecret", false));

from("direct:deleteSecret")
.to(azureKeyVault("deleteSecret", true));
.autoStartup(false)
.id("deleteSecret")
.to(azureKeyVault("deleteSecret", false));

from("direct:purgeDeletedSecret")
.autoStartup(false)
.id("purgeDeletedSecret")
.to(azureKeyVault("purgeDeletedSecret", false));

from("direct:createSecretIdentity")
.to(azureKeyVault("createSecret", true));

from("direct:getSecretIdentity")
.to(azureKeyVault("getSecret", true));

from("direct:deleteSecretIdentity")
.to(azureKeyVault("deleteSecret", true));

from("direct:purgeDeletedSecretIdentity")
.to(azureKeyVault("purgeDeletedSecret", true));

from("direct:propertyPlaceholder")
.process(exchange -> {
Message message = exchange.getMessage();
Expand All @@ -45,13 +65,15 @@ public void configure() throws Exception {

private String azureKeyVault(String operation, boolean useIdentity) {
StringBuilder sb = new StringBuilder("azure-key-vault://{{camel.vault.azure.vaultName}}" +
"?clientId=RAW({{camel.vault.azure.clientId}})" +
"&clientSecret=RAW({{camel.vault.azure.clientSecret}})" +
"&tenantId=RAW({{camel.vault.azure.tenantId}})" +
"&operation=" + operation);
"?operation=" + operation);

if (useIdentity) {
sb.append("&credentialType=AZURE_IDENTITY");
} else {
//can not use i.e. RAW({{camel.vault.azure.clientSecret}}) as the value is not set in identity profiles
sb.append("&clientId=").append(System.getenv("AZURE_CLIENT_ID"))
.append("&clientSecret=").append(System.getenv("AZURE_CLIENT_SECRET"))
.append("&tenantId=").append(System.getenv("AZURE_TENANT_ID"));
}
return sb.toString();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@
## See the License for the specific language governing permissions and
## limitations under the License.
## ---------------------------------------------------------------------------
#
camel.vault.azure.tenantId = ${AZURE_TENANT_ID:placeholderTenantId}
camel.vault.azure.clientId = ${AZURE_CLIENT_ID:placeholderClientId}
camel.vault.azure.clientSecret = ${AZURE_CLIENT_SECRET:placeholderClientSecret}
camel.vault.azure.vaultName = ${AZURE_VAULT_NAME:cq-vault-testing}
camel.vault.azure.vaultName = ${AZURE_VAULT_NAME:cq-vault-testing}
#following properties are added by the test profile if needed
#camel.vault.azure.tenantId = ${AZURE_TENANT_ID:placeholderTenantId}
#camel.vault.azure.clientId = ${AZURE_CLIENT_ID:placeholderClientId}
#camel.vault.azure.clientSecret = ${AZURE_CLIENT_SECRET:placeholderClientSecret}
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/*
* 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.quarkus.component.azure.key.vault.it;

import java.util.LinkedList;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

import com.azure.messaging.eventhubs.EventData;
import com.azure.messaging.eventhubs.EventHubClientBuilder;
import com.azure.messaging.eventhubs.EventHubConsumerAsyncClient;
import com.azure.messaging.eventhubs.EventHubProducerClient;
import com.azure.messaging.eventhubs.models.EventPosition;
import io.restassured.RestAssured;
import org.hamcrest.CoreMatchers;
import org.jboss.logging.Logger;
import org.junit.jupiter.api.Test;
import org.testcontainers.shaded.org.awaitility.Awaitility;

import static org.hamcrest.Matchers.is;

// Azure Key Vault is not supported by Azurite https://github.com/Azure/Azurite/issues/619
abstract class AbstractAzureKeyVaultContextReloadTest {

private static final Logger LOG = Logger.getLogger(AbstractAzureKeyVaultContextReloadTest.class);
private static final String SECRET_NAME_FOR_REFRESH_PREFIX = "cq-secret-context-refresh-";
private static final String AZURE_VAULT_EVENT_HUBS_CONNECTION_STRING = "AZURE_VAULT_EVENT_HUBS_CONNECTION_STRING";

private final boolean useIdentity;

public AbstractAzureKeyVaultContextReloadTest(boolean useIdentity) {
this.useIdentity = useIdentity;
}

private String generateRefreshEvent(String secretName) {
return "[{\n" +
" \"subject\": \"" + SECRET_NAME_FOR_REFRESH_PREFIX + (useIdentity ? "Identity-" : "") + ".*\",\n" +
" \"eventType\": \"Microsoft.KeyVault.SecretNewVersionCreated\"\n" +
"}]";
}

@Test
void contextReload() {
String secretName = SECRET_NAME_FOR_REFRESH_PREFIX + (useIdentity ? "Identity-" : "") + UUID.randomUUID();
String secretValue = "Hello Camel Quarkus Azure Key Vault From Refresh";
try {
// Create secret
RestAssured.given()
.body(secretValue)
.post("/azure-key-vault/secret/true/{secretName}", secretName)
.then()
.statusCode(200)
.body(is(secretName));

// Retrieve secret
RestAssured.given()
.get("/azure-key-vault/secret/true/{secretName}", secretName)
.then()
.statusCode(200);

//force reload by sending a msg
try (EventHubProducerClient client = new EventHubClientBuilder()
.connectionString(System.getenv(AZURE_VAULT_EVENT_HUBS_CONNECTION_STRING))
.buildProducerClient()) {

EventData eventData = new EventData(generateRefreshEvent(secretName).getBytes());
List<EventData> finalEventData = new LinkedList<>();
finalEventData.add(eventData);
client.send(finalEventData);
} catch (Exception e) {
LOG.info("Failed to send a refresh message", e);
}

//await context reload
Awaitility.await().pollInterval(10, TimeUnit.SECONDS).atMost(1, TimeUnit.MINUTES).untilAsserted(
() -> {
RestAssured.get("/azure-key-vault/context/reload")
.then()
.statusCode(200)
.body(CoreMatchers.is("true"));
});
} finally {

//move cursor of events to ignore old ones (old events are deleted after 1 hour)
try {
String connectionString = System.getenv(AZURE_VAULT_EVENT_HUBS_CONNECTION_STRING);
String consumerGroup = EventHubClientBuilder.DEFAULT_CONSUMER_GROUP_NAME;

try (EventHubConsumerAsyncClient consumer = new EventHubClientBuilder()
.connectionString(connectionString)
.consumerGroup(consumerGroup)
.buildAsyncConsumerClient()) {

// Move consumer to the latest position, skipping old messages
consumer.receiveFromPartition("0", EventPosition.latest())
.subscribe(event -> {
System.out.println("Processing new event: " + event.toString());
}, error -> {
System.err.println("Error receiving events: " + error);
});
}
} catch (Exception e) {
LOG.info("Failed to clear event hub.", e);
}

AzureKeyVaultUtil.deleteSecretImmediately(secretName);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/*
* 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.quarkus.component.azure.key.vault.it;

import java.util.UUID;

import io.restassured.RestAssured;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import static org.hamcrest.Matchers.is;

// Azure Key Vault is not supported by Azurite https://github.com/Azure/Azurite/issues/619
abstract class AbstractAzureKeyVaultTest {

private final boolean useIdentity;

public AbstractAzureKeyVaultTest(boolean useIdentity) {
this.useIdentity = useIdentity;
}

@BeforeEach
public void beforeEach() {
//routes without identity have to be started
if (!useIdentity) {
RestAssured.given()
.post("/azure-key-vault/secret/routes/start")
.then()
.statusCode(204);
}
}

@AfterEach
public void afterEach() {
//routes without identity have to be stopped
if (!useIdentity) {
RestAssured.given()
.post("/azure-key-vault/secret/routes/stop")
.then()
.statusCode(204);
}
}

@Test
void secretCreateRetrieveDeletePurge() {
String secretName = "cq-create" + (useIdentity ? "-identity-" : "-") + UUID.randomUUID().toString();
String secret = "Hello Camel Quarkus Azure Key Vault";

try {
// Create secret
RestAssured.given()
.body(secret)
.post("/azure-key-vault/secret/" + useIdentity + "/{secretName}", secretName)
.then()
.statusCode(200)
.body(is(secretName));

// Retrieve secret
RestAssured.given()
.get("/azure-key-vault/secret/" + useIdentity + "/{secretName}", secretName)
.then()
.statusCode(200)
.body(is(secret));
} finally {
AzureKeyVaultUtil.deleteSecretImmediately(secretName, useIdentity);
}
}

}
Loading

0 comments on commit f88b1ea

Please sign in to comment.