This is an automated email from the ASF dual-hosted git repository. yufei 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 6a4d2a18c Policy Store: Check whether Policy is in use before dropping and support `detach-all` flag (#1467) 6a4d2a18c is described below commit 6a4d2a18c379df83cea2a8f0588fb145ad262fb7 Author: Honah (Jonas) J. <hon...@apache.org> AuthorDate: Tue Apr 29 01:29:17 2025 -0500 Policy Store: Check whether Policy is in use before dropping and support `detach-all` flag (#1467) --- .../apache/polaris/service/it/env/PolicyApi.java | 15 +++++- .../test/PolarisPolicyServiceIntegrationTest.java | 16 +++++- .../AtomicOperationMetaStoreManager.java | 36 +++++++++++++- .../core/persistence/dao/entity/BaseResult.java | 3 ++ .../TransactionalMetaStoreManagerImpl.java | 37 +++++++++++++- .../TreeMapTransactionalPersistenceImpl.java | 4 +- ...PolicyValidator.java => PolicyMappingUtil.java} | 22 ++++---- .../policy/exceptions/PolicyInUseException.java | 34 +++++++++++++ .../BaseMaintenancePolicyValidator.java | 23 +-------- .../BasePolarisMetaStoreManagerTest.java | 5 ++ .../persistence/PolarisTestMetaStoreManager.java | 58 ++++++++++++++++++++++ .../service/quarkus/catalog/PolicyCatalogTest.java | 25 ++++++++++ .../service/catalog/policy/PolicyCatalog.java | 9 +++- .../service/exception/PolarisExceptionMapper.java | 3 ++ 14 files changed, 250 insertions(+), 40 deletions(-) diff --git a/integration-tests/src/main/java/org/apache/polaris/service/it/env/PolicyApi.java b/integration-tests/src/main/java/org/apache/polaris/service/it/env/PolicyApi.java index 7597aaef3..6061ef8c9 100644 --- a/integration-tests/src/main/java/org/apache/polaris/service/it/env/PolicyApi.java +++ b/integration-tests/src/main/java/org/apache/polaris/service/it/env/PolicyApi.java @@ -28,6 +28,7 @@ import java.util.Map; import org.apache.iceberg.catalog.Namespace; import org.apache.iceberg.rest.RESTUtil; import org.apache.polaris.core.policy.PolicyType; +import org.apache.polaris.core.policy.exceptions.PolicyInUseException; import org.apache.polaris.service.types.ApplicablePolicy; import org.apache.polaris.service.types.AttachPolicyRequest; import org.apache.polaris.service.types.CreatePolicyRequest; @@ -72,12 +73,24 @@ public class PolicyApi extends RestApi { } public void dropPolicy(String catalog, PolicyIdentifier policyIdentifier) { + dropPolicy(catalog, policyIdentifier, null); + } + + public void dropPolicy(String catalog, PolicyIdentifier policyIdentifier, Boolean detachAll) { String ns = RESTUtil.encodeNamespace(policyIdentifier.getNamespace()); + Map<String, String> queryParams = new HashMap<>(); + if (detachAll != null) { + queryParams.put("detach-all", detachAll.toString()); + } try (Response res = request( "polaris/v1/{cat}/namespaces/{ns}/policies/{policy}", - Map.of("cat", catalog, "ns", ns, "policy", policyIdentifier.getName())) + Map.of("cat", catalog, "ns", ns, "policy", policyIdentifier.getName()), + queryParams) .delete()) { + if (res.getStatus() == Response.Status.BAD_REQUEST.getStatusCode()) { + throw new PolicyInUseException("Policy in use"); + } Assertions.assertThat(res.getStatus()).isEqualTo(Response.Status.NO_CONTENT.getStatusCode()); } } 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 055fa411c..2b833ba74 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 @@ -50,6 +50,7 @@ import org.apache.polaris.core.admin.model.StorageConfigInfo; import org.apache.polaris.core.catalog.PolarisCatalogHelpers; import org.apache.polaris.core.entity.CatalogEntity; import org.apache.polaris.core.policy.PredefinedPolicyTypes; +import org.apache.polaris.core.policy.exceptions.PolicyInUseException; import org.apache.polaris.service.it.env.ClientCredentials; import org.apache.polaris.service.it.env.IcebergHelper; import org.apache.polaris.service.it.env.IntegrationTestsHelper; @@ -268,8 +269,21 @@ public class PolarisPolicyServiceIntegrationTest { PredefinedPolicyTypes.DATA_COMPACTION, EXAMPLE_TABLE_MAINTENANCE_POLICY_CONTENT, "test policy"); - policyApi.dropPolicy(currentCatalogName, NS1_P1); + + PolicyAttachmentTarget catalogTarget = + PolicyAttachmentTarget.builder().setType(PolicyAttachmentTarget.TypeEnum.CATALOG).build(); + policyApi.attachPolicy(currentCatalogName, NS1_P1, catalogTarget, Map.of()); + + // dropPolicy should fail because the policy is attached to the catalog + Assertions.assertThatThrownBy(() -> policyApi.dropPolicy(currentCatalogName, NS1_P1)) + .isInstanceOf(PolicyInUseException.class); + + // with detach-all=true, the policy and the attachment should be dropped + policyApi.dropPolicy(currentCatalogName, NS1_P1, true); Assertions.assertThat(policyApi.listPolicies(currentCatalogName, NS1)).hasSize(0); + // The policy mapping record should be dropped + Assertions.assertThat(policyApi.getApplicablePolicies(currentCatalogName, null, null, null)) + .hasSize(0); } @Test diff --git a/polaris-core/src/main/java/org/apache/polaris/core/persistence/AtomicOperationMetaStoreManager.java b/polaris-core/src/main/java/org/apache/polaris/core/persistence/AtomicOperationMetaStoreManager.java index d6cfd8429..2170550dd 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/persistence/AtomicOperationMetaStoreManager.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/persistence/AtomicOperationMetaStoreManager.java @@ -64,6 +64,7 @@ import org.apache.polaris.core.persistence.dao.entity.ResolvedEntityResult; import org.apache.polaris.core.persistence.dao.entity.ScopedCredentialsResult; import org.apache.polaris.core.policy.PolarisPolicyMappingRecord; import org.apache.polaris.core.policy.PolicyEntity; +import org.apache.polaris.core.policy.PolicyMappingUtil; import org.apache.polaris.core.policy.PolicyType; import org.apache.polaris.core.storage.PolarisCredentialProperty; import org.apache.polaris.core.storage.PolarisStorageConfigurationInfo; @@ -191,6 +192,27 @@ public class AtomicOperationMetaStoreManager extends BaseMetaStoreManager { ms.loadAllGrantRecordsOnSecurable(callCtx, entity.getCatalogId(), entity.getId()); ms.deleteAllEntityGrantRecords(callCtx, entity, grantsOnGrantee, grantsOnSecurable); + if (entity.getType() == PolarisEntityType.POLICY + || PolicyMappingUtil.isValidTargetEntityType(entity.getType(), entity.getSubType())) { + // Best-effort cleanup - for policy and potential target entities, drop all policy mapping + // records related + // TODO: Support some more formal garbage-collection mechanism similar to grant records case + // above + try { + final List<PolarisPolicyMappingRecord> mappingOnPolicy = + (entity.getType() == PolarisEntityType.POLICY) + ? ms.loadAllTargetsOnPolicy(callCtx, entity.getCatalogId(), entity.getId()) + : List.of(); + final List<PolarisPolicyMappingRecord> mappingOnTarget = + (entity.getType() == PolarisEntityType.POLICY) + ? List.of() + : ms.loadAllPoliciesOnTarget(callCtx, entity.getCatalogId(), entity.getId()); + ms.deleteAllEntityPolicyMappingRecords(callCtx, entity, mappingOnTarget, mappingOnPolicy); + } catch (UnsupportedOperationException e) { + // Policy mapping persistence not implemented, but we should not block dropping entities + } + } + // Now determine the set of entities on the other side of the grants we just removed. Grants // from/to these entities has been removed, hence we need to update the grant version of // each entity. Collect the id of each. @@ -1177,6 +1199,18 @@ public class AtomicOperationMetaStoreManager extends BaseMetaStoreManager { callCtx, null, refreshEntityToDrop.getCatalogId(), refreshEntityToDrop.getId())) { return new DropEntityResult(BaseResult.ReturnStatus.NAMESPACE_NOT_EMPTY, null); } + } else if (refreshEntityToDrop.getType() == PolarisEntityType.POLICY && !cleanup) { + try { + // need to check if the policy is attached to any entity + List<PolarisPolicyMappingRecord> records = + ms.loadAllTargetsOnPolicy( + callCtx, refreshEntityToDrop.getCatalogId(), refreshEntityToDrop.getId()); + if (!records.isEmpty()) { + return new DropEntityResult(BaseResult.ReturnStatus.POLICY_HAS_MAPPINGS, null); + } + } catch (UnsupportedOperationException e) { + // Policy mapping persistence not implemented, but we should not block dropping entities + } } // simply delete that entity. Will be removed from entities_active, added to the @@ -1186,7 +1220,7 @@ public class AtomicOperationMetaStoreManager extends BaseMetaStoreManager { // if cleanup, schedule a cleanup task for the entity. do this here, so that drop and scheduling // the cleanup task is transactional. Otherwise, we'll be unable to schedule the cleanup task // later - if (cleanup) { + if (cleanup && refreshEntityToDrop.getType() != PolarisEntityType.POLICY) { PolarisBaseEntity taskEntity = new PolarisEntity.Builder() .setId(ms.generateNewId(callCtx)) diff --git a/polaris-core/src/main/java/org/apache/polaris/core/persistence/dao/entity/BaseResult.java b/polaris-core/src/main/java/org/apache/polaris/core/persistence/dao/entity/BaseResult.java index a4eee22cc..a8f3d5aef 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/persistence/dao/entity/BaseResult.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/persistence/dao/entity/BaseResult.java @@ -117,6 +117,9 @@ public class BaseResult { // policy mapping of same type already exists POLICY_MAPPING_OF_SAME_TYPE_ALREADY_EXISTS(15), + + // policy has mappings and cannot be dropped + POLICY_HAS_MAPPINGS(16), ; // code for the enum diff --git a/polaris-core/src/main/java/org/apache/polaris/core/persistence/transactional/TransactionalMetaStoreManagerImpl.java b/polaris-core/src/main/java/org/apache/polaris/core/persistence/transactional/TransactionalMetaStoreManagerImpl.java index 22120c7a3..9f508eb4e 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/persistence/transactional/TransactionalMetaStoreManagerImpl.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/persistence/transactional/TransactionalMetaStoreManagerImpl.java @@ -65,6 +65,7 @@ import org.apache.polaris.core.persistence.dao.entity.ResolvedEntityResult; import org.apache.polaris.core.persistence.dao.entity.ScopedCredentialsResult; import org.apache.polaris.core.policy.PolarisPolicyMappingRecord; import org.apache.polaris.core.policy.PolicyEntity; +import org.apache.polaris.core.policy.PolicyMappingUtil; import org.apache.polaris.core.policy.PolicyType; import org.apache.polaris.core.storage.PolarisCredentialProperty; import org.apache.polaris.core.storage.PolarisStorageConfigurationInfo; @@ -193,6 +194,28 @@ public class TransactionalMetaStoreManagerImpl extends BaseMetaStoreManager { ms.writeEntityInCurrentTxn(callCtx, entityGrantChanged, false, originalEntity); } + if (entity.getType() == PolarisEntityType.POLICY + || PolicyMappingUtil.isValidTargetEntityType(entity.getType(), entity.getSubType())) { + // Best-effort cleanup - for policy and potential target entities, drop all policy mapping + // records related + try { + final List<PolarisPolicyMappingRecord> mappingOnPolicy = + (entity.getType() == PolarisEntityType.POLICY) + ? ms.loadAllTargetsOnPolicyInCurrentTxn( + callCtx, entity.getCatalogId(), entity.getId()) + : List.of(); + final List<PolarisPolicyMappingRecord> mappingOnTarget = + (entity.getType() == PolarisEntityType.POLICY) + ? List.of() + : ms.loadAllPoliciesOnTargetInCurrentTxn( + callCtx, entity.getCatalogId(), entity.getId()); + ms.deleteAllEntityPolicyMappingRecordsInCurrentTxn( + callCtx, entity, mappingOnTarget, mappingOnPolicy); + } catch (UnsupportedOperationException e) { + // Policy mapping persistence not implemented, but we should not block dropping entities + } + } + // remove the entity being dropped now ms.deleteEntityInCurrentTxn(callCtx, entity); @@ -1359,6 +1382,18 @@ public class TransactionalMetaStoreManagerImpl extends BaseMetaStoreManager { callCtx, null, refreshEntityToDrop.getCatalogId(), refreshEntityToDrop.getId())) { return new DropEntityResult(BaseResult.ReturnStatus.NAMESPACE_NOT_EMPTY, null); } + } else if (refreshEntityToDrop.getType() == PolarisEntityType.POLICY && !cleanup) { + // need to check if the policy is attached to any entity + try { + List<PolarisPolicyMappingRecord> records = + ms.loadAllTargetsOnPolicyInCurrentTxn( + callCtx, refreshEntityToDrop.getCatalogId(), refreshEntityToDrop.getId()); + if (!records.isEmpty()) { + return new DropEntityResult(BaseResult.ReturnStatus.POLICY_HAS_MAPPINGS, null); + } + } catch (UnsupportedOperationException e) { + // Policy mapping persistence not implemented, but we should not block dropping entities + } } // simply delete that entity. Will be removed from entities_active, added to the @@ -1368,7 +1403,7 @@ public class TransactionalMetaStoreManagerImpl extends BaseMetaStoreManager { // if cleanup, schedule a cleanup task for the entity. do this here, so that drop and scheduling // the cleanup task is transactional. Otherwise, we'll be unable to schedule the cleanup task // later - if (cleanup) { + if (cleanup && refreshEntityToDrop.getType() != PolarisEntityType.POLICY) { PolarisBaseEntity taskEntity = new PolarisEntity.Builder() .setId(ms.generateNewIdInCurrentTxn(callCtx)) diff --git a/polaris-core/src/main/java/org/apache/polaris/core/persistence/transactional/TreeMapTransactionalPersistenceImpl.java b/polaris-core/src/main/java/org/apache/polaris/core/persistence/transactional/TreeMapTransactionalPersistenceImpl.java index e9cbd53f3..39ce364d3 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/persistence/transactional/TreeMapTransactionalPersistenceImpl.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/persistence/transactional/TreeMapTransactionalPersistenceImpl.java @@ -589,9 +589,9 @@ public class TreeMapTransactionalPersistenceImpl extends AbstractTransactionalPe // also delete the other side. We need to delete these mapping one at a time versus doing a // range delete - mappingOnTarget.forEach(record -> this.store.getSlicePolicyMappingRecords().delete(record)); - mappingOnPolicy.forEach( + mappingOnTarget.forEach( record -> this.store.getSlicePolicyMappingRecordsByPolicy().delete(record)); + mappingOnPolicy.forEach(record -> this.store.getSlicePolicyMappingRecords().delete(record)); } /** {@inheritDoc} */ diff --git a/polaris-core/src/main/java/org/apache/polaris/core/policy/validator/maintenance/BaseMaintenancePolicyValidator.java b/polaris-core/src/main/java/org/apache/polaris/core/policy/PolicyMappingUtil.java similarity index 73% copy from polaris-core/src/main/java/org/apache/polaris/core/policy/validator/maintenance/BaseMaintenancePolicyValidator.java copy to polaris-core/src/main/java/org/apache/polaris/core/policy/PolicyMappingUtil.java index 86308f555..3bf1296c2 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/policy/validator/maintenance/BaseMaintenancePolicyValidator.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/policy/PolicyMappingUtil.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package org.apache.polaris.core.policy.validator.maintenance; +package org.apache.polaris.core.policy; import static org.apache.polaris.core.entity.PolarisEntityType.CATALOG; import static org.apache.polaris.core.entity.PolarisEntityType.NAMESPACE; @@ -25,21 +25,21 @@ import static org.apache.polaris.core.entity.PolarisEntityType.TABLE_LIKE; import java.util.Set; import org.apache.polaris.core.entity.PolarisEntitySubType; import org.apache.polaris.core.entity.PolarisEntityType; -import org.apache.polaris.core.policy.validator.InvalidPolicyException; -import org.apache.polaris.core.policy.validator.PolicyValidator; -public class BaseMaintenancePolicyValidator implements PolicyValidator { - public static final BaseMaintenancePolicyValidator INSTANCE = - new BaseMaintenancePolicyValidator(); +public class PolicyMappingUtil { private static final Set<PolarisEntityType> ATTACHABLE_ENTITY_TYPES = Set.of(CATALOG, NAMESPACE, TABLE_LIKE); - @Override - public void validate(String content) throws InvalidPolicyException {} - - @Override - public boolean canAttach(PolarisEntityType entityType, PolarisEntitySubType entitySubType) { + /** + * Checks if the given entity type and subtype are valid targets for policy attachment. + * + * <p>This method excludes non-attachable entities such as catalog-role and task. Each policy type + * may impose additional specific rules * to determine whether it can target a particular entity + * type or subtype. + */ + public static boolean isValidTargetEntityType( + PolarisEntityType entityType, PolarisEntitySubType entitySubType) { if (entityType == null) { return false; } diff --git a/polaris-core/src/main/java/org/apache/polaris/core/policy/exceptions/PolicyInUseException.java b/polaris-core/src/main/java/org/apache/polaris/core/policy/exceptions/PolicyInUseException.java new file mode 100644 index 000000000..14d165497 --- /dev/null +++ b/polaris-core/src/main/java/org/apache/polaris/core/policy/exceptions/PolicyInUseException.java @@ -0,0 +1,34 @@ +/* + * 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.exceptions; + +import com.google.errorprone.annotations.FormatMethod; +import org.apache.polaris.core.exceptions.PolarisException; + +public class PolicyInUseException extends PolarisException { + @FormatMethod + public PolicyInUseException(String message, Object... args) { + super(String.format(message, args)); + } + + @FormatMethod + public PolicyInUseException(Throwable cause, String message, Object... args) { + super(String.format(message, args), cause); + } +} diff --git a/polaris-core/src/main/java/org/apache/polaris/core/policy/validator/maintenance/BaseMaintenancePolicyValidator.java b/polaris-core/src/main/java/org/apache/polaris/core/policy/validator/maintenance/BaseMaintenancePolicyValidator.java index 86308f555..f5a93e8ca 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/policy/validator/maintenance/BaseMaintenancePolicyValidator.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/policy/validator/maintenance/BaseMaintenancePolicyValidator.java @@ -18,13 +18,9 @@ */ package org.apache.polaris.core.policy.validator.maintenance; -import static org.apache.polaris.core.entity.PolarisEntityType.CATALOG; -import static org.apache.polaris.core.entity.PolarisEntityType.NAMESPACE; -import static org.apache.polaris.core.entity.PolarisEntityType.TABLE_LIKE; - -import java.util.Set; 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; @@ -32,26 +28,11 @@ public class BaseMaintenancePolicyValidator implements PolicyValidator { public static final BaseMaintenancePolicyValidator INSTANCE = new BaseMaintenancePolicyValidator(); - private static final Set<PolarisEntityType> ATTACHABLE_ENTITY_TYPES = - Set.of(CATALOG, NAMESPACE, TABLE_LIKE); - @Override public void validate(String content) throws InvalidPolicyException {} @Override public boolean canAttach(PolarisEntityType entityType, PolarisEntitySubType entitySubType) { - if (entityType == null) { - return false; - } - - if (!ATTACHABLE_ENTITY_TYPES.contains(entityType)) { - return false; - } - - if (entityType == TABLE_LIKE && entitySubType != PolarisEntitySubType.ICEBERG_TABLE) { - return false; - } - - return true; + return PolicyMappingUtil.isValidTargetEntityType(entityType, entitySubType); } } diff --git a/polaris-core/src/testFixtures/java/org/apache/polaris/core/persistence/BasePolarisMetaStoreManagerTest.java b/polaris-core/src/testFixtures/java/org/apache/polaris/core/persistence/BasePolarisMetaStoreManagerTest.java index d11bed82d..f58313712 100644 --- a/polaris-core/src/testFixtures/java/org/apache/polaris/core/persistence/BasePolarisMetaStoreManagerTest.java +++ b/polaris-core/src/testFixtures/java/org/apache/polaris/core/persistence/BasePolarisMetaStoreManagerTest.java @@ -292,6 +292,11 @@ public abstract class BasePolarisMetaStoreManagerTest { polarisTestMetaStoreManager.testPolicyMapping(); } + @Test + protected void testPolicyMappingCleanup() { + polarisTestMetaStoreManager.testPolicyMappingCleanup(); + } + @Test protected void testLoadTasks() { for (int i = 0; i < 20; i++) { diff --git a/polaris-core/src/testFixtures/java/org/apache/polaris/core/persistence/PolarisTestMetaStoreManager.java b/polaris-core/src/testFixtures/java/org/apache/polaris/core/persistence/PolarisTestMetaStoreManager.java index b401d4efb..428ecf00c 100644 --- a/polaris-core/src/testFixtures/java/org/apache/polaris/core/persistence/PolarisTestMetaStoreManager.java +++ b/polaris-core/src/testFixtures/java/org/apache/polaris/core/persistence/PolarisTestMetaStoreManager.java @@ -835,6 +835,12 @@ public class PolarisTestMetaStoreManager { Assertions.assertThat(dropResult.isSuccess()).isFalse(); Assertions.assertThat(dropResult.failedBecauseNotEmpty()).isTrue(); Assertions.assertThat(dropResult.isEntityUnDroppable()).isFalse(); + } else if (entityToDrop.getType() == PolarisEntityType.POLICY) { + // When dropping policy with cleanup = true, we do not need cleanup task + Assertions.assertThat(dropResult.isSuccess()).isEqualTo(exists); + Assertions.assertThat(dropResult.failedBecauseNotEmpty()).isFalse(); + Assertions.assertThat(dropResult.isEntityUnDroppable()).isFalse(); + Assertions.assertThat(dropResult.getCleanupTaskId()).isNull(); } else { Assertions.assertThat(dropResult.isSuccess()).isEqualTo(exists); Assertions.assertThat(dropResult.failedBecauseNotEmpty()).isFalse(); @@ -2788,4 +2794,56 @@ public class PolarisTestMetaStoreManager { detachPolicyFromTarget(List.of(catalog, N1, N1_N2), N1_N2_T1, List.of(catalog, N1), N1_P1); detachPolicyFromTarget(List.of(catalog, N1, N1_N2), N1_N2_T1, List.of(catalog, N5), N5_P3); } + + void testPolicyMappingCleanup() { + PolarisBaseEntity catalog = this.createTestCatalog("test"); + Assertions.assertThat(catalog).isNotNull(); + + PolarisBaseEntity N1 = + this.ensureExistsByName(List.of(catalog), PolarisEntityType.NAMESPACE, "N1"); + PolarisBaseEntity N1_N2 = + this.ensureExistsByName(List.of(catalog, N1), PolarisEntityType.NAMESPACE, "N2"); + PolarisBaseEntity N1_N2_T1 = + this.ensureExistsByName( + List.of(catalog, N1, N1_N2), + PolarisEntityType.TABLE_LIKE, + PolarisEntitySubType.ANY_SUBTYPE, + "T1"); + + PolarisBaseEntity N1_N2_T3 = + this.createEntity( + List.of(catalog, N1, N1_N2), + PolarisEntityType.TABLE_LIKE, + PolarisEntitySubType.ICEBERG_TABLE, + "T3"); + PolicyEntity N1_P1 = + this.createPolicy(List.of(catalog, N1), "P1", PredefinedPolicyTypes.DATA_COMPACTION); + + PolicyEntity N1_P2 = + this.createPolicy(List.of(catalog, N1), "P2", PredefinedPolicyTypes.DATA_COMPACTION); + + attachPolicyToTarget(List.of(catalog, N1, N1_N2), N1_N2_T3, List.of(catalog, N1), N1_P1); + LoadPolicyMappingsResult loadPolicyMappingsResult = + polarisMetaStoreManager.loadPoliciesOnEntity(polarisCallContext, N1_N2_T3); + Assertions.assertThat(loadPolicyMappingsResult.isSuccess()).isTrue(); + Assertions.assertThat(loadPolicyMappingsResult.getEntities()).hasSize(1); + + // Drop N1_N2_T1, the corresponding policy mapping should be cleaned-up + this.dropEntity(List.of(catalog, N1, N1_N2), N1_N2_T3); + + BasePersistence ms = polarisCallContext.getMetaStore(); + Assertions.assertThat( + ms.loadAllTargetsOnPolicy(polarisCallContext, N1_P1.getCatalogId(), N1_P1.getId())) + .isEmpty(); + + attachPolicyToTarget(List.of(catalog, N1, N1_N2), N1_N2_T1, List.of(catalog, N1), N1_P2); + + // Drop N1_P2, the dropEntity helper will have cleanup enabled to detach the policy from all + // targets + this.dropEntity(List.of(catalog, N1), N1_P2); + loadPolicyMappingsResult = + polarisMetaStoreManager.loadPoliciesOnEntity(polarisCallContext, N1_N2_T1); + Assertions.assertThat(loadPolicyMappingsResult.isSuccess()).isTrue(); + Assertions.assertThat(loadPolicyMappingsResult.getEntities()).isEmpty(); + } } diff --git a/quarkus/service/src/test/java/org/apache/polaris/service/quarkus/catalog/PolicyCatalogTest.java b/quarkus/service/src/test/java/org/apache/polaris/service/quarkus/catalog/PolicyCatalogTest.java index 57bd52132..a5bf98ba9 100644 --- a/quarkus/service/src/test/java/org/apache/polaris/service/quarkus/catalog/PolicyCatalogTest.java +++ b/quarkus/service/src/test/java/org/apache/polaris/service/quarkus/catalog/PolicyCatalogTest.java @@ -77,6 +77,7 @@ import org.apache.polaris.core.persistence.dao.entity.PrincipalSecretsResult; import org.apache.polaris.core.persistence.transactional.TransactionalPersistence; import org.apache.polaris.core.policy.PredefinedPolicyTypes; import org.apache.polaris.core.policy.exceptions.NoSuchPolicyException; +import org.apache.polaris.core.policy.exceptions.PolicyInUseException; import org.apache.polaris.core.policy.exceptions.PolicyVersionMismatchException; import org.apache.polaris.core.policy.validator.InvalidPolicyException; import org.apache.polaris.core.secrets.UserSecretsManager; @@ -506,6 +507,30 @@ public class PolicyCatalogTest { .isInstanceOf(NoSuchPolicyException.class); } + @Test + public void testDropPolicyInUse() { + icebergCatalog.createNamespace(NS); + policyCatalog.createPolicy( + POLICY1, PredefinedPolicyTypes.DATA_COMPACTION.getName(), "test", "{\"enable\": false}"); + var target = new PolicyAttachmentTarget(PolicyAttachmentTarget.TypeEnum.CATALOG, List.of()); + policyCatalog.attachPolicy(POLICY1, target, null); + + assertThatThrownBy(() -> policyCatalog.dropPolicy(POLICY1, false)) + .isInstanceOf(PolicyInUseException.class); + + // The policy is still attached to the catalog + List<ApplicablePolicy> applicablePolicies = + policyCatalog.getApplicablePolicies(null, null, null); + assertThat(applicablePolicies.size()).isEqualTo(1); + + // Drop the policy with detach-all flag + policyCatalog.dropPolicy(POLICY1, true); + + // The policy should be detached from the catalog and dropped + applicablePolicies = policyCatalog.getApplicablePolicies(null, null, null); + assertThat(applicablePolicies.size()).isEqualTo(0); + } + @Test public void testDropPolicyNotExist() { icebergCatalog.createNamespace(NS); diff --git a/service/common/src/main/java/org/apache/polaris/service/catalog/policy/PolicyCatalog.java b/service/common/src/main/java/org/apache/polaris/service/catalog/policy/PolicyCatalog.java index 45e5f17e6..066d513a3 100644 --- a/service/common/src/main/java/org/apache/polaris/service/catalog/policy/PolicyCatalog.java +++ b/service/common/src/main/java/org/apache/polaris/service/catalog/policy/PolicyCatalog.java @@ -18,6 +18,7 @@ */ package org.apache.polaris.service.catalog.policy; +import static org.apache.polaris.core.persistence.dao.entity.BaseResult.ReturnStatus.POLICY_HAS_MAPPINGS; import static org.apache.polaris.core.persistence.dao.entity.BaseResult.ReturnStatus.POLICY_MAPPING_OF_SAME_TYPE_ALREADY_EXISTS; import static org.apache.polaris.service.types.PolicyAttachmentTarget.TypeEnum.CATALOG; @@ -54,6 +55,7 @@ import org.apache.polaris.core.policy.PolicyEntity; import org.apache.polaris.core.policy.PolicyType; import org.apache.polaris.core.policy.exceptions.NoSuchPolicyException; import org.apache.polaris.core.policy.exceptions.PolicyAttachException; +import org.apache.polaris.core.policy.exceptions.PolicyInUseException; import org.apache.polaris.core.policy.exceptions.PolicyVersionMismatchException; import org.apache.polaris.core.policy.validator.PolicyValidators; import org.apache.polaris.service.types.ApplicablePolicy; @@ -254,7 +256,6 @@ public class PolicyCatalog { } public boolean dropPolicy(PolicyIdentifier policyIdentifier, boolean detachAll) { - // TODO: Implement detachAll when we support attach/detach policy var resolvedPolicyPath = getResolvedPathWrapper(policyIdentifier); var catalogPath = resolvedPolicyPath.getRawParentPath(); var policyEntity = resolvedPolicyPath.getRawLeafEntity(); @@ -265,9 +266,13 @@ public class PolicyCatalog { PolarisEntity.toCoreList(catalogPath), policyEntity, Map.of(), - false); + detachAll); if (!result.isSuccess()) { + if (result.getReturnStatus() == POLICY_HAS_MAPPINGS) { + throw new PolicyInUseException("Policy %s is still attached to entities", policyIdentifier); + } + throw new IllegalStateException( String.format( "Failed to drop policy %s error status: %s with extraInfo: %s", diff --git a/service/common/src/main/java/org/apache/polaris/service/exception/PolarisExceptionMapper.java b/service/common/src/main/java/org/apache/polaris/service/exception/PolarisExceptionMapper.java index 57df2cbc5..7eac23bd5 100644 --- a/service/common/src/main/java/org/apache/polaris/service/exception/PolarisExceptionMapper.java +++ b/service/common/src/main/java/org/apache/polaris/service/exception/PolarisExceptionMapper.java @@ -28,6 +28,7 @@ import org.apache.polaris.core.exceptions.PolarisException; import org.apache.polaris.core.persistence.PolicyMappingAlreadyExistsException; import org.apache.polaris.core.policy.exceptions.NoSuchPolicyException; import org.apache.polaris.core.policy.exceptions.PolicyAttachException; +import org.apache.polaris.core.policy.exceptions.PolicyInUseException; import org.apache.polaris.core.policy.exceptions.PolicyVersionMismatchException; import org.apache.polaris.core.policy.validator.InvalidPolicyException; import org.apache.polaris.service.context.UnresolvableRealmContextException; @@ -59,6 +60,8 @@ public class PolarisExceptionMapper implements ExceptionMapper<PolarisException> return Response.Status.CONFLICT; } else if (exception instanceof PolicyMappingAlreadyExistsException) { return Response.Status.CONFLICT; + } else if (exception instanceof PolicyInUseException) { + return Response.Status.BAD_REQUEST; } else { return Response.Status.INTERNAL_SERVER_ERROR; }