Skip to content

Commit 720546b

Browse files
feat(secret): add SecretService caller guard with ENFORCE default
Add caller guard modes (ENFORCE, AUDIT, DISABLED) to SecretService, thread OperationContext through encrypt/decrypt call sites, and default SECRET_SERVICE_CALLER_GUARD_MODE to ENFORCE. Document secure-by-default secret handling for OSS datahub-actions and Cloud embedded executors. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent f5f18a1 commit 720546b

43 files changed

Lines changed: 790 additions & 361 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/connection/ConnectionMapper.java

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import com.linkedin.entity.EnvelopedAspect;
1414
import com.linkedin.entity.EnvelopedAspectMap;
1515
import com.linkedin.metadata.Constants;
16+
import io.datahubproject.metadata.context.OperationContext;
1617
import io.datahubproject.metadata.services.SecretService;
1718
import javax.annotation.Nonnull;
1819
import javax.annotation.Nullable;
@@ -70,7 +71,9 @@ private static DataHubConnectionDetails mapConnectionDetails(
7071
com.linkedin.datahub.graphql.generated.DataHubConnectionDetailsType.valueOf(
7172
gmsDetails.getType().toString()));
7273
if (gmsDetails.hasJson() && ConnectionUtils.canManageConnections(context)) {
73-
result.setJson(mapJsonConnectionDetails(gmsDetails.getJson(), secretService));
74+
result.setJson(
75+
mapJsonConnectionDetails(
76+
context.getOperationContext(), gmsDetails.getJson(), secretService));
7477
}
7578
if (gmsDetails.hasName()) {
7679
result.setName(gmsDetails.getName());
@@ -79,11 +82,18 @@ private static DataHubConnectionDetails mapConnectionDetails(
7982
}
8083

8184
private static DataHubJsonConnection mapJsonConnectionDetails(
85+
@Nullable final OperationContext opContext,
8286
@Nonnull final com.linkedin.connection.DataHubJsonConnection gmsJsonConnection,
8387
@Nonnull final SecretService secretService) {
8488
final DataHubJsonConnection result = new DataHubJsonConnection();
85-
// Decrypt the BLOB!
86-
result.setBlob(secretService.decrypt(gmsJsonConnection.getEncryptedBlob()));
89+
// Browsers should never receive decrypted connection blobs — they contain raw credentials.
90+
// Skip decryption for human agents; the SecretService guard acts as a backstop.
91+
if (opContext != null
92+
&& opContext.getRequestContext() != null
93+
&& opContext.getRequestContext().getAgentClass().isHuman()) {
94+
return result;
95+
}
96+
result.setBlob(secretService.decrypt(opContext, gmsJsonConnection.getEncryptedBlob()));
8797
return result;
8898
}
8999

datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/connection/UpsertConnectionResolver.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,9 @@ public CompletableFuture<DataHubConnection> get(final DataFetchingEnvironment en
6262
input.getJson() != null
6363
// Encrypt payload
6464
? new DataHubJsonConnection()
65-
.setEncryptedBlob(_secretService.encrypt(input.getJson().getBlob()))
65+
.setEncryptedBlob(
66+
_secretService.encrypt(
67+
context.getOperationContext(), input.getJson().getBlob()))
6668
: null,
6769
input.getName());
6870

datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/ingest/secret/CreateSecretResolver.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ public CompletableFuture<String> get(final DataFetchingEnvironment environment)
6363
DataHubSecretValueMapper.map(
6464
null,
6565
input.getName(),
66-
_secretService.encrypt(input.getValue()),
66+
_secretService.encrypt(context.getOperationContext(), input.getValue()),
6767
input.getDescription(),
6868
new AuditStamp()
6969
.setActor(UrnUtils.getUrn(context.getActorUrn()))

datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/ingest/secret/GetSecretValuesResolver.java

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,9 @@ public CompletableFuture<List<SecretValue>> get(final DataFetchingEnvironment en
8080
final DataHubSecretValue secretValue =
8181
new DataHubSecretValue(aspect.getValue().data());
8282
// Now decrypt the encrypted secret.
83-
final String decryptedSecretValue = decryptSecret(secretValue.getValue());
83+
final String decryptedSecretValue =
84+
_secretService.decrypt(
85+
context.getOperationContext(), secretValue.getValue());
8486
return new SecretValue(secretValue.getName(), decryptedSecretValue);
8587
} else {
8688
// No secret exists
@@ -100,8 +102,4 @@ public CompletableFuture<List<SecretValue>> get(final DataFetchingEnvironment en
100102
throw new AuthorizationException(
101103
"Unauthorized to perform this action. Please contact your DataHub administrator.");
102104
}
103-
104-
private String decryptSecret(final String encryptedSecret) {
105-
return _secretService.decrypt(encryptedSecret);
106-
}
107105
}

datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/ingest/secret/UpdateSecretResolver.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ public CompletableFuture<String> get(final DataFetchingEnvironment environment)
6161
DataHubSecretValueMapper.map(
6262
response,
6363
input.getName(),
64-
secretService.encrypt(input.getValue()),
64+
secretService.encrypt(context.getOperationContext(), input.getValue()),
6565
input.getDescription(),
6666
null);
6767

datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/connection/UpsertConnectionResolverTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,8 @@ public class UpsertConnectionResolverTest {
4242
public void setUp() {
4343
connectionService = Mockito.mock(ConnectionService.class);
4444
secretService = Mockito.mock(SecretService.class);
45-
Mockito.when(secretService.encrypt("{}")).thenReturn("encrypted");
46-
Mockito.when(secretService.decrypt("encrypted")).thenReturn("{}");
45+
Mockito.when(secretService.encrypt(Mockito.any(), Mockito.eq("{}"))).thenReturn("encrypted");
46+
Mockito.when(secretService.decrypt(Mockito.any(), Mockito.eq("encrypted"))).thenReturn("{}");
4747
resolver = new UpsertConnectionResolver(connectionService, secretService);
4848
}
4949

datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/ingest/secret/CreateSecretResolverTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ public void testGetSuccess() throws Exception {
3333
// Create resolver
3434
EntityClient mockClient = Mockito.mock(EntityClient.class);
3535
SecretService mockSecretService = Mockito.mock(SecretService.class);
36-
Mockito.when(mockSecretService.encrypt(Mockito.eq(TEST_INPUT.getValue())))
36+
Mockito.when(mockSecretService.encrypt(Mockito.any(), Mockito.eq(TEST_INPUT.getValue())))
3737
.thenReturn("encryptedvalue");
3838
CreateSecretResolver resolver = new CreateSecretResolver(mockClient, mockSecretService);
3939

datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/ingest/secret/GetSecretValuesResolverTest.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@ public void testGetSuccess() throws Exception {
3838
// Create resolver
3939
EntityClient mockClient = Mockito.mock(EntityClient.class);
4040
SecretService mockSecretService = Mockito.mock(SecretService.class);
41-
Mockito.when(mockSecretService.decrypt(Mockito.eq(getTestSecretValue().getValue())))
41+
Mockito.when(
42+
mockSecretService.decrypt(Mockito.any(), Mockito.eq(getTestSecretValue().getValue())))
4243
.thenReturn(decryptedSecretValue);
4344

4445
DataHubSecretValue returnedValue = getTestSecretValue();

datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/ingest/secret/UpdateSecretResolverTest.java

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,8 @@ public void testGetSuccess() throws Exception {
7373
Mockito.when(mockEnv.getContext()).thenReturn(mockContext);
7474

7575
Mockito.when(mockClient.exists(any(), any())).thenReturn(true);
76-
Mockito.when(mockSecretService.encrypt(TEST_INPUT.getValue())).thenReturn("encrypted_value");
76+
Mockito.when(mockSecretService.encrypt(Mockito.any(), Mockito.eq(TEST_INPUT.getValue())))
77+
.thenReturn("encrypted_value");
7778
final EntityResponse entityResponse = new EntityResponse();
7879
final EnvelopedAspectMap aspectMap = new EnvelopedAspectMap();
7980
aspectMap.put(
@@ -85,7 +86,8 @@ public void testGetSuccess() throws Exception {
8586

8687
// Invoke the resolver
8788
resolver.get(mockEnv).join();
88-
Mockito.verify(mockSecretService, Mockito.times(1)).encrypt(TEST_INPUT.getValue());
89+
Mockito.verify(mockSecretService, Mockito.times(1))
90+
.encrypt(Mockito.any(), Mockito.eq(TEST_INPUT.getValue()));
8991
Mockito.verify(mockClient, Mockito.times(1)).ingestProposal(any(), any(), anyBoolean());
9092
}
9193

@@ -101,7 +103,9 @@ public void testGetSuccessWithSpecialCharacters() throws Exception {
101103

102104
Mockito.when(mockClient.exists(any(), any())).thenReturn(true);
103105
// The secret value should NOT be escaped before encryption
104-
Mockito.when(mockSecretService.encrypt(TEST_INPUT_WITH_SPECIAL_CHARS.getValue()))
106+
Mockito.when(
107+
mockSecretService.encrypt(
108+
Mockito.any(), Mockito.eq(TEST_INPUT_WITH_SPECIAL_CHARS.getValue())))
105109
.thenReturn("encrypted_special_value");
106110
final EntityResponse entityResponse = new EntityResponse();
107111
final EnvelopedAspectMap aspectMap = new EnvelopedAspectMap();
@@ -117,7 +121,7 @@ public void testGetSuccessWithSpecialCharacters() throws Exception {
117121

118122
// Verify that the secret value was passed to encrypt WITHOUT escaping
119123
Mockito.verify(mockSecretService, Mockito.times(1))
120-
.encrypt(Mockito.eq("password\nwith/slashes\"and\\backslashes"));
124+
.encrypt(Mockito.any(), Mockito.eq("password\nwith/slashes\"and\\backslashes"));
121125
Mockito.verify(mockClient, Mockito.times(1)).ingestProposal(any(), any(), anyBoolean());
122126
}
123127

docs-website/sidebars.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -721,6 +721,7 @@ module.exports = {
721721
},
722722
{
723723
"DataHub Cloud Release History": [
724+
"docs/managed-datahub/release-notes/v_2_0_0",
724725
"docs/managed-datahub/release-notes/v_1_1_0",
725726
"docs/managed-datahub/release-notes/v_1_0_0",
726727
"docs/managed-datahub/release-notes/v_0_3_17",

0 commit comments

Comments
 (0)