From 6d7bfa5875765b4abf8b72f7df18ec0046ee9970 Mon Sep 17 00:00:00 2001 From: Jason Date: Wed, 13 Aug 2025 11:11:00 +0300 Subject: [PATCH 1/4] add refresh credentials property to loadTableResult --- .../core/rest/PolarisResourcePaths.java | 11 ++++++ .../iceberg/IcebergCatalogAdapter.java | 34 ++++++++++++++++++- 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/polaris-core/src/main/java/org/apache/polaris/core/rest/PolarisResourcePaths.java b/polaris-core/src/main/java/org/apache/polaris/core/rest/PolarisResourcePaths.java index 8a30d79624..16eea08da2 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/rest/PolarisResourcePaths.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/rest/PolarisResourcePaths.java @@ -57,6 +57,17 @@ public String genericTables(Namespace ns) { "polaris", "v1", prefix, "namespaces", RESTUtil.encodeNamespace(ns), "generic-tables"); } + public String credentialsPath(TableIdentifier ident) { + return SLASH.join( + "v1", + prefix, + "namespaces", + RESTUtil.encodeNamespace(ident.namespace()), + "tables", + RESTUtil.encodeString(ident.name()), + "credentials"); + } + public String genericTable(TableIdentifier ident) { return SLASH.join( "polaris", diff --git a/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogAdapter.java b/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogAdapter.java index 76401582a9..970dfc1325 100644 --- a/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogAdapter.java +++ b/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogAdapter.java @@ -40,6 +40,7 @@ import java.util.Set; import java.util.function.Function; import org.apache.iceberg.MetadataUpdate; +import org.apache.iceberg.aws.AwsClientProperties; import org.apache.iceberg.catalog.Namespace; import org.apache.iceberg.catalog.TableIdentifier; import org.apache.iceberg.exceptions.BadRequestException; @@ -75,7 +76,9 @@ import org.apache.polaris.core.persistence.resolver.ResolverFactory; import org.apache.polaris.core.persistence.resolver.ResolverStatus; import org.apache.polaris.core.rest.PolarisEndpoints; +import org.apache.polaris.core.rest.PolarisResourcePaths; import org.apache.polaris.core.secrets.UserSecretsManager; +import org.apache.polaris.core.storage.StorageAccessProperty; import org.apache.polaris.service.catalog.AccessDelegationMode; import org.apache.polaris.service.catalog.CatalogPrefixParser; import org.apache.polaris.service.catalog.api.IcebergRestCatalogApiService; @@ -430,16 +433,45 @@ public Response loadTable( .loadTableIfStale(tableIdentifier, ifNoneMatch, snapshots) .orElseThrow(() -> new WebApplicationException(Response.Status.NOT_MODIFIED)); } else { - response = + LoadTableResponse originalResponse = catalog .loadTableWithAccessDelegationIfStale(tableIdentifier, ifNoneMatch, snapshots) .orElseThrow(() -> new WebApplicationException(Response.Status.NOT_MODIFIED)); + + if (delegationModes.contains(VENDED_CREDENTIALS)) { + response = + injectRefreshVendedCredentialProperties( + originalResponse, + new PolarisResourcePaths(prefix).credentialsPath(tableIdentifier)); + } else { + response = originalResponse; + } } return tryInsertETagHeader(Response.ok(response), response, namespace, table).build(); }); } + private LoadTableResponse injectRefreshVendedCredentialProperties( + LoadTableResponse originalResponse, String credentialsEndpoint) { + LoadTableResponse.Builder loadResponseBuilder = + LoadTableResponse.builder().withTableMetadata(originalResponse.tableMetadata()); + loadResponseBuilder.addAllConfig(originalResponse.config()); + loadResponseBuilder.addAllCredentials(originalResponse.credentials()); + loadResponseBuilder.addConfig( + AwsClientProperties.REFRESH_CREDENTIALS_ENDPOINT, credentialsEndpoint); + // Only enable credential refresh for currently supported credential types + if (originalResponse.credentials().stream() + .anyMatch( + credential -> + credential + .config() + .containsKey(StorageAccessProperty.AWS_SECRET_KEY.getPropertyName()))) { + loadResponseBuilder.addConfig(AwsClientProperties.REFRESH_CREDENTIALS_ENABLED, "true"); + } + return loadResponseBuilder.build(); + } + @Override public Response tableExists( String prefix, From fee6795d872cd02e797d2284128c9981813c8534 Mon Sep 17 00:00:00 2001 From: Jason Date: Wed, 13 Aug 2025 15:07:28 +0300 Subject: [PATCH 2/4] IcebergCatalogAdapterTest: Added test to ensure refresh credentials endpoint is included --- .../iceberg/IcebergCatalogAdapterTest.java | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogAdapterTest.java b/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogAdapterTest.java index f8f948a66d..7204911e41 100644 --- a/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogAdapterTest.java +++ b/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogAdapterTest.java @@ -19,17 +19,23 @@ package org.apache.polaris.service.catalog.iceberg; +import static org.assertj.core.api.Assertions.assertThat; + import java.io.IOException; import java.lang.reflect.Field; import java.util.List; import java.util.Map; import java.util.stream.Stream; import org.apache.iceberg.Schema; +import org.apache.iceberg.aws.AwsClientProperties; import org.apache.iceberg.catalog.Namespace; import org.apache.iceberg.catalog.TableIdentifier; import org.apache.iceberg.inmemory.InMemoryCatalog; +import org.apache.iceberg.rest.requests.CreateTableRequest; import org.apache.iceberg.rest.responses.ListNamespacesResponse; import org.apache.iceberg.rest.responses.ListTablesResponse; +import org.apache.iceberg.rest.responses.LoadTableResponse; +import org.apache.iceberg.types.Types; import org.apache.polaris.core.admin.model.AuthenticationParameters; import org.apache.polaris.core.admin.model.AwsStorageConfigInfo; import org.apache.polaris.core.admin.model.BearerAuthenticationParameters; @@ -43,6 +49,7 @@ import org.assertj.core.api.Assertions; import org.assertj.core.util.Strings; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; @@ -242,4 +249,60 @@ private static Stream paginationTestCases() { Arguments.of("5", 5), Arguments.of("5", 10)); } + + @Test + void testLoadTableReturnsCredentialsRefreshEndpoint() throws IOException { + try (InMemoryCatalog inMemoryCatalog = new InMemoryCatalog()) { + // Initialize and replace the default handler with one backed by in-memory catalog + inMemoryCatalog.initialize("inMemory", Map.of()); + mockCatalogAdapter(inMemoryCatalog); + + // Create a namespace and table + String namespace = "test_ns"; + String tableName = "test_table"; + inMemoryCatalog.createNamespace(Namespace.of(namespace)); + + Schema schema = + new Schema( + Types.NestedField.required(1, "id", Types.LongType.get()), + Types.NestedField.optional(2, "name", Types.StringType.get())); + + CreateTableRequest createTableRequest = + CreateTableRequest.builder().withName(tableName).withSchema(schema).build(); + + // Create the table first + catalogAdapter.createTable( + FEDERATED_CATALOG_NAME, + namespace, + createTableRequest, + "vended-credentials", + testServices.realmContext(), + testServices.securityContext()); + + // Load the table with vended credentials access delegation mode + LoadTableResponse response = + (LoadTableResponse) + catalogAdapter + .loadTable( + FEDERATED_CATALOG_NAME, + namespace, + tableName, + "vended-credentials", + null, + null, + testServices.realmContext(), + testServices.securityContext()) + .getEntity(); + + // Verify that the response contains the credentials refresh endpoint configuration + assertThat(response.config()).containsKey(AwsClientProperties.REFRESH_CREDENTIALS_ENDPOINT); + + String expectedEndpoint = + String.format( + "v1/%s/namespaces/%s/tables/%s/credentials", + FEDERATED_CATALOG_NAME, namespace, tableName); + assertThat(response.config().get(AwsClientProperties.REFRESH_CREDENTIALS_ENDPOINT)) + .isEqualTo(expectedEndpoint); + } + } } From 99075da5e03b61887fec0533c41522faa4571e68 Mon Sep 17 00:00:00 2001 From: Jason Date: Mon, 18 Aug 2025 12:52:20 +0300 Subject: [PATCH 3/4] fixup! add refresh credentials property to loadTableResult --- .../iceberg/IcebergCatalogAdapter.java | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogAdapter.java b/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogAdapter.java index 970dfc1325..65a98a476c 100644 --- a/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogAdapter.java +++ b/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogAdapter.java @@ -62,6 +62,7 @@ import org.apache.iceberg.rest.responses.ConfigResponse; import org.apache.iceberg.rest.responses.ImmutableLoadCredentialsResponse; import org.apache.iceberg.rest.responses.LoadTableResponse; +import org.apache.polaris.core.admin.model.StorageConfigInfo; import org.apache.polaris.core.auth.PolarisAuthorizer; import org.apache.polaris.core.auth.PolarisPrincipal; import org.apache.polaris.core.catalog.ExternalCatalogFactory; @@ -78,7 +79,6 @@ import org.apache.polaris.core.rest.PolarisEndpoints; import org.apache.polaris.core.rest.PolarisResourcePaths; import org.apache.polaris.core.secrets.UserSecretsManager; -import org.apache.polaris.core.storage.StorageAccessProperty; import org.apache.polaris.service.catalog.AccessDelegationMode; import org.apache.polaris.service.catalog.CatalogPrefixParser; import org.apache.polaris.service.catalog.api.IcebergRestCatalogApiService; @@ -454,22 +454,25 @@ public Response loadTable( private LoadTableResponse injectRefreshVendedCredentialProperties( LoadTableResponse originalResponse, String credentialsEndpoint) { - LoadTableResponse.Builder loadResponseBuilder = - LoadTableResponse.builder().withTableMetadata(originalResponse.tableMetadata()); - loadResponseBuilder.addAllConfig(originalResponse.config()); - loadResponseBuilder.addAllCredentials(originalResponse.credentials()); - loadResponseBuilder.addConfig( - AwsClientProperties.REFRESH_CREDENTIALS_ENDPOINT, credentialsEndpoint); // Only enable credential refresh for currently supported credential types if (originalResponse.credentials().stream() .anyMatch( credential -> credential - .config() - .containsKey(StorageAccessProperty.AWS_SECRET_KEY.getPropertyName()))) { + .prefix() + .toLowerCase() + .startsWith(StorageConfigInfo.StorageTypeEnum.S3.name().toLowerCase()))) { + LoadTableResponse.Builder loadResponseBuilder = + LoadTableResponse.builder().withTableMetadata(originalResponse.tableMetadata()); + loadResponseBuilder.addAllConfig(originalResponse.config()); + loadResponseBuilder.addAllCredentials(originalResponse.credentials()); + loadResponseBuilder.addConfig( + AwsClientProperties.REFRESH_CREDENTIALS_ENDPOINT, credentialsEndpoint); loadResponseBuilder.addConfig(AwsClientProperties.REFRESH_CREDENTIALS_ENABLED, "true"); + return loadResponseBuilder.build(); + } else { + return originalResponse; } - return loadResponseBuilder.build(); } @Override From c900855a9b304b23f7f30c15ea6a7703d484700f Mon Sep 17 00:00:00 2001 From: Jason Date: Wed, 20 Aug 2025 10:57:33 +0300 Subject: [PATCH 4/4] fixed lint error --- .../service/catalog/iceberg/IcebergCatalogAdapter.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogAdapter.java b/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogAdapter.java index 65a98a476c..cff8e69be4 100644 --- a/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogAdapter.java +++ b/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogAdapter.java @@ -35,6 +35,7 @@ import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.SecurityContext; import java.util.EnumSet; +import java.util.Locale; import java.util.Map; import java.util.Optional; import java.util.Set; @@ -460,8 +461,9 @@ private LoadTableResponse injectRefreshVendedCredentialProperties( credential -> credential .prefix() - .toLowerCase() - .startsWith(StorageConfigInfo.StorageTypeEnum.S3.name().toLowerCase()))) { + .toLowerCase(Locale.ROOT) + .startsWith( + StorageConfigInfo.StorageTypeEnum.S3.name().toLowerCase(Locale.ROOT)))) { LoadTableResponse.Builder loadResponseBuilder = LoadTableResponse.builder().withTableMetadata(originalResponse.tableMetadata()); loadResponseBuilder.addAllConfig(originalResponse.config());