Skip to content

Commit 15359a2

Browse files
committed
FIX Query Runner - Server passes secret prefixes to ingestion
1 parent b8f5500 commit 15359a2

File tree

3 files changed

+76
-31
lines changed

3 files changed

+76
-31
lines changed

docker/development/helm/values-k8s-test.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ openmetadata:
99
# Database configuration (using local PostgreSQL via host machine)
1010
database:
1111
host: host.docker.internal
12-
port: 5433
12+
port: 5432
1313
driverClass: org.postgresql.Driver
1414
dbScheme: postgresql
1515
auth:

openmetadata-service/src/main/java/org/openmetadata/service/secrets/SecretsManager.java

Lines changed: 7 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -197,25 +197,6 @@ public Object decryptQueryRunnerConfig(Object authConfig) {
197197
}
198198
}
199199

200-
/**
201-
* Decrypts a single secret value that may be Fernet-encrypted and/or stored in external SM.
202-
* Handles the two-layer encryption: Fernet wrapping around external SM references.
203-
*
204-
* @param value The value to decrypt (may be fernet:..., secret:..., or plain text)
205-
* @return The decrypted value
206-
*/
207-
public String decryptSecretIfNeeded(String value) {
208-
if (value == null || value.isEmpty()) {
209-
return value;
210-
}
211-
// First, Fernet-decrypt if tokenized
212-
String fernetDecrypted = Fernet.isTokenized(value) ? fernet.decrypt(value) : value;
213-
// Then, fetch from external SM if it's a secret reference
214-
return Boolean.TRUE.equals(isSecret(fernetDecrypted))
215-
? getSecretValue(fernetDecrypted)
216-
: fernetDecrypted;
217-
}
218-
219200
/**
220201
* Deletes QueryRunner config secrets from the secrets manager.
221202
* Uses the same path structure as encryptQueryRunnerConfig:
@@ -469,16 +450,12 @@ private Object decryptPasswordFields(Object toDecryptObject) {
469450
String fieldValue = (String) obj;
470451
// get setMethod
471452
Method toSet = ReflectionUtil.getToSetMethod(toDecryptObject, obj, fieldName);
472-
// First Fernet-decrypt if tokenized, then fetch from external SM if it's a secret
473-
// reference
474-
String fernetDecrypted =
475-
Fernet.isTokenized(fieldValue) ? fernet.decrypt(fieldValue) : fieldValue;
476-
String finalValue =
477-
Boolean.TRUE.equals(isSecret(fernetDecrypted))
478-
? getSecretValue(fernetDecrypted)
479-
: fernetDecrypted;
480-
// set new value
481-
ReflectionUtil.setValueInMethod(toDecryptObject, finalValue, toSet);
453+
// Only Fernet-decrypt. Secret references (secret:/path) are returned as-is
454+
// for the ingestion client to resolve. The server does not fetch secrets.
455+
ReflectionUtil.setValueInMethod(
456+
toDecryptObject,
457+
Fernet.isTokenized(fieldValue) ? fernet.decrypt(fieldValue) : fieldValue,
458+
toSet);
482459
}
483460
});
484461
return toDecryptObject;
@@ -512,7 +489,7 @@ private Object getSecretFields(Object toDecryptObject) {
512489
String fieldValue = (String) obj;
513490
// get setMethod
514491
Method toSet = ReflectionUtil.getToSetMethod(toDecryptObject, obj, fieldName);
515-
// set new value
492+
// Fetch from SM if it's a secret reference
516493
ReflectionUtil.setValueInMethod(
517494
toDecryptObject,
518495
Boolean.TRUE.equals(isSecret(fieldValue))

openmetadata-service/src/test/java/org/openmetadata/service/secrets/AWSSecretsManagerTest.java

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,23 +12,33 @@
1212
*/
1313
package org.openmetadata.service.secrets;
1414

15+
import static org.junit.jupiter.api.Assertions.assertEquals;
1516
import static org.mockito.ArgumentMatchers.any;
1617
import static org.mockito.Mockito.lenient;
18+
import static org.mockito.Mockito.never;
1719
import static org.mockito.Mockito.reset;
20+
import static org.mockito.Mockito.verify;
21+
import static org.openmetadata.schema.api.services.CreateDatabaseService.DatabaseServiceType.Mysql;
1822

1923
import java.util.HashMap;
2024
import java.util.List;
2125
import java.util.Map;
2226
import org.junit.jupiter.api.AfterAll;
2327
import org.junit.jupiter.api.BeforeAll;
28+
import org.junit.jupiter.api.Test;
2429
import org.mockito.Mock;
30+
import org.openmetadata.schema.entity.services.ServiceType;
2531
import org.openmetadata.schema.security.secrets.SecretsManagerConfiguration;
2632
import org.openmetadata.schema.security.secrets.SecretsManagerProvider;
33+
import org.openmetadata.schema.services.connections.database.MysqlConnection;
34+
import org.openmetadata.schema.services.connections.database.common.basicAuth;
35+
import org.openmetadata.schema.utils.JsonUtils;
2736
import software.amazon.awssdk.services.secretsmanager.SecretsManagerClient;
2837
import software.amazon.awssdk.services.secretsmanager.model.CreateSecretRequest;
2938
import software.amazon.awssdk.services.secretsmanager.model.CreateSecretResponse;
3039
import software.amazon.awssdk.services.secretsmanager.model.GetSecretValueRequest;
3140
import software.amazon.awssdk.services.secretsmanager.model.GetSecretValueResponse;
41+
import software.amazon.awssdk.services.secretsmanager.model.ResourceNotFoundException;
3242
import software.amazon.awssdk.services.secretsmanager.model.UpdateSecretRequest;
3343
import software.amazon.awssdk.services.secretsmanager.model.UpdateSecretResponse;
3444

@@ -98,4 +108,62 @@ void setUpSpecific(SecretsManagerConfiguration config) {
98108
protected SecretsManagerProvider expectedSecretManagerProvider() {
99109
return SecretsManagerProvider.MANAGED_AWS;
100110
}
111+
112+
/**
113+
* Tests the Hybrid SaaS scenario where customers provide external secret references.
114+
*
115+
* <p>In Hybrid SaaS deployments:
116+
*
117+
* <ul>
118+
* <li>Customers provide secret references like "secret:/customer/path/to/secret"
119+
* <li>These references point to secrets in the CUSTOMER's AWS account, not Collate's
120+
* <li>The ingestion runs on the customer's cloud and resolves secrets there
121+
* <li>OpenMetadata server should NOT try to fetch these secrets during decrypt
122+
* </ul>
123+
*
124+
* <p>This test verifies that when a user provides a "secret:" prefixed value:
125+
*
126+
* <ol>
127+
* <li>Encryption: keeps the reference (Fernet-wrapped for DB storage), no new secret created
128+
* <li>Decryption: returns the reference as-is WITHOUT trying to fetch from SM
129+
* </ol>
130+
*
131+
* <p>REGRESSION: PR #25236 (Query Runner integration) changed decryptPasswordFields() to always
132+
* call getSecretValue() for "secret:" prefixed values, breaking this scenario.
133+
*/
134+
@Test
135+
void testHybridSaasExternalSecretReferenceShouldNotBeFetchedDuringDecrypt() {
136+
String externalSecretReference = "secret:/customer/aws/account/database/password";
137+
138+
Map<String, Map<String, String>> mysqlConnection =
139+
Map.of("authType", Map.of("password", externalSecretReference));
140+
141+
MysqlConnection encryptedConnection =
142+
(MysqlConnection)
143+
secretsManager.encryptServiceConnectionConfig(
144+
mysqlConnection, Mysql.value(), "hybrid-customer-service", ServiceType.DATABASE);
145+
146+
verify(secretsManagerClient, never()).createSecret(any(CreateSecretRequest.class));
147+
148+
reset(secretsManagerClient);
149+
lenient()
150+
.when(secretsManagerClient.getSecretValue(any(GetSecretValueRequest.class)))
151+
.thenThrow(
152+
ResourceNotFoundException.builder()
153+
.message("Secrets Manager can't find the specified secret.")
154+
.build());
155+
156+
MysqlConnection decryptedConnection =
157+
(MysqlConnection)
158+
secretsManager.decryptServiceConnectionConfig(
159+
encryptedConnection, Mysql.value(), ServiceType.DATABASE);
160+
161+
String decryptedPassword =
162+
JsonUtils.convertValue(decryptedConnection.getAuthType(), basicAuth.class).getPassword();
163+
assertEquals(
164+
externalSecretReference,
165+
decryptedPassword,
166+
"External secret reference should be returned as-is during decryption, "
167+
+ "not fetched from the server's secrets manager");
168+
}
101169
}

0 commit comments

Comments
 (0)