This is an automated email from the ASF dual-hosted git repository.

dimas pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/polaris.git


The following commit(s) were added to refs/heads/main by this push:
     new f97c5eb50 Support S3 storage that does not have STS (#2672)
f97c5eb50 is described below

commit f97c5eb50016489129575aab62d5efb3efb7552e
Author: Dmitri Bourlatchkov <[email protected]>
AuthorDate: Mon Sep 29 10:11:17 2025 -0400

    Support S3 storage that does not have STS (#2672)
    
    * Support S3 storage that does not have STS
    
    This change is backward compatible with old catalogs that have storage 
configuration for S3 systems with STS.
    
    * Add new property to S3 storage config: `stsUnavailable` (defaults to 
"available").
    
    * Do not call STS when unavailable in `AwsCredentialsStorageIntegration`, 
but still put other properties (e.g. s3.endpoint) into `AccessConfig`
    
    Relates to #2615
    Relates #2207
---
 CHANGELOG.md                                       |  2 +
 .../apache/polaris/core/entity/CatalogEntity.java  |  2 +
 .../aws/AwsCredentialsStorageIntegration.java      | 77 ++++++++++++----------
 .../storage/aws/AwsStorageConfigurationInfo.java   |  6 ++
 .../aws/AwsStorageConfigurationInfoTest.java       |  8 +++
 .../service/it/RestCatalogMinIOSpecialIT.java      | 59 ++++++++++++-----
 .../polaris/service/admin/PolarisServiceImpl.java  |  4 ++
 .../service/admin/ManagementServiceTest.java       |  6 ++
 spec/polaris-management-service.yml                |  7 ++
 9 files changed, 118 insertions(+), 53 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3b1b9adfd..f81d6b50c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -41,6 +41,8 @@ request adding CHANGELOG notes for breaking (!) changes and 
possibly other secti
 
 - Added a Management API endpoint to reset principal credentials, controlled 
by the `ENABLE_CREDENTIAL_RESET` (default: true) feature flag.
 
+- Added support for S3-compatible storage that does not have STS (use 
`stsUavailable: true` in catalog storage configuration)
+
 ### Changes
 
 * The following APIs will now return the newly-created objects as part of the 
successful 201 response: createCatalog, createPrincipalRole, createCatalogRole. 
diff --git 
a/polaris-core/src/main/java/org/apache/polaris/core/entity/CatalogEntity.java 
b/polaris-core/src/main/java/org/apache/polaris/core/entity/CatalogEntity.java
index 44f261c4c..a31299504 100644
--- 
a/polaris-core/src/main/java/org/apache/polaris/core/entity/CatalogEntity.java
+++ 
b/polaris-core/src/main/java/org/apache/polaris/core/entity/CatalogEntity.java
@@ -155,6 +155,7 @@ public class CatalogEntity extends PolarisEntity implements 
LocationBasedEntity
             .setEndpoint(awsConfig.getEndpoint())
             .setStsEndpoint(awsConfig.getStsEndpoint())
             .setPathStyleAccess(awsConfig.getPathStyleAccess())
+            .setStsUnavailable(awsConfig.getStsUnavailable())
             .setEndpointInternal(awsConfig.getEndpointInternal())
             .build();
       }
@@ -299,6 +300,7 @@ public class CatalogEntity extends PolarisEntity implements 
LocationBasedEntity
                     .endpoint(awsConfigModel.getEndpoint())
                     .stsEndpoint(awsConfigModel.getStsEndpoint())
                     .pathStyleAccess(awsConfigModel.getPathStyleAccess())
+                    .stsUnavailable(awsConfigModel.getStsUnavailable())
                     .endpointInternal(awsConfigModel.getEndpointInternal())
                     .build();
             config = awsConfig;
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 3e93ba7b4..8023f7a60 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
@@ -79,43 +79,46 @@ public class AwsCredentialsStorageIntegration
     int storageCredentialDurationSeconds =
         realmConfig.getConfig(STORAGE_CREDENTIAL_DURATION_SECONDS);
     AwsStorageConfigurationInfo storageConfig = config();
-    AssumeRoleRequest.Builder request =
-        AssumeRoleRequest.builder()
-            .externalId(storageConfig.getExternalId())
-            .roleArn(storageConfig.getRoleARN())
-            .roleSessionName("PolarisAwsCredentialsStorageIntegration")
-            .policy(
-                policyString(
-                        storageConfig.getAwsPartition(),
-                        allowListOperation,
-                        allowedReadLocations,
-                        allowedWriteLocations)
-                    .toJson())
-            .durationSeconds(storageCredentialDurationSeconds);
-    credentialsProvider.ifPresent(
-        cp -> request.overrideConfiguration(b -> b.credentialsProvider(cp)));
-
     String region = storageConfig.getRegion();
-    @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.Builder accessConfig = AccessConfig.builder();
-    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())
-        .ifPresent(
-            i -> {
-              accessConfig.put(
-                  StorageAccessProperty.EXPIRATION_TIME, 
String.valueOf(i.toEpochMilli()));
-              accessConfig.put(
-                  StorageAccessProperty.AWS_SESSION_TOKEN_EXPIRES_AT_MS,
-                  String.valueOf(i.toEpochMilli()));
-            });
+
+    if (shouldUseSts(storageConfig)) {
+      AssumeRoleRequest.Builder request =
+          AssumeRoleRequest.builder()
+              .externalId(storageConfig.getExternalId())
+              .roleArn(storageConfig.getRoleARN())
+              .roleSessionName("PolarisAwsCredentialsStorageIntegration")
+              .policy(
+                  policyString(
+                          storageConfig.getAwsPartition(),
+                          allowListOperation,
+                          allowedReadLocations,
+                          allowedWriteLocations)
+                      .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())
+          .ifPresent(
+              i -> {
+                accessConfig.put(
+                    StorageAccessProperty.EXPIRATION_TIME, 
String.valueOf(i.toEpochMilli()));
+                accessConfig.put(
+                    StorageAccessProperty.AWS_SESSION_TOKEN_EXPIRES_AT_MS,
+                    String.valueOf(i.toEpochMilli()));
+              });
+    }
 
     if (region != null) {
       accessConfig.put(StorageAccessProperty.CLIENT_REGION, region);
@@ -149,6 +152,10 @@ public class AwsCredentialsStorageIntegration
     return accessConfig.build();
   }
 
+  private boolean shouldUseSts(AwsStorageConfigurationInfo storageConfig) {
+    return !Boolean.TRUE.equals(storageConfig.getStsUnavailable());
+  }
+
   /**
    * generate an IamPolicy from the input readLocations and writeLocations, 
optionally with list
    * support. Credentials will be scoped to exactly the resources provided. If 
read and write
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 3a2d70663..b3d7d6079 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
@@ -98,6 +98,12 @@ public abstract class AwsStorageConfigurationInfo extends 
PolarisStorageConfigur
   /** Flag indicating whether path-style bucket access should be forced in S3 
clients. */
   public abstract @Nullable Boolean getPathStyleAccess();
 
+  /**
+   * Flag indicating whether STS is available or not. It is modeled in the 
negative to simplify
+   * support for unset values ({@code null} being interpreted as {@code 
false}).
+   */
+  public abstract @Nullable Boolean getStsUnavailable();
+
   /** Endpoint URI for STS API calls */
   @Nullable
   public abstract String getStsEndpoint();
diff --git 
a/polaris-core/src/test/java/org/apache/polaris/core/storage/aws/AwsStorageConfigurationInfoTest.java
 
b/polaris-core/src/test/java/org/apache/polaris/core/storage/aws/AwsStorageConfigurationInfoTest.java
index 0e238775b..3460dc23f 100644
--- 
a/polaris-core/src/test/java/org/apache/polaris/core/storage/aws/AwsStorageConfigurationInfoTest.java
+++ 
b/polaris-core/src/test/java/org/apache/polaris/core/storage/aws/AwsStorageConfigurationInfoTest.java
@@ -118,6 +118,14 @@ public class AwsStorageConfigurationInfoTest {
     
assertThat(newBuilder().pathStyleAccess(true).build().getPathStyleAccess()).isTrue();
   }
 
+  @Test
+  public void testStsUnavailable() {
+    assertThat(newBuilder().build().getStsUnavailable()).isNull();
+    
assertThat(newBuilder().stsUnavailable(null).build().getStsUnavailable()).isNull();
+    
assertThat(newBuilder().stsUnavailable(false).build().getStsUnavailable()).isFalse();
+    
assertThat(newBuilder().stsUnavailable(true).build().getStsUnavailable()).isTrue();
+  }
+
   @Test
   public void testRoleArnParsing() {
     AwsStorageConfigurationInfo awsConfig =
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 7fd263e44..561e76938 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
@@ -19,12 +19,15 @@
 package org.apache.polaris.service.it;
 
 import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.apache.iceberg.CatalogProperties.TABLE_DEFAULT_PREFIX;
 import static 
org.apache.iceberg.aws.AwsClientProperties.REFRESH_CREDENTIALS_ENDPOINT;
 import static org.apache.iceberg.aws.s3.S3FileIOProperties.ACCESS_KEY_ID;
 import static org.apache.iceberg.aws.s3.S3FileIOProperties.ENDPOINT;
 import static org.apache.iceberg.aws.s3.S3FileIOProperties.SECRET_ACCESS_KEY;
 import static org.apache.iceberg.types.Types.NestedField.optional;
 import static org.apache.iceberg.types.Types.NestedField.required;
+import static org.apache.polaris.core.storage.StorageAccessProperty.AWS_KEY_ID;
+import static 
org.apache.polaris.core.storage.StorageAccessProperty.AWS_SECRET_KEY;
 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;
@@ -165,8 +168,10 @@ public class RestCatalogMinIOSpecialIT {
       Optional<String> endpoint,
       Optional<String> stsEndpoint,
       boolean pathStyleAccess,
-      Optional<AccessDelegationMode> delegationMode) {
-    return createCatalog(endpoint, stsEndpoint, pathStyleAccess, 
Optional.empty(), delegationMode);
+      Optional<AccessDelegationMode> delegationMode,
+      boolean stsEnabled) {
+    return createCatalog(
+        endpoint, stsEndpoint, pathStyleAccess, Optional.empty(), 
delegationMode, stsEnabled);
   }
 
   private RESTCatalog createCatalog(
@@ -174,11 +179,13 @@ public class RestCatalogMinIOSpecialIT {
       Optional<String> stsEndpoint,
       boolean pathStyleAccess,
       Optional<String> endpointInternal,
-      Optional<AccessDelegationMode> delegationMode) {
+      Optional<AccessDelegationMode> delegationMode,
+      boolean stsEnabled) {
     AwsStorageConfigInfo.Builder storageConfig =
         AwsStorageConfigInfo.builder()
             .setStorageType(StorageConfigInfo.StorageTypeEnum.S3)
             .setPathStyleAccess(pathStyleAccess)
+            .setStsUnavailable(!stsEnabled)
             .setAllowedLocations(List.of(storageBase.toString()));
 
     endpoint.ifPresent(storageConfig::setEndpoint);
@@ -187,6 +194,12 @@ public class RestCatalogMinIOSpecialIT {
 
     CatalogProperties.Builder catalogProps =
         CatalogProperties.builder(storageBase.toASCIIString() + "/" + 
catalogName);
+    if (!stsEnabled) {
+      catalogProps.addProperty(
+          TABLE_DEFAULT_PREFIX + AWS_KEY_ID.getPropertyName(), 
MINIO_ACCESS_KEY);
+      catalogProps.addProperty(
+          TABLE_DEFAULT_PREFIX + AWS_SECRET_KEY.getPropertyName(), 
MINIO_SECRET_KEY);
+    }
     Catalog catalog =
         PolarisCatalog.builder()
             .setType(Catalog.TypeEnum.INTERNAL)
@@ -227,9 +240,12 @@ public class RestCatalogMinIOSpecialIT {
   }
 
   @ParameterizedTest
-  @ValueSource(booleans = {true, false})
-  public void testCreateTable(boolean pathStyle) throws IOException {
-    LoadTableResponse response = doTestCreateTable(pathStyle, 
Optional.empty());
+  @CsvSource("true,  true,")
+  @CsvSource("false, true,")
+  @CsvSource("true,  false,")
+  @CsvSource("false, false,")
+  public void testCreateTable(boolean pathStyle, boolean stsEnabled) throws 
IOException {
+    LoadTableResponse response = doTestCreateTable(pathStyle, 
Optional.empty(), stsEnabled);
     assertThat(response.config()).doesNotContainKey(SECRET_ACCESS_KEY);
     assertThat(response.config()).doesNotContainKey(ACCESS_KEY_ID);
     
assertThat(response.config()).doesNotContainKey(REFRESH_CREDENTIALS_ENDPOINT);
@@ -239,7 +255,8 @@ public class RestCatalogMinIOSpecialIT {
   @ParameterizedTest
   @ValueSource(booleans = {true, false})
   public void testCreateTableVendedCredentials(boolean pathStyle) throws 
IOException {
-    LoadTableResponse response = doTestCreateTable(pathStyle, 
Optional.of(VENDED_CREDENTIALS));
+    LoadTableResponse response =
+        doTestCreateTable(pathStyle, Optional.of(VENDED_CREDENTIALS), true);
     assertThat(response.config())
         .containsEntry(
             REFRESH_CREDENTIALS_ENDPOINT,
@@ -247,10 +264,10 @@ public class RestCatalogMinIOSpecialIT {
     assertThat(response.credentials()).hasSize(1);
   }
 
-  private LoadTableResponse doTestCreateTable(boolean pathStyle, 
Optional<AccessDelegationMode> dm)
-      throws IOException {
+  private LoadTableResponse doTestCreateTable(
+      boolean pathStyle, Optional<AccessDelegationMode> dm, boolean 
stsEnabled) throws IOException {
     try (RESTCatalog restCatalog =
-        createCatalog(Optional.of(endpoint), Optional.empty(), pathStyle, dm)) 
{
+        createCatalog(Optional.of(endpoint), Optional.empty(), pathStyle, dm, 
stsEnabled)) {
       LoadTableResponse loadTableResponse = doTestCreateTable(restCatalog, dm);
       if (pathStyle) {
         assertThat(loadTableResponse.config())
@@ -268,7 +285,8 @@ public class RestCatalogMinIOSpecialIT {
             Optional.of(endpoint),
             false,
             Optional.of(endpoint),
-            Optional.empty())) {
+            Optional.empty(),
+            true)) {
       StorageConfigInfo storageConfig =
           managementApi.getCatalog(catalogName).getStorageConfigInfo();
       assertThat((AwsStorageConfigInfo) storageConfig)
@@ -319,18 +337,22 @@ public class RestCatalogMinIOSpecialIT {
   }
 
   @ParameterizedTest
-  @CsvSource("true,")
-  @CsvSource("false,")
-  @CsvSource("true,VENDED_CREDENTIALS")
-  @CsvSource("false,VENDED_CREDENTIALS")
-  public void testAppendFiles(boolean pathStyle, AccessDelegationMode 
delegationMode)
+  @CsvSource("true,  true,")
+  @CsvSource("false, true,")
+  @CsvSource("true,  false,")
+  @CsvSource("false, false,")
+  @CsvSource("true,  true,  VENDED_CREDENTIALS")
+  @CsvSource("false, true,  VENDED_CREDENTIALS")
+  public void testAppendFiles(
+      boolean pathStyle, boolean stsEnabled, AccessDelegationMode 
delegationMode)
       throws IOException {
     try (RESTCatalog restCatalog =
         createCatalog(
             Optional.of(endpoint),
             Optional.of(endpoint),
             pathStyle,
-            Optional.ofNullable(delegationMode))) {
+            Optional.ofNullable(delegationMode),
+            stsEnabled)) {
       catalogApi.createNamespace(catalogName, "test-ns");
       TableIdentifier id = TableIdentifier.of("test-ns", "t1");
       Table table = restCatalog.createTable(id, SCHEMA);
@@ -344,7 +366,8 @@ public class RestCatalogMinIOSpecialIT {
               table
                   .locationProvider()
                   .newDataLocation(
-                      String.format("test-file-%s-%s.txt", pathStyle, 
delegationMode)));
+                      String.format(
+                          "test-file-%s-%s-%s.txt", pathStyle, delegationMode, 
stsEnabled)));
       OutputFile f1 = io.newOutputFile(loc.toString());
       try (PositionOutputStream os = f1.create()) {
         os.write("Hello World".getBytes(UTF_8));
diff --git 
a/runtime/service/src/main/java/org/apache/polaris/service/admin/PolarisServiceImpl.java
 
b/runtime/service/src/main/java/org/apache/polaris/service/admin/PolarisServiceImpl.java
index 54ff3e1ce..d78896325 100644
--- 
a/runtime/service/src/main/java/org/apache/polaris/service/admin/PolarisServiceImpl.java
+++ 
b/runtime/service/src/main/java/org/apache/polaris/service/admin/PolarisServiceImpl.java
@@ -160,6 +160,10 @@ public class PolarisServiceImpl
             || s3Config.getEndpointInternal() != null) {
           throw new IllegalArgumentException("Explicitly setting S3 endpoints 
is not allowed.");
         }
+
+        if (s3Config.getStsUnavailable() != null) {
+          throw new IllegalArgumentException("Explicitly disabling STS is not 
allowed.");
+        }
       }
     }
   }
diff --git 
a/runtime/service/src/test/java/org/apache/polaris/service/admin/ManagementServiceTest.java
 
b/runtime/service/src/test/java/org/apache/polaris/service/admin/ManagementServiceTest.java
index 5ce603178..ea034a674 100644
--- 
a/runtime/service/src/test/java/org/apache/polaris/service/admin/ManagementServiceTest.java
+++ 
b/runtime/service/src/test/java/org/apache/polaris/service/admin/ManagementServiceTest.java
@@ -140,6 +140,12 @@ public class ManagementServiceTest {
     assertThatThrownBy(createCatalog::get)
         .isInstanceOf(IllegalArgumentException.class)
         .hasMessage("Explicitly setting S3 endpoints is not allowed.");
+
+    storageConfig.setEndpointInternal(null);
+    storageConfig.setStsUnavailable(false);
+    assertThatThrownBy(createCatalog::get)
+        .isInstanceOf(IllegalArgumentException.class)
+        .hasMessage("Explicitly disabling STS is not allowed.");
   }
 
   @Test
diff --git a/spec/polaris-management-service.yml 
b/spec/polaris-management-service.yml
index 7e93f01ab..84546bbf4 100644
--- a/spec/polaris-management-service.yml
+++ b/spec/polaris-management-service.yml
@@ -1119,6 +1119,13 @@ components:
                 endpoint for STS requests made by the Polaris Server 
(optional). If not set, defaults to
                 'endpointInternal' (which in turn defaults to `endpoint`).
               example: "https://sts.example.com:1234";
+            stsUnavailable:
+              type: boolean
+              description: >-
+                if set to `true`, instructs Polaris Servers to avoid using the 
STS endpoints when obtaining credentials
+                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.
             endpointInternal:
               type: string
               description: >-

Reply via email to