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);