You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@polaris.apache.org by yz...@apache.org on 2026/03/28 00:46:45 UTC

(polaris) 01/01: add tests

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

yzou pushed a commit to branch yzou-generic-table-credential-vending
in repository https://gitbox.apache.org/repos/asf/polaris.git

commit f859b8c38ef546ed535221846c40912c7a84e2a0
Author: Yun Zou <yu...@gmail.com>
AuthorDate: Mon Mar 23 15:34:27 2026 -0700

    add tests
---
 .../polaris/core/config/FeatureConfiguration.java  | 11 +++
 .../service/catalog/common/CatalogHandler.java     | 40 +++++++++
 .../service/catalog/common/CatalogUtils.java       | 20 ++++-
 .../generic/GenericTableCatalogHandler.java        | 94 +++++++++++++++++++++-
 .../generic/GenericTableCatalogHandlerFactory.java |  3 +
 .../catalog/iceberg/IcebergCatalogHandler.java     | 33 +-------
 6 files changed, 165 insertions(+), 36 deletions(-)

diff --git a/polaris-core/src/main/java/org/apache/polaris/core/config/FeatureConfiguration.java b/polaris-core/src/main/java/org/apache/polaris/core/config/FeatureConfiguration.java
index 973d5b8227..f1a661a7ec 100644
--- a/polaris-core/src/main/java/org/apache/polaris/core/config/FeatureConfiguration.java
+++ b/polaris-core/src/main/java/org/apache/polaris/core/config/FeatureConfiguration.java
@@ -321,6 +321,17 @@ public class FeatureConfiguration<T> extends PolarisConfiguration<T> {
           .defaultValue(true)
           .buildFeatureConfiguration();
 
+  public static final FeatureConfiguration<Boolean> ENABLE_GENERIC_TABLES_CREDENTIAL_VENDING =
+      PolarisConfiguration.<Boolean>builder()
+          .key("ENABLE_GENERIC_TABLES_CREDENTIAL_VENDING")
+          .catalogConfig("polaris.config.enable.generic-tables.credential-vending")
+          .description(
+              "If set to true, credential vending is enabled for generic tables. "
+                  + "When a generic table has a base location, scoped storage credentials "
+                  + "will be returned in the load and create responses.")
+          .defaultValue(false)
+          .buildFeatureConfiguration();
+
   public static final FeatureConfiguration<Long> ENTITY_CACHE_WEIGHER_TARGET =
       PolarisConfiguration.<Long>builder()
           .key("ENTITY_CACHE_WEIGHER_TARGET")
diff --git a/runtime/service/src/main/java/org/apache/polaris/service/catalog/common/CatalogHandler.java b/runtime/service/src/main/java/org/apache/polaris/service/catalog/common/CatalogHandler.java
index 5605cab033..b1ac58cd83 100644
--- a/runtime/service/src/main/java/org/apache/polaris/service/catalog/common/CatalogHandler.java
+++ b/runtime/service/src/main/java/org/apache/polaris/service/catalog/common/CatalogHandler.java
@@ -24,11 +24,14 @@ import static org.apache.polaris.service.catalog.common.ExceptionUtils.notFoundE
 
 import java.util.Arrays;
 import java.util.EnumSet;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Optional;
+import java.util.Set;
 import org.apache.iceberg.catalog.Namespace;
 import org.apache.iceberg.catalog.TableIdentifier;
 import org.apache.iceberg.exceptions.AlreadyExistsException;
+import org.apache.iceberg.exceptions.ForbiddenException;
 import org.apache.polaris.core.auth.PolarisAuthorizableOperation;
 import org.apache.polaris.core.auth.PolarisAuthorizer;
 import org.apache.polaris.core.auth.PolarisPrincipal;
@@ -45,6 +48,7 @@ import org.apache.polaris.core.persistence.resolver.ResolutionManifestFactory;
 import org.apache.polaris.core.persistence.resolver.ResolvedPathKey;
 import org.apache.polaris.core.persistence.resolver.ResolverPath;
 import org.apache.polaris.core.persistence.resolver.ResolverStatus;
+import org.apache.polaris.core.storage.PolarisStorageActions;
 import org.apache.polaris.service.types.PolicyIdentifier;
 import org.immutables.value.Value;
 
@@ -263,6 +267,42 @@ public abstract class CatalogHandler {
     initializeCatalog();
   }
 
+  /**
+   * Authorizes a load-table-like operation with optional credential vending delegation. When
+   * credential vending is requested, this method attempts write delegation authorization first,
+   * falling back to read delegation. The returned set of storage actions reflects the granted
+   * access level.
+   *
+   * @param tableIdentifier the table to authorize access for
+   * @param subType the entity subtype (e.g., ICEBERG_TABLE, GENERIC_TABLE)
+   * @param requestCredentialVending if true, attempt delegation-based authorization with credential
+   *     vending; if false, perform a simple LOAD_TABLE authorization
+   * @return the set of storage actions granted; empty if credential vending was not requested
+   */
+  protected Set<PolarisStorageActions> authorizeLoadTableLike(
+      TableIdentifier tableIdentifier,
+      PolarisEntitySubType subType,
+      boolean requestCredentialVending) {
+    if (!requestCredentialVending) {
+      authorizeBasicTableLikeOperationOrThrow(
+          PolarisAuthorizableOperation.LOAD_TABLE, subType, tableIdentifier);
+      return Set.of();
+    }
+
+    Set<PolarisStorageActions> actionsRequested =
+        new HashSet<>(Set.of(PolarisStorageActions.READ, PolarisStorageActions.LIST));
+    try {
+      authorizeBasicTableLikeOperationOrThrow(
+          PolarisAuthorizableOperation.LOAD_TABLE_WITH_WRITE_DELEGATION, subType, tableIdentifier);
+      actionsRequested.add(PolarisStorageActions.WRITE);
+    } catch (ForbiddenException e) {
+      authorizeBasicTableLikeOperationOrThrow(
+          PolarisAuthorizableOperation.LOAD_TABLE_WITH_READ_DELEGATION, subType, tableIdentifier);
+    }
+
+    return actionsRequested;
+  }
+
   protected void authorizeCollectionOfTableLikeOperationOrThrow(
       PolarisAuthorizableOperation op,
       final PolarisEntitySubType subType,
diff --git a/runtime/service/src/main/java/org/apache/polaris/service/catalog/common/CatalogUtils.java b/runtime/service/src/main/java/org/apache/polaris/service/catalog/common/CatalogUtils.java
index 0dcb1ba2b8..3ad077a64f 100644
--- a/runtime/service/src/main/java/org/apache/polaris/service/catalog/common/CatalogUtils.java
+++ b/runtime/service/src/main/java/org/apache/polaris/service/catalog/common/CatalogUtils.java
@@ -50,9 +50,25 @@ public class CatalogUtils {
    */
   public static PolarisResolvedPathWrapper findResolvedStorageEntity(
       PolarisResolutionManifestCatalogView resolvedEntityView, TableIdentifier tableIdentifier) {
+    return findResolvedStorageEntity(
+        resolvedEntityView, tableIdentifier, PolarisEntitySubType.ICEBERG_TABLE);
+  }
+
+  /**
+   * Find the resolved entity path that may contain storage information for a given table-like
+   * subtype.
+   *
+   * @param resolvedEntityView The resolved entity view containing catalog entities.
+   * @param tableIdentifier The table identifier for which to find storage information.
+   * @param subType The entity subtype to resolve (e.g., ICEBERG_TABLE, GENERIC_TABLE).
+   * @return The resolved path wrapper that may contain storage information.
+   */
+  public static PolarisResolvedPathWrapper findResolvedStorageEntity(
+      PolarisResolutionManifestCatalogView resolvedEntityView,
+      TableIdentifier tableIdentifier,
+      PolarisEntitySubType subType) {
     PolarisResolvedPathWrapper resolvedTableEntities =
-        resolvedEntityView.getResolvedPath(
-            ResolvedPathKey.ofTableLike(tableIdentifier), PolarisEntitySubType.ICEBERG_TABLE);
+        resolvedEntityView.getResolvedPath(ResolvedPathKey.ofTableLike(tableIdentifier), subType);
     if (resolvedTableEntities != null) {
       return resolvedTableEntities;
     }
diff --git a/runtime/service/src/main/java/org/apache/polaris/service/catalog/generic/GenericTableCatalogHandler.java b/runtime/service/src/main/java/org/apache/polaris/service/catalog/generic/GenericTableCatalogHandler.java
index 0acae1d310..fa4f91efaf 100644
--- a/runtime/service/src/main/java/org/apache/polaris/service/catalog/generic/GenericTableCatalogHandler.java
+++ b/runtime/service/src/main/java/org/apache/polaris/service/catalog/generic/GenericTableCatalogHandler.java
@@ -21,7 +21,10 @@ package org.apache.polaris.service.catalog.generic;
 import io.smallrye.common.annotation.Identifier;
 import jakarta.enterprise.inject.Instance;
 import java.util.LinkedHashSet;
+import java.util.List;
 import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
 import org.apache.iceberg.catalog.Namespace;
 import org.apache.iceberg.catalog.TableIdentifier;
 import org.apache.polaris.core.auth.PolarisAuthorizableOperation;
@@ -34,8 +37,13 @@ import org.apache.polaris.core.credentials.PolarisCredentialManager;
 import org.apache.polaris.core.entity.CatalogEntity;
 import org.apache.polaris.core.entity.PolarisEntitySubType;
 import org.apache.polaris.core.entity.table.GenericTableEntity;
+import org.apache.polaris.core.persistence.PolarisResolvedPathWrapper;
+import org.apache.polaris.core.storage.PolarisStorageActions;
+import org.apache.polaris.core.storage.StorageAccessConfig;
 import org.apache.polaris.immutables.PolarisImmutable;
 import org.apache.polaris.service.catalog.common.CatalogHandler;
+import org.apache.polaris.service.catalog.common.CatalogUtils;
+import org.apache.polaris.service.catalog.io.StorageAccessConfigProvider;
 import org.apache.polaris.service.types.GenericTable;
 import org.apache.polaris.service.types.ListGenericTablesResponse;
 import org.apache.polaris.service.types.LoadGenericTableResponse;
@@ -51,6 +59,8 @@ public abstract class GenericTableCatalogHandler extends CatalogHandler {
 
   protected abstract Instance<ExternalCatalogFactory> externalCatalogFactories();
 
+  protected abstract StorageAccessConfigProvider storageAccessConfigProvider();
+
   private GenericTableCatalog genericTableCatalog;
 
   @Override
@@ -126,7 +136,21 @@ public abstract class GenericTableCatalogHandler extends CatalogHandler {
             .setProperties(createdEntity.getPropertiesAsMap())
             .build();
 
-    return LoadGenericTableResponse.builder().setTable(createdTable).build();
+    List<org.apache.polaris.service.types.StorageAccessConfig> storageAccessConfigs =
+        isCredentialVendingEnabled()
+            ? vendCredentials(
+                identifier,
+                createdEntity,
+                Set.of(
+                    PolarisStorageActions.READ,
+                    PolarisStorageActions.WRITE,
+                    PolarisStorageActions.LIST))
+            : List.of();
+
+    return LoadGenericTableResponse.builder()
+        .setTable(createdTable)
+        .setStorageAccessConfigs(storageAccessConfigs)
+        .build();
   }
 
   public boolean dropGenericTable(TableIdentifier identifier) {
@@ -137,8 +161,12 @@ public abstract class GenericTableCatalogHandler extends CatalogHandler {
   }
 
   public LoadGenericTableResponse loadGenericTable(TableIdentifier identifier) {
-    PolarisAuthorizableOperation op = PolarisAuthorizableOperation.LOAD_TABLE;
-    authorizeBasicTableLikeOperationOrThrow(op, PolarisEntitySubType.GENERIC_TABLE, identifier);
+    ensureResolutionManifestForTable(identifier);
+    boolean credentialVendingEnabled = isCredentialVendingEnabled();
+
+    Set<PolarisStorageActions> actionsRequested =
+        authorizeLoadTableLike(
+            identifier, PolarisEntitySubType.GENERIC_TABLE, credentialVendingEnabled);
 
     GenericTableEntity loadedEntity = this.genericTableCatalog.loadGenericTable(identifier);
     GenericTable loadedTable =
@@ -150,6 +178,64 @@ public abstract class GenericTableCatalogHandler extends CatalogHandler {
             .setProperties(loadedEntity.getPropertiesAsMap())
             .build();
 
-    return LoadGenericTableResponse.builder().setTable(loadedTable).build();
+    List<org.apache.polaris.service.types.StorageAccessConfig> storageAccessConfigs =
+        credentialVendingEnabled
+            ? vendCredentials(identifier, loadedEntity, actionsRequested)
+            : List.of();
+
+    return LoadGenericTableResponse.builder()
+        .setTable(loadedTable)
+        .setStorageAccessConfigs(storageAccessConfigs)
+        .build();
+  }
+
+  private boolean isCredentialVendingEnabled() {
+    return realmConfig()
+        .getConfig(
+            FeatureConfiguration.ENABLE_GENERIC_TABLES_CREDENTIAL_VENDING,
+            resolutionManifest.getResolvedCatalogEntity());
+  }
+
+  private List<org.apache.polaris.service.types.StorageAccessConfig> vendCredentials(
+      TableIdentifier tableIdentifier,
+      GenericTableEntity entity,
+      Set<PolarisStorageActions> actions) {
+    String baseLocation = entity.getBaseLocation();
+    if (baseLocation == null || baseLocation.isEmpty()) {
+      LOGGER.debug(
+          "No base location set for generic table {}, skipping credential vending",
+          tableIdentifier);
+      return List.of();
+    }
+
+    PolarisResolvedPathWrapper resolvedStoragePath =
+        CatalogUtils.findResolvedStorageEntity(
+            resolutionManifest, tableIdentifier, PolarisEntitySubType.GENERIC_TABLE);
+    if (resolvedStoragePath == null) {
+      LOGGER.debug(
+          "Unable to find storage configuration information for generic table {}", tableIdentifier);
+      return List.of();
+    }
+
+    Set<String> tableLocations = Set.of(baseLocation);
+    StorageAccessConfig storageAccessConfig =
+        storageAccessConfigProvider()
+            .getStorageAccessConfig(
+                tableIdentifier, tableLocations, actions, Optional.empty(), resolvedStoragePath);
+
+    Map<String, String> credentials = storageAccessConfig.credentials();
+    if (credentials.isEmpty()) {
+      LOGGER.debug("No credentials vended for generic table {}", tableIdentifier);
+      return List.of();
+    }
+
+    Map<String, String> allConfig = new java.util.HashMap<>(credentials);
+    allConfig.putAll(storageAccessConfig.extraProperties());
+
+    return List.of(
+        org.apache.polaris.service.types.StorageAccessConfig.builder()
+            .setPrefix(baseLocation)
+            .setConfig(allConfig)
+            .build());
   }
 }
diff --git a/runtime/service/src/main/java/org/apache/polaris/service/catalog/generic/GenericTableCatalogHandlerFactory.java b/runtime/service/src/main/java/org/apache/polaris/service/catalog/generic/GenericTableCatalogHandlerFactory.java
index d0f42ca552..bcd77727b4 100644
--- a/runtime/service/src/main/java/org/apache/polaris/service/catalog/generic/GenericTableCatalogHandlerFactory.java
+++ b/runtime/service/src/main/java/org/apache/polaris/service/catalog/generic/GenericTableCatalogHandlerFactory.java
@@ -29,6 +29,7 @@ import org.apache.polaris.core.context.CallContext;
 import org.apache.polaris.core.credentials.PolarisCredentialManager;
 import org.apache.polaris.core.persistence.PolarisMetaStoreManager;
 import org.apache.polaris.core.persistence.resolver.ResolutionManifestFactory;
+import org.apache.polaris.service.catalog.io.StorageAccessConfigProvider;
 
 @RequestScoped
 public class GenericTableCatalogHandlerFactory {
@@ -39,6 +40,7 @@ public class GenericTableCatalogHandlerFactory {
   @Inject PolarisAuthorizer authorizer;
   @Inject PolarisCredentialManager credentialManager;
   @Inject @Any Instance<ExternalCatalogFactory> externalCatalogFactories;
+  @Inject StorageAccessConfigProvider storageAccessConfigProvider;
 
   public GenericTableCatalogHandler createHandler(String catalogName, PolarisPrincipal principal) {
     return ImmutableGenericTableCatalogHandler.builder()
@@ -50,6 +52,7 @@ public class GenericTableCatalogHandlerFactory {
         .authorizer(authorizer)
         .credentialManager(credentialManager)
         .externalCatalogFactories(externalCatalogFactories)
+        .storageAccessConfigProvider(storageAccessConfigProvider)
         .build();
   }
 }
diff --git a/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogHandler.java b/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogHandler.java
index 3b378c7d04..e21da8fcb1 100644
--- a/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogHandler.java
+++ b/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogHandler.java
@@ -40,7 +40,6 @@ import java.time.ZoneOffset;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.EnumSet;
-import java.util.HashSet;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
@@ -742,36 +741,10 @@ public abstract class IcebergCatalogHandler extends CatalogHandler implements Au
 
   private Set<PolarisStorageActions> authorizeLoadTable(
       TableIdentifier tableIdentifier, EnumSet<AccessDelegationMode> delegationModes) {
-    if (delegationModes.isEmpty()) {
-      authorizeBasicTableLikeOperationOrThrow(
-          PolarisAuthorizableOperation.LOAD_TABLE,
-          PolarisEntitySubType.ICEBERG_TABLE,
-          tableIdentifier);
-      return Set.of();
-    }
-
-    // Here we have a single method that falls through multiple candidate
-    // PolarisAuthorizableOperations because instead of identifying the desired operation up-front
-    // and
-    // failing the authz check if grants aren't found, we find the first most-privileged authz match
-    // and respond according to that.
-    PolarisAuthorizableOperation read =
-        PolarisAuthorizableOperation.LOAD_TABLE_WITH_READ_DELEGATION;
-    PolarisAuthorizableOperation write =
-        PolarisAuthorizableOperation.LOAD_TABLE_WITH_WRITE_DELEGATION;
-
+    boolean requestCredentialVending = delegationModes.contains(VENDED_CREDENTIALS);
     Set<PolarisStorageActions> actionsRequested =
-        new HashSet<>(Set.of(PolarisStorageActions.READ, PolarisStorageActions.LIST));
-    try {
-      // TODO: Refactor to have a boolean-return version of the helpers so we can fallthrough
-      // easily.
-      authorizeBasicTableLikeOperationOrThrow(
-          write, PolarisEntitySubType.ICEBERG_TABLE, tableIdentifier);
-      actionsRequested.add(PolarisStorageActions.WRITE);
-    } catch (ForbiddenException e) {
-      authorizeBasicTableLikeOperationOrThrow(
-          read, PolarisEntitySubType.ICEBERG_TABLE, tableIdentifier);
-    }
+        authorizeLoadTableLike(
+            tableIdentifier, PolarisEntitySubType.ICEBERG_TABLE, requestCredentialVending);
 
     checkAllowExternalCatalogCredentialVending(delegationModes);