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

hansva pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/hop.git


The following commit(s) were added to refs/heads/main by this push:
     new bf4551bc5b fix encrypted string in headers, fixes #6697 (#7435)
bf4551bc5b is described below

commit bf4551bc5bb1762201d51da23dc42b4f5ebc367c
Author: Hans Van Akelyen <[email protected]>
AuthorDate: Mon Jul 6 13:58:22 2026 +0200

    fix encrypted string in headers, fixes #6697 (#7435)
---
 .../apache/hop/pipeline/transforms/rest/Rest.java  | 40 ++---------
 .../hop/pipeline/transforms/rest/RestTest.java     | 78 +++++++++++++++++-----
 2 files changed, 64 insertions(+), 54 deletions(-)

diff --git 
a/plugins/transforms/rest/src/main/java/org/apache/hop/pipeline/transforms/rest/Rest.java
 
b/plugins/transforms/rest/src/main/java/org/apache/hop/pipeline/transforms/rest/Rest.java
index caf1d04d95..f4d83347a8 100644
--- 
a/plugins/transforms/rest/src/main/java/org/apache/hop/pipeline/transforms/rest/Rest.java
+++ 
b/plugins/transforms/rest/src/main/java/org/apache/hop/pipeline/transforms/rest/Rest.java
@@ -310,7 +310,6 @@ public class Rest extends BaseTransform<RestMeta, RestData> 
{
       }
 
       MultivaluedMap<String, Object> headerMap = new MultivaluedHashMap<>();
-      addApiKeyHeaderIfAbsent(headerMap);
 
       boolean acceptHeaderProvided = false;
       String contentType = null;
@@ -337,8 +336,10 @@ public class Rest extends BaseTransform<RestMeta, 
RestData> {
       }
 
       /* Jersey Invocation.Builder.headers(MultivaluedMap) replaces all 
headers — so Bearer / API-key
-       * set inside RestConnection.getInvocationBuilder(...) would be 
stripped. Merge connection auth
-       * into outbound headers unless the caller already supplied 
Authorization. */
+       * set inside RestConnection.getInvocationBuilder(...) would be 
stripped. This is the single
+       * place that merges connection auth into the outbound headers: it 
decrypts the credential
+       * (so an "Encrypted ..." API key / token is sent in clear) and skips 
when the row already
+       * supplied Authorization or the configured API-key header (rows win). */
       if (!StringUtils.isEmpty(meta.getConnectionName()) && connection != 
null) {
         connection.applyBearerAndApiKeyHeaders(invocationBuilder, headerMap);
       }
@@ -1336,39 +1337,6 @@ public class Rest extends BaseTransform<RestMeta, 
RestData> {
     return s.substring(0, maxChars) + "...";
   }
 
-  /**
-   * set the Authentication/Authorization header from the connection first, if 
available. this
-   * transform's headers will override this value if available.
-   *
-   * @param headers the mutable map containing HTTP headers that will be sent 
with the request
-   */
-  private void addApiKeyHeaderIfAbsent(MultivaluedMap<String, Object> headers) 
{
-    if (connection == null) {
-      return;
-    }
-
-    if (!"API Key".equalsIgnoreCase(connection.getAuthType())) {
-      return;
-    }
-
-    String headerName = resolve(connection.getAuthorizationHeaderName());
-    String headerValue = resolve(connection.getAuthorizationHeaderValue());
-    String prefix = resolve(connection.getAuthorizationPrefix());
-
-    if (Utils.isEmpty(headerName) || Utils.isEmpty(headerValue)) {
-      return;
-    }
-
-    if (!Utils.isEmpty(prefix)) {
-      headerValue = prefix + " " + headerValue;
-    }
-
-    // Only add header if not already present
-    if (!headers.containsKey(headerName)) {
-      headers.putSingle(headerName, headerValue);
-    }
-  }
-
   private void setConfig() throws HopException {
     if (data.config == null) {
       // Use ApacheHttpClient for supporting proxy authentication.
diff --git 
a/plugins/transforms/rest/src/test/java/org/apache/hop/pipeline/transforms/rest/RestTest.java
 
b/plugins/transforms/rest/src/test/java/org/apache/hop/pipeline/transforms/rest/RestTest.java
index 98ad1e2036..85d74108b5 100644
--- 
a/plugins/transforms/rest/src/test/java/org/apache/hop/pipeline/transforms/rest/RestTest.java
+++ 
b/plugins/transforms/rest/src/test/java/org/apache/hop/pipeline/transforms/rest/RestTest.java
@@ -27,6 +27,7 @@ import static org.mockito.Mockito.mockStatic;
 import static org.mockito.Mockito.spy;
 
 import jakarta.ws.rs.client.Client;
+import jakarta.ws.rs.client.Invocation;
 import jakarta.ws.rs.core.MediaType;
 import jakarta.ws.rs.core.MultivaluedHashMap;
 import jakarta.ws.rs.core.MultivaluedMap;
@@ -34,6 +35,12 @@ import jakarta.ws.rs.core.Response;
 import java.lang.reflect.Field;
 import java.lang.reflect.Method;
 import java.nio.charset.StandardCharsets;
+import org.apache.hop.core.encryption.Encr;
+import org.apache.hop.core.encryption.HopTwoWayPasswordEncoder;
+import org.apache.hop.core.encryption.TwoWayPasswordEncoderPlugin;
+import org.apache.hop.core.encryption.TwoWayPasswordEncoderPluginType;
+import org.apache.hop.core.plugins.PluginRegistry;
+import org.apache.hop.core.variables.Variables;
 import org.apache.hop.metadata.rest.RestConnection;
 import org.apache.hop.pipeline.PipelineMeta;
 import org.apache.hop.pipeline.engines.local.LocalPipelineEngine;
@@ -49,8 +56,14 @@ class RestTest {
   private MockedStatic<Client> mockedClient;
 
   @BeforeEach
-  void setUpStaticMocks() {
+  void setUpStaticMocks() throws Exception {
     mockedClient = mockStatic(Client.class);
+    PluginRegistry.getInstance()
+        .registerPluginClass(
+            HopTwoWayPasswordEncoder.class.getName(),
+            TwoWayPasswordEncoderPluginType.class,
+            TwoWayPasswordEncoderPlugin.class);
+    Encr.init("Hop");
   }
 
   @AfterEach
@@ -164,25 +177,56 @@ class RestTest {
   }
 
   @Test
-  void testAddApiKeyHeaderIfAbsentAddsPrefixedHeaderWithoutOverriding() throws 
Exception {
-    Rest rest = newRest();
-    RestConnection connection = mock(RestConnection.class);
-    doReturn("API Key").when(connection).getAuthType();
-    doReturn("Authorization").when(connection).getAuthorizationHeaderName();
-    doReturn("secret").when(connection).getAuthorizationHeaderValue();
-    doReturn("Bearer").when(connection).getAuthorizationPrefix();
+  void testConnectionApiKeyHeaderIsDecryptedAndRowWins() {
+    // Regression for #6697: an "Encrypted ..." API key configured on the REST 
connection must be
+    // decrypted before it is sent (it used to be forwarded verbatim from the 
transform, giving a
+    // 401), while a header already supplied on the incoming row must still 
win over connection
+    // auth.
+    String encryptedValue = 
Encr.encryptPasswordIfNotUsingVariables("my_super_secret");
+    assertTrue(encryptedValue.startsWith(Encr.PASSWORD_ENCRYPTED_PREFIX));
+
+    RestConnection connection = new RestConnection(new Variables());
+    connection.setAuthType(RestConnection.API_KEY);
+    connection.setAuthorizationHeaderName("X-API-Key");
+    connection.setAuthorizationPrefix("Token");
+    connection.setAuthorizationHeaderValue(encryptedValue);
 
-    Field connectionField = Rest.class.getDeclaredField("connection");
-    connectionField.setAccessible(true);
-    connectionField.set(rest, connection);
+    Invocation.Builder invocationBuilder = mock(Invocation.Builder.class);
 
+    // Fresh row: the connection contributes the decrypted, prefixed value.
     MultivaluedMap<String, Object> headers = new MultivaluedHashMap<>();
-    invokePrivate(rest, "addApiKeyHeaderIfAbsent", headers);
-    assertEquals("Bearer secret", headers.getFirst("Authorization"));
+    connection.applyBearerAndApiKeyHeaders(invocationBuilder, headers);
+    assertEquals("Token my_super_secret", headers.getFirst("X-API-Key"));
+
+    // Row already supplied the header (case-insensitively): connection auth 
is skipped, row wins
+    // and no second (differently-cased) copy is appended.
+    MultivaluedMap<String, Object> rowHeaders = new MultivaluedHashMap<>();
+    rowHeaders.putSingle("x-api-key", "row_value");
+    connection.applyBearerAndApiKeyHeaders(invocationBuilder, rowHeaders);
+    assertEquals(1, rowHeaders.get("x-api-key").size());
+    assertEquals("row_value", rowHeaders.getFirst("x-api-key"));
+    assertNull(rowHeaders.getFirst("X-API-Key"));
+  }
+
+  @Test
+  void testConnectionApiKeyHeaderIsNotDuplicated() {
+    // Regression for #6697: the connection's API-key header must be emitted 
exactly once — the
+    // original bug sent it doubled (e.g. "my_super_secret,my_super_secret" -> 
HTTP 401). The test
+    // button, RestConnection.getInvocationBuilder(...) and the transform all 
funnel auth through
+    // this same merge, so re-applying it must stay idempotent instead of 
appending a second value.
+    RestConnection connection = new RestConnection(new Variables());
+    connection.setAuthType(RestConnection.API_KEY);
+    connection.setAuthorizationHeaderName("X-API-Key");
+    connection.setAuthorizationHeaderValue("my_super_secret");
+
+    Invocation.Builder invocationBuilder = mock(Invocation.Builder.class);
+    MultivaluedMap<String, Object> headers = new MultivaluedHashMap<>();
+
+    connection.applyBearerAndApiKeyHeaders(invocationBuilder, headers);
+    connection.applyBearerAndApiKeyHeaders(invocationBuilder, headers);
 
-    headers.putSingle("Authorization", "existing");
-    invokePrivate(rest, "addApiKeyHeaderIfAbsent", headers);
-    assertEquals("existing", headers.getFirst("Authorization"));
+    assertEquals(1, headers.get("X-API-Key").size());
+    assertEquals("my_super_secret", headers.getFirst("X-API-Key"));
   }
 
   private Rest newRest() {
@@ -210,8 +254,6 @@ class RestTest {
                   .getDeclaredMethod(methodName, String.class, 
java.nio.charset.Charset.class);
           case "trackResponseBytes" ->
               target.getClass().getDeclaredMethod(methodName, Response.class, 
String.class);
-          case "addApiKeyHeaderIfAbsent" ->
-              target.getClass().getDeclaredMethod(methodName, 
MultivaluedMap.class);
           default -> throw new NoSuchMethodException(methodName);
         };
     method.setAccessible(true);

Reply via email to