diff --git a/.gitignore b/.gitignore index aa43219f3a..7d6bc5ab53 100644 --- a/.gitignore +++ b/.gitignore @@ -106,10 +106,11 @@ hs_err_pid* # Python Virtual Env .venv +venv # Development Experience # You can create runtime/defaults/src/main/resources/application-local.properties file # to override default properties for local development. # And then use `./gradlew run -Dquarkus.profile=local` to run Polaris with dev profile. -application-local.properties \ No newline at end of file +application-local.properties diff --git a/polaris-core/src/main/java/org/apache/polaris/core/auth/PolarisPrincipal.java b/polaris-core/src/main/java/org/apache/polaris/core/auth/PolarisPrincipal.java index e3be050147..1f1bc27d8b 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/auth/PolarisPrincipal.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/auth/PolarisPrincipal.java @@ -18,6 +18,7 @@ */ package org.apache.polaris.core.auth; +import jakarta.annotation.Nullable; import java.security.Principal; import java.util.Map; import java.util.Set; @@ -39,7 +40,23 @@ public interface PolarisPrincipal extends Principal { * @param roles the set of roles associated with the principal */ static PolarisPrincipal of(PrincipalEntity principalEntity, Set roles) { - return of(principalEntity.getName(), principalEntity.getInternalPropertiesAsMap(), roles); + return of(principalEntity.getName(), principalEntity.getInternalPropertiesAsMap(), roles, null); + } + + /** + * Creates a new instance of {@link PolarisPrincipal} from the given {@link PrincipalEntity} and + * roles. + * + *

The created principal will have the same ID and name as the {@link PrincipalEntity}, and its + * properties will be derived from the internal properties of the entity. + * + * @param principalEntity the principal entity representing the user or service + * @param roles the set of roles associated with the principal + * @param token the access token of the current user + */ + static PolarisPrincipal of(PrincipalEntity principalEntity, Set roles, String token) { + return of( + principalEntity.getName(), principalEntity.getInternalPropertiesAsMap(), roles, token); } /** @@ -51,9 +68,24 @@ static PolarisPrincipal of(PrincipalEntity principalEntity, Set roles) { * @param roles the set of roles associated with the principal */ static PolarisPrincipal of(String name, Map properties, Set roles) { + return of(name, properties, roles, null); + } + + /** + * Creates a new instance of {@link PolarisPrincipal} with the specified ID, name, roles, and + * properties. + * + * @param name the name of the principal + * @param properties additional properties associated with the principal + * @param roles the set of roles associated with the principal + * @param token the access token of the current user + */ + static PolarisPrincipal of( + String name, Map properties, Set roles, @Nullable String token) { return ImmutablePolarisPrincipal.builder() .name(name) .properties(properties) + .token(token) .roles(roles) .build(); } @@ -74,4 +106,8 @@ static PolarisPrincipal of(String name, Map properties, Set getProperties(); + + /** Returns the access token of the current user. */ + @Nullable + String getToken(); } diff --git a/polaris-core/src/main/java/org/apache/polaris/core/persistence/AtomicOperationMetaStoreManager.java b/polaris-core/src/main/java/org/apache/polaris/core/persistence/AtomicOperationMetaStoreManager.java index d902237527..862962a486 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/persistence/AtomicOperationMetaStoreManager.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/persistence/AtomicOperationMetaStoreManager.java @@ -1600,7 +1600,8 @@ public void deletePrincipalSecrets( boolean allowListOperation, @Nonnull Set allowedReadLocations, @Nonnull Set allowedWriteLocations, - Optional refreshCredentialsEndpoint) { + Optional refreshCredentialsEndpoint, + Optional token) { // get meta store session we should be using BasePersistence ms = callCtx.getMetaStore(); @@ -1641,7 +1642,8 @@ public void deletePrincipalSecrets( allowListOperation, allowedReadLocations, allowedWriteLocations, - refreshCredentialsEndpoint); + refreshCredentialsEndpoint, + token); return new ScopedCredentialsResult(storageAccessConfig); } catch (Exception ex) { return new ScopedCredentialsResult( diff --git a/polaris-core/src/main/java/org/apache/polaris/core/persistence/TransactionWorkspaceMetaStoreManager.java b/polaris-core/src/main/java/org/apache/polaris/core/persistence/TransactionWorkspaceMetaStoreManager.java index e788b999c1..ac57d55066 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/persistence/TransactionWorkspaceMetaStoreManager.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/persistence/TransactionWorkspaceMetaStoreManager.java @@ -324,7 +324,8 @@ public void deletePrincipalSecrets( boolean allowListOperation, @Nonnull Set allowedReadLocations, @Nonnull Set allowedWriteLocations, - Optional refreshCredentialsEndpoint) { + Optional refreshCredentialsEndpoint, + Optional token) { return delegate.getSubscopedCredsForEntity( callCtx, catalogId, @@ -333,7 +334,8 @@ public void deletePrincipalSecrets( allowListOperation, allowedReadLocations, allowedWriteLocations, - refreshCredentialsEndpoint); + refreshCredentialsEndpoint, + token); } @Override diff --git a/polaris-core/src/main/java/org/apache/polaris/core/persistence/transactional/TransactionalMetaStoreManagerImpl.java b/polaris-core/src/main/java/org/apache/polaris/core/persistence/transactional/TransactionalMetaStoreManagerImpl.java index 753c58ab6b..be32ecf2a1 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/persistence/transactional/TransactionalMetaStoreManagerImpl.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/persistence/transactional/TransactionalMetaStoreManagerImpl.java @@ -2098,7 +2098,8 @@ private PolarisEntityResolver resolveSecurableToRoleGrant( boolean allowListOperation, @Nonnull Set allowedReadLocations, @Nonnull Set allowedWriteLocations, - Optional refreshCredentialsEndpoint) { + Optional refreshCredentialsEndpoint, + Optional token) { // get meta store session we should be using TransactionalPersistence ms = ((TransactionalPersistence) callCtx.getMetaStore()); @@ -2134,7 +2135,8 @@ private PolarisEntityResolver resolveSecurableToRoleGrant( allowListOperation, allowedReadLocations, allowedWriteLocations, - refreshCredentialsEndpoint); + refreshCredentialsEndpoint, + token); return new ScopedCredentialsResult(storageAccessConfig); } catch (Exception ex) { return new ScopedCredentialsResult( diff --git a/polaris-core/src/main/java/org/apache/polaris/core/storage/PolarisCredentialVendor.java b/polaris-core/src/main/java/org/apache/polaris/core/storage/PolarisCredentialVendor.java index ee90294c69..b93b641a65 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/storage/PolarisCredentialVendor.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/storage/PolarisCredentialVendor.java @@ -53,5 +53,6 @@ ScopedCredentialsResult getSubscopedCredsForEntity( boolean allowListOperation, @Nonnull Set allowedReadLocations, @Nonnull Set allowedWriteLocations, - Optional refreshCredentialsEndpoint); + Optional refreshCredentialsEndpoint, + Optional token); } diff --git a/polaris-core/src/main/java/org/apache/polaris/core/storage/PolarisStorageIntegration.java b/polaris-core/src/main/java/org/apache/polaris/core/storage/PolarisStorageIntegration.java index 8a2ae7c3a8..ae5357dcf2 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/storage/PolarisStorageIntegration.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/storage/PolarisStorageIntegration.java @@ -67,7 +67,8 @@ public abstract StorageAccessConfig getSubscopedCreds( boolean allowListOperation, @Nonnull Set allowedReadLocations, @Nonnull Set allowedWriteLocations, - Optional refreshCredentialsEndpoint); + Optional refreshCredentialsEndpoint, + Optional token); /** * Validate access for the provided operation actions and locations. diff --git a/polaris-core/src/main/java/org/apache/polaris/core/storage/StorageCredentialsVendor.java b/polaris-core/src/main/java/org/apache/polaris/core/storage/StorageCredentialsVendor.java index 59bcf86c8e..5b63607c25 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/storage/StorageCredentialsVendor.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/storage/StorageCredentialsVendor.java @@ -67,7 +67,8 @@ public ScopedCredentialsResult getSubscopedCredsForEntity( boolean allowListOperation, @Nonnull Set allowedReadLocations, @Nonnull Set allowedWriteLocations, - Optional refreshCredentialsEndpoint) { + Optional refreshCredentialsEndpoint, + Optional token) { return polarisCredentialVendor.getSubscopedCredsForEntity( callContext.getPolarisCallContext(), entity.getCatalogId(), @@ -76,6 +77,7 @@ public ScopedCredentialsResult getSubscopedCredsForEntity( allowListOperation, allowedReadLocations, allowedWriteLocations, - refreshCredentialsEndpoint); + refreshCredentialsEndpoint, + token); } } diff --git a/polaris-core/src/main/java/org/apache/polaris/core/storage/aws/AwsCredentialsStorageIntegration.java b/polaris-core/src/main/java/org/apache/polaris/core/storage/aws/AwsCredentialsStorageIntegration.java index 2996006954..6ca09116b3 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/storage/aws/AwsCredentialsStorageIntegration.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/storage/aws/AwsCredentialsStorageIntegration.java @@ -44,7 +44,8 @@ import software.amazon.awssdk.policybuilder.iam.IamStatement; import software.amazon.awssdk.services.sts.StsClient; import software.amazon.awssdk.services.sts.model.AssumeRoleRequest; -import software.amazon.awssdk.services.sts.model.AssumeRoleResponse; +import software.amazon.awssdk.services.sts.model.AssumeRoleWithWebIdentityRequest; +import software.amazon.awssdk.services.sts.model.Credentials; /** Credential vendor that supports generating */ public class AwsCredentialsStorageIntegration @@ -81,7 +82,8 @@ public StorageAccessConfig getSubscopedCreds( boolean allowListOperation, @Nonnull Set allowedReadLocations, @Nonnull Set allowedWriteLocations, - Optional refreshCredentialsEndpoint) { + Optional refreshCredentialsEndpoint, + Optional token) { int storageCredentialDurationSeconds = realmConfig.getConfig(STORAGE_CREDENTIAL_DURATION_SECONDS); AwsStorageConfigurationInfo storageConfig = config(); @@ -90,35 +92,58 @@ public StorageAccessConfig getSubscopedCreds( StorageAccessConfig.Builder accessConfig = StorageAccessConfig.builder(); if (shouldUseSts(storageConfig)) { - AssumeRoleRequest.Builder request = - AssumeRoleRequest.builder() - .externalId(storageConfig.getExternalId()) - .roleArn(storageConfig.getRoleARN()) - .roleSessionName("PolarisAwsCredentialsStorageIntegration") - .policy( - policyString( - storageConfig, - allowListOperation, - allowedReadLocations, - allowedWriteLocations, - region, - accountId) - .toJson()) - .durationSeconds(storageCredentialDurationSeconds); - credentialsProvider.ifPresent( - cp -> request.overrideConfiguration(b -> b.credentialsProvider(cp))); @SuppressWarnings("resource") // Note: stsClientProvider returns "thin" clients that do not need closing StsClient stsClient = stsClientProvider.stsClient(StsDestination.of(storageConfig.getStsEndpointUri(), region)); - - AssumeRoleResponse response = stsClient.assumeRole(request.build()); - accessConfig.put(StorageAccessProperty.AWS_KEY_ID, response.credentials().accessKeyId()); - accessConfig.put( - StorageAccessProperty.AWS_SECRET_KEY, response.credentials().secretAccessKey()); - accessConfig.put(StorageAccessProperty.AWS_TOKEN, response.credentials().sessionToken()); - Optional.ofNullable(response.credentials().expiration()) + Credentials credentials; + if (Boolean.TRUE.equals(storageConfig.getPropagateApiUserIdentity())) { + AssumeRoleWithWebIdentityRequest.Builder request = + AssumeRoleWithWebIdentityRequest.builder() + .webIdentityToken( + token.orElseThrow( + () -> + new IllegalArgumentException( + "Token must be provided when PROPAGATE_API_USER_IDENTITY is true"))) + .roleArn(storageConfig.getRoleARN()) + .roleSessionName("PolarisAwsCredentialsStorageIntegration") + .policy( + policyString( + storageConfig, + allowListOperation, + allowedReadLocations, + allowedWriteLocations, + region, + accountId) + .toJson()) + .durationSeconds(storageCredentialDurationSeconds); + + credentials = stsClient.assumeRoleWithWebIdentity(request.build()).credentials(); + } else { + AssumeRoleRequest.Builder request = + AssumeRoleRequest.builder() + .externalId(storageConfig.getExternalId()) + .roleArn(storageConfig.getRoleARN()) + .roleSessionName("PolarisAwsCredentialsStorageIntegration") + .policy( + policyString( + storageConfig, + allowListOperation, + allowedReadLocations, + allowedWriteLocations, + region, + accountId) + .toJson()) + .durationSeconds(storageCredentialDurationSeconds); + credentialsProvider.ifPresent( + cp -> request.overrideConfiguration(b -> b.credentialsProvider(cp))); + credentials = stsClient.assumeRole(request.build()).credentials(); + } + accessConfig.put(StorageAccessProperty.AWS_KEY_ID, credentials.accessKeyId()); + accessConfig.put(StorageAccessProperty.AWS_SECRET_KEY, credentials.secretAccessKey()); + accessConfig.put(StorageAccessProperty.AWS_TOKEN, credentials.sessionToken()); + Optional.ofNullable(credentials.expiration()) .ifPresent( i -> { accessConfig.put( diff --git a/polaris-core/src/main/java/org/apache/polaris/core/storage/aws/AwsStorageConfigurationInfo.java b/polaris-core/src/main/java/org/apache/polaris/core/storage/aws/AwsStorageConfigurationInfo.java index 197da698fd..e1348dcd8f 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/storage/aws/AwsStorageConfigurationInfo.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/storage/aws/AwsStorageConfigurationInfo.java @@ -64,6 +64,8 @@ public String getFileIoImplClassName() { @Nullable public abstract String getRoleARN(); + public abstract @Nullable Boolean getPropagateApiUserIdentity(); + /** KMS Key ARN for server-side encryption,used for writes, optional */ @Nullable public abstract String getCurrentKmsKey(); diff --git a/polaris-core/src/main/java/org/apache/polaris/core/storage/azure/AzureCredentialsStorageIntegration.java b/polaris-core/src/main/java/org/apache/polaris/core/storage/azure/AzureCredentialsStorageIntegration.java index 0b189b3116..b20237ff3d 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/storage/azure/AzureCredentialsStorageIntegration.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/storage/azure/AzureCredentialsStorageIntegration.java @@ -78,7 +78,8 @@ public StorageAccessConfig getSubscopedCreds( boolean allowListOperation, @Nonnull Set allowedReadLocations, @Nonnull Set allowedWriteLocations, - Optional refreshCredentialsEndpoint) { + Optional refreshCredentialsEndpoint, + Optional token) { String loc = !allowedWriteLocations.isEmpty() ? allowedWriteLocations.stream().findAny().orElse(null) diff --git a/polaris-core/src/main/java/org/apache/polaris/core/storage/cache/StorageCredentialCache.java b/polaris-core/src/main/java/org/apache/polaris/core/storage/cache/StorageCredentialCache.java index 0f22863e59..124b16134d 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/storage/cache/StorageCredentialCache.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/storage/cache/StorageCredentialCache.java @@ -109,7 +109,8 @@ public StorageAccessConfig getOrGenerateSubScopeCreds( boolean allowListOperation, @Nonnull Set allowedReadLocations, @Nonnull Set allowedWriteLocations, - Optional refreshCredentialsEndpoint) { + Optional refreshCredentialsEndpoint, + Optional token) { RealmContext realmContext = storageCredentialsVendor.getRealmContext(); RealmConfig realmConfig = storageCredentialsVendor.getRealmConfig(); if (!isTypeSupported(polarisEntity.getType())) { @@ -134,7 +135,8 @@ public StorageAccessConfig getOrGenerateSubScopeCreds( allowListOperation, allowedReadLocations, allowedWriteLocations, - refreshCredentialsEndpoint); + refreshCredentialsEndpoint, + token); if (scopedCredentialsResult.isSuccess()) { long maxCacheDurationMs = maxCacheDurationMs(realmConfig); return new StorageCredentialCacheEntry( diff --git a/polaris-core/src/main/java/org/apache/polaris/core/storage/gcp/GcpCredentialsStorageIntegration.java b/polaris-core/src/main/java/org/apache/polaris/core/storage/gcp/GcpCredentialsStorageIntegration.java index 5f524d9ae4..727d751fc6 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/storage/gcp/GcpCredentialsStorageIntegration.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/storage/gcp/GcpCredentialsStorageIntegration.java @@ -77,7 +77,8 @@ public StorageAccessConfig getSubscopedCreds( boolean allowListOperation, @Nonnull Set allowedReadLocations, @Nonnull Set allowedWriteLocations, - Optional refreshCredentialsEndpoint) { + Optional refreshCredentialsEndpoint, + Optional token) { try { sourceCredentials.refresh(); } catch (IOException e) { @@ -93,9 +94,9 @@ public StorageAccessConfig getSubscopedCreds( .setSourceCredential(sourceCredentials) .setCredentialAccessBoundary(accessBoundary) .build(); - AccessToken token; + AccessToken accessToken; try { - token = credentials.refreshAccessToken(); + accessToken = credentials.refreshAccessToken(); } catch (IOException e) { LOGGER .atError() @@ -110,10 +111,10 @@ public StorageAccessConfig getSubscopedCreds( // If expires_in missing, use source credential's expire time, which require another api call to // get. StorageAccessConfig.Builder accessConfig = StorageAccessConfig.builder(); - accessConfig.put(StorageAccessProperty.GCS_ACCESS_TOKEN, token.getTokenValue()); + accessConfig.put(StorageAccessProperty.GCS_ACCESS_TOKEN, accessToken.getTokenValue()); accessConfig.put( StorageAccessProperty.GCS_ACCESS_TOKEN_EXPIRES_AT, - String.valueOf(token.getExpirationTime().getTime())); + String.valueOf(accessToken.getExpirationTime().getTime())); refreshCredentialsEndpoint.ifPresent( endpoint -> { diff --git a/polaris-core/src/test/java/org/apache/polaris/core/storage/InMemoryStorageIntegrationTest.java b/polaris-core/src/test/java/org/apache/polaris/core/storage/InMemoryStorageIntegrationTest.java index e9640cef81..435eb53e52 100644 --- a/polaris-core/src/test/java/org/apache/polaris/core/storage/InMemoryStorageIntegrationTest.java +++ b/polaris-core/src/test/java/org/apache/polaris/core/storage/InMemoryStorageIntegrationTest.java @@ -199,7 +199,8 @@ public StorageAccessConfig getSubscopedCreds( boolean allowListOperation, @Nonnull Set allowedReadLocations, @Nonnull Set allowedWriteLocations, - Optional refreshCredentialsEndpoint) { + Optional refreshCredentialsEndpoint, + Optional token) { return null; } } diff --git a/polaris-core/src/test/java/org/apache/polaris/core/storage/cache/StorageCredentialCacheTest.java b/polaris-core/src/test/java/org/apache/polaris/core/storage/cache/StorageCredentialCacheTest.java index c4db872317..b40d3f9420 100644 --- a/polaris-core/src/test/java/org/apache/polaris/core/storage/cache/StorageCredentialCacheTest.java +++ b/polaris-core/src/test/java/org/apache/polaris/core/storage/cache/StorageCredentialCacheTest.java @@ -80,6 +80,7 @@ public void testBadResult() { Mockito.anyBoolean(), Mockito.anySet(), Mockito.anySet(), + Mockito.any(), Mockito.any())) .thenReturn(badResult); PolarisEntity polarisEntity = @@ -94,6 +95,7 @@ public void testBadResult() { true, Set.of("s3://bucket1/path"), Set.of("s3://bucket3/path"), + Optional.empty(), Optional.empty())) .isInstanceOf(UnprocessableEntityException.class) .hasMessage("Failed to get subscoped credentials: extra_error_info"); @@ -109,6 +111,7 @@ public void testCacheHit() { Mockito.anyBoolean(), Mockito.anySet(), Mockito.anySet(), + Mockito.any(), Mockito.any())) .thenReturn(mockedScopedCreds.get(0)) .thenReturn(mockedScopedCreds.get(1)) @@ -125,6 +128,7 @@ public void testCacheHit() { true, Set.of("s3://bucket1/path", "s3://bucket2/path"), Set.of("s3://bucket3/path", "s3://bucket4/path"), + Optional.empty(), Optional.empty()); Assertions.assertThat(storageCredentialCache.getEstimatedSize()).isEqualTo(1); @@ -135,6 +139,7 @@ public void testCacheHit() { true, Set.of("s3://bucket1/path", "s3://bucket2/path"), Set.of("s3://bucket3/path", "s3://bucket4/path"), + Optional.empty(), Optional.empty()); Assertions.assertThat(storageCredentialCache.getEstimatedSize()).isEqualTo(1); } @@ -149,6 +154,7 @@ public void testCacheEvict() throws Exception { Mockito.anyBoolean(), Mockito.anySet(), Mockito.anySet(), + Mockito.any(), Mockito.any())) .thenReturn(mockedScopedCreds.get(0)) .thenReturn(mockedScopedCreds.get(1)) @@ -173,6 +179,7 @@ public void testCacheEvict() throws Exception { true, Set.of("s3://bucket1/path", "s3://bucket2/path"), Set.of("s3://bucket/path"), + Optional.empty(), Optional.empty()); Assertions.assertThat(storageCredentialCache.getIfPresent(cacheKey)).isNull(); @@ -182,6 +189,7 @@ public void testCacheEvict() throws Exception { true, Set.of("s3://bucket1/path", "s3://bucket2/path"), Set.of("s3://bucket/path"), + Optional.empty(), Optional.empty()); Assertions.assertThat(storageCredentialCache.getIfPresent(cacheKey)).isNull(); @@ -191,6 +199,7 @@ public void testCacheEvict() throws Exception { true, Set.of("s3://bucket1/path", "s3://bucket2/path"), Set.of("s3://bucket/path"), + Optional.empty(), Optional.empty()); Assertions.assertThat(storageCredentialCache.getIfPresent(cacheKey)).isNull(); } @@ -205,6 +214,7 @@ public void testCacheGenerateNewEntries() { Mockito.anyBoolean(), Mockito.anySet(), Mockito.anySet(), + Mockito.any(), Mockito.any())) .thenReturn(mockedScopedCreds.get(0)) .thenReturn(mockedScopedCreds.get(1)) @@ -219,6 +229,7 @@ public void testCacheGenerateNewEntries() { true, Set.of("s3://bucket1/path", "s3://bucket2/path"), Set.of("s3://bucket/path"), + Optional.empty(), Optional.empty()); Assertions.assertThat(storageCredentialCache.getEstimatedSize()).isEqualTo(++cacheSize); } @@ -236,6 +247,7 @@ public void testCacheGenerateNewEntries() { /* allowedListAction= */ true, Set.of("s3://bucket1/path", "s3://bucket2/path"), Set.of("s3://bucket/path"), + Optional.empty(), Optional.empty()); Assertions.assertThat(storageCredentialCache.getEstimatedSize()).isEqualTo(++cacheSize); } @@ -247,6 +259,7 @@ public void testCacheGenerateNewEntries() { /* allowedListAction= */ false, Set.of("s3://bucket1/path", "s3://bucket2/path"), Set.of("s3://bucket/path"), + Optional.empty(), Optional.empty()); Assertions.assertThat(storageCredentialCache.getEstimatedSize()).isEqualTo(++cacheSize); } @@ -258,6 +271,7 @@ public void testCacheGenerateNewEntries() { /* allowedListAction= */ false, Set.of("s3://bucket1/path", "s3://bucket2/path"), Set.of("s3://differentbucket/path"), + Optional.empty(), Optional.empty()); Assertions.assertThat(storageCredentialCache.getEstimatedSize()).isEqualTo(++cacheSize); } @@ -274,6 +288,7 @@ public void testCacheGenerateNewEntries() { /* allowedListAction= */ false, Set.of("s3://differentbucket/path", "s3://bucket2/path"), Set.of("s3://bucket/path"), + Optional.empty(), Optional.empty()); Assertions.assertThat(storageCredentialCache.getEstimatedSize()).isEqualTo(++cacheSize); } @@ -290,6 +305,7 @@ public void testCacheNotAffectedBy() { Mockito.anyBoolean(), Mockito.anySet(), Mockito.anySet(), + Mockito.any(), Mockito.any())) .thenReturn(mockedScopedCreds.get(0)) .thenReturn(mockedScopedCreds.get(1)) @@ -302,6 +318,7 @@ public void testCacheNotAffectedBy() { true, Set.of("s3://bucket1/path", "s3://bucket2/path"), Set.of("s3://bucket3/path", "s3://bucket4/path"), + Optional.empty(), Optional.empty()); } Assertions.assertThat(storageCredentialCache.getEstimatedSize()).isEqualTo(entityList.size()); @@ -314,6 +331,7 @@ public void testCacheNotAffectedBy() { true, Set.of("s3://bucket1/path", "s3://bucket2/path"), Set.of("s3://bucket3/path", "s3://bucket4/path"), + Optional.empty(), Optional.empty()); Assertions.assertThat(storageCredentialCache.getEstimatedSize()).isEqualTo(entityList.size()); } @@ -326,6 +344,7 @@ public void testCacheNotAffectedBy() { true, Set.of("s3://bucket1/path", "s3://bucket2/path"), Set.of("s3://bucket3/path", "s3://bucket4/path"), + Optional.empty(), Optional.empty()); Assertions.assertThat(storageCredentialCache.getEstimatedSize()).isEqualTo(entityList.size()); } @@ -337,6 +356,7 @@ public void testCacheNotAffectedBy() { true, Set.of("s3://bucket2/path", "s3://bucket1/path"), Set.of("s3://bucket3/path", "s3://bucket4/path"), + Optional.empty(), Optional.empty()); Assertions.assertThat(storageCredentialCache.getEstimatedSize()).isEqualTo(entityList.size()); } @@ -349,6 +369,7 @@ public void testCacheNotAffectedBy() { true, Set.of("s3://bucket2/path", "s3://bucket1/path"), Set.of("s3://bucket4/path", "s3://bucket3/path"), + Optional.empty(), Optional.empty()); Assertions.assertThat(storageCredentialCache.getEstimatedSize()).isEqualTo(entityList.size()); } @@ -428,6 +449,7 @@ public void testExtraProperties() { Mockito.anyBoolean(), Mockito.anySet(), Mockito.anySet(), + Mockito.any(), Mockito.any())) .thenReturn(properties); List entityList = getPolarisEntities(); @@ -439,6 +461,7 @@ public void testExtraProperties() { true, Set.of("s3://bucket1/path", "s3://bucket2/path"), Set.of("s3://bucket3/path", "s3://bucket4/path"), + Optional.empty(), Optional.empty()); Assertions.assertThat(config.credentials()) .containsExactly(Map.entry("s3.secret-access-key", "super-secret-123")); diff --git a/polaris-core/src/test/java/org/apache/polaris/service/storage/aws/AwsCredentialsStorageIntegrationTest.java b/polaris-core/src/test/java/org/apache/polaris/service/storage/aws/AwsCredentialsStorageIntegrationTest.java index e742746561..2a9ccd9216 100644 --- a/polaris-core/src/test/java/org/apache/polaris/service/storage/aws/AwsCredentialsStorageIntegrationTest.java +++ b/polaris-core/src/test/java/org/apache/polaris/service/storage/aws/AwsCredentialsStorageIntegrationTest.java @@ -19,12 +19,15 @@ package org.apache.polaris.service.storage.aws; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; import jakarta.annotation.Nonnull; +import jakarta.ws.rs.core.SecurityContext; import java.time.Instant; import java.util.List; import java.util.Optional; import java.util.Set; +import org.apache.polaris.core.auth.PolarisPrincipal; import org.apache.polaris.core.storage.BaseStorageIntegrationTest; import org.apache.polaris.core.storage.StorageAccessConfig; import org.apache.polaris.core.storage.StorageAccessProperty; @@ -36,6 +39,7 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; import org.mockito.Mockito; +import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; import software.amazon.awssdk.policybuilder.iam.IamAction; import software.amazon.awssdk.policybuilder.iam.IamCondition; import software.amazon.awssdk.policybuilder.iam.IamConditionOperator; @@ -46,6 +50,8 @@ import software.amazon.awssdk.services.sts.StsClient; import software.amazon.awssdk.services.sts.model.AssumeRoleRequest; import software.amazon.awssdk.services.sts.model.AssumeRoleResponse; +import software.amazon.awssdk.services.sts.model.AssumeRoleWithWebIdentityRequest; +import software.amazon.awssdk.services.sts.model.AssumeRoleWithWebIdentityResponse; import software.amazon.awssdk.services.sts.model.Credentials; class AwsCredentialsStorageIntegrationTest extends BaseStorageIntegrationTest { @@ -62,11 +68,21 @@ class AwsCredentialsStorageIntegrationTest extends BaseStorageIntegrationTest { .expiration(EXPIRE_TIME) .build()) .build(); + public static final AssumeRoleWithWebIdentityResponse ASSUME_ROLE_WITH_WEB_IDENTITY_RESPONSE = + AssumeRoleWithWebIdentityResponse.builder() + .credentials( + Credentials.builder() + .accessKeyId("accessKey") + .secretAccessKey("secretKey") + .sessionToken("sess") + .expiration(EXPIRE_TIME) + .build()) + .build(); public static final String AWS_PARTITION = "aws"; @ParameterizedTest @ValueSource(strings = {"s3a", "s3"}) - public void testGetSubscopedCreds(String scheme) { + public void testGetSubscopedCredsWithExternalId(String scheme) { StsClient stsClient = Mockito.mock(StsClient.class); String roleARN = "arn:aws:iam::012345678901:role/jdoe"; String externalId = "externalId"; @@ -112,6 +128,61 @@ public void testGetSubscopedCreds(String scheme) { "/namespace/table/credentials"); } + @ParameterizedTest + @ValueSource(strings = {"s3a", "s3"}) + public void testGetSubscopedCredsWithUserWebToken(String scheme) { + StsClient stsClient = Mockito.mock(StsClient.class); + String roleARN = "arn:aws:iam::012345678901:role/jdoe"; + String webIdentityToken = "webIdentityToken"; + PolarisPrincipal principal = mock(PolarisPrincipal.class); + SecurityContext context = mock(SecurityContext.class); + Mockito.when(context.getUserPrincipal()).thenReturn(principal); + Mockito.when( + stsClient.assumeRoleWithWebIdentity( + Mockito.isA(AssumeRoleWithWebIdentityRequest.class))) + .thenAnswer( + invocation -> { + assertThat(invocation.getArguments()[0]) + .isInstanceOf(AssumeRoleWithWebIdentityRequest.class) + .asInstanceOf( + InstanceOfAssertFactories.type(AssumeRoleWithWebIdentityRequest.class)) + .returns(webIdentityToken, AssumeRoleWithWebIdentityRequest::webIdentityToken) + .returns(roleARN, AssumeRoleWithWebIdentityRequest::roleArn); + return ASSUME_ROLE_WITH_WEB_IDENTITY_RESPONSE; + }); + Mockito.when(principal.getToken()).thenReturn(webIdentityToken); + String warehouseDir = scheme + "://bucket/path/to/warehouse"; + Optional credentialsProvider = Optional.empty(); + StorageAccessConfig accessConfig = + new AwsCredentialsStorageIntegration( + AwsStorageConfigurationInfo.builder() + .addAllowedLocation(warehouseDir) + .roleARN(roleARN) + .propagateApiUserIdentity(true) + .build(), + (destination) -> stsClient, + credentialsProvider, + context) + .getSubscopedCreds( + EMPTY_REALM_CONFIG, + true, + Set.of(warehouseDir + "/namespace/table"), + Set.of(warehouseDir + "/namespace/table"), + Optional.of("/namespace/table/credentials")); + assertThat(accessConfig.credentials()) + .isNotEmpty() + .containsEntry(StorageAccessProperty.AWS_TOKEN.getPropertyName(), "sess") + .containsEntry(StorageAccessProperty.AWS_KEY_ID.getPropertyName(), "accessKey") + .containsEntry(StorageAccessProperty.AWS_SECRET_KEY.getPropertyName(), "secretKey") + .containsEntry( + StorageAccessProperty.AWS_SESSION_TOKEN_EXPIRES_AT_MS.getPropertyName(), + String.valueOf(EXPIRE_TIME.toEpochMilli())); + assertThat(accessConfig.extraProperties()) + .containsEntry( + StorageAccessProperty.AWS_REFRESH_CREDENTIALS_ENDPOINT.getPropertyName(), + "/namespace/table/credentials"); + } + @ParameterizedTest @ValueSource(strings = {AWS_PARTITION, "aws-cn", "aws-us-gov"}) public void testGetSubscopedCredsInlinePolicy(String awsPartition) { @@ -250,6 +321,7 @@ public void testGetSubscopedCredsInlinePolicy(String awsPartition) { true, Set.of(s3Path(bucket, firstPath), s3Path(bucket, secondPath)), Set.of(s3Path(bucket, firstPath)), + Optional.empty(), Optional.empty()); assertThat(storageAccessConfig.credentials()) .isNotEmpty() @@ -351,6 +423,7 @@ public void testGetSubscopedCredsInlinePolicyWithoutList() { false, /* allowList = false*/ Set.of(s3Path(bucket, firstPath), s3Path(bucket, secondPath)), Set.of(s3Path(bucket, firstPath)), + Optional.empty(), Optional.empty()); assertThat(storageAccessConfig.credentials()) .isNotEmpty() @@ -466,6 +539,7 @@ public void testGetSubscopedCredsInlinePolicyWithoutWrites() { true, /* allowList = true */ Set.of(s3Path(bucket, firstPath), s3Path(bucket, secondPath)), Set.of(), + Optional.empty(), Optional.empty()); assertThat(storageAccessConfig.credentials()) .isNotEmpty() @@ -553,6 +627,7 @@ public void testGetSubscopedCredsInlinePolicyWithEmptyReadAndWrite() { true, /* allowList = true */ Set.of(), Set.of(), + Optional.empty(), Optional.empty()); assertThat(storageAccessConfig.credentials()) .isNotEmpty() @@ -596,6 +671,7 @@ public void testClientRegion(String awsPartition) { true, /* allowList = true */ Set.of(), Set.of(), + Optional.empty(), Optional.empty()); assertThat(storageAccessConfig.credentials()) .containsEntry(StorageAccessProperty.AWS_TOKEN.getPropertyName(), "sess") @@ -637,6 +713,7 @@ public void testNoClientRegion(String awsPartition) { true, /* allowList = true */ Set.of(), Set.of(), + Optional.empty(), Optional.empty()); assertThat(storageAccessConfig.credentials()) .isNotEmpty() @@ -720,6 +797,7 @@ public void testKmsKeyPolicyLogic() { true, Set.of(s3Path(bucket, warehouseKeyPrefix + "/table")), Set.of(s3Path(bucket, warehouseKeyPrefix + "/table")), + Optional.empty(), Optional.empty()); // Test with allowed KMS keys and read-only permissions @@ -765,6 +843,7 @@ public void testKmsKeyPolicyLogic() { true, Set.of(s3Path(bucket, warehouseKeyPrefix + "/table")), Set.of(), + Optional.empty(), Optional.empty()); // Test with no KMS keys and read-only (should add wildcard KMS access) @@ -801,6 +880,7 @@ public void testKmsKeyPolicyLogic() { true, Set.of(s3Path(bucket, warehouseKeyPrefix + "/table")), Set.of(), + Optional.empty(), Optional.empty()); // Test with no KMS keys and write permissions (should not add KMS statement) @@ -834,6 +914,7 @@ public void testKmsKeyPolicyLogic() { true, Set.of(s3Path(bucket, warehouseKeyPrefix + "/table")), Set.of(s3Path(bucket, warehouseKeyPrefix + "/table")), + Optional.empty(), Optional.empty()); } diff --git a/polaris-core/src/test/java/org/apache/polaris/service/storage/azure/AzureCredentialStorageIntegrationTest.java b/polaris-core/src/test/java/org/apache/polaris/service/storage/azure/AzureCredentialStorageIntegrationTest.java index 42a8bd3272..10f0832c06 100644 --- a/polaris-core/src/test/java/org/apache/polaris/service/storage/azure/AzureCredentialStorageIntegrationTest.java +++ b/polaris-core/src/test/java/org/apache/polaris/service/storage/azure/AzureCredentialStorageIntegrationTest.java @@ -354,6 +354,7 @@ private StorageAccessConfig subscopedCredsForOperations( allowListAction, new HashSet<>(allowedReadLoc), new HashSet<>(allowedWriteLoc), + Optional.empty(), Optional.empty()); } diff --git a/polaris-core/src/test/java/org/apache/polaris/service/storage/gcp/GcpCredentialsStorageIntegrationTest.java b/polaris-core/src/test/java/org/apache/polaris/service/storage/gcp/GcpCredentialsStorageIntegrationTest.java index b0be0883d8..3e19c9c1e8 100644 --- a/polaris-core/src/test/java/org/apache/polaris/service/storage/gcp/GcpCredentialsStorageIntegrationTest.java +++ b/polaris-core/src/test/java/org/apache/polaris/service/storage/gcp/GcpCredentialsStorageIntegrationTest.java @@ -175,7 +175,8 @@ private StorageAccessConfig subscopedCredsForOperations( allowListAction, new HashSet<>(allowedReadLoc), new HashSet<>(allowedWriteLoc), - Optional.of(REFRESH_ENDPOINT)); + Optional.of(REFRESH_ENDPOINT), + Optional.empty()); } private JsonNode readResource(ObjectMapper mapper, String name) throws IOException { diff --git a/polaris-core/src/testFixtures/java/org/apache/polaris/core/persistence/BaseResolverTest.java b/polaris-core/src/testFixtures/java/org/apache/polaris/core/persistence/BaseResolverTest.java index d40d13813f..f797325921 100644 --- a/polaris-core/src/testFixtures/java/org/apache/polaris/core/persistence/BaseResolverTest.java +++ b/polaris-core/src/testFixtures/java/org/apache/polaris/core/persistence/BaseResolverTest.java @@ -435,7 +435,9 @@ private Resolver allocateResolver( .collect(Collectors.toList())); PolarisPrincipal authenticatedPrincipal = PolarisPrincipal.of( - PrincipalEntity.of(P1), Optional.ofNullable(principalRolesScope).orElse(Set.of())); + PrincipalEntity.of(P1), + Optional.ofNullable(principalRolesScope).orElse(Set.of()), + "thisisatoken"); return new Resolver( diagServices, callCtx(), diff --git a/runtime/service/src/main/java/org/apache/polaris/service/auth/DefaultAuthenticator.java b/runtime/service/src/main/java/org/apache/polaris/service/auth/DefaultAuthenticator.java index 83a3a419d7..69ed980056 100644 --- a/runtime/service/src/main/java/org/apache/polaris/service/auth/DefaultAuthenticator.java +++ b/runtime/service/src/main/java/org/apache/polaris/service/auth/DefaultAuthenticator.java @@ -94,7 +94,8 @@ public PolarisPrincipal authenticate(PolarisCredential credentials) { PrincipalEntity principalEntity = resolvePrincipalEntity(credentials); Set principalRoles = resolvePrincipalRoles(credentials, principalEntity); - PolarisPrincipal polarisPrincipal = PolarisPrincipal.of(principalEntity, principalRoles); + PolarisPrincipal polarisPrincipal = + PolarisPrincipal.of(principalEntity, principalRoles, credentials.getToken()); LOGGER.debug("Resolved principal: {}", polarisPrincipal); return polarisPrincipal; diff --git a/runtime/service/src/main/java/org/apache/polaris/service/auth/PolarisCredential.java b/runtime/service/src/main/java/org/apache/polaris/service/auth/PolarisCredential.java index f86565e669..4cd437f88a 100644 --- a/runtime/service/src/main/java/org/apache/polaris/service/auth/PolarisCredential.java +++ b/runtime/service/src/main/java/org/apache/polaris/service/auth/PolarisCredential.java @@ -32,13 +32,26 @@ public interface PolarisCredential extends Credential { static PolarisCredential of( @Nullable Long principalId, @Nullable String principalName, Set principalRoles) { + return of(null, principalId, principalName, principalRoles); + } + + static PolarisCredential of( + @Nullable String token, + @Nullable Long principalId, + @Nullable String principalName, + Set principalRoles) { return ImmutablePolarisCredential.builder() + .token(token) .principalId(principalId) .principalName(principalName) .principalRoles(principalRoles) .build(); } + /** the JWT token used to authenticate the user to Polaris */ + @Nullable + String getToken(); + /** The principal id, or null if unknown. Used for principal lookups by id. */ @Nullable Long getPrincipalId(); diff --git a/runtime/service/src/main/java/org/apache/polaris/service/auth/external/OidcPolarisCredentialAugmentor.java b/runtime/service/src/main/java/org/apache/polaris/service/auth/external/OidcPolarisCredentialAugmentor.java index 30fe86cccd..c7fd692c2f 100644 --- a/runtime/service/src/main/java/org/apache/polaris/service/auth/external/OidcPolarisCredentialAugmentor.java +++ b/runtime/service/src/main/java/org/apache/polaris/service/auth/external/OidcPolarisCredentialAugmentor.java @@ -90,7 +90,12 @@ protected SecurityIdentity setPolarisCredential( principalMapper.mapPrincipalId(identity).stream().boxed().findFirst().orElse(null); String principalName = principalMapper.mapPrincipalName(identity).orElse(null); Set principalRoles = rolesMapper.mapPrincipalRoles(identity); - PolarisCredential credential = PolarisCredential.of(principalId, principalName, principalRoles); + String token = null; + if (identity.getPrincipal() instanceof JsonWebToken jwt) { + token = jwt.getRawToken(); + } + PolarisCredential credential = + PolarisCredential.of(token, principalId, principalName, principalRoles); // Note: we don't change the identity roles here, this will be done later on // by the AuthenticatingAugmentor, which will also validate them. return QuarkusSecurityIdentity.builder(identity).addCredential(credential).build(); diff --git a/runtime/service/src/main/java/org/apache/polaris/service/catalog/io/StorageAccessConfigProvider.java b/runtime/service/src/main/java/org/apache/polaris/service/catalog/io/StorageAccessConfigProvider.java index d6316c6e7a..4e1ff151a4 100644 --- a/runtime/service/src/main/java/org/apache/polaris/service/catalog/io/StorageAccessConfigProvider.java +++ b/runtime/service/src/main/java/org/apache/polaris/service/catalog/io/StorageAccessConfigProvider.java @@ -19,12 +19,16 @@ package org.apache.polaris.service.catalog.io; +import com.google.common.annotations.VisibleForTesting; import jakarta.annotation.Nonnull; import jakarta.enterprise.context.RequestScoped; import jakarta.inject.Inject; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.SecurityContext; import java.util.Optional; import java.util.Set; import org.apache.iceberg.catalog.TableIdentifier; +import org.apache.polaris.core.auth.PolarisPrincipal; import org.apache.polaris.core.config.FeatureConfiguration; import org.apache.polaris.core.entity.PolarisEntity; import org.apache.polaris.core.persistence.PolarisResolvedPathWrapper; @@ -49,6 +53,7 @@ public class StorageAccessConfigProvider { private final StorageCredentialCache storageCredentialCache; private final StorageCredentialsVendor storageCredentialsVendor; + @Context SecurityContext securityContext; @Inject public StorageAccessConfigProvider( @@ -58,6 +63,16 @@ public StorageAccessConfigProvider( this.storageCredentialsVendor = storageCredentialsVendor; } + @VisibleForTesting + public StorageAccessConfigProvider( + StorageCredentialCache storageCredentialCache, + StorageCredentialsVendor storageCredentialsVendor, + SecurityContext securityContext) { + this.storageCredentialCache = storageCredentialCache; + this.storageCredentialsVendor = storageCredentialsVendor; + this.securityContext = securityContext; + } + /** * Vends credentials for accessing table storage at explicit locations. * @@ -119,8 +134,9 @@ public StorageAccessConfig getStorageAccessConfig( allowList, tableLocations, writeLocations, - refreshCredentialsEndpoint); - + refreshCredentialsEndpoint, + Optional.ofNullable( + ((PolarisPrincipal) securityContext.getUserPrincipal()).getToken())); LOGGER .atDebug() .addKeyValue("tableIdentifier", tableIdentifier) diff --git a/runtime/service/src/main/java/org/apache/polaris/service/storage/PolarisStorageIntegrationProviderImpl.java b/runtime/service/src/main/java/org/apache/polaris/service/storage/PolarisStorageIntegrationProviderImpl.java index 706acb4222..7dab400759 100644 --- a/runtime/service/src/main/java/org/apache/polaris/service/storage/PolarisStorageIntegrationProviderImpl.java +++ b/runtime/service/src/main/java/org/apache/polaris/service/storage/PolarisStorageIntegrationProviderImpl.java @@ -114,7 +114,8 @@ public StorageAccessConfig getSubscopedCreds( boolean allowListOperation, @Nonnull Set allowedReadLocations, @Nonnull Set allowedWriteLocations, - Optional refreshCredentialsEndpoint) { + Optional refreshCredentialsEndpoint, + Optional token) { return StorageAccessConfig.builder().supportsCredentialVending(false).build(); } diff --git a/runtime/service/src/test/java/org/apache/polaris/service/auth/external/OidcPolarisCredentialAugmentorTest.java b/runtime/service/src/test/java/org/apache/polaris/service/auth/external/OidcPolarisCredentialAugmentorTest.java index 588ead41ca..d9e702c00a 100644 --- a/runtime/service/src/test/java/org/apache/polaris/service/auth/external/OidcPolarisCredentialAugmentorTest.java +++ b/runtime/service/src/test/java/org/apache/polaris/service/auth/external/OidcPolarisCredentialAugmentorTest.java @@ -109,7 +109,7 @@ public void testAugmentOidcPrincipal() { .addRole("ROLE1") .addAttribute(TENANT_CONFIG_ATTRIBUTE, config) .build(); - + when(oidcPrincipal.getRawToken()).thenReturn("this_is_a_token"); when(principalMapper.mapPrincipalId(identity)).thenReturn(OptionalLong.of(123L)); when(principalMapper.mapPrincipalName(identity)).thenReturn(Optional.of("root")); when(principalRolesMapper.mapPrincipalRoles(identity)).thenReturn(Set.of("MAPPED_ROLE1")); @@ -122,7 +122,7 @@ public void testAugmentOidcPrincipal() { assertThat(result).isNotNull(); assertThat(result.getPrincipal()).isSameAs(oidcPrincipal); assertThat(result.getCredential(PolarisCredential.class)) - .isEqualTo(PolarisCredential.of(123L, "root", Set.of("MAPPED_ROLE1"))); + .isEqualTo(PolarisCredential.of("this_is_a_token", 123L, "root", Set.of("MAPPED_ROLE1"))); // the identity roles should not change, since this is done by the ActiveRolesAugmentor assertThat(result.getRoles()).containsExactlyInAnyOrder("ROLE1"); } diff --git a/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/AbstractIcebergCatalogTest.java b/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/AbstractIcebergCatalogTest.java index 644adb5eed..45f56b0258 100644 --- a/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/AbstractIcebergCatalogTest.java +++ b/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/AbstractIcebergCatalogTest.java @@ -1891,6 +1891,7 @@ public void testDropTableWithPurge() { true, Set.of(tableMetadata.location()), Set.of(tableMetadata.location()), + Optional.empty(), Optional.empty()) .getStorageAccessConfig() .credentials(); diff --git a/runtime/service/src/testFixtures/java/org/apache/polaris/service/TestServices.java b/runtime/service/src/testFixtures/java/org/apache/polaris/service/TestServices.java index 8a5ea4ee33..5eeb68c893 100644 --- a/runtime/service/src/testFixtures/java/org/apache/polaris/service/TestServices.java +++ b/runtime/service/src/testFixtures/java/org/apache/polaris/service/TestServices.java @@ -295,7 +295,8 @@ public String getAuthenticationScheme() { StorageCredentialsVendor storageCredentialsVendor = new StorageCredentialsVendor(metaStoreManager, callContext); StorageAccessConfigProvider storageAccessConfigProvider = - new StorageAccessConfigProvider(storageCredentialCache, storageCredentialsVendor); + new StorageAccessConfigProvider( + storageCredentialCache, storageCredentialsVendor, securityContext); FileIOFactory fileIOFactory = fileIOFactorySupplier.get(); TaskExecutor taskExecutor = Mockito.mock(TaskExecutor.class); diff --git a/spec/polaris-management-service.yml b/spec/polaris-management-service.yml index a797496d26..dc7fbf875c 100644 --- a/spec/polaris-management-service.yml +++ b/spec/polaris-management-service.yml @@ -1136,6 +1136,12 @@ components: for accessing data and metadata files within the related catalog. Setting this property to `true` effectively disables vending storage credentials to clients. This setting is intended for configuring catalogs with S3-compatible storage implementations that do not support STS. + propagateApiUserIdentity: + type: boolean + description: >- + if set to `true`, instructs Polaris Servers to use the end-user's credentials when + accessing the S3 STS server. + default: false endpointInternal: type: string description: >-