From 19922d7afca0a3887afd65e7f88062bd8b56de46 Mon Sep 17 00:00:00 2001 From: Honah J Date: Wed, 10 Sep 2025 14:12:59 -0500 Subject: [PATCH] Add generic table to valid target list --- .../PolarisPolicyServiceIntegrationTest.java | 26 +++++ .../core/policy/PredefinedPolicyTypes.java | 3 +- .../TableConversionPolicyContent.java | 106 ++++++++++++++++++ .../policy/validator/PolicyValidators.java | 8 +- .../TableConversionPolicyValidator.java | 42 +++++++ .../polaris/core/policy/PolicyTypeTest.java | 3 +- .../catalog/common/CatalogHandler.java | 20 +++- .../service/catalog/policy/PolicyCatalog.java | 29 ++++- .../catalog/policy/PolicyCatalogHandler.java | 7 +- .../catalog/policy/PolicyCatalogUtils.java | 10 +- .../service/admin/PolarisAuthzTestBase.java | 7 ++ .../policy/AbstractPolicyCatalogTest.java | 34 ++++++ .../policy/PolicyCatalogHandlerAuthzTest.java | 42 +++++++ 13 files changed, 322 insertions(+), 15 deletions(-) create mode 100644 polaris-core/src/main/java/org/apache/polaris/core/policy/content/conversion/TableConversionPolicyContent.java create mode 100644 polaris-core/src/main/java/org/apache/polaris/core/policy/validator/conversion/TableConversionPolicyValidator.java diff --git a/integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisPolicyServiceIntegrationTest.java b/integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisPolicyServiceIntegrationTest.java index 2557466de2..7547db9d23 100644 --- a/integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisPolicyServiceIntegrationTest.java +++ b/integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisPolicyServiceIntegrationTest.java @@ -64,6 +64,7 @@ import org.apache.polaris.core.policy.exceptions.PolicyInUseException; import org.apache.polaris.service.it.env.CatalogConfig; import org.apache.polaris.service.it.env.ClientCredentials; +import org.apache.polaris.service.it.env.GenericTableApi; import org.apache.polaris.service.it.env.IcebergHelper; import org.apache.polaris.service.it.env.IntegrationTestsHelper; import org.apache.polaris.service.it.env.ManagementApi; @@ -108,7 +109,9 @@ public class PolarisPolicyServiceIntegrationTest { private static final PolicyIdentifier NS1_P1 = new PolicyIdentifier(NS1, "P1"); private static final PolicyIdentifier NS1_P2 = new PolicyIdentifier(NS1, "P2"); private static final PolicyIdentifier NS1_P3 = new PolicyIdentifier(NS1, "P3"); + private static final PolicyIdentifier NS1_P4 = new PolicyIdentifier(NS1, "P4"); private static final TableIdentifier NS2_T1 = TableIdentifier.of(NS2, "T1"); + private static final TableIdentifier NS2_T2 = TableIdentifier.of(NS2, "T2"); private static final String NS1_NAME = RESTUtil.encodeNamespace(NS1); private static final String INVALID_NAMESPACE = "INVALID_NAMESPACE"; @@ -124,6 +127,7 @@ public class PolarisPolicyServiceIntegrationTest { private static PolarisClient client; private static ManagementApi managementApi; private static PolicyApi policyApi; + private GenericTableApi genericTableApi; private RESTCatalog restCatalog; private String currentCatalogName; @@ -221,6 +225,7 @@ public void before(TestInfo testInfo) { principalRoleName, currentCatalogName, catalogRole); policyApi = client.policyApi(principalToken); + genericTableApi = client.genericTableApi(principalToken); } @AfterEach @@ -668,11 +673,19 @@ public void testPolicyMapping() { PredefinedPolicyTypes.ORPHAN_FILE_REMOVAL, EXAMPLE_TABLE_MAINTENANCE_POLICY_CONTENT, "test policy"); + Policy p4 = + policyApi.createPolicy( + currentCatalogName, + NS1_P4, + PredefinedPolicyTypes.TABLE_CONVERSION, + "{\"enable\": false}", + "test policy"); restCatalog .buildTable( NS2_T1, new Schema(Types.NestedField.optional(1, "string", Types.StringType.get()))) .create(); + genericTableApi.createGenericTable(currentCatalogName, NS2_T2, "delta", Map.of()); PolicyAttachmentTarget catalogTarget = PolicyAttachmentTarget.builder().setType(PolicyAttachmentTarget.TypeEnum.CATALOG).build(); @@ -686,10 +699,16 @@ NS2_T1, new Schema(Types.NestedField.optional(1, "string", Types.StringType.get( .setType(PolicyAttachmentTarget.TypeEnum.TABLE_LIKE) .setPath(PolarisCatalogHelpers.tableIdentifierToList(NS2_T1)) .build(); + PolicyAttachmentTarget genericTableTarget = + PolicyAttachmentTarget.builder() + .setType(PolicyAttachmentTarget.TypeEnum.TABLE_LIKE) + .setPath(PolarisCatalogHelpers.tableIdentifierToList(NS2_T2)) + .build(); policyApi.attachPolicy(currentCatalogName, NS1_P1, catalogTarget, Map.of()); policyApi.attachPolicy(currentCatalogName, NS1_P2, namespaceTarget, Map.of()); policyApi.attachPolicy(currentCatalogName, NS1_P3, tableTarget, Map.of()); + policyApi.attachPolicy(currentCatalogName, NS1_P4, genericTableTarget, Map.of()); List applicablePoliciesOnCatalog = policyApi.getApplicablePolicies(currentCatalogName, null, null, null); @@ -715,15 +734,22 @@ NS2_T1, new Schema(Types.NestedField.optional(1, "string", Types.StringType.get( currentCatalogName, NS2, NS2_T1.name(), PredefinedPolicyTypes.METADATA_COMPACTION)) .containsExactlyInAnyOrder(policyToApplicablePolicy(p2, true, NS1)); + Assertions.assertThat( + policyApi.getApplicablePolicies(currentCatalogName, NS2, NS2_T2.name(), null)) + .containsExactlyInAnyOrder(policyToApplicablePolicy(p4, false, NS1)); + policyApi.detachPolicy(currentCatalogName, NS1_P1, catalogTarget); policyApi.detachPolicy(currentCatalogName, NS1_P2, namespaceTarget); policyApi.detachPolicy(currentCatalogName, NS1_P3, tableTarget); + policyApi.detachPolicy(currentCatalogName, NS1_P4, genericTableTarget); policyApi.dropPolicy(currentCatalogName, NS1_P1); policyApi.dropPolicy(currentCatalogName, NS1_P2); policyApi.dropPolicy(currentCatalogName, NS1_P3); + policyApi.dropPolicy(currentCatalogName, NS1_P4); restCatalog.dropTable(NS2_T1); + genericTableApi.dropGenericTable(currentCatalogName, NS2_T2); } @Test diff --git a/polaris-core/src/main/java/org/apache/polaris/core/policy/PredefinedPolicyTypes.java b/polaris-core/src/main/java/org/apache/polaris/core/policy/PredefinedPolicyTypes.java index 40128f979b..4e41dea58f 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/policy/PredefinedPolicyTypes.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/policy/PredefinedPolicyTypes.java @@ -28,7 +28,8 @@ public enum PredefinedPolicyTypes implements PolicyType { DATA_COMPACTION(0, "system.data-compaction", true), METADATA_COMPACTION(1, "system.metadata-compaction", true), ORPHAN_FILE_REMOVAL(2, "system.orphan-file-removal", true), - SNAPSHOT_EXPIRY(3, "system.snapshot-expiry", true); + SNAPSHOT_EXPIRY(3, "system.snapshot-expiry", true), + TABLE_CONVERSION(4, "system.table-conversion", true); private final int code; private final String name; diff --git a/polaris-core/src/main/java/org/apache/polaris/core/policy/content/conversion/TableConversionPolicyContent.java b/polaris-core/src/main/java/org/apache/polaris/core/policy/content/conversion/TableConversionPolicyContent.java new file mode 100644 index 0000000000..f6860fc5db --- /dev/null +++ b/polaris-core/src/main/java/org/apache/polaris/core/policy/content/conversion/TableConversionPolicyContent.java @@ -0,0 +1,106 @@ +/* + * 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.core.policy.content.conversion; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.google.common.base.Strings; +import java.util.Map; +import java.util.Set; +import org.apache.polaris.core.policy.content.PolicyContentUtil; +import org.apache.polaris.core.policy.content.StrictBooleanDeserializer; +import org.apache.polaris.core.policy.validator.InvalidPolicyException; + +public class TableConversionPolicyContent { + private static final String DEFAULT_POLICY_SCHEMA_VERSION = "2025-09-09"; + private static final Set POLICY_SCHEMA_VERSIONS = Set.of(DEFAULT_POLICY_SCHEMA_VERSION); + + @JsonDeserialize(using = StrictBooleanDeserializer.class) + private Boolean enable; + + private String version; + private Map config; + + @JsonCreator + public TableConversionPolicyContent( + @JsonProperty(value = "enable", required = true) boolean enable) { + this.enable = enable; + } + + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + + public Boolean enabled() { + return enable; + } + + public void setEnabled(Boolean enable) { + this.enable = enable; + } + + public Map getConfig() { + return config; + } + + public void setConfig(Map config) { + this.config = config; + } + + static void validateVersion( + String content, + TableConversionPolicyContent policy, + String defaultVersion, + Set allVersions) { + if (policy == null) { + throw new InvalidPolicyException("Invalid policy: " + content); + } + + if (Strings.isNullOrEmpty(policy.getVersion())) { + policy.setVersion(defaultVersion); + } + + if (!allVersions.contains(policy.getVersion())) { + throw new InvalidPolicyException("Invalid policy version: " + policy.getVersion()); + } + } + + public static TableConversionPolicyContent fromString(String content) { + if (Strings.isNullOrEmpty(content)) { + throw new InvalidPolicyException("Policy is empty"); + } + + TableConversionPolicyContent policy; + try { + policy = PolicyContentUtil.MAPPER.readValue(content, TableConversionPolicyContent.class); + } catch (Exception e) { + throw new InvalidPolicyException(e); + } + + validateVersion(content, policy, DEFAULT_POLICY_SCHEMA_VERSION, POLICY_SCHEMA_VERSIONS); + + return policy; + } +} diff --git a/polaris-core/src/main/java/org/apache/polaris/core/policy/validator/PolicyValidators.java b/polaris-core/src/main/java/org/apache/polaris/core/policy/validator/PolicyValidators.java index 3be2387833..69b7c89bf9 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/policy/validator/PolicyValidators.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/policy/validator/PolicyValidators.java @@ -22,11 +22,13 @@ import org.apache.polaris.core.entity.PolarisEntity; import org.apache.polaris.core.policy.PolicyEntity; import org.apache.polaris.core.policy.PredefinedPolicyTypes; +import org.apache.polaris.core.policy.content.conversion.TableConversionPolicyContent; import org.apache.polaris.core.policy.content.maintenance.DataCompactionPolicyContent; import org.apache.polaris.core.policy.content.maintenance.MetadataCompactionPolicyContent; import org.apache.polaris.core.policy.content.maintenance.OrphanFileRemovalPolicyContent; import org.apache.polaris.core.policy.content.maintenance.SnapshotExpiryPolicyContent; import org.apache.polaris.core.policy.exceptions.PolicyAttachException; +import org.apache.polaris.core.policy.validator.conversion.TableConversionPolicyValidator; import org.apache.polaris.core.policy.validator.maintenance.BaseMaintenancePolicyValidator; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -66,6 +68,9 @@ public static void validate(PolicyEntity policy) { case ORPHAN_FILE_REMOVAL: OrphanFileRemovalPolicyContent.fromString(policy.getContent()); break; + case TABLE_CONVERSION: + TableConversionPolicyContent.fromString(policy.getContent()); + break; default: throw new IllegalArgumentException("Unsupported policy type: " + type.getName()); } @@ -97,7 +102,8 @@ public static boolean canAttach(PolicyEntity policy, PolarisEntity targetEntity) case SNAPSHOT_EXPIRY: case ORPHAN_FILE_REMOVAL: return BaseMaintenancePolicyValidator.INSTANCE.canAttach(entityType, entitySubType); - + case TABLE_CONVERSION: + return TableConversionPolicyValidator.INSTANCE.canAttach(entityType, entitySubType); default: LOGGER.warn("Attachment not supported for policy type: {}", policyType.getName()); return false; diff --git a/polaris-core/src/main/java/org/apache/polaris/core/policy/validator/conversion/TableConversionPolicyValidator.java b/polaris-core/src/main/java/org/apache/polaris/core/policy/validator/conversion/TableConversionPolicyValidator.java new file mode 100644 index 0000000000..9e5a1ee7cb --- /dev/null +++ b/polaris-core/src/main/java/org/apache/polaris/core/policy/validator/conversion/TableConversionPolicyValidator.java @@ -0,0 +1,42 @@ +/* + * 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.core.policy.validator.conversion; + +import org.apache.polaris.core.entity.PolarisEntitySubType; +import org.apache.polaris.core.entity.PolarisEntityType; +import org.apache.polaris.core.policy.PolicyMappingUtil; +import org.apache.polaris.core.policy.validator.InvalidPolicyException; +import org.apache.polaris.core.policy.validator.PolicyValidator; + +public class TableConversionPolicyValidator implements PolicyValidator { + public static final TableConversionPolicyValidator INSTANCE = + new TableConversionPolicyValidator(); + + @Override + public void validate(String content) throws InvalidPolicyException {} + + @Override + public boolean canAttach(PolarisEntityType entityType, PolarisEntitySubType entitySubType) { + // TODO: refactor this, the util method should be more general + return PolicyMappingUtil.isValidTargetEntityType(entityType, entitySubType) + || (entityType == PolarisEntityType.TABLE_LIKE + && entitySubType == PolarisEntitySubType.GENERIC_TABLE); + } +} diff --git a/polaris-core/src/test/java/org/apache/polaris/core/policy/PolicyTypeTest.java b/polaris-core/src/test/java/org/apache/polaris/core/policy/PolicyTypeTest.java index d120d32482..66b7720a29 100644 --- a/polaris-core/src/test/java/org/apache/polaris/core/policy/PolicyTypeTest.java +++ b/polaris-core/src/test/java/org/apache/polaris/core/policy/PolicyTypeTest.java @@ -34,7 +34,8 @@ static Stream predefinedPolicyTypes() { Arguments.of(0, "system.data-compaction", true), Arguments.of(1, "system.metadata-compaction", true), Arguments.of(2, "system.orphan-file-removal", true), - Arguments.of(3, "system.snapshot-expiry", true)); + Arguments.of(3, "system.snapshot-expiry", true), + Arguments.of(4, "system.table-conversion", true)); } @ParameterizedTest 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 89a2e22302..3ce35ae3d0 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 @@ -242,6 +242,13 @@ protected void authorizeCreateTableLikeUnderNamespaceOperationOrThrow( protected void authorizeBasicTableLikeOperationOrThrow( PolarisAuthorizableOperation op, PolarisEntitySubType subType, TableIdentifier identifier) { + authorizeBasicTableLikeOperationOrThrow(op, List.of(subType), identifier); + } + + protected void authorizeBasicTableLikeOperationOrThrow( + PolarisAuthorizableOperation op, + List subTypes, + TableIdentifier identifier) { resolutionManifest = resolutionManifestFactory.createResolutionManifest( callContext, securityContext, catalogName); @@ -254,10 +261,17 @@ protected void authorizeBasicTableLikeOperationOrThrow( true /* optional */), identifier); resolutionManifest.resolveAll(); - PolarisResolvedPathWrapper target = - resolutionManifest.getResolvedPath(identifier, PolarisEntityType.TABLE_LIKE, subType, true); + PolarisResolvedPathWrapper target = null; + for (PolarisEntitySubType subType : subTypes) { + target = + resolutionManifest.getResolvedPath( + identifier, PolarisEntityType.TABLE_LIKE, subType, true); + if (target != null) { + break; + } + } if (target == null) { - throwNotFoundExceptionForTableLikeEntity(identifier, List.of(subType)); + throwNotFoundExceptionForTableLikeEntity(identifier, subTypes); } authorizer.authorizeOrThrow( polarisPrincipal, diff --git a/runtime/service/src/main/java/org/apache/polaris/service/catalog/policy/PolicyCatalog.java b/runtime/service/src/main/java/org/apache/polaris/service/catalog/policy/PolicyCatalog.java index d142e7c568..2095038095 100644 --- a/runtime/service/src/main/java/org/apache/polaris/service/catalog/policy/PolicyCatalog.java +++ b/runtime/service/src/main/java/org/apache/polaris/service/catalog/policy/PolicyCatalog.java @@ -394,6 +394,8 @@ private List getEffectivePolicies( return List.of(); } + PolarisEntity targetEntity = path.getLast(); + Map inheritablePolicies = new LinkedHashMap<>(); Set directAttachedInheritablePolicies = new HashSet<>(); List nonInheritablePolicies = new ArrayList<>(); @@ -405,7 +407,8 @@ private List getEffectivePolicies( for (var policy : currentPolicies) { // For non-last entities, we only carry forward inheritable policies - if (policy.getPolicyType().isInheritable()) { + if (policy.getPolicyType().isInheritable() + && PolicyValidators.canAttach(policy, targetEntity)) { // Put in map; overwrites by policyType if encountered again inheritablePolicies.put(policy.getPolicyType().getName(), policy); } @@ -413,10 +416,11 @@ private List getEffectivePolicies( } // Now handle the last entity's policies - List lastPolicies = getPolicies(path.getLast(), policyType); + List lastPolicies = getPolicies(targetEntity, policyType); for (var policy : lastPolicies) { - if (policy.getPolicyType().isInheritable()) { + if (policy.getPolicyType().isInheritable() + && PolicyValidators.canAttach(policy, targetEntity)) { // Overwrite anything by the same policyType in the inherited map inheritablePolicies.put(policy.getPolicyType().getName(), policy); directAttachedInheritablePolicies.add(policy.getId()); @@ -468,7 +472,13 @@ private List getFullPath(Namespace namespace, String targetName) resolvedEntityView.getResolvedPath( tableIdentifier, PolarisEntityType.TABLE_LIKE, PolarisEntitySubType.ICEBERG_TABLE); if (resolvedTableEntity == null) { - throw new NoSuchTableException("Iceberg Table does not exist: %s", tableIdentifier); + // TODO: need refactor to allow multiple subType values + resolvedTableEntity = + resolvedEntityView.getResolvedPath( + tableIdentifier, PolarisEntityType.TABLE_LIKE, PolarisEntitySubType.GENERIC_TABLE); + if (resolvedTableEntity == null) { + throw new NoSuchTableException("Table does not exist: %s", tableIdentifier); + } } return resolvedTableEntity.getRawFullPath(); } @@ -508,12 +518,19 @@ private PolarisResolvedPathWrapper getResolvedPathWrapper( } case TABLE_LIKE -> { var tableIdentifier = TableIdentifier.of(target.getPath().toArray(new String[0])); - // only Iceberg tables are supported + // TODO: need refactor to allow multiple subTyps var resolvedTableEntity = resolvedEntityView.getResolvedPath( tableIdentifier, PolarisEntityType.TABLE_LIKE, PolarisEntitySubType.ICEBERG_TABLE); if (resolvedTableEntity == null) { - throw new NoSuchTableException("Iceberg Table does not exist: %s", tableIdentifier); + resolvedTableEntity = + resolvedEntityView.getResolvedPath( + tableIdentifier, + PolarisEntityType.TABLE_LIKE, + PolarisEntitySubType.GENERIC_TABLE); + if (resolvedTableEntity == null) { + throw new NoSuchTableException("Iceberg Table does not exist: %s", tableIdentifier); + } } yield resolvedTableEntity; } diff --git a/runtime/service/src/main/java/org/apache/polaris/service/catalog/policy/PolicyCatalogHandler.java b/runtime/service/src/main/java/org/apache/polaris/service/catalog/policy/PolicyCatalogHandler.java index 96012a2c0b..b1d206f4fe 100644 --- a/runtime/service/src/main/java/org/apache/polaris/service/catalog/policy/PolicyCatalogHandler.java +++ b/runtime/service/src/main/java/org/apache/polaris/service/catalog/policy/PolicyCatalogHandler.java @@ -210,7 +210,9 @@ private void authorizeGetApplicablePoliciesOperationOrThrow( PolarisAuthorizableOperation.GET_APPLICABLE_POLICIES_ON_TABLE; // only Iceberg tables are supported authorizeBasicTableLikeOperationOrThrow( - op, PolarisEntitySubType.ICEBERG_TABLE, tableIdentifier); + op, + List.of(PolarisEntitySubType.ICEBERG_TABLE, PolarisEntitySubType.GENERIC_TABLE), + tableIdentifier); } } @@ -307,7 +309,8 @@ private PolarisAuthorizableOperation determinePolicyMappingOperation( : PolarisAuthorizableOperation.DETACH_POLICY_FROM_NAMESPACE; case TABLE_LIKE -> { PolarisEntitySubType subType = targetWrapper.getRawLeafEntity().getSubType(); - if (subType == PolarisEntitySubType.ICEBERG_TABLE) { + if (subType == PolarisEntitySubType.ICEBERG_TABLE + || subType == PolarisEntitySubType.GENERIC_TABLE) { yield isAttach ? PolarisAuthorizableOperation.ATTACH_POLICY_TO_TABLE : PolarisAuthorizableOperation.DETACH_POLICY_FROM_TABLE; diff --git a/runtime/service/src/main/java/org/apache/polaris/service/catalog/policy/PolicyCatalogUtils.java b/runtime/service/src/main/java/org/apache/polaris/service/catalog/policy/PolicyCatalogUtils.java index 42364a3e45..d47b1d9b89 100644 --- a/runtime/service/src/main/java/org/apache/polaris/service/catalog/policy/PolicyCatalogUtils.java +++ b/runtime/service/src/main/java/org/apache/polaris/service/catalog/policy/PolicyCatalogUtils.java @@ -52,7 +52,15 @@ public static PolarisResolvedPathWrapper getResolvedPathWrapper( resolutionManifest.getResolvedPath( tableIdentifier, PolarisEntityType.TABLE_LIKE, PolarisEntitySubType.ICEBERG_TABLE); if (resolvedTableEntity == null) { - throw new NoSuchTableException("Iceberg Table does not exist: %s", tableIdentifier); + // TODO: refactor the API to avoid this second lookup + resolvedTableEntity = + resolutionManifest.getResolvedPath( + tableIdentifier, + PolarisEntityType.TABLE_LIKE, + PolarisEntitySubType.GENERIC_TABLE); + if (resolvedTableEntity == null) { + throw new NoSuchTableException("Table does not exist: %s", tableIdentifier); + } } yield resolvedTableEntity; } diff --git a/runtime/service/src/test/java/org/apache/polaris/service/admin/PolarisAuthzTestBase.java b/runtime/service/src/test/java/org/apache/polaris/service/admin/PolarisAuthzTestBase.java index fee88c25a5..b416511f12 100644 --- a/runtime/service/src/test/java/org/apache/polaris/service/admin/PolarisAuthzTestBase.java +++ b/runtime/service/src/test/java/org/apache/polaris/service/admin/PolarisAuthzTestBase.java @@ -151,6 +151,8 @@ public Map getConfigOverrides() { // A policy directly under ns1 protected static final PolicyIdentifier POLICY_NS1_1 = new PolicyIdentifier(NS1, "layer1_policy"); + protected static final PolicyIdentifier POLICY_NS1_2 = + new PolicyIdentifier(NS1, "layer1_policy2"); // Two tables under ns1a protected static final TableIdentifier TABLE_NS1A_1 = TableIdentifier.of(NS1A, "table1"); @@ -334,6 +336,11 @@ public void before(TestInfo testInfo) { PredefinedPolicyTypes.DATA_COMPACTION.getName(), "test_policy", "{\"enable\": false}"); + policyCatalog.createPolicy( + POLICY_NS1_2, + PredefinedPolicyTypes.TABLE_CONVERSION.getName(), + "test_policy", + "{\"enable\": false}"); baseCatalog .buildView(VIEW_NS1_1) diff --git a/runtime/service/src/test/java/org/apache/polaris/service/catalog/policy/AbstractPolicyCatalogTest.java b/runtime/service/src/test/java/org/apache/polaris/service/catalog/policy/AbstractPolicyCatalogTest.java index 6a603e3772..5328b12555 100644 --- a/runtime/service/src/test/java/org/apache/polaris/service/catalog/policy/AbstractPolicyCatalogTest.java +++ b/runtime/service/src/test/java/org/apache/polaris/service/catalog/policy/AbstractPolicyCatalogTest.java @@ -22,6 +22,7 @@ import static org.apache.polaris.core.policy.PredefinedPolicyTypes.DATA_COMPACTION; import static org.apache.polaris.core.policy.PredefinedPolicyTypes.METADATA_COMPACTION; import static org.apache.polaris.core.policy.PredefinedPolicyTypes.ORPHAN_FILE_REMOVAL; +import static org.apache.polaris.core.policy.PredefinedPolicyTypes.TABLE_CONVERSION; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatCode; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -36,6 +37,7 @@ import java.lang.reflect.Method; import java.util.Arrays; import java.util.List; +import java.util.Map; import java.util.Set; import org.apache.iceberg.CatalogProperties; import org.apache.iceberg.Schema; @@ -51,6 +53,7 @@ import org.apache.polaris.core.auth.PolarisAuthorizer; import org.apache.polaris.core.auth.PolarisAuthorizerImpl; import org.apache.polaris.core.auth.PolarisPrincipal; +import org.apache.polaris.core.catalog.GenericTableCatalog; import org.apache.polaris.core.config.FeatureConfiguration; import org.apache.polaris.core.config.PolarisConfigurationStore; import org.apache.polaris.core.config.RealmConfig; @@ -77,6 +80,7 @@ import org.apache.polaris.core.storage.cache.StorageCredentialCache; import org.apache.polaris.service.admin.PolarisAdminService; import org.apache.polaris.service.catalog.PolarisPassthroughResolutionView; +import org.apache.polaris.service.catalog.generic.PolarisGenericTableCatalog; import org.apache.polaris.service.catalog.iceberg.IcebergCatalog; import org.apache.polaris.service.catalog.io.DefaultFileIOFactory; import org.apache.polaris.service.catalog.io.FileIOFactory; @@ -132,6 +136,7 @@ public abstract class AbstractPolicyCatalogTest { @Inject ResolutionManifestFactory resolutionManifestFactory; private PolicyCatalog policyCatalog; + private GenericTableCatalog genericTableCatalog; private IcebergCatalog icebergCatalog; private AwsStorageConfigInfo storageConfigModel; private String realmName; @@ -250,6 +255,8 @@ public void before(TestInfo testInfo) { .thenReturn((PolarisStorageIntegration) storageIntegration); this.policyCatalog = new PolicyCatalog(metaStoreManager, polarisContext, passthroughView); + this.genericTableCatalog = + new PolarisGenericTableCatalog(metaStoreManager, polarisContext, passthroughView); this.icebergCatalog = new IcebergCatalog( diagServices, @@ -487,6 +494,33 @@ public void testAttachPolicy() { assertThat(policyCatalog.getApplicablePolicies(null, null, null).size()).isEqualTo(1); } + @Test + public void testAttachPolicyToGenericTable() { + icebergCatalog.createNamespace(NS); + TableIdentifier GENERIC_TABLE = TableIdentifier.of(NS, "test-generic-table"); + genericTableCatalog.createGenericTable(GENERIC_TABLE, "delta", "test", "test", Map.of()); + policyCatalog.createPolicy(POLICY1, TABLE_CONVERSION.getName(), "test", "{\"enable\": false}"); + var target = + new PolicyAttachmentTarget( + PolicyAttachmentTarget.TypeEnum.TABLE_LIKE, + List.of(GENERIC_TABLE.toString().split("\\."))); + policyCatalog.attachPolicy(POLICY1, target, null); + assertThat(policyCatalog.getApplicablePolicies(NS, GENERIC_TABLE.name(), null).size()) + .isEqualTo(1); + } + + @Test + public void testAttachPolicyToCatalogAndGenericTableInherit() { + icebergCatalog.createNamespace(NS); + TableIdentifier GENERIC_TABLE = TableIdentifier.of(NS, "test-generic-table"); + genericTableCatalog.createGenericTable(GENERIC_TABLE, "delta", "test", "test", Map.of()); + policyCatalog.createPolicy(POLICY1, TABLE_CONVERSION.getName(), "test", "{\"enable\": false}"); + var target = new PolicyAttachmentTarget(PolicyAttachmentTarget.TypeEnum.CATALOG, List.of()); + policyCatalog.attachPolicy(POLICY1, target, null); + assertThat(policyCatalog.getApplicablePolicies(NS, GENERIC_TABLE.name(), null).size()) + .isEqualTo(1); + } + @Test public void testAttachPolicyConflict() { icebergCatalog.createNamespace(NS); diff --git a/runtime/service/src/test/java/org/apache/polaris/service/catalog/policy/PolicyCatalogHandlerAuthzTest.java b/runtime/service/src/test/java/org/apache/polaris/service/catalog/policy/PolicyCatalogHandlerAuthzTest.java index 990a9cff28..cab1a282c0 100644 --- a/runtime/service/src/test/java/org/apache/polaris/service/catalog/policy/PolicyCatalogHandlerAuthzTest.java +++ b/runtime/service/src/test/java/org/apache/polaris/service/catalog/policy/PolicyCatalogHandlerAuthzTest.java @@ -451,6 +451,34 @@ public void testAttachPolicyToTableSufficientPrivileges() { () -> newWrapper(Set.of(PRINCIPAL_ROLE2)).detachPolicy(POLICY_NS1_1, detachPolicyRequest)); } + @Test + public void testAttachPolicyToGenericTableSufficientPrivileges() { + assertSuccess( + adminService.grantPrivilegeOnCatalogToRole( + CATALOG_NAME, CATALOG_ROLE2, PolarisPrivilege.POLICY_DETACH)); + assertSuccess( + adminService.grantPrivilegeOnCatalogToRole( + CATALOG_NAME, CATALOG_ROLE2, PolarisPrivilege.TABLE_DETACH_POLICY)); + + PolicyAttachmentTarget tableTarget = + PolicyAttachmentTarget.builder() + .setType(PolicyAttachmentTarget.TypeEnum.TABLE_LIKE) + .setPath(PolarisCatalogHelpers.tableIdentifierToList(TABLE_NS1_1_GENERIC)) + .build(); + AttachPolicyRequest attachPolicyRequest = + AttachPolicyRequest.builder().setTarget(tableTarget).build(); + DetachPolicyRequest detachPolicyRequest = + DetachPolicyRequest.builder().setTarget(tableTarget).build(); + + doTestSufficientPrivilegeSets( + List.of( + Set.of(PolarisPrivilege.POLICY_ATTACH, PolarisPrivilege.TABLE_ATTACH_POLICY), + Set.of(PolarisPrivilege.CATALOG_MANAGE_METADATA), + Set.of(PolarisPrivilege.CATALOG_MANAGE_CONTENT)), + () -> newWrapper(Set.of(PRINCIPAL_ROLE1)).attachPolicy(POLICY_NS1_2, attachPolicyRequest), + () -> newWrapper(Set.of(PRINCIPAL_ROLE2)).detachPolicy(POLICY_NS1_2, detachPolicyRequest)); + } + @Test public void testAttachPolicyToTableInsufficientPrivileges() { PolicyAttachmentTarget tableTarget = @@ -750,6 +778,20 @@ public void testGetApplicablePoliciesOnTableSufficientPrivileges() { null /* cleanupAction */); } + @Test + public void testGetApplicablePoliciesOnGenericTableSufficientPrivileges() { + doTestSufficientPrivileges( + List.of( + PolarisPrivilege.TABLE_READ_PROPERTIES, + PolarisPrivilege.TABLE_WRITE_PROPERTIES, + PolarisPrivilege.CATALOG_MANAGE_METADATA), + () -> + newWrapper() + .getApplicablePolicies( + TABLE_NS1_1_GENERIC.namespace(), TABLE_NS1_1_GENERIC.name(), null), + null /* cleanupAction */); + } + @Test public void testGetApplicablePoliciesOnTableInsufficientPrivileges() { doTestInsufficientPrivileges(