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

davsclaus pushed a commit to branch camel-4.22.x
in repository https://gitbox.apache.org/repos/asf/camel.git


The following commit(s) were added to refs/heads/camel-4.22.x by this push:
     new 2057b6a86309 [backport camel-4.22.x] CAMEL-26256: camel-cyberark-vault 
- retry a rejected token once and only when a new one can be obtained (#29306)
2057b6a86309 is described below

commit 2057b6a863094746980e6f7f20ec906166f4c316
Author: Andrea Cosentino <[email protected]>
AuthorDate: Fri Nov 6 10:32:28 2026 +0100

    [backport camel-4.22.x] CAMEL-26256: camel-cyberark-vault - retry a 
rejected token once and only when a new one can be obtained (#29306)
    
    CAMEL-26256: camel-cyberark-vault - retry a rejected token once and only 
when a new one can be obtained
    
    Backport of the ConjurClientImpl part (item 1) of apache/camel#29209 to 
camel-4.22.x: on a 401 the client logs in again and retries once, and only when 
it has an apiKey or a password; with only an authToken the 401 fails at once 
instead of recursing until a StackOverflowError. The start-time credential 
check of #29209 (item 2) stays on main.
    
    Signed-off-by: Andrea Cosentino <[email protected]>
    Co-authored-by: Claude Opus 5.5 <[email protected]>
---
 .../vault/client/impl/ConjurClientImpl.java        |  30 +++-
 .../impl/ConjurClientReauthenticationTest.java     | 177 +++++++++++++++++++++
 2 files changed, 200 insertions(+), 7 deletions(-)

diff --git 
a/components/camel-cyberark-vault/src/main/java/org/apache/camel/component/cyberark/vault/client/impl/ConjurClientImpl.java
 
b/components/camel-cyberark-vault/src/main/java/org/apache/camel/component/cyberark/vault/client/impl/ConjurClientImpl.java
index 339f3154e367..d0d4af414f43 100644
--- 
a/components/camel-cyberark-vault/src/main/java/org/apache/camel/component/cyberark/vault/client/impl/ConjurClientImpl.java
+++ 
b/components/camel-cyberark-vault/src/main/java/org/apache/camel/component/cyberark/vault/client/impl/ConjurClientImpl.java
@@ -42,7 +42,7 @@ public class ConjurClientImpl implements ConjurClient {
     private final String username;
     private final String password;
     private final String apiKey;
-    private String authToken;
+    private volatile String authToken;
     private final HttpClient httpClient;
 
     public ConjurClientImpl(
@@ -66,6 +66,10 @@ public class ConjurClientImpl implements ConjurClient {
 
     @Override
     public String retrieveSecret(String secretId, String version) {
+        return retrieveSecret(secretId, version, true);
+    }
+
+    private String retrieveSecret(String secretId, String version, boolean 
retryOnUnauthorized) {
         try {
             // Ensure we have a valid token
             if (authToken == null) {
@@ -94,11 +98,11 @@ public class ConjurClientImpl implements ConjurClient {
 
             if (response.statusCode() == 200) {
                 return response.body();
-            } else if (response.statusCode() == 401) {
-                // Token expired, re-authenticate and retry
+            } else if (response.statusCode() == 401 && retryOnUnauthorized && 
canReauthenticate()) {
+                // Token expired, re-authenticate and retry once
                 LOG.debug("Token expired, re-authenticating");
                 authToken = authenticate();
-                return retrieveSecret(secretId, version);
+                return retrieveSecret(secretId, version, false);
             } else {
                 throw new IOException(
                         "Failed to retrieve secret: HTTP " + 
response.statusCode() + " - " + response.body());
@@ -157,6 +161,10 @@ public class ConjurClientImpl implements ConjurClient {
 
     @Override
     public void createSecret(String secretId, String secretValue) {
+        createSecret(secretId, secretValue, true);
+    }
+
+    private void createSecret(String secretId, String secretValue, boolean 
retryOnUnauthorized) {
         try {
             // Ensure we have a valid token
             if (authToken == null) {
@@ -182,11 +190,11 @@ public class ConjurClientImpl implements ConjurClient {
 
             if (response.statusCode() == 201 || response.statusCode() == 200) {
                 LOG.debug("Successfully created/updated secret: {}", secretId);
-            } else if (response.statusCode() == 401) {
-                // Token expired, re-authenticate and retry
+            } else if (response.statusCode() == 401 && retryOnUnauthorized && 
canReauthenticate()) {
+                // Token expired, re-authenticate and retry once
                 LOG.debug("Token expired, re-authenticating");
                 authToken = authenticate();
-                createSecret(secretId, secretValue);
+                createSecret(secretId, secretValue, false);
             } else {
                 throw new IOException(
                         "Failed to create/update secret: HTTP " + 
response.statusCode() + " - " + response.body());
@@ -196,6 +204,14 @@ public class ConjurClientImpl implements ConjurClient {
         }
     }
 
+    /**
+     * Whether a new access token can be obtained after Conjur rejects the 
current one. That needs an API key or a
+     * password: a client that only has a pre-authenticated token cannot get a 
new token.
+     */
+    private boolean canReauthenticate() {
+        return apiKey != null || password != null;
+    }
+
     @Override
     public void close() throws Exception {
         authToken = null;
diff --git 
a/components/camel-cyberark-vault/src/test/java/org/apache/camel/component/cyberark/vault/client/impl/ConjurClientReauthenticationTest.java
 
b/components/camel-cyberark-vault/src/test/java/org/apache/camel/component/cyberark/vault/client/impl/ConjurClientReauthenticationTest.java
new file mode 100644
index 000000000000..6c84e2a1ea40
--- /dev/null
+++ 
b/components/camel-cyberark-vault/src/test/java/org/apache/camel/component/cyberark/vault/client/impl/ConjurClientReauthenticationTest.java
@@ -0,0 +1,177 @@
+/*
+ * 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.cyberark.vault.client.impl;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+import java.util.List;
+import java.util.Queue;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import com.sun.net.httpserver.HttpExchange;
+import com.sun.net.httpserver.HttpServer;
+import org.apache.camel.component.cyberark.vault.client.ConjurClient;
+import org.apache.camel.component.cyberark.vault.client.ConjurClientFactory;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * A 401 from Conjur makes the client authenticate again and retry once, and 
only when it has an API key or a password
+ * to get a new token. A fake Conjur answers the queued statuses to the secret 
requests, and then succeeds: a client
+ * that retries more than it should gets the secret instead of the expected 
failure.
+ */
+class ConjurClientReauthenticationTest {
+
+    private static final String ACCOUNT = "myaccount";
+    private static final String SECRET_ID = "db/password";
+
+    private final AtomicInteger logins = new AtomicInteger();
+    private final List<String> secretRequestTokens = new 
CopyOnWriteArrayList<>();
+    private final List<String> storedValues = new CopyOnWriteArrayList<>();
+    private final Queue<Integer> secretStatuses = new 
ConcurrentLinkedQueue<>();
+
+    private HttpServer server;
+    private String url;
+
+    @BeforeEach
+    void startConjur() throws IOException {
+        server = HttpServer.create(new 
InetSocketAddress(InetAddress.getLoopbackAddress(), 0), 0);
+        server.createContext("/authn/", exchange -> {
+            exchange.getRequestBody().readAllBytes();
+            respond(exchange, 200, "token-" + logins.incrementAndGet());
+        });
+        server.createContext("/secrets/", exchange -> {
+            String body = new String(exchange.getRequestBody().readAllBytes(), 
StandardCharsets.UTF_8);
+            secretRequestTokens.add(tokenOf(exchange));
+            Integer status = secretStatuses.poll();
+            if ("POST".equals(exchange.getRequestMethod())) {
+                storedValues.add(body);
+                respond(exchange, status != null ? status : 201, "");
+            } else {
+                respond(exchange, status != null ? status : 200, 
"secret-value");
+            }
+        });
+        server.start();
+        url = "http://localhost:"; + server.getAddress().getPort();
+    }
+
+    @AfterEach
+    void stopConjur() {
+        server.stop(0);
+    }
+
+    @Test
+    void retrieveSecretWithOnlyATokenFailsWithTheUnauthorizedResponse() throws 
Exception {
+        secretStatuses.add(401);
+        secretStatuses.add(401);
+
+        try (ConjurClient client = ConjurClientFactory.createWithToken(url, 
ACCOUNT, "my-token")) {
+            RuntimeException e = assertThrows(RuntimeException.class, () -> 
client.retrieveSecret(SECRET_ID));
+            assertTrue(e.getCause().getMessage().contains("HTTP 401"), 
e.getCause().getMessage());
+        }
+        assertEquals(List.of("my-token"), secretRequestTokens);
+        assertEquals(0, logins.get());
+    }
+
+    @Test
+    void retrieveSecretAuthenticatesAgainAndRetriesOnce() throws Exception {
+        secretStatuses.add(401);
+
+        try (ConjurClient client = ConjurClientFactory.createWithApiKey(url, 
ACCOUNT, "host/myapp", "my-api-key")) {
+            assertEquals("secret-value", client.retrieveSecret(SECRET_ID));
+        }
+        assertEquals(List.of("token-1", "token-2"), secretRequestTokens);
+        assertEquals(2, logins.get());
+    }
+
+    @Test
+    void retrieveSecretFailsWhenTheNewTokenIsRejectedToo() throws Exception {
+        secretStatuses.add(401);
+        secretStatuses.add(401);
+
+        try (ConjurClient client = 
ConjurClientFactory.createWithCredentials(url, ACCOUNT, "admin", 
"my-password")) {
+            RuntimeException e = assertThrows(RuntimeException.class, () -> 
client.retrieveSecret(SECRET_ID));
+            assertTrue(e.getCause().getMessage().contains("HTTP 401"), 
e.getCause().getMessage());
+        }
+        assertEquals(List.of("token-1", "token-2"), secretRequestTokens);
+        assertEquals(2, logins.get());
+    }
+
+    @Test
+    void createSecretWithOnlyATokenFailsWithTheUnauthorizedResponse() throws 
Exception {
+        secretStatuses.add(401);
+        secretStatuses.add(401);
+
+        try (ConjurClient client = ConjurClientFactory.createWithToken(url, 
ACCOUNT, "my-token")) {
+            RuntimeException e = assertThrows(RuntimeException.class, () -> 
client.createSecret(SECRET_ID, "new-value"));
+            assertTrue(e.getCause().getMessage().contains("HTTP 401"), 
e.getCause().getMessage());
+        }
+        assertEquals(List.of("my-token"), secretRequestTokens);
+        assertEquals(0, logins.get());
+    }
+
+    @Test
+    void createSecretAuthenticatesAgainAndRetriesOnce() throws Exception {
+        secretStatuses.add(401);
+
+        try (ConjurClient client = ConjurClientFactory.createWithApiKey(url, 
ACCOUNT, "host/myapp", "my-api-key")) {
+            client.createSecret(SECRET_ID, "new-value");
+        }
+        assertEquals(List.of("token-1", "token-2"), secretRequestTokens);
+        assertEquals(List.of("new-value", "new-value"), storedValues);
+        assertEquals(2, logins.get());
+    }
+
+    @Test
+    void createSecretFailsWhenTheNewTokenIsRejectedToo() throws Exception {
+        secretStatuses.add(401);
+        secretStatuses.add(401);
+
+        try (ConjurClient client = 
ConjurClientFactory.createWithCredentials(url, ACCOUNT, "admin", 
"my-password")) {
+            RuntimeException e = assertThrows(RuntimeException.class, () -> 
client.createSecret(SECRET_ID, "new-value"));
+            assertTrue(e.getCause().getMessage().contains("HTTP 401"), 
e.getCause().getMessage());
+        }
+        assertEquals(List.of("token-1", "token-2"), secretRequestTokens);
+        assertEquals(2, logins.get());
+    }
+
+    private static String tokenOf(HttpExchange exchange) {
+        // Authorization: Token token="<base64 of the access token>"
+        String authorization = 
exchange.getRequestHeaders().getFirst("Authorization");
+        String encoded = authorization.substring("Token token=\"".length(), 
authorization.length() - 1);
+        return new String(Base64.getDecoder().decode(encoded), 
StandardCharsets.UTF_8);
+    }
+
+    private static void respond(HttpExchange exchange, int status, String 
body) throws IOException {
+        byte[] bytes = body.getBytes(StandardCharsets.UTF_8);
+        exchange.sendResponseHeaders(status, bytes.length == 0 ? -1 : 
bytes.length);
+        try (OutputStream out = exchange.getResponseBody()) {
+            out.write(bytes);
+        }
+    }
+}

Reply via email to