diff --git a/polaris-core/src/main/java/org/apache/polaris/core/storage/AccessConfig.java b/polaris-core/src/main/java/org/apache/polaris/core/storage/AccessConfig.java index e15fd2e916..94e74a3d66 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/storage/AccessConfig.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/storage/AccessConfig.java @@ -23,6 +23,7 @@ import java.util.Map; import java.util.Optional; import org.apache.polaris.immutables.PolarisImmutable; +import org.immutables.value.Value; @PolarisImmutable public interface AccessConfig { @@ -38,6 +39,15 @@ public interface AccessConfig { Optional expiresAt(); + /** + * Indicates whether the storage integration subsystem that produced this object is capable of + * credential vending in principle. + */ + @Value.Default + default boolean supportsCredentialVending() { + return true; + } + default String get(StorageAccessProperty key) { if (key.isCredential()) { return credentials().get(key.getPropertyName()); @@ -64,6 +74,9 @@ interface Builder { @CanIgnoreReturnValue Builder expiresAt(Instant expiresAt); + @CanIgnoreReturnValue + Builder supportsCredentialVending(boolean supportsCredentialVending); + default Builder put(StorageAccessProperty key, String value) { if (key.isExpirationTimestamp()) { expiresAt(Instant.ofEpochMilli(Long.parseLong(value))); diff --git a/runtime/service/src/intTest/java/org/apache/polaris/service/it/RestCatalogMinIOSpecialIT.java b/runtime/service/src/intTest/java/org/apache/polaris/service/it/RestCatalogMinIOSpecialIT.java index 561e76938f..3cc2ac9648 100644 --- a/runtime/service/src/intTest/java/org/apache/polaris/service/it/RestCatalogMinIOSpecialIT.java +++ b/runtime/service/src/intTest/java/org/apache/polaris/service/it/RestCatalogMinIOSpecialIT.java @@ -31,6 +31,7 @@ import static org.apache.polaris.service.catalog.AccessDelegationMode.VENDED_CREDENTIALS; import static org.apache.polaris.service.it.env.PolarisClient.polarisClient; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import com.google.common.collect.ImmutableMap; import io.quarkus.test.junit.QuarkusIntegrationTest; @@ -301,6 +302,62 @@ public void testInternalEndpoints() throws IOException { } } + @Test + public void testCreateTableFailureWithCredentialVendingWithoutSts() throws IOException { + try (RESTCatalog restCatalog = + createCatalog( + Optional.of(endpoint), + Optional.of("http://sts.example.com"), // not called + false, + Optional.of(VENDED_CREDENTIALS), + false)) { + StorageConfigInfo storageConfig = + managementApi.getCatalog(catalogName).getStorageConfigInfo(); + assertThat((AwsStorageConfigInfo) storageConfig) + .extracting( + AwsStorageConfigInfo::getEndpoint, + AwsStorageConfigInfo::getStsEndpoint, + AwsStorageConfigInfo::getEndpointInternal, + AwsStorageConfigInfo::getPathStyleAccess, + AwsStorageConfigInfo::getStsUnavailable) + .containsExactly(endpoint, "http://sts.example.com", null, false, true); + + catalogApi.createNamespace(catalogName, "test-ns"); + TableIdentifier id = TableIdentifier.of("test-ns", "t2"); + // Credential vending is not supported without STS + assertThatThrownBy(() -> restCatalog.createTable(id, SCHEMA)) + .hasMessageContaining("but no credentials are available") + .hasMessageContaining(id.toString()); + } + } + + @Test + public void testLoadTableFailureWithCredentialVendingWithoutSts() throws IOException { + try (RESTCatalog restCatalog = + createCatalog( + Optional.of(endpoint), + Optional.of("http://sts.example.com"), // not called + false, + Optional.empty(), + false)) { + + catalogApi.createNamespace(catalogName, "test-ns"); + TableIdentifier id = TableIdentifier.of("test-ns", "t3"); + restCatalog.createTable(id, SCHEMA); + + // Credential vending is not supported without STS + assertThatThrownBy( + () -> + catalogApi.loadTable( + catalogName, + id, + "ALL", + Map.of("X-Iceberg-Access-Delegation", VENDED_CREDENTIALS.protocolValue()))) + .hasMessageContaining("but no credentials are available") + .hasMessageContaining(id.toString()); + } + } + public LoadTableResponse doTestCreateTable( RESTCatalog restCatalog, Optional dm) { catalogApi.createNamespace(catalogName, "test-ns"); diff --git a/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalog.java b/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalog.java index a338c007c8..0e21cdf128 100644 --- a/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalog.java +++ b/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalog.java @@ -844,7 +844,7 @@ public AccessConfig getAccessConfig( .atWarn() .addKeyValue("tableIdentifier", tableIdentifier) .log("Table entity has no storage configuration in its hierarchy"); - return AccessConfig.builder().build(); + return AccessConfig.builder().supportsCredentialVending(false).build(); } return FileIOUtil.refreshAccessConfig( callContext, diff --git a/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogHandler.java b/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogHandler.java index 67e3c75ea0..604792da4c 100644 --- a/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogHandler.java +++ b/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogHandler.java @@ -804,13 +804,22 @@ private LoadTableResponse.Builder buildLoadTableResponseWithDelegationCredential credentialDelegation.getAccessConfig( tableIdentifier, tableMetadata, actions, refreshCredentialsEndpoint); Map credentialConfig = accessConfig.credentials(); - if (!credentialConfig.isEmpty() && delegationModes.contains(VENDED_CREDENTIALS)) { - responseBuilder.addAllConfig(credentialConfig); - responseBuilder.addCredential( - ImmutableCredential.builder() - .prefix(tableMetadata.location()) - .config(credentialConfig) - .build()); + if (delegationModes.contains(VENDED_CREDENTIALS)) { + if (!credentialConfig.isEmpty()) { + responseBuilder.addAllConfig(credentialConfig); + responseBuilder.addCredential( + ImmutableCredential.builder() + .prefix(tableMetadata.location()) + .config(credentialConfig) + .build()); + } else { + Boolean skipCredIndirection = + realmConfig.getConfig(FeatureConfiguration.SKIP_CREDENTIAL_SUBSCOPING_INDIRECTION); + Preconditions.checkArgument( + !accessConfig.supportsCredentialVending() || skipCredIndirection, + "Credential vending was requested for table %s, but no credentials are available", + tableIdentifier); + } } responseBuilder.addAllConfig(accessConfig.extraProperties()); } 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 e04a9525ba..23ec20abc3 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 @@ -115,7 +115,7 @@ public AccessConfig getSubscopedCreds( @Nonnull Set allowedReadLocations, @Nonnull Set allowedWriteLocations, Optional refreshCredentialsEndpoint) { - return AccessConfig.builder().build(); + return AccessConfig.builder().supportsCredentialVending(false).build(); } @Override