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);