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

snazy 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 355adae1f Make `*StorageConfigurationInfo` types immutable (#2236)
355adae1f is described below

commit 355adae1f6169b91bc78b0fe71ff863ee19798e8
Author: Robert Stupp <sn...@snazy.de>
AuthorDate: Tue Aug 5 13:47:40 2025 +0200

    Make `*StorageConfigurationInfo` types immutable (#2236)
    
    This change eventually enables usage of the `*StorageConfigurationInfo` in 
the `StorageCredentialCacheKey` due to the then memoized hash-code values, to 
eliminate a couple of JSON re-serializations.
---
 .../apache/polaris/core/entity/CatalogEntity.java  |  48 +++---
 .../core/storage/FileStorageConfigurationInfo.java |  24 ++-
 .../storage/PolarisStorageConfigurationInfo.java   |  45 +++---
 .../core/storage/StorageConfigurationOverride.java |  18 ++-
 .../storage/aws/AwsStorageConfigurationInfo.java   | 165 +++++----------------
 .../azure/AzureStorageConfigurationInfo.java       |  72 +++------
 .../storage/gcp/GcpStorageConfigurationInfo.java   |  48 +++---
 .../storage/InMemoryStorageIntegrationTest.java    |  44 +++---
 .../aws/AwsStorageConfigurationInfoTest.java       |  64 ++++----
 .../PolarisStorageConfigurationInfoTest.java       | 118 +++++++++++++++
 .../aws/AwsCredentialsStorageIntegrationTest.java  | 112 +++++++-------
 .../AzureCredentialStorageIntegrationTest.java     |   9 +-
 .../gcp/GcpCredentialsStorageIntegrationTest.java  |  10 +-
 13 files changed, 395 insertions(+), 382 deletions(-)

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 ba8627f06..622df1fca 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
@@ -22,7 +22,6 @@ import static 
org.apache.polaris.core.admin.model.StorageConfigInfo.StorageTypeE
 
 import jakarta.annotation.Nonnull;
 import jakarta.annotation.Nullable;
-import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashMap;
 import java.util.HashSet;
@@ -269,37 +268,40 @@ public class CatalogEntity extends PolarisEntity 
implements LocationBasedEntity
           case S3:
             AwsStorageConfigInfo awsConfigModel = (AwsStorageConfigInfo) 
storageConfigModel;
             AwsStorageConfigurationInfo awsConfig =
-                new AwsStorageConfigurationInfo(
-                    PolarisStorageConfigurationInfo.StorageType.S3,
-                    new ArrayList<>(allowedLocations),
-                    awsConfigModel.getRoleArn(),
-                    awsConfigModel.getExternalId(),
-                    awsConfigModel.getRegion(),
-                    awsConfigModel.getEndpoint(),
-                    awsConfigModel.getStsEndpoint(),
-                    awsConfigModel.getPathStyleAccess(),
-                    awsConfigModel.getEndpointInternal());
+                AwsStorageConfigurationInfo.builder()
+                    .allowedLocations(allowedLocations)
+                    .roleARN(awsConfigModel.getRoleArn())
+                    .externalId(awsConfigModel.getExternalId())
+                    .region(awsConfigModel.getRegion())
+                    .endpoint(awsConfigModel.getEndpoint())
+                    .stsEndpoint(awsConfigModel.getStsEndpoint())
+                    .pathStyleAccess(awsConfigModel.getPathStyleAccess())
+                    .endpointInternal(awsConfigModel.getEndpointInternal())
+                    .build();
             awsConfig.validateArn(awsConfigModel.getRoleArn());
             config = awsConfig;
             break;
           case AZURE:
             AzureStorageConfigInfo azureConfigModel = (AzureStorageConfigInfo) 
storageConfigModel;
-            AzureStorageConfigurationInfo azureConfigInfo =
-                new AzureStorageConfigurationInfo(
-                    new ArrayList<>(allowedLocations), 
azureConfigModel.getTenantId());
-            
azureConfigInfo.setMultiTenantAppName(azureConfigModel.getMultiTenantAppName());
-            azureConfigInfo.setConsentUrl(azureConfigModel.getConsentUrl());
-            config = azureConfigInfo;
+            config =
+                AzureStorageConfigurationInfo.builder()
+                    .allowedLocations(allowedLocations)
+                    .tenantId(azureConfigModel.getTenantId())
+                    
.multiTenantAppName(azureConfigModel.getMultiTenantAppName())
+                    .consentUrl(azureConfigModel.getConsentUrl())
+                    .build();
             break;
           case GCS:
-            GcpStorageConfigurationInfo gcpConfig =
-                new GcpStorageConfigurationInfo(new 
ArrayList<>(allowedLocations));
-            gcpConfig.setGcpServiceAccount(
-                ((GcpStorageConfigInfo) 
storageConfigModel).getGcsServiceAccount());
-            config = gcpConfig;
+            config =
+                GcpStorageConfigurationInfo.builder()
+                    .allowedLocations(allowedLocations)
+                    .gcpServiceAccount(
+                        ((GcpStorageConfigInfo) 
storageConfigModel).getGcsServiceAccount())
+                    .build();
             break;
           case FILE:
-            config = new FileStorageConfigurationInfo(new 
ArrayList<>(allowedLocations));
+            config =
+                
FileStorageConfigurationInfo.builder().allowedLocations(allowedLocations).build();
             break;
           default:
             throw new IllegalStateException(
diff --git 
a/polaris-core/src/main/java/org/apache/polaris/core/storage/FileStorageConfigurationInfo.java
 
b/polaris-core/src/main/java/org/apache/polaris/core/storage/FileStorageConfigurationInfo.java
index 6b1d48c5c..6b4fe51f7 100644
--- 
a/polaris-core/src/main/java/org/apache/polaris/core/storage/FileStorageConfigurationInfo.java
+++ 
b/polaris-core/src/main/java/org/apache/polaris/core/storage/FileStorageConfigurationInfo.java
@@ -18,22 +18,25 @@
  */
 package org.apache.polaris.core.storage;
 
-import com.fasterxml.jackson.annotation.JsonProperty;
-import jakarta.annotation.Nonnull;
-import java.util.List;
+import com.fasterxml.jackson.annotation.JsonTypeName;
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
 import java.util.Locale;
+import org.apache.polaris.immutables.PolarisImmutable;
 
 /**
  * Support for file:// URLs in storage configuration. This is pretty-much only 
used for testing.
  * Supports URLs that start with file:// or /, but also supports wildcard (*) 
to support certain
  * test cases.
  */
-public class FileStorageConfigurationInfo extends 
PolarisStorageConfigurationInfo {
+@PolarisImmutable
+@JsonSerialize(as = ImmutableFileStorageConfigurationInfo.class)
+@JsonDeserialize(as = ImmutableFileStorageConfigurationInfo.class)
+@JsonTypeName("FileStorageConfigurationInfo")
+public abstract class FileStorageConfigurationInfo extends 
PolarisStorageConfigurationInfo {
 
-  public FileStorageConfigurationInfo(
-      @JsonProperty(value = "allowedLocations", required = true) @Nonnull
-          List<String> allowedLocations) {
-    super(StorageType.FILE, allowedLocations);
+  public static ImmutableFileStorageConfigurationInfo.Builder builder() {
+    return ImmutableFileStorageConfigurationInfo.builder();
   }
 
   @Override
@@ -41,6 +44,11 @@ public class FileStorageConfigurationInfo extends 
PolarisStorageConfigurationInf
     return "org.apache.iceberg.hadoop.HadoopFileIO";
   }
 
+  @Override
+  public StorageType getStorageType() {
+    return StorageType.FILE;
+  }
+
   @Override
   public void validatePrefixForStorageType(String loc) {
     if (getStorageType().getPrefixes().stream()
diff --git 
a/polaris-core/src/main/java/org/apache/polaris/core/storage/PolarisStorageConfigurationInfo.java
 
b/polaris-core/src/main/java/org/apache/polaris/core/storage/PolarisStorageConfigurationInfo.java
index d039b1f61..414156fb4 100644
--- 
a/polaris-core/src/main/java/org/apache/polaris/core/storage/PolarisStorageConfigurationInfo.java
+++ 
b/polaris-core/src/main/java/org/apache/polaris/core/storage/PolarisStorageConfigurationInfo.java
@@ -18,8 +18,9 @@
  */
 package org.apache.polaris.core.storage;
 
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
 import com.fasterxml.jackson.annotation.JsonInclude;
-import com.fasterxml.jackson.annotation.JsonProperty;
 import com.fasterxml.jackson.annotation.JsonSubTypes;
 import com.fasterxml.jackson.annotation.JsonTypeInfo;
 import com.fasterxml.jackson.core.JsonProcessingException;
@@ -46,6 +47,7 @@ import 
org.apache.polaris.core.entity.table.IcebergTableLikeEntity;
 import org.apache.polaris.core.storage.aws.AwsStorageConfigurationInfo;
 import org.apache.polaris.core.storage.azure.AzureStorageConfigurationInfo;
 import org.apache.polaris.core.storage.gcp.GcpStorageConfigurationInfo;
+import org.immutables.value.Value;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -65,40 +67,28 @@ import org.slf4j.LoggerFactory;
   @JsonSubTypes.Type(value = GcpStorageConfigurationInfo.class),
   @JsonSubTypes.Type(value = FileStorageConfigurationInfo.class),
 })
+@JsonIgnoreProperties(ignoreUnknown = true)
 public abstract class PolarisStorageConfigurationInfo {
 
   private static final Logger LOGGER =
       LoggerFactory.getLogger(PolarisStorageConfigurationInfo.class);
 
-  // a list of allowed locations
-  private final List<String> allowedLocations;
-
-  // storage type
-  private final StorageType storageType;
-
-  public PolarisStorageConfigurationInfo(
-      @JsonProperty(value = "storageType", required = true) @Nonnull 
StorageType storageType,
-      @JsonProperty(value = "allowedLocations", required = true) @Nonnull
-          List<String> allowedLocations) {
-    this(storageType, allowedLocations, true);
-  }
-
-  protected PolarisStorageConfigurationInfo(
-      StorageType storageType, List<String> allowedLocations, boolean 
validatePrefix) {
-    this.allowedLocations = allowedLocations;
-    this.storageType = storageType;
-    if (validatePrefix) {
-      allowedLocations.forEach(this::validatePrefixForStorageType);
+  @Value.Check
+  protected void check() {
+    if (validatePrefix()) {
+      getAllowedLocations().forEach(this::validatePrefixForStorageType);
     }
   }
 
-  public List<String> getAllowedLocations() {
-    return allowedLocations;
+  @JsonIgnore
+  @Value.Auxiliary
+  public boolean validatePrefix() {
+    return true;
   }
 
-  public StorageType getStorageType() {
-    return storageType;
-  }
+  public abstract List<String> getAllowedLocations();
+
+  public abstract StorageType getStorageType();
 
   private static final ObjectMapper DEFAULT_MAPPER;
 
@@ -213,11 +203,12 @@ public abstract class PolarisStorageConfigurationInfo {
 
   /** Validate if the provided allowed locations are valid for the storage 
type */
   protected void validatePrefixForStorageType(String loc) {
-    if (storageType.prefixes.stream().noneMatch(p -> 
loc.toLowerCase(Locale.ROOT).startsWith(p))) {
+    if (getStorageType().prefixes.stream()
+        .noneMatch(p -> loc.toLowerCase(Locale.ROOT).startsWith(p))) {
       throw new IllegalArgumentException(
           String.format(
               "Location prefix not allowed: '%s', expected prefixes: '%s'",
-              loc, String.join(",", storageType.prefixes)));
+              loc, String.join(",", getStorageType().prefixes)));
     }
   }
 
diff --git 
a/polaris-core/src/main/java/org/apache/polaris/core/storage/StorageConfigurationOverride.java
 
b/polaris-core/src/main/java/org/apache/polaris/core/storage/StorageConfigurationOverride.java
index 1d695dbb1..b38ebf2fc 100644
--- 
a/polaris-core/src/main/java/org/apache/polaris/core/storage/StorageConfigurationOverride.java
+++ 
b/polaris-core/src/main/java/org/apache/polaris/core/storage/StorageConfigurationOverride.java
@@ -30,20 +30,36 @@ import java.util.List;
 public class StorageConfigurationOverride extends 
PolarisStorageConfigurationInfo {
 
   private final PolarisStorageConfigurationInfo parentStorageConfiguration;
+  private final List<String> allowedLocations;
 
   public StorageConfigurationOverride(
       @Nonnull PolarisStorageConfigurationInfo parentStorageConfiguration,
       List<String> allowedLocations) {
-    super(parentStorageConfiguration.getStorageType(), allowedLocations, 
false);
     this.parentStorageConfiguration = parentStorageConfiguration;
+    this.allowedLocations = List.copyOf(allowedLocations);
     allowedLocations.forEach(this::validatePrefixForStorageType);
   }
 
+  @Override
+  public List<String> getAllowedLocations() {
+    return allowedLocations;
+  }
+
+  @Override
+  public StorageType getStorageType() {
+    return parentStorageConfiguration.getStorageType();
+  }
+
   @Override
   public String getFileIoImplClassName() {
     return parentStorageConfiguration.getFileIoImplClassName();
   }
 
+  @Override
+  public boolean validatePrefix() {
+    return false;
+  }
+
   // delegate to the wrapped class in case they override the parent behavior
   @Override
   protected void validatePrefixForStorageType(String loc) {
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 bd325d103..4181d205e 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
@@ -18,20 +18,27 @@
  */
 package org.apache.polaris.core.storage.aws;
 
-import com.fasterxml.jackson.annotation.JsonCreator;
 import com.fasterxml.jackson.annotation.JsonIgnore;
-import com.fasterxml.jackson.annotation.JsonProperty;
-import com.google.common.base.MoreObjects;
-import jakarta.annotation.Nonnull;
+import com.fasterxml.jackson.annotation.JsonTypeName;
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
 import jakarta.annotation.Nullable;
 import java.net.URI;
-import java.util.List;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 import org.apache.polaris.core.storage.PolarisStorageConfigurationInfo;
+import org.apache.polaris.immutables.PolarisImmutable;
 
 /** Aws Polaris Storage Configuration information */
-public class AwsStorageConfigurationInfo extends 
PolarisStorageConfigurationInfo {
+@PolarisImmutable
+@JsonSerialize(as = ImmutableAwsStorageConfigurationInfo.class)
+@JsonDeserialize(as = ImmutableAwsStorageConfigurationInfo.class)
+@JsonTypeName("AwsStorageConfigurationInfo")
+public abstract class AwsStorageConfigurationInfo extends 
PolarisStorageConfigurationInfo {
+
+  public static ImmutableAwsStorageConfigurationInfo.Builder builder() {
+    return ImmutableAwsStorageConfigurationInfo.builder();
+  }
 
   // Technically, it should be 
^arn:(aws|aws-cn|aws-us-gov):iam::(\d{12}):role/.+$,
   @JsonIgnore
@@ -39,74 +46,9 @@ public class AwsStorageConfigurationInfo extends 
PolarisStorageConfigurationInfo
 
   private static final Pattern ROLE_ARN_PATTERN_COMPILED = 
Pattern.compile(ROLE_ARN_PATTERN);
 
-  // AWS role to be assumed
-  private final @Nonnull String roleARN;
-
-  // AWS external ID, optional
-  @JsonProperty(value = "externalId")
-  private @Nullable String externalId = null;
-
-  /** User ARN for the service principal */
-  @JsonProperty(value = "userARN")
-  private @Nullable String userARN = null;
-
-  /** User ARN for the service principal */
-  @JsonProperty(value = "region")
-  private @Nullable String region = null;
-
-  /** Endpoint URI for S3 API calls */
-  @JsonProperty(value = "endpoint")
-  private @Nullable String endpoint;
-
-  /** Endpoint URI for internal Polaris calls to S3 API */
-  @JsonProperty(value = "endpointInternal")
-  private @Nullable String endpointInternal;
-
-  /** Endpoint URI for STS API calls */
-  @JsonProperty(value = "stsEndpoint")
-  private @Nullable String stsEndpoint;
-
-  /** A flag indicating whether path-style bucket access should be forced in 
S3 clients. */
-  @JsonProperty(value = "pathStyleAccess")
-  private Boolean pathStyleAccess;
-
-  @JsonCreator
-  public AwsStorageConfigurationInfo(
-      @JsonProperty(value = "storageType", required = true) @Nonnull 
StorageType storageType,
-      @JsonProperty(value = "allowedLocations", required = true) @Nonnull
-          List<String> allowedLocations,
-      @JsonProperty(value = "roleARN", required = true) @Nonnull String 
roleARN,
-      @JsonProperty(value = "externalId") @Nullable String externalId,
-      @JsonProperty(value = "region", required = false) @Nullable String 
region,
-      @JsonProperty(value = "endpoint") @Nullable String endpoint,
-      @JsonProperty(value = "stsEndpoint") @Nullable String stsEndpoint,
-      @JsonProperty(value = "pathStyleAccess") @Nullable Boolean 
pathStyleAccess,
-      @JsonProperty(value = "endpointInternal") @Nullable String 
endpointInternal) {
-    super(storageType, allowedLocations);
-    this.roleARN = roleARN;
-    this.externalId = externalId;
-    this.region = region;
-    this.endpoint = endpoint;
-    this.stsEndpoint = stsEndpoint;
-    this.pathStyleAccess = pathStyleAccess;
-    this.endpointInternal = endpointInternal;
-  }
-
-  public AwsStorageConfigurationInfo(
-      @Nonnull StorageType storageType,
-      @Nonnull List<String> allowedLocations,
-      @Nonnull String roleARN,
-      @Nullable String region) {
-    this(storageType, allowedLocations, roleARN, null, region, null, null, 
null, null);
-  }
-
-  public AwsStorageConfigurationInfo(
-      @Nonnull StorageType storageType,
-      @Nonnull List<String> allowedLocations,
-      @Nonnull String roleARN,
-      @Nullable String externalId,
-      @Nullable String region) {
-    this(storageType, allowedLocations, roleARN, externalId, region, null, 
null, null, null);
+  @Override
+  public StorageType getStorageType() {
+    return StorageType.S3;
   }
 
   @Override
@@ -127,76 +69,62 @@ public class AwsStorageConfigurationInfo extends 
PolarisStorageConfigurationInfo
     }
   }
 
-  public @Nonnull String getRoleARN() {
-    return roleARN;
-  }
-
-  public @Nullable String getExternalId() {
-    return externalId;
-  }
+  public abstract String getRoleARN();
 
-  public void setExternalId(@Nullable String externalId) {
-    this.externalId = externalId;
-  }
-
-  public @Nullable String getUserARN() {
-    return userARN;
-  }
+  /** AWS external ID, optional */
+  @Nullable
+  public abstract String getExternalId();
 
-  public void setUserARN(@Nullable String userARN) {
-    this.userARN = userARN;
-  }
+  /** User ARN for the service principal */
+  @Nullable
+  public abstract String getUserARN();
 
-  public @Nullable String getRegion() {
-    return region;
-  }
+  /** AWS region */
+  @Nullable
+  public abstract String getRegion();
 
-  public void setRegion(@Nullable String region) {
-    this.region = region;
-  }
+  /** Endpoint URI for S3 API calls */
+  @Nullable
+  public abstract String getEndpoint();
 
+  /** Internal endpoint URI for S3 API calls */
   @Nullable
-  public String getEndpoint() {
-    return endpoint;
-  }
+  public abstract String getEndpointInternal();
 
   @JsonIgnore
   @Nullable
   public URI getEndpointUri() {
-    return endpoint == null ? null : URI.create(endpoint);
+    return getEndpoint() == null ? null : URI.create(getEndpoint());
   }
 
   @JsonIgnore
   @Nullable
   public URI getInternalEndpointUri() {
-    return endpointInternal == null ? getEndpointUri() : 
URI.create(endpointInternal);
+    return getEndpointInternal() == null ? getEndpointUri() : 
URI.create(getEndpointInternal());
   }
 
-  /** Returns a flag indicating whether path-style bucket access should be 
forced in S3 clients. */
-  public @Nullable Boolean getPathStyleAccess() {
-    return pathStyleAccess;
-  }
+  /** Flag indicating whether path-style bucket access should be forced in S3 
clients. */
+  public abstract @Nullable Boolean getPathStyleAccess();
 
+  /** Endpoint URI for STS API calls */
   @Nullable
-  public String getStsEndpoint() {
-    return stsEndpoint;
-  }
+  public abstract String getStsEndpoint();
 
   /** Returns the STS endpoint if set, defaulting to {@link #getEndpointUri()} 
otherwise. */
   @JsonIgnore
   @Nullable
   public URI getStsEndpointUri() {
-    return stsEndpoint == null ? getInternalEndpointUri() : 
URI.create(stsEndpoint);
+    return getStsEndpoint() == null ? getInternalEndpointUri() : 
URI.create(getStsEndpoint());
   }
 
   @JsonIgnore
   public String getAwsAccountId() {
-    return parseAwsAccountId(roleARN);
+    return parseAwsAccountId(getRoleARN());
   }
 
   @JsonIgnore
   public String getAwsPartition() {
-    return parseAwsPartition(roleARN);
+    return parseAwsPartition(getRoleARN());
   }
 
   private static String parseAwsAccountId(String arn) {
@@ -218,17 +146,4 @@ public class AwsStorageConfigurationInfo extends 
PolarisStorageConfigurationInfo
       throw new IllegalArgumentException("ARN does not match the expected role 
ARN pattern");
     }
   }
-
-  @Override
-  public String toString() {
-    return MoreObjects.toStringHelper(this)
-        .add("storageType", getStorageType())
-        .add("storageType", getStorageType().name())
-        .add("roleARN", roleARN)
-        .add("userARN", userARN)
-        .add("externalId", externalId)
-        .add("allowedLocation", getAllowedLocations())
-        .add("region", region)
-        .toString();
-  }
 }
diff --git 
a/polaris-core/src/main/java/org/apache/polaris/core/storage/azure/AzureStorageConfigurationInfo.java
 
b/polaris-core/src/main/java/org/apache/polaris/core/storage/azure/AzureStorageConfigurationInfo.java
index 53695b481..fb4d6c44a 100644
--- 
a/polaris-core/src/main/java/org/apache/polaris/core/storage/azure/AzureStorageConfigurationInfo.java
+++ 
b/polaris-core/src/main/java/org/apache/polaris/core/storage/azure/AzureStorageConfigurationInfo.java
@@ -18,36 +18,23 @@
  */
 package org.apache.polaris.core.storage.azure;
 
-import com.fasterxml.jackson.annotation.JsonCreator;
-import com.fasterxml.jackson.annotation.JsonProperty;
-import com.google.common.base.MoreObjects;
-import jakarta.annotation.Nonnull;
+import com.fasterxml.jackson.annotation.JsonTypeName;
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
 import jakarta.annotation.Nullable;
-import java.util.List;
 import java.util.Objects;
 import org.apache.polaris.core.storage.PolarisStorageConfigurationInfo;
+import org.apache.polaris.immutables.PolarisImmutable;
 
 /** Azure storage configuration information. */
-public class AzureStorageConfigurationInfo extends 
PolarisStorageConfigurationInfo {
+@PolarisImmutable
+@JsonSerialize(as = ImmutableAzureStorageConfigurationInfo.class)
+@JsonDeserialize(as = ImmutableAzureStorageConfigurationInfo.class)
+@JsonTypeName("AzureStorageConfigurationInfo")
+public abstract class AzureStorageConfigurationInfo extends 
PolarisStorageConfigurationInfo {
 
-  // Azure tenant id
-  private final @Nonnull String tenantId;
-
-  /** The multi tenant app name for the service principal */
-  @JsonProperty(value = "multiTenantAppName", required = false)
-  private @Nullable String multiTenantAppName = null;
-
-  /** The consent url to the Azure permissions request page */
-  @JsonProperty(value = "consentUrl", required = false)
-  private @Nullable String consentUrl = null;
-
-  @JsonCreator
-  public AzureStorageConfigurationInfo(
-      @JsonProperty(value = "allowedLocations", required = true) @Nonnull
-          List<String> allowedLocations,
-      @JsonProperty(value = "tenantId", required = true) @Nonnull String 
tenantId) {
-    super(StorageType.AZURE, allowedLocations);
-    this.tenantId = tenantId;
+  public static ImmutableAzureStorageConfigurationInfo.Builder builder() {
+    return ImmutableAzureStorageConfigurationInfo.builder();
   }
 
   @Override
@@ -55,36 +42,21 @@ public class AzureStorageConfigurationInfo extends 
PolarisStorageConfigurationIn
     return "org.apache.iceberg.azure.adlsv2.ADLSFileIO";
   }
 
-  public @Nonnull String getTenantId() {
-    return tenantId;
-  }
-
-  public @Nullable String getMultiTenantAppName() {
-    return multiTenantAppName;
-  }
-
-  public void setMultiTenantAppName(@Nullable String multiTenantAppName) {
-    this.multiTenantAppName = multiTenantAppName;
+  @Override
+  public StorageType getStorageType() {
+    return StorageType.AZURE;
   }
 
-  public @Nullable String getConsentUrl() {
-    return consentUrl;
-  }
+  /** Azure tenant ID. */
+  public abstract String getTenantId();
 
-  public void setConsentUrl(@Nullable String consentUrl) {
-    this.consentUrl = consentUrl;
-  }
+  /** The multi tenant app name for the service principal */
+  @Nullable
+  public abstract String getMultiTenantAppName();
 
-  @Override
-  public String toString() {
-    return MoreObjects.toStringHelper(this)
-        .add("storageType", getStorageType())
-        .add("tenantId", tenantId)
-        .add("allowedLocation", getAllowedLocations())
-        .add("multiTenantAppName", multiTenantAppName)
-        .add("consentUrl", consentUrl)
-        .toString();
-  }
+  /** The consent url to the Azure permissions request page */
+  @Nullable
+  public abstract String getConsentUrl();
 
   @Override
   public void validatePrefixForStorageType(String loc) {
diff --git 
a/polaris-core/src/main/java/org/apache/polaris/core/storage/gcp/GcpStorageConfigurationInfo.java
 
b/polaris-core/src/main/java/org/apache/polaris/core/storage/gcp/GcpStorageConfigurationInfo.java
index e664d4bd3..13f2da66d 100644
--- 
a/polaris-core/src/main/java/org/apache/polaris/core/storage/gcp/GcpStorageConfigurationInfo.java
+++ 
b/polaris-core/src/main/java/org/apache/polaris/core/storage/gcp/GcpStorageConfigurationInfo.java
@@ -18,26 +18,22 @@
  */
 package org.apache.polaris.core.storage.gcp;
 
-import com.fasterxml.jackson.annotation.JsonCreator;
-import com.fasterxml.jackson.annotation.JsonProperty;
-import com.google.common.base.MoreObjects;
-import jakarta.annotation.Nonnull;
+import com.fasterxml.jackson.annotation.JsonTypeName;
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
 import jakarta.annotation.Nullable;
-import java.util.List;
 import org.apache.polaris.core.storage.PolarisStorageConfigurationInfo;
+import org.apache.polaris.immutables.PolarisImmutable;
 
 /** Gcp storage storage configuration information. */
-public class GcpStorageConfigurationInfo extends 
PolarisStorageConfigurationInfo {
-
-  /** The gcp service account */
-  @JsonProperty(value = "gcpServiceAccount", required = false)
-  private @Nullable String gcpServiceAccount = null;
-
-  @JsonCreator
-  public GcpStorageConfigurationInfo(
-      @JsonProperty(value = "allowedLocations", required = true) @Nonnull
-          List<String> allowedLocations) {
-    super(StorageType.GCS, allowedLocations);
+@PolarisImmutable
+@JsonSerialize(as = ImmutableGcpStorageConfigurationInfo.class)
+@JsonDeserialize(as = ImmutableGcpStorageConfigurationInfo.class)
+@JsonTypeName("GcpStorageConfigurationInfo")
+public abstract class GcpStorageConfigurationInfo extends 
PolarisStorageConfigurationInfo {
+
+  public static ImmutableGcpStorageConfigurationInfo.Builder builder() {
+    return ImmutableGcpStorageConfigurationInfo.builder();
   }
 
   @Override
@@ -45,20 +41,12 @@ public class GcpStorageConfigurationInfo extends 
PolarisStorageConfigurationInfo
     return "org.apache.iceberg.gcp.gcs.GCSFileIO";
   }
 
-  public void setGcpServiceAccount(@Nullable String gcpServiceAccount) {
-    this.gcpServiceAccount = gcpServiceAccount;
-  }
-
-  public @Nullable String getGcpServiceAccount() {
-    return gcpServiceAccount;
-  }
-
   @Override
-  public String toString() {
-    return MoreObjects.toStringHelper(this)
-        .add("storageType", getStorageType())
-        .add("allowedLocation", getAllowedLocations())
-        .add("gcpServiceAccount", gcpServiceAccount)
-        .toString();
+  public StorageType getStorageType() {
+    return StorageType.GCS;
   }
+
+  /** The gcp service account */
+  @Nullable
+  public abstract String getGcpServiceAccount();
 }
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 47a760f03..1bf6e4bb2 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
@@ -20,7 +20,6 @@ package org.apache.polaris.core.storage;
 
 import jakarta.annotation.Nonnull;
 import jakarta.annotation.Nullable;
-import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import org.apache.polaris.core.config.PolarisConfigurationStore;
@@ -48,14 +47,14 @@ class InMemoryStorageIntegrationTest {
     Map<String, Map<PolarisStorageActions, 
PolarisStorageIntegration.ValidationResult>> result =
         storage.validateAccessToLocations(
             realmConfig,
-            new AwsStorageConfigurationInfo(
-                PolarisStorageConfigurationInfo.StorageType.S3,
-                List.of(
+            AwsStorageConfigurationInfo.builder()
+                .addAllowedLocations(
                     allowedScheme + "://bucket/path/to/warehouse",
                     allowedScheme + "://bucket/anotherpath/to/warehouse",
-                    allowedScheme + "://bucket2/warehouse/"),
-                "arn:aws:iam::012345678901:role/jdoe",
-                "us-east-2"),
+                    allowedScheme + "://bucket2/warehouse/")
+                .roleARN("arn:aws:iam::012345678901:role/jdoe")
+                .region("us-east-2")
+                .build(),
             Set.of(PolarisStorageActions.READ),
             Set.of(
                 locationScheme + "://bucket/path/to/warehouse/namespace/table",
@@ -83,11 +82,11 @@ class InMemoryStorageIntegrationTest {
   @Test
   public void testAwsAccountIdParsing() {
     AwsStorageConfigurationInfo awsConfig =
-        new AwsStorageConfigurationInfo(
-            PolarisStorageConfigurationInfo.StorageType.S3,
-            List.of("s3://bucket/path/to/warehouse"),
-            "arn:aws:iam::012345678901:role/jdoe",
-            "us-east-2");
+        AwsStorageConfigurationInfo.builder()
+            .addAllowedLocation("s3://bucket/path/to/warehouse")
+            .roleARN("arn:aws:iam::012345678901:role/jdoe")
+            .region("us-east-2")
+            .build();
 
     String expectedAccountId = "012345678901";
     String actualAccountId = awsConfig.getAwsAccountId();
@@ -105,7 +104,7 @@ class InMemoryStorageIntegrationTest {
     Map<String, Map<PolarisStorageActions, 
PolarisStorageIntegration.ValidationResult>> result =
         storage.validateAccessToLocations(
             realmConfig,
-            new FileStorageConfigurationInfo(List.of("file://", "*")),
+            
FileStorageConfigurationInfo.builder().addAllowedLocations("file://", 
"*").build(),
             Set.of(PolarisStorageActions.READ),
             Set.of(
                 s3Scheme + "://bucket/path/to/warehouse/namespace/table",
@@ -147,11 +146,10 @@ class InMemoryStorageIntegrationTest {
     Map<String, Map<PolarisStorageActions, 
PolarisStorageIntegration.ValidationResult>> result =
         storage.validateAccessToLocations(
             realmConfig,
-            new AwsStorageConfigurationInfo(
-                PolarisStorageConfigurationInfo.StorageType.S3,
-                List.of(),
-                "arn:aws:iam::012345678901:role/jdoe",
-                "us-east-2"),
+            AwsStorageConfigurationInfo.builder()
+                .roleARN("arn:aws:iam::012345678901:role/jdoe")
+                .region("us-east-2")
+                .build(),
             Set.of(PolarisStorageActions.READ),
             Set.of(
                 "s3://bucket/path/to/warehouse/namespace/table",
@@ -184,11 +182,11 @@ class InMemoryStorageIntegrationTest {
     Map<String, Map<PolarisStorageActions, 
PolarisStorageIntegration.ValidationResult>> result =
         storage.validateAccessToLocations(
             realmConfig,
-            new AwsStorageConfigurationInfo(
-                PolarisStorageConfigurationInfo.StorageType.S3,
-                List.of("s3://bucket/path/to/warehouse"),
-                "arn:aws:iam::012345678901:role/jdoe",
-                "us-east-2"),
+            AwsStorageConfigurationInfo.builder()
+                .addAllowedLocation("s3://bucket/path/to/warehouse")
+                .roleARN("arn:aws:iam::012345678901:role/jdoe")
+                .region("us-east-2")
+                .build(),
             Set.of(PolarisStorageActions.READ),
             // trying to read a prefix under the allowed location
             Set.of("s3://bucket/path/to"));
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 8605b8142..faca7193f 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
@@ -19,53 +19,44 @@
 
 package org.apache.polaris.core.storage.aws;
 
-import static 
org.apache.polaris.core.storage.PolarisStorageConfigurationInfo.StorageType.S3;
 import static org.assertj.core.api.Assertions.assertThat;
 
 import java.net.URI;
-import java.util.List;
 import org.junit.jupiter.api.Test;
 
 public class AwsStorageConfigurationInfoTest {
 
-  private static AwsStorageConfigurationInfo config(String endpoint, String 
stsEndpoint) {
-    return config(endpoint, stsEndpoint, false);
-  }
-
-  private static AwsStorageConfigurationInfo config(
-      String endpoint, String stsEndpoint, Boolean pathStyle) {
-    return config(endpoint, stsEndpoint, pathStyle, null);
-  }
-
-  private static AwsStorageConfigurationInfo config(
-      String endpoint, String stsEndpoint, Boolean pathStyle, String 
internalEndpoint) {
-    return new AwsStorageConfigurationInfo(
-        S3, List.of(), "role", null, null, endpoint, stsEndpoint, pathStyle, 
internalEndpoint);
-  }
-
   @Test
   public void testStsEndpoint() {
-    assertThat(config(null, null))
+    assertThat(newBuilder().build())
         .extracting(
             AwsStorageConfigurationInfo::getEndpointUri,
             AwsStorageConfigurationInfo::getStsEndpointUri)
         .containsExactly(null, null);
-    assertThat(config(null, "http://sts.example.com";))
+    assertThat(newBuilder().stsEndpoint("http://sts.example.com";).build())
         .extracting(
             AwsStorageConfigurationInfo::getEndpointUri,
             AwsStorageConfigurationInfo::getStsEndpointUri)
         .containsExactly(null, URI.create("http://sts.example.com";));
-    assertThat(config("http://s3.example.com";, null))
+    assertThat(newBuilder().endpoint("http://s3.example.com";).build())
         .extracting(
             AwsStorageConfigurationInfo::getEndpointUri,
             AwsStorageConfigurationInfo::getStsEndpointUri)
         .containsExactly(URI.create("http://s3.example.com";), 
URI.create("http://s3.example.com";));
-    assertThat(config("http://s3.example.com";, "http://sts.example.com";))
+    assertThat(
+            newBuilder()
+                .endpoint("http://s3.example.com";)
+                .stsEndpoint("http://sts.example.com";)
+                .build())
         .extracting(
             AwsStorageConfigurationInfo::getEndpointUri,
             AwsStorageConfigurationInfo::getStsEndpointUri)
         .containsExactly(URI.create("http://s3.example.com";), 
URI.create("http://sts.example.com";));
-    assertThat(config("http://s3.example.com";, null, false, 
"http://int.example.com";))
+    assertThat(
+            newBuilder()
+                .endpoint("http://s3.example.com";)
+                .endpointInternal("http://int.example.com";)
+                .build())
         .extracting(
             AwsStorageConfigurationInfo::getEndpointUri,
             AwsStorageConfigurationInfo::getStsEndpointUri,
@@ -76,31 +67,42 @@ public class AwsStorageConfigurationInfoTest {
             URI.create("http://int.example.com";));
   }
 
+  private static ImmutableAwsStorageConfigurationInfo.Builder newBuilder() {
+    return AwsStorageConfigurationInfo.builder().roleARN("role");
+  }
+
   @Test
   public void testInternalEndpoint() {
-    assertThat(config(null, null))
+    assertThat(newBuilder().build())
         .extracting(
             AwsStorageConfigurationInfo::getEndpointUri,
             AwsStorageConfigurationInfo::getInternalEndpointUri)
         .containsExactly(null, null);
-    assertThat(config(null, "http://sts.example.com";))
+    assertThat(newBuilder().stsEndpoint("http://sts.example.com";).build())
         .extracting(
             AwsStorageConfigurationInfo::getEndpointUri,
             AwsStorageConfigurationInfo::getInternalEndpointUri)
         .containsExactly(null, null);
-    assertThat(config("http://s3.example.com";, null))
+    assertThat(newBuilder().endpoint("http://s3.example.com";).build())
         .extracting(
             AwsStorageConfigurationInfo::getEndpointUri,
             AwsStorageConfigurationInfo::getInternalEndpointUri)
         .containsExactly(URI.create("http://s3.example.com";), 
URI.create("http://s3.example.com";));
     assertThat(
-            config(
-                "http://s3.example.com";, "http://sts.example.com";, false, 
"http://int.example.com";))
+            newBuilder()
+                .endpoint("http://s3.example.com";)
+                .stsEndpoint("http://sts.example.com";)
+                .endpointInternal("http://int.example.com";)
+                .build())
         .extracting(
             AwsStorageConfigurationInfo::getEndpointUri,
             AwsStorageConfigurationInfo::getInternalEndpointUri)
         .containsExactly(URI.create("http://s3.example.com";), 
URI.create("http://int.example.com";));
-    assertThat(config(null, "http://sts.example.com";, false, 
"http://int.example.com";))
+    assertThat(
+            newBuilder()
+                .stsEndpoint("http://sts.example.com";)
+                .endpointInternal("http://int.example.com";)
+                .build())
         .extracting(
             AwsStorageConfigurationInfo::getEndpointUri,
             AwsStorageConfigurationInfo::getInternalEndpointUri)
@@ -109,8 +111,8 @@ public class AwsStorageConfigurationInfoTest {
 
   @Test
   public void testPathStyleAccess() {
-    assertThat(config(null, null, null).getPathStyleAccess()).isNull();
-    assertThat(config(null, null, false).getPathStyleAccess()).isFalse();
-    assertThat(config(null, null, true).getPathStyleAccess()).isTrue();
+    
assertThat(newBuilder().pathStyleAccess(null).build().getPathStyleAccess()).isNull();
+    
assertThat(newBuilder().pathStyleAccess(false).build().getPathStyleAccess()).isFalse();
+    
assertThat(newBuilder().pathStyleAccess(true).build().getPathStyleAccess()).isTrue();
   }
 }
diff --git 
a/polaris-core/src/test/java/org/apache/polaris/service/storage/PolarisStorageConfigurationInfoTest.java
 
b/polaris-core/src/test/java/org/apache/polaris/service/storage/PolarisStorageConfigurationInfoTest.java
new file mode 100644
index 000000000..fe15147ed
--- /dev/null
+++ 
b/polaris-core/src/test/java/org/apache/polaris/service/storage/PolarisStorageConfigurationInfoTest.java
@@ -0,0 +1,118 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.polaris.service.storage;
+
+import static org.junit.jupiter.params.provider.Arguments.arguments;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import java.util.stream.Stream;
+import org.apache.polaris.core.storage.FileStorageConfigurationInfo;
+import org.apache.polaris.core.storage.PolarisStorageConfigurationInfo;
+import org.apache.polaris.core.storage.aws.AwsStorageConfigurationInfo;
+import org.apache.polaris.core.storage.azure.AzureStorageConfigurationInfo;
+import org.apache.polaris.core.storage.gcp.GcpStorageConfigurationInfo;
+import org.assertj.core.api.SoftAssertions;
+import org.assertj.core.api.junit.jupiter.InjectSoftAssertions;
+import org.assertj.core.api.junit.jupiter.SoftAssertionsExtension;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+@ExtendWith(SoftAssertionsExtension.class)
+public class PolarisStorageConfigurationInfoTest {
+  @InjectSoftAssertions protected SoftAssertions soft;
+
+  private static ObjectMapper mapper;
+
+  @BeforeAll
+  public static void setup() {
+    mapper = new ObjectMapper();
+  }
+
+  @ParameterizedTest
+  @MethodSource
+  public void reserialization(PolarisStorageConfigurationInfo configInfo, 
String serialized)
+      throws Exception {
+    var jsonStr = configInfo.serialize();
+
+    var asJsonNode = mapper.readValue(jsonStr, JsonNode.class);
+    var serializedJsonNode = mapper.readValue(serialized, JsonNode.class);
+    soft.assertThat(asJsonNode).isEqualTo(serializedJsonNode);
+
+    var deserialized = mapper.readValue(serialized, 
PolarisStorageConfigurationInfo.class);
+    soft.assertThat(deserialized).isEqualTo(configInfo);
+
+    var reserialized = mapper.readValue(jsonStr, 
PolarisStorageConfigurationInfo.class);
+    soft.assertThat(reserialized).isEqualTo(configInfo);
+  }
+
+  static Stream<Arguments> reserialization() {
+    return Stream.of(
+        arguments(
+            AwsStorageConfigurationInfo.builder()
+                .addAllowedLocations("s3://foo/bar", "s3://no/where")
+                .roleARN("arn:foo")
+                .region("no-where-1")
+                .build(),
+            
"{\"@type\":\"AwsStorageConfigurationInfo\",\"storageType\":\"S3\",\"allowedLocations\":[\"s3://foo/bar\",\"s3://no/where\"],\"roleARN\":\"arn:foo\",\"region\":\"no-where-1\",\"fileIoImplClassName\":\"org.apache.iceberg.aws.s3.S3FileIO\"}"),
+        arguments(
+            AwsStorageConfigurationInfo.builder()
+                .addAllowedLocations("s3://foo/bar", "s3://no/where")
+                .region("no-where-1")
+                .roleARN("arn:foo")
+                .externalId("external-id")
+                .build(),
+            
"{\"@type\":\"AwsStorageConfigurationInfo\",\"storageType\":\"S3\",\"allowedLocations\":[\"s3://foo/bar\",\"s3://no/where\"],\"roleARN\":\"arn:foo\",\"externalId\":\"external-id\",\"region\":\"no-where-1\",\"fileIoImplClassName\":\"org.apache.iceberg.aws.s3.S3FileIO\"}"),
+        arguments(
+            AwsStorageConfigurationInfo.builder()
+                .addAllowedLocations("s3://foo/bar", "s3://no/where")
+                .region("no-where-1")
+                .roleARN("arn:foo")
+                .externalId("external-id")
+                .endpoint("http://127.9.9.9/";)
+                .stsEndpoint("http://127.9.9.9/sts/";)
+                .endpointInternal("http://127.8.8.8/internal/";)
+                .pathStyleAccess(true)
+                .build(),
+            
"{\"@type\":\"AwsStorageConfigurationInfo\",\"storageType\":\"S3\",\"allowedLocations\":[\"s3://foo/bar\",\"s3://no/where\"],\"roleARN\":\"arn:foo\",\"externalId\":\"external-id\",\"region\":\"no-where-1\",\"endpoint\":\"http://127.9.9.9/\",\"stsEndpoint\":\"http://127.9.9.9/sts/\",\"endpointInternal\":\"http://127.8.8.8/internal/\",\"pathStyleAccess\":true,\"fileIoImplClassName\":\"org.apache.iceberg.aws.s3.S3FileIO\"}";),
+        //
+        arguments(
+            GcpStorageConfigurationInfo.builder()
+                .addAllowedLocations("gs://foo/bar", "gs://meep/moo")
+                .build(),
+            
"{\"@type\":\"GcpStorageConfigurationInfo\",\"allowedLocations\":[\"gs://foo/bar\",\"gs://meep/moo\"],\"storageType\":\"GCS\",\"fileIoImplClassName\":\"org.apache.iceberg.gcp.gcs.GCSFileIO\"}"),
+        //
+        arguments(
+            AzureStorageConfigurationInfo.builder()
+                .addAllowedLocations("abfs://f...@bar.baz/", 
"abfss://b...@meep.buzz/")
+                .tenantId("tenant-id")
+                .build(),
+            
"{\"@type\":\"AzureStorageConfigurationInfo\",\"allowedLocations\":[\"abfs://f...@bar.baz/\",\"abfss://b...@meep.buzz/\"],\"tenantId\":\"tenant-id\",\"storageType\":\"AZURE\",\"fileIoImplClassName\":\"org.apache.iceberg.azure.adlsv2.ADLSFileIO\"}"),
+        //
+        arguments(
+            FileStorageConfigurationInfo.builder()
+                .addAllowedLocations("file:///tmp/bar", "file:///meep/moo")
+                .build(),
+            
"{\"@type\":\"FileStorageConfigurationInfo\",\"allowedLocations\":[\"file:///tmp/bar\",\"file:///meep/moo\"],\"storageType\":\"FILE\",\"fileIoImplClassName\":\"org.apache.iceberg.hadoop.HadoopFileIO\"}"));
+  }
+}
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 604086a29..10ba4b908 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
@@ -18,7 +18,6 @@
  */
 package org.apache.polaris.service.storage.aws;
 
-import static 
org.apache.polaris.core.storage.PolarisStorageConfigurationInfo.StorageType.S3;
 import static org.assertj.core.api.Assertions.assertThat;
 
 import jakarta.annotation.Nonnull;
@@ -27,7 +26,6 @@ import java.util.List;
 import java.util.Set;
 import org.apache.polaris.core.storage.AccessConfig;
 import org.apache.polaris.core.storage.BaseStorageIntegrationTest;
-import org.apache.polaris.core.storage.PolarisStorageConfigurationInfo;
 import org.apache.polaris.core.storage.StorageAccessProperty;
 import org.apache.polaris.core.storage.aws.AwsCredentialsStorageIntegration;
 import org.apache.polaris.core.storage.aws.AwsStorageConfigurationInfo;
@@ -87,8 +85,11 @@ class AwsCredentialsStorageIntegrationTest extends 
BaseStorageIntegrationTest {
     String warehouseDir = scheme + "://bucket/path/to/warehouse";
     AccessConfig accessConfig =
         new AwsCredentialsStorageIntegration(
-                new AwsStorageConfigurationInfo(
-                    S3, List.of(warehouseDir), roleARN, externalId, null),
+                AwsStorageConfigurationInfo.builder()
+                    .addAllowedLocation(warehouseDir)
+                    .roleARN(roleARN)
+                    .externalId(externalId)
+                    .build(),
                 stsClient)
             .getSubscopedCreds(
                 EMPTY_REALM_CONFIG,
@@ -108,7 +109,6 @@ class AwsCredentialsStorageIntegrationTest extends 
BaseStorageIntegrationTest {
   @ParameterizedTest
   @ValueSource(strings = {AWS_PARTITION, "aws-cn", "aws-us-gov"})
   public void testGetSubscopedCredsInlinePolicy(String awsPartition) {
-    PolarisStorageConfigurationInfo.StorageType storageType = S3;
     String roleARN;
     String region;
     switch (awsPartition) {
@@ -231,12 +231,12 @@ class AwsCredentialsStorageIntegrationTest extends 
BaseStorageIntegrationTest {
         Assertions.assertThatThrownBy(
                 () ->
                     new AwsCredentialsStorageIntegration(
-                            new AwsStorageConfigurationInfo(
-                                storageType,
-                                List.of(s3Path(bucket, warehouseKeyPrefix)),
-                                roleARN,
-                                externalId,
-                                region),
+                            AwsStorageConfigurationInfo.builder()
+                                .addAllowedLocation(s3Path(bucket, 
warehouseKeyPrefix))
+                                .roleARN(roleARN)
+                                .externalId(externalId)
+                                .region(region)
+                                .build(),
                             stsClient)
                         .getSubscopedCreds(
                             EMPTY_REALM_CONFIG,
@@ -249,12 +249,12 @@ class AwsCredentialsStorageIntegrationTest extends 
BaseStorageIntegrationTest {
       case "aws-us-gov":
         AccessConfig accessConfig =
             new AwsCredentialsStorageIntegration(
-                    new AwsStorageConfigurationInfo(
-                        storageType,
-                        List.of(s3Path(bucket, warehouseKeyPrefix)),
-                        roleARN,
-                        externalId,
-                        region),
+                    AwsStorageConfigurationInfo.builder()
+                        .addAllowedLocation(s3Path(bucket, warehouseKeyPrefix))
+                        .roleARN(roleARN)
+                        .externalId(externalId)
+                        .region(region)
+                        .build(),
                     stsClient)
                 .getSubscopedCreds(
                     EMPTY_REALM_CONFIG,
@@ -349,12 +349,12 @@ class AwsCredentialsStorageIntegrationTest extends 
BaseStorageIntegrationTest {
             });
     AccessConfig accessConfig =
         new AwsCredentialsStorageIntegration(
-                new AwsStorageConfigurationInfo(
-                    S3,
-                    List.of(s3Path(bucket, warehouseKeyPrefix)),
-                    roleARN,
-                    externalId,
-                    "us-east-2"),
+                AwsStorageConfigurationInfo.builder()
+                    .addAllowedLocation(s3Path(bucket, warehouseKeyPrefix))
+                    .roleARN(roleARN)
+                    .externalId(externalId)
+                    .region("us-east-2")
+                    .build(),
                 stsClient)
             .getSubscopedCreds(
                 EMPTY_REALM_CONFIG,
@@ -443,12 +443,12 @@ class AwsCredentialsStorageIntegrationTest extends 
BaseStorageIntegrationTest {
             });
     AccessConfig accessConfig =
         new AwsCredentialsStorageIntegration(
-                new AwsStorageConfigurationInfo(
-                    S3,
-                    List.of(s3Path(bucket, warehouseKeyPrefix)),
-                    roleARN,
-                    externalId,
-                    "us-east-2"),
+                AwsStorageConfigurationInfo.builder()
+                    .addAllowedLocation(s3Path(bucket, warehouseKeyPrefix))
+                    .roleARN(roleARN)
+                    .externalId(externalId)
+                    .region("us-east-2")
+                    .build(),
                 stsClient)
             .getSubscopedCreds(
                 EMPTY_REALM_CONFIG,
@@ -509,12 +509,12 @@ class AwsCredentialsStorageIntegrationTest extends 
BaseStorageIntegrationTest {
             });
     AccessConfig accessConfig =
         new AwsCredentialsStorageIntegration(
-                new AwsStorageConfigurationInfo(
-                    S3,
-                    List.of(s3Path(bucket, warehouseKeyPrefix)),
-                    roleARN,
-                    externalId,
-                    "us-east-2"),
+                AwsStorageConfigurationInfo.builder()
+                    .addAllowedLocation(s3Path(bucket, warehouseKeyPrefix))
+                    .roleARN(roleARN)
+                    .externalId(externalId)
+                    .region("us-east-2")
+                    .build(),
                 stsClient)
             .getSubscopedCreds(EMPTY_REALM_CONFIG, true, /* allowList = true 
*/ Set.of(), Set.of());
     assertThat(accessConfig.credentials())
@@ -546,12 +546,12 @@ class AwsCredentialsStorageIntegrationTest extends 
BaseStorageIntegrationTest {
         Assertions.assertThatThrownBy(
                 () ->
                     new AwsCredentialsStorageIntegration(
-                            new AwsStorageConfigurationInfo(
-                                S3,
-                                List.of(s3Path(bucket, warehouseKeyPrefix)),
-                                roleARN,
-                                externalId,
-                                clientRegion),
+                            AwsStorageConfigurationInfo.builder()
+                                .addAllowedLocation(s3Path(bucket, 
warehouseKeyPrefix))
+                                .roleARN(roleARN)
+                                .externalId(externalId)
+                                .region(clientRegion)
+                                .build(),
                             stsClient)
                         .getSubscopedCreds(
                             EMPTY_REALM_CONFIG, true, /* allowList = true */ 
Set.of(), Set.of()))
@@ -561,12 +561,12 @@ class AwsCredentialsStorageIntegrationTest extends 
BaseStorageIntegrationTest {
       case "aws-us-gov":
         AccessConfig accessConfig =
             new AwsCredentialsStorageIntegration(
-                    new AwsStorageConfigurationInfo(
-                        S3,
-                        List.of(s3Path(bucket, warehouseKeyPrefix)),
-                        roleARN,
-                        externalId,
-                        clientRegion),
+                    AwsStorageConfigurationInfo.builder()
+                        .addAllowedLocation(s3Path(bucket, warehouseKeyPrefix))
+                        .roleARN(roleARN)
+                        .externalId(externalId)
+                        .region(clientRegion)
+                        .build(),
                     stsClient)
                 .getSubscopedCreds(
                     EMPTY_REALM_CONFIG, true, /* allowList = true */ Set.of(), 
Set.of());
@@ -597,8 +597,11 @@ class AwsCredentialsStorageIntegrationTest extends 
BaseStorageIntegrationTest {
       case AWS_PARTITION:
         AccessConfig accessConfig =
             new AwsCredentialsStorageIntegration(
-                    new AwsStorageConfigurationInfo(
-                        S3, List.of(s3Path(bucket, warehouseKeyPrefix)), 
roleARN, externalId, null),
+                    AwsStorageConfigurationInfo.builder()
+                        .addAllowedLocation(s3Path(bucket, warehouseKeyPrefix))
+                        .roleARN(roleARN)
+                        .externalId(externalId)
+                        .build(),
                     stsClient)
                 .getSubscopedCreds(
                     EMPTY_REALM_CONFIG, true, /* allowList = true */ Set.of(), 
Set.of());
@@ -611,12 +614,11 @@ class AwsCredentialsStorageIntegrationTest extends 
BaseStorageIntegrationTest {
         Assertions.assertThatThrownBy(
                 () ->
                     new AwsCredentialsStorageIntegration(
-                            new AwsStorageConfigurationInfo(
-                                PolarisStorageConfigurationInfo.StorageType.S3,
-                                List.of(s3Path(bucket, warehouseKeyPrefix)),
-                                roleARN,
-                                externalId,
-                                null),
+                            AwsStorageConfigurationInfo.builder()
+                                .addAllowedLocation(s3Path(bucket, 
warehouseKeyPrefix))
+                                .roleARN(roleARN)
+                                .externalId(externalId)
+                                .build(),
                             stsClient)
                         .getSubscopedCreds(
                             EMPTY_REALM_CONFIG, true, /* allowList = true */ 
Set.of(), Set.of()))
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 d93dcc63f..768f4c330 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
@@ -340,11 +340,12 @@ public class AzureCredentialStorageIntegrationTest 
extends BaseStorageIntegratio
 
   private AccessConfig subscopedCredsForOperations(
       List<String> allowedReadLoc, List<String> allowedWriteLoc, boolean 
allowListAction) {
-    List<String> allowedLoc = new ArrayList<>();
-    allowedLoc.addAll(allowedReadLoc);
-    allowedLoc.addAll(allowedWriteLoc);
     AzureStorageConfigurationInfo azureConfig =
-        new AzureStorageConfigurationInfo(allowedLoc, tenantId);
+        AzureStorageConfigurationInfo.builder()
+            .addAllAllowedLocations(allowedReadLoc)
+            .addAllAllowedLocations(allowedWriteLoc)
+            .tenantId(tenantId)
+            .build();
     AzureCredentialsStorageIntegration azureCredsIntegration =
         new AzureCredentialsStorageIntegration(azureConfig);
     return azureCredsIntegration.getSubscopedCreds(
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 e0985199b..da890627e 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
@@ -37,7 +37,6 @@ import com.google.cloud.storage.Storage;
 import com.google.cloud.storage.StorageException;
 import com.google.cloud.storage.StorageOptions;
 import java.io.IOException;
-import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Date;
 import java.util.HashSet;
@@ -157,10 +156,11 @@ class GcpCredentialsStorageIntegrationTest extends 
BaseStorageIntegrationTest {
   private AccessConfig subscopedCredsForOperations(
       List<String> allowedReadLoc, List<String> allowedWriteLoc, boolean 
allowListAction)
       throws IOException {
-    List<String> allowedLoc = new ArrayList<>();
-    allowedLoc.addAll(allowedReadLoc);
-    allowedLoc.addAll(allowedWriteLoc);
-    GcpStorageConfigurationInfo gcpConfig = new 
GcpStorageConfigurationInfo(allowedLoc);
+    GcpStorageConfigurationInfo gcpConfig =
+        GcpStorageConfigurationInfo.builder()
+            .addAllAllowedLocations(allowedReadLoc)
+            .addAllAllowedLocations(allowedWriteLoc)
+            .build();
     GcpCredentialsStorageIntegration gcpCredsIntegration =
         new GcpCredentialsStorageIntegration(
             gcpConfig,

Reply via email to