diff --git a/polaris-core/src/main/java/org/apache/polaris/core/entity/PolarisEntityType.java b/polaris-core/src/main/java/org/apache/polaris/core/entity/PolarisEntityType.java index af50eed6ff..4a3eada34a 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/entity/PolarisEntityType.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/entity/PolarisEntityType.java @@ -34,7 +34,8 @@ public enum PolarisEntityType { // generic table is either a view or a real table TABLE_LIKE(7, NAMESPACE, false, false), TASK(8, ROOT, false, false), - FILE(9, TABLE_LIKE, false, false); + FILE(9, TABLE_LIKE, false, false), + POLICY(10, NAMESPACE, false, false); // to efficiently map a code to its corresponding entity type, use a reverse array which // is initialized below diff --git a/polaris-core/src/main/java/org/apache/polaris/core/policy/PolicyEntity.java b/polaris-core/src/main/java/org/apache/polaris/core/policy/PolicyEntity.java new file mode 100644 index 0000000000..470822bb41 --- /dev/null +++ b/polaris-core/src/main/java/org/apache/polaris/core/policy/PolicyEntity.java @@ -0,0 +1,129 @@ +/* + * 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; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.google.common.base.Preconditions; +import org.apache.iceberg.catalog.Namespace; +import org.apache.iceberg.rest.RESTUtil; +import org.apache.polaris.core.entity.NamespaceEntity; +import org.apache.polaris.core.entity.PolarisBaseEntity; +import org.apache.polaris.core.entity.PolarisEntity; +import org.apache.polaris.core.entity.PolarisEntityType; + +public class PolicyEntity extends PolarisEntity { + + public static final String POLICY_TYPE_CODE_KEY = "policy-type-code"; + public static final String POLICY_DESCRIPTION_KEY = "policy-description"; + public static final String POLICY_VERSION_KEY = "policy-version"; + public static final String POLICY_CONTENT_KEY = "policy-content"; + + PolicyEntity(PolarisBaseEntity sourceEntity) { + super(sourceEntity); + } + + public static PolicyEntity of(PolarisBaseEntity sourceEntity) { + if (sourceEntity != null) { + return new PolicyEntity(sourceEntity); + } + + return null; + } + + @JsonIgnore + public PolicyType getPolicyType() { + return PolicyType.fromCode(getPolicyTypeCode()); + } + + @JsonIgnore + public int getPolicyTypeCode() { + Preconditions.checkArgument( + getPropertiesAsMap().containsKey(POLICY_TYPE_CODE_KEY), + "Invalid policy entity: policy type must exist"); + String policyTypeCode = getPropertiesAsMap().get(POLICY_TYPE_CODE_KEY); + return Integer.parseInt(policyTypeCode); + } + + @JsonIgnore + public String getDescription() { + return getPropertiesAsMap().get(POLICY_DESCRIPTION_KEY); + } + + @JsonIgnore + public String getContent() { + return getPropertiesAsMap().get(POLICY_CONTENT_KEY); + } + + @JsonIgnore + public int getPolicyVersion() { + return Integer.parseInt(getPropertiesAsMap().get(POLICY_VERSION_KEY)); + } + + public static class Builder extends PolarisEntity.BaseBuilder { + public Builder(Namespace namespace, String policyName, PolicyType policyType) { + super(); + setType(PolarisEntityType.POLICY); + setParentNamespace(namespace); + setName(policyName); + setPolicyType(policyType); + setPolicyVersion(0); + } + + public Builder(PolicyEntity original) { + super(original); + } + + @Override + public PolicyEntity build() { + Preconditions.checkArgument( + properties.containsKey(POLICY_TYPE_CODE_KEY), "Policy type must be specified"); + + return new PolicyEntity(buildBase()); + } + + public Builder setParentNamespace(Namespace namespace) { + if (namespace != null && !namespace.isEmpty()) { + internalProperties.put( + NamespaceEntity.PARENT_NAMESPACE_KEY, RESTUtil.encodeNamespace(namespace)); + } + return this; + } + + public Builder setPolicyType(PolicyType policyType) { + Preconditions.checkArgument(policyType != null, "Policy type must be specified"); + properties.put(POLICY_TYPE_CODE_KEY, Integer.toString(policyType.getCode())); + return this; + } + + public Builder setDescription(String description) { + properties.put(POLICY_DESCRIPTION_KEY, description); + return this; + } + + public Builder setPolicyVersion(int version) { + properties.put(POLICY_VERSION_KEY, Integer.toString(version)); + return this; + } + + public Builder setContent(String content) { + properties.put(POLICY_CONTENT_KEY, content); + return this; + } + } +} diff --git a/polaris-core/src/main/java/org/apache/polaris/core/policy/PolicyType.java b/polaris-core/src/main/java/org/apache/polaris/core/policy/PolicyType.java new file mode 100644 index 0000000000..029d5c37ed --- /dev/null +++ b/polaris-core/src/main/java/org/apache/polaris/core/policy/PolicyType.java @@ -0,0 +1,84 @@ +/* + * 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; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import jakarta.annotation.Nullable; + +/** + * Represents a policy type in Polaris. A policy type defines a category of policies that may be + * either predefined or custom (user-defined). + * + *

A policy type can be either inheritable or non-inheritable. Inheritable policies are passed + * down to lower-level entities (e.g., from a namespace to a table). + */ +public interface PolicyType { + + /** + * Retrieves the unique type code associated with this policy type. + * + * @return the type code of the policy type + */ + @JsonValue + int getCode(); + + /** + * Retrieves the human-readable name of this policy type. + * + * @return the name of the policy type + */ + String getName(); + + /** + * Determines whether this policy type is inheritable. + * + * @return {@code true} if the policy type is inheritable, otherwise {@code false} + */ + boolean isInheritable(); + + /** + * Retrieves a {@link PolicyType} instance corresponding to the given type code. + * + *

This method searches for the policy type in predefined policy types. If a custom policy type + * storage mechanism is implemented in the future, it may also check registered custom policy + * types. + * + * @param code the type code of the policy type + * @return the corresponding {@link PolicyType}, or {@code null} if no matching type is found + */ + @JsonCreator + static @Nullable PolicyType fromCode(int code) { + return PredefinedPolicyTypes.fromCode(code); + } + + /** + * Retrieves a {@link PolicyType} instance corresponding to the given policy name. + * + *

This method searches for the policy type in predefined policy types. If a custom policy type + * storage mechanism is implemented in the future, it may also check registered custom policy + * types. + * + * @param name the name of the policy type + * @return the corresponding {@link PolicyType}, or {@code null} if no matching type is found + */ + static @Nullable PolicyType fromName(String name) { + return PredefinedPolicyTypes.fromName(name); + } +} 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 new file mode 100644 index 0000000000..babe1ac7a9 --- /dev/null +++ b/polaris-core/src/main/java/org/apache/polaris/core/policy/PredefinedPolicyTypes.java @@ -0,0 +1,108 @@ +/* + * 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; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import com.google.common.collect.ImmutableMap; +import jakarta.annotation.Nullable; + +/* Represents all predefined policy types in Polaris */ +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_RETENTION(3, "system.snapshot-retention", true); + + private final int code; + private final String name; + private final boolean isInheritable; + private static final PredefinedPolicyTypes[] REVERSE_CODE_MAPPING_ARRAY; + private static final ImmutableMap REVERSE_NAME_MAPPING_ARRAY; + + static { + int maxId = 0; + for (PredefinedPolicyTypes policyType : PredefinedPolicyTypes.values()) { + if (maxId < policyType.code) { + maxId = policyType.code; + } + } + + REVERSE_CODE_MAPPING_ARRAY = new PredefinedPolicyTypes[maxId + 1]; + ImmutableMap.Builder builder = ImmutableMap.builder(); + // populate both + for (PredefinedPolicyTypes policyType : PredefinedPolicyTypes.values()) { + REVERSE_CODE_MAPPING_ARRAY[policyType.code] = policyType; + builder.put(policyType.name, policyType); + } + REVERSE_NAME_MAPPING_ARRAY = builder.build(); + } + + PredefinedPolicyTypes(int code, String name, boolean isInheritable) { + this.code = code; + this.name = name; + this.isInheritable = isInheritable; + } + + /** {@inheritDoc} */ + @Override + @JsonValue + public int getCode() { + return code; + } + + /** {@inheritDoc} */ + @Override + public String getName() { + return name; + } + + /** {@inheritDoc} */ + @Override + public boolean isInheritable() { + return isInheritable; + } + + /** + * Retrieves a {@link PredefinedPolicyTypes} instance corresponding to the given type code. + * + * @param code the type code of the predefined policy type + * @return the corresponding {@link PredefinedPolicyTypes}, or {@code null} if no matching type is + * found + */ + @JsonCreator + public static @Nullable PredefinedPolicyTypes fromCode(int code) { + if (code >= REVERSE_CODE_MAPPING_ARRAY.length) { + return null; + } + + return REVERSE_CODE_MAPPING_ARRAY[code]; + } + + /** + * Retrieves a {@link PredefinedPolicyTypes} instance corresponding to the given policy name. + * + * @param name the name of the predefined policy type + * @return the corresponding {@link PredefinedPolicyTypes}, or {@code null} if no matching type is + * found + */ + public static @Nullable PredefinedPolicyTypes fromName(String name) { + return REVERSE_NAME_MAPPING_ARRAY.get(name); + } +} diff --git a/polaris-core/src/test/java/org/apache/polaris/core/persistence/ResolverTest.java b/polaris-core/src/test/java/org/apache/polaris/core/persistence/ResolverTest.java index fbcb2f9c94..dd21a16f3b 100644 --- a/polaris-core/src/test/java/org/apache/polaris/core/persistence/ResolverTest.java +++ b/polaris-core/src/test/java/org/apache/polaris/core/persistence/ResolverTest.java @@ -105,6 +105,9 @@ public class ResolverTest { * - (N1/N4) * - N5/N6/T5 * - N5/N6/T6 + * - N7/N8/POL1 + * - N7/N8/POL2 + * - N7/POL3 * - R1(TABLE_READ on N1/N2, VIEW_CREATE on C, TABLE_LIST on N2, TABLE_DROP on N5/N6/T5) * - R2(TABLE_WRITE_DATA on N5, VIEW_LIST on C) * - PR1(R1, R2) @@ -230,6 +233,19 @@ void testResolvePath(boolean useCache) { new ResolverPath(List.of("N5", "N6", "T5"), PolarisEntityType.TABLE_LIKE); this.resolveDriver(this.cache, "test", N5_N6_T5, null, null); + // N7/N8 which exists + ResolverPath N7_N8 = new ResolverPath(List.of("N7", "N8"), PolarisEntityType.NAMESPACE); + this.resolveDriver(this.cache, "test", N7_N8, null, null); + + // N7/N8/POL1 which exists + ResolverPath N7_N8_POL1 = + new ResolverPath(List.of("N7", "N8", "POL1"), PolarisEntityType.POLICY); + this.resolveDriver(this.cache, "test", N7_N8_POL1, null, null); + + // N7/POL3 which exists + ResolverPath N7_POL3 = new ResolverPath(List.of("N7", "POL3"), PolarisEntityType.POLICY); + this.resolveDriver(this.cache, "test", N7_POL3, null, null); + // Error scenarios: N5/N6/T8 which does not exists ResolverPath N5_N6_T8 = new ResolverPath(List.of("N5", "N6", "T8"), PolarisEntityType.TABLE_LIKE); diff --git a/polaris-core/src/test/java/org/apache/polaris/core/policy/PolicyEntityTest.java b/polaris-core/src/test/java/org/apache/polaris/core/policy/PolicyEntityTest.java new file mode 100644 index 0000000000..68be34bb69 --- /dev/null +++ b/polaris-core/src/test/java/org/apache/polaris/core/policy/PolicyEntityTest.java @@ -0,0 +1,65 @@ +/* + * 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; + +import java.util.stream.Stream; +import org.apache.iceberg.catalog.Namespace; +import org.apache.polaris.core.entity.PolarisEntityType; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +public class PolicyEntityTest { + + static Stream policyTypes() { + return Stream.of( + Arguments.of(PredefinedPolicyTypes.DATA_COMPACTION), + Arguments.of(PredefinedPolicyTypes.METADATA_COMPACTION), + Arguments.of(PredefinedPolicyTypes.ORPHAN_FILE_REMOVAL), + Arguments.of(PredefinedPolicyTypes.METADATA_COMPACTION)); + } + + @ParameterizedTest + @MethodSource("policyTypes") + public void testPolicyEntity(PolicyType policyType) { + PolicyEntity entity = + new PolicyEntity.Builder(Namespace.of("NS1"), "testPolicy", policyType) + .setContent("test_content") + .setPolicyVersion(0) + .build(); + Assertions.assertThat(entity.getType()).isEqualTo(PolarisEntityType.POLICY); + Assertions.assertThat(entity.getPolicyType()).isEqualTo(policyType); + Assertions.assertThat(entity.getPolicyTypeCode()).isEqualTo(policyType.getCode()); + Assertions.assertThat(entity.getContent()).isEqualTo("test_content"); + } + + @Test + public void testBuildPolicyEntityWithoutPolicyTye() { + Assertions.assertThatThrownBy( + () -> + new PolicyEntity.Builder(Namespace.of("NS1"), "testPolicy", null) + .setContent("test_content") + .setPolicyVersion(0) + .build()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Policy type must be specified"); + } +} 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 new file mode 100644 index 0000000000..a690cfd06d --- /dev/null +++ b/polaris-core/src/test/java/org/apache/polaris/core/policy/PolicyTypeTest.java @@ -0,0 +1,56 @@ +/* + * 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; + +import java.util.stream.Stream; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +public class PolicyTypeTest { + + static Stream predefinedPolicyTypes() { + return Stream.of( + 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-retention", true)); + } + + @ParameterizedTest + @MethodSource("predefinedPolicyTypes") + public void testPredefinedPolicyTypeFromCode(int code, String name, boolean isInheritable) { + PolicyType policyType = PolicyType.fromCode(code); + Assertions.assertThat(policyType).isNotNull(); + Assertions.assertThat(policyType.getCode()).isEqualTo(code); + Assertions.assertThat(policyType.getName()).isEqualTo(name); + Assertions.assertThat(policyType.isInheritable()).isEqualTo(isInheritable); + } + + @ParameterizedTest + @MethodSource("predefinedPolicyTypes") + public void testPredefinedPolicyTypeFromName(int code, String name, boolean isInheritable) { + PolicyType policyType = PolicyType.fromName(name); + Assertions.assertThat(policyType).isNotNull(); + Assertions.assertThat(policyType.getCode()).isEqualTo(code); + Assertions.assertThat(policyType.getName()).isEqualTo(name); + Assertions.assertThat(policyType.isInheritable()).isEqualTo(isInheritable); + } +} 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 55849baf44..2fd2865db7 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 @@ -49,6 +49,9 @@ import org.apache.polaris.core.persistence.dao.entity.EntityResult; import org.apache.polaris.core.persistence.dao.entity.LoadGrantsResult; import org.apache.polaris.core.persistence.dao.entity.ResolvedEntityResult; +import org.apache.polaris.core.policy.PolicyEntity; +import org.apache.polaris.core.policy.PolicyType; +import org.apache.polaris.core.policy.PredefinedPolicyTypes; import org.assertj.core.api.Assertions; /** Test the Polaris persistence layer */ @@ -583,12 +586,22 @@ public PolarisBaseEntity createEntity( PolarisEntityType entityType, PolarisEntitySubType entitySubType, String name) { + return createEntity(catalogPath, entityType, entitySubType, name, null); + } + + public PolarisBaseEntity createEntity( + List catalogPath, + PolarisEntityType entityType, + PolarisEntitySubType entitySubType, + String name, + Map properties) { return createEntity( catalogPath, entityType, entitySubType, name, - polarisMetaStoreManager.generateNewEntityId(this.polarisCallContext).getId()); + polarisMetaStoreManager.generateNewEntityId(this.polarisCallContext).getId(), + properties); } PolarisBaseEntity createEntity( @@ -597,6 +610,16 @@ PolarisBaseEntity createEntity( PolarisEntitySubType entitySubType, String name, long entityId) { + return createEntity(catalogPath, entityType, entitySubType, name, entityId, null); + } + + PolarisBaseEntity createEntity( + List catalogPath, + PolarisEntityType entityType, + PolarisEntitySubType entitySubType, + String name, + long entityId, + Map properties) { long parentId; long catalogId; if (catalogPath != null) { @@ -608,6 +631,7 @@ PolarisBaseEntity createEntity( } PolarisBaseEntity newEntity = new PolarisBaseEntity(catalogId, entityId, entityType, entitySubType, parentId, name); + newEntity.setPropertiesAsMap(properties); PolarisBaseEntity entity = polarisMetaStoreManager .createEntityIfNotExists(this.polarisCallContext, catalogPath, newEntity) @@ -650,6 +674,26 @@ PolarisBaseEntity createEntity( return createEntity(catalogPath, entityType, PolarisEntitySubType.NULL_SUBTYPE, name); } + PolarisBaseEntity createEntity( + List catalogPath, + PolarisEntityType entityType, + String name, + Map properties) { + return createEntity( + catalogPath, entityType, PolarisEntitySubType.NULL_SUBTYPE, name, properties); + } + + /** Create a policy entity */ + PolicyEntity createPolicy( + List catalogPath, String name, PolicyType policyType) { + return PolicyEntity.of( + createEntity( + catalogPath, + PolarisEntityType.POLICY, + name, + Map.of("policy-type-code", Integer.toString(policyType.getCode())))); + } + /** Drop the entity if it exists. */ void dropEntity(List catalogPath, PolarisBaseEntity entityToDrop) { // see if the entity exists @@ -922,7 +966,7 @@ void revokeToGrantee( /** * Create a test catalog. This is a new catalog which will have the following objects (N is for a - * namespace, T for a table, V for a view, R for a role, P for a principal): + * namespace, T for a table, V for a view, R for a role, P for a principal, POL for a policy): * *

    * - C
@@ -935,6 +979,9 @@ void revokeToGrantee(
    * - (N1/N4)
    * - N5/N6/T5
    * - N5/N6/T6
+   * - N7/N8/POL1
+   * - N7/N8/POL2
+   * - N7/POL3
    * - R1(TABLE_READ on N1/N2, VIEW_CREATE on C, TABLE_LIST on N1/N2, TABLE_DROP on N5/N6/T5)
    * - R2(TABLE_WRITE_DATA on N5, VIEW_LIST on C)
    * - PR1(R1, R2)
@@ -1001,6 +1048,14 @@ PolarisBaseEntity createTestCatalog(String catalogName) {
         PolarisEntitySubType.TABLE,
         "T6");
 
+    PolarisBaseEntity N7 = this.createEntity(List.of(catalog), PolarisEntityType.NAMESPACE, "N7");
+    PolarisBaseEntity N7_N8 =
+        this.createEntity(List.of(catalog, N7), PolarisEntityType.NAMESPACE, "N8");
+    this.createPolicy(List.of(catalog, N7, N7_N8), "POL1", PredefinedPolicyTypes.DATA_COMPACTION);
+    this.createPolicy(
+        List.of(catalog, N7, N7_N8), "POL2", PredefinedPolicyTypes.METADATA_COMPACTION);
+    this.createPolicy(List.of(catalog, N7), "POL3", PredefinedPolicyTypes.SNAPSHOT_RETENTION);
+
     // the two catalog roles
     PolarisBaseEntity R1 =
         this.createEntity(List.of(catalog), PolarisEntityType.CATALOG_ROLE, "R1");
@@ -1670,6 +1725,17 @@ void testCreateTestCatalog() {
         PolarisEntityType.TABLE_LIKE,
         PolarisEntitySubType.TABLE,
         "T6");
+    PolarisBaseEntity N7 =
+        this.ensureExistsByName(List.of(catalog), PolarisEntityType.NAMESPACE, "N7");
+    PolarisBaseEntity N7_N8 =
+        this.ensureExistsByName(
+            List.of(catalog, N7),
+            PolarisEntityType.NAMESPACE,
+            PolarisEntitySubType.ANY_SUBTYPE,
+            "N8");
+    this.ensureExistsByName(List.of(catalog, N7, N7_N8), PolarisEntityType.POLICY, "POL1");
+    this.ensureExistsByName(List.of(catalog, N7, N7_N8), PolarisEntityType.POLICY, "POL2");
+    this.ensureExistsByName(List.of(catalog, N7), PolarisEntityType.POLICY, "POL3");
     PolarisBaseEntity R1 =
         this.ensureExistsByName(List.of(catalog), PolarisEntityType.CATALOG_ROLE, "R1");
     PolarisBaseEntity R2 =
@@ -1703,7 +1769,8 @@ void testBrowse() {
         PolarisEntityType.NAMESPACE,
         List.of(
             ImmutablePair.of("N1", PolarisEntitySubType.NULL_SUBTYPE),
-            ImmutablePair.of("N5", PolarisEntitySubType.NULL_SUBTYPE)));
+            ImmutablePair.of("N5", PolarisEntitySubType.NULL_SUBTYPE),
+            ImmutablePair.of("N7", PolarisEntitySubType.NULL_SUBTYPE)));
 
     // should see 3 top-level catalog roles including the admin one
     this.validateListReturn(
@@ -1804,6 +1871,18 @@ void testBrowse() {
                 PolarisEntitySubType.NULL_SUBTYPE),
             ImmutablePair.of("PR1", PolarisEntitySubType.NULL_SUBTYPE),
             ImmutablePair.of("PR2", PolarisEntitySubType.NULL_SUBTYPE)));
+
+    // list 2 policies under N7_N8
+    PolarisBaseEntity N7 =
+        this.ensureExistsByName(List.of(catalog), PolarisEntityType.NAMESPACE, "N7");
+    PolarisBaseEntity N7_N8 =
+        this.ensureExistsByName(List.of(catalog, N7), PolarisEntityType.NAMESPACE, "N8");
+    this.validateListReturn(
+        List.of(catalog, N7, N7_N8),
+        PolarisEntityType.POLICY,
+        List.of(
+            ImmutablePair.of("POL1", PolarisEntitySubType.NULL_SUBTYPE),
+            ImmutablePair.of("POL2", PolarisEntitySubType.NULL_SUBTYPE)));
   }
 
   /** Test that entity updates works well */
@@ -1972,6 +2051,26 @@ void testDropEntities() {
     this.dropEntity(List.of(catalog, N5), N5_N6);
     this.dropEntity(List.of(catalog), N5);
 
+    PolarisBaseEntity N7 =
+        this.ensureExistsByName(List.of(catalog), PolarisEntityType.NAMESPACE, "N7");
+    PolarisBaseEntity N7_N8 =
+        this.ensureExistsByName(
+            List.of(catalog, N7),
+            PolarisEntityType.NAMESPACE,
+            PolarisEntitySubType.ANY_SUBTYPE,
+            "N8");
+    PolarisBaseEntity POL1 =
+        this.ensureExistsByName(List.of(catalog, N7, N7_N8), PolarisEntityType.POLICY, "POL1");
+    PolarisBaseEntity POL2 =
+        this.ensureExistsByName(List.of(catalog, N7, N7_N8), PolarisEntityType.POLICY, "POL2");
+    PolarisBaseEntity POL3 =
+        this.ensureExistsByName(List.of(catalog, N7), PolarisEntityType.POLICY, "POL3");
+    this.dropEntity(List.of(catalog, N7, N7_N8), POL1);
+    this.dropEntity(List.of(catalog, N7, N7_N8), POL2);
+    this.dropEntity(List.of(catalog, N7), POL3);
+    this.dropEntity(List.of(catalog, N7), N7_N8);
+    this.dropEntity(List.of(catalog), N7);
+
     // attempt to drop the catalog again, should fail because of role R1
     this.dropEntity(null, catalog);