diff --git a/polaris-core/src/main/java/org/apache/polaris/core/auth/PolarisAuthorizableOperation.java b/polaris-core/src/main/java/org/apache/polaris/core/auth/PolarisAuthorizableOperation.java index 0957a4e9db..1eaea6c3f9 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/auth/PolarisAuthorizableOperation.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/auth/PolarisAuthorizableOperation.java @@ -18,7 +18,9 @@ */ package org.apache.polaris.core.auth; +import static org.apache.polaris.core.entity.PolarisPrivilege.CATALOG_ATTACH_POLICY; import static org.apache.polaris.core.entity.PolarisPrivilege.CATALOG_CREATE; +import static org.apache.polaris.core.entity.PolarisPrivilege.CATALOG_DETACH_POLICY; import static org.apache.polaris.core.entity.PolarisPrivilege.CATALOG_DROP; import static org.apache.polaris.core.entity.PolarisPrivilege.CATALOG_LIST; import static org.apache.polaris.core.entity.PolarisPrivilege.CATALOG_LIST_GRANTS; @@ -33,14 +35,18 @@ import static org.apache.polaris.core.entity.PolarisPrivilege.CATALOG_ROLE_READ_PROPERTIES; import static org.apache.polaris.core.entity.PolarisPrivilege.CATALOG_ROLE_WRITE_PROPERTIES; import static org.apache.polaris.core.entity.PolarisPrivilege.CATALOG_WRITE_PROPERTIES; +import static org.apache.polaris.core.entity.PolarisPrivilege.NAMESPACE_ATTACH_POLICY; import static org.apache.polaris.core.entity.PolarisPrivilege.NAMESPACE_CREATE; +import static org.apache.polaris.core.entity.PolarisPrivilege.NAMESPACE_DETACH_POLICY; import static org.apache.polaris.core.entity.PolarisPrivilege.NAMESPACE_DROP; import static org.apache.polaris.core.entity.PolarisPrivilege.NAMESPACE_LIST; import static org.apache.polaris.core.entity.PolarisPrivilege.NAMESPACE_LIST_GRANTS; import static org.apache.polaris.core.entity.PolarisPrivilege.NAMESPACE_MANAGE_GRANTS_ON_SECURABLE; import static org.apache.polaris.core.entity.PolarisPrivilege.NAMESPACE_READ_PROPERTIES; import static org.apache.polaris.core.entity.PolarisPrivilege.NAMESPACE_WRITE_PROPERTIES; +import static org.apache.polaris.core.entity.PolarisPrivilege.POLICY_ATTACH; import static org.apache.polaris.core.entity.PolarisPrivilege.POLICY_CREATE; +import static org.apache.polaris.core.entity.PolarisPrivilege.POLICY_DETACH; import static org.apache.polaris.core.entity.PolarisPrivilege.POLICY_DROP; import static org.apache.polaris.core.entity.PolarisPrivilege.POLICY_LIST; import static org.apache.polaris.core.entity.PolarisPrivilege.POLICY_READ; @@ -64,7 +70,9 @@ import static org.apache.polaris.core.entity.PolarisPrivilege.PRINCIPAL_ROTATE_CREDENTIALS; import static org.apache.polaris.core.entity.PolarisPrivilege.PRINCIPAL_WRITE_PROPERTIES; import static org.apache.polaris.core.entity.PolarisPrivilege.SERVICE_MANAGE_ACCESS; +import static org.apache.polaris.core.entity.PolarisPrivilege.TABLE_ATTACH_POLICY; import static org.apache.polaris.core.entity.PolarisPrivilege.TABLE_CREATE; +import static org.apache.polaris.core.entity.PolarisPrivilege.TABLE_DETACH_POLICY; import static org.apache.polaris.core.entity.PolarisPrivilege.TABLE_DROP; import static org.apache.polaris.core.entity.PolarisPrivilege.TABLE_LIST; import static org.apache.polaris.core.entity.PolarisPrivilege.TABLE_LIST_GRANTS; @@ -192,6 +200,16 @@ public enum PolarisAuthorizableOperation { DROP_POLICY(POLICY_DROP), UPDATE_POLICY(POLICY_WRITE), LIST_POLICY(POLICY_LIST), + ATTACH_POLICY_TO_CATALOG(POLICY_ATTACH, CATALOG_ATTACH_POLICY), + ATTACH_POLICY_TO_NAMESPACE(POLICY_ATTACH, NAMESPACE_ATTACH_POLICY), + ATTACH_POLICY_TO_TABLE(POLICY_ATTACH, TABLE_ATTACH_POLICY), + DETACH_POLICY_FROM_CATALOG(POLICY_DETACH, CATALOG_DETACH_POLICY), + DETACH_POLICY_FROM_NAMESPACE(POLICY_DETACH, NAMESPACE_DETACH_POLICY), + DETACH_POLICY_FROM_TABLE(POLICY_DETACH, TABLE_DETACH_POLICY), + GET_APPLICABLE_POLICIES_ON_CATALOG(CATALOG_READ_PROPERTIES), + GET_APPLICABLE_POLICIES_ON_NAMESPACE(NAMESPACE_READ_PROPERTIES), + GET_APPLICABLE_POLICIES_ON_TABLE(TABLE_READ_PROPERTIES), + GET_APPLICABLE_POLICIES_ON_VIEW(VIEW_READ_PROPERTIES), ; private final EnumSet privilegesOnTarget; diff --git a/polaris-core/src/main/java/org/apache/polaris/core/auth/PolarisAuthorizerImpl.java b/polaris-core/src/main/java/org/apache/polaris/core/auth/PolarisAuthorizerImpl.java index abeefa2b6a..ae606ff965 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/auth/PolarisAuthorizerImpl.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/auth/PolarisAuthorizerImpl.java @@ -18,7 +18,9 @@ */ package org.apache.polaris.core.auth; +import static org.apache.polaris.core.entity.PolarisPrivilege.CATALOG_ATTACH_POLICY; import static org.apache.polaris.core.entity.PolarisPrivilege.CATALOG_CREATE; +import static org.apache.polaris.core.entity.PolarisPrivilege.CATALOG_DETACH_POLICY; import static org.apache.polaris.core.entity.PolarisPrivilege.CATALOG_DROP; import static org.apache.polaris.core.entity.PolarisPrivilege.CATALOG_FULL_METADATA; import static org.apache.polaris.core.entity.PolarisPrivilege.CATALOG_LIST; @@ -39,7 +41,9 @@ import static org.apache.polaris.core.entity.PolarisPrivilege.CATALOG_ROLE_USAGE; import static org.apache.polaris.core.entity.PolarisPrivilege.CATALOG_ROLE_WRITE_PROPERTIES; import static org.apache.polaris.core.entity.PolarisPrivilege.CATALOG_WRITE_PROPERTIES; +import static org.apache.polaris.core.entity.PolarisPrivilege.NAMESPACE_ATTACH_POLICY; import static org.apache.polaris.core.entity.PolarisPrivilege.NAMESPACE_CREATE; +import static org.apache.polaris.core.entity.PolarisPrivilege.NAMESPACE_DETACH_POLICY; import static org.apache.polaris.core.entity.PolarisPrivilege.NAMESPACE_DROP; import static org.apache.polaris.core.entity.PolarisPrivilege.NAMESPACE_FULL_METADATA; import static org.apache.polaris.core.entity.PolarisPrivilege.NAMESPACE_LIST; @@ -47,7 +51,9 @@ import static org.apache.polaris.core.entity.PolarisPrivilege.NAMESPACE_MANAGE_GRANTS_ON_SECURABLE; import static org.apache.polaris.core.entity.PolarisPrivilege.NAMESPACE_READ_PROPERTIES; import static org.apache.polaris.core.entity.PolarisPrivilege.NAMESPACE_WRITE_PROPERTIES; +import static org.apache.polaris.core.entity.PolarisPrivilege.POLICY_ATTACH; import static org.apache.polaris.core.entity.PolarisPrivilege.POLICY_CREATE; +import static org.apache.polaris.core.entity.PolarisPrivilege.POLICY_DETACH; import static org.apache.polaris.core.entity.PolarisPrivilege.POLICY_DROP; import static org.apache.polaris.core.entity.PolarisPrivilege.POLICY_FULL_METADATA; import static org.apache.polaris.core.entity.PolarisPrivilege.POLICY_LIST; @@ -75,7 +81,9 @@ import static org.apache.polaris.core.entity.PolarisPrivilege.PRINCIPAL_ROTATE_CREDENTIALS; import static org.apache.polaris.core.entity.PolarisPrivilege.PRINCIPAL_WRITE_PROPERTIES; import static org.apache.polaris.core.entity.PolarisPrivilege.SERVICE_MANAGE_ACCESS; +import static org.apache.polaris.core.entity.PolarisPrivilege.TABLE_ATTACH_POLICY; import static org.apache.polaris.core.entity.PolarisPrivilege.TABLE_CREATE; +import static org.apache.polaris.core.entity.PolarisPrivilege.TABLE_DETACH_POLICY; import static org.apache.polaris.core.entity.PolarisPrivilege.TABLE_DROP; import static org.apache.polaris.core.entity.PolarisPrivilege.TABLE_FULL_METADATA; import static org.apache.polaris.core.entity.PolarisPrivilege.TABLE_LIST; @@ -495,6 +503,20 @@ public class PolarisAuthorizerImpl implements PolarisAuthorizer { POLICY_FULL_METADATA, CATALOG_MANAGE_METADATA, CATALOG_MANAGE_CONTENT)); + SUPER_PRIVILEGES.putAll(POLICY_ATTACH, List.of(POLICY_ATTACH, CATALOG_MANAGE_CONTENT)); + SUPER_PRIVILEGES.putAll(POLICY_DETACH, List.of(POLICY_DETACH, CATALOG_MANAGE_CONTENT)); + SUPER_PRIVILEGES.putAll( + CATALOG_ATTACH_POLICY, List.of(CATALOG_ATTACH_POLICY, CATALOG_MANAGE_CONTENT)); + SUPER_PRIVILEGES.putAll( + NAMESPACE_ATTACH_POLICY, List.of(NAMESPACE_ATTACH_POLICY, CATALOG_MANAGE_CONTENT)); + SUPER_PRIVILEGES.putAll( + TABLE_ATTACH_POLICY, List.of(TABLE_ATTACH_POLICY, CATALOG_MANAGE_CONTENT)); + SUPER_PRIVILEGES.putAll( + CATALOG_DETACH_POLICY, List.of(CATALOG_DETACH_POLICY, CATALOG_MANAGE_CONTENT)); + SUPER_PRIVILEGES.putAll( + NAMESPACE_DETACH_POLICY, List.of(NAMESPACE_DETACH_POLICY, CATALOG_MANAGE_CONTENT)); + SUPER_PRIVILEGES.putAll( + TABLE_DETACH_POLICY, List.of(TABLE_DETACH_POLICY, CATALOG_MANAGE_CONTENT)); } private final PolarisConfigurationStore featureConfig; diff --git a/polaris-core/src/main/java/org/apache/polaris/core/entity/PolarisPrivilege.java b/polaris-core/src/main/java/org/apache/polaris/core/entity/PolarisPrivilege.java index 03585790b9..71b7b0df83 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/entity/PolarisPrivilege.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/entity/PolarisPrivilege.java @@ -142,6 +142,14 @@ public enum PolarisPrivilege { POLICY_WRITE(73, PolarisEntityType.POLICY), POLICY_LIST(74, PolarisEntityType.NAMESPACE), POLICY_FULL_METADATA(75, PolarisEntityType.POLICY), + POLICY_ATTACH(76, PolarisEntityType.POLICY), + POLICY_DETACH(77, PolarisEntityType.POLICY), + CATALOG_ATTACH_POLICY(78, PolarisEntityType.CATALOG), + NAMESPACE_ATTACH_POLICY(79, PolarisEntityType.NAMESPACE), + TABLE_ATTACH_POLICY(80, PolarisEntityType.TABLE_LIKE, PolarisEntitySubType.ICEBERG_TABLE), + CATALOG_DETACH_POLICY(81, PolarisEntityType.CATALOG), + NAMESPACE_DETACH_POLICY(82, PolarisEntityType.NAMESPACE), + TABLE_DETACH_POLICY(83, PolarisEntityType.TABLE_LIKE, PolarisEntitySubType.ICEBERG_TABLE), ; /** diff --git a/quarkus/service/src/test/java/org/apache/polaris/service/quarkus/admin/PolarisAuthzTestBase.java b/quarkus/service/src/test/java/org/apache/polaris/service/quarkus/admin/PolarisAuthzTestBase.java index 10972b602c..6f3498d319 100644 --- a/quarkus/service/src/test/java/org/apache/polaris/service/quarkus/admin/PolarisAuthzTestBase.java +++ b/quarkus/service/src/test/java/org/apache/polaris/service/quarkus/admin/PolarisAuthzTestBase.java @@ -627,24 +627,44 @@ protected void doTestInsufficientPrivileges( Runnable action, Function grantAction, Function revokeAction) { - for (PolarisPrivilege privilege : insufficientPrivileges) { - // Grant the single privilege at a catalog level to cascade to all objects. - Assertions.assertThat(grantAction.apply(privilege)).isTrue(); + doTestInsufficientPrivilegeSets( + insufficientPrivileges.stream().map(priv -> Set.of(priv)).toList(), + principalName, + action, + grantAction, + revokeAction); + } - // Should be insufficient - try { - Assertions.assertThatThrownBy(() -> action.run()) - .isInstanceOf(ForbiddenException.class) - .hasMessageContaining(principalName) - .hasMessageContaining("is not authorized"); - } catch (Throwable t) { - Assertions.fail( - String.format("Expected failure with insufficientPrivilege '%s'", privilege), t); - } + /** + * Tests each "insufficient" privilege individually using CATALOG_ROLE1 by granting at the + * CATALOG_NAME level, ensuring the action fails, then revoking after each test case. + */ + protected void doTestInsufficientPrivilegeSets( + List> insufficientPrivilegeSets, + String principalName, + Runnable action, + Function grantAction, + Function revokeAction) { + for (Set privilegeSet : insufficientPrivilegeSets) { + for (PolarisPrivilege privilege : privilegeSet) { + // Grant the single privilege at a catalog level to cascade to all objects. + Assertions.assertThat(grantAction.apply(privilege)).isTrue(); - // Revoking only matters in case there are some multi-privilege actions being tested with - // only granting individual privileges in isolation. - Assertions.assertThat(revokeAction.apply(privilege)).isTrue(); + // Should be insufficient + try { + Assertions.assertThatThrownBy(() -> action.run()) + .isInstanceOf(ForbiddenException.class) + .hasMessageContaining(principalName) + .hasMessageContaining("is not authorized"); + } catch (Throwable t) { + Assertions.fail( + String.format("Expected failure with insufficientPrivilege '%s'", privilege), t); + } + + // Revoking only matters in case there are some multi-privilege actions being tested with + // only granting individual privileges in isolation. + Assertions.assertThat(revokeAction.apply(privilege)).isTrue(); + } } } } diff --git a/quarkus/service/src/test/java/org/apache/polaris/service/quarkus/catalog/PolicyCatalogHandlerAuthzTest.java b/quarkus/service/src/test/java/org/apache/polaris/service/quarkus/catalog/PolicyCatalogHandlerAuthzTest.java index 74a54b807c..b821be31e5 100644 --- a/quarkus/service/src/test/java/org/apache/polaris/service/quarkus/catalog/PolicyCatalogHandlerAuthzTest.java +++ b/quarkus/service/src/test/java/org/apache/polaris/service/quarkus/catalog/PolicyCatalogHandlerAuthzTest.java @@ -19,14 +19,19 @@ package org.apache.polaris.service.quarkus.catalog; import io.quarkus.test.junit.QuarkusTest; +import java.util.Arrays; import java.util.List; import java.util.Set; import org.apache.polaris.core.auth.AuthenticatedPolarisPrincipal; +import org.apache.polaris.core.catalog.PolarisCatalogHelpers; import org.apache.polaris.core.entity.PolarisPrivilege; import org.apache.polaris.core.policy.PredefinedPolicyTypes; import org.apache.polaris.service.catalog.policy.PolicyCatalogHandler; import org.apache.polaris.service.quarkus.admin.PolarisAuthzTestBase; +import org.apache.polaris.service.types.AttachPolicyRequest; import org.apache.polaris.service.types.CreatePolicyRequest; +import org.apache.polaris.service.types.DetachPolicyRequest; +import org.apache.polaris.service.types.PolicyAttachmentTarget; import org.apache.polaris.service.types.PolicyIdentifier; import org.apache.polaris.service.types.UpdatePolicyRequest; import org.assertj.core.api.Assertions; @@ -77,6 +82,17 @@ private void doTestSufficientPrivileges( PRINCIPAL_NAME); } + /** + * @param sufficientPrivileges each set of concurrent privileges expected to be sufficient + * together. + * @param action + * @param cleanupAction + */ + private void doTestSufficientPrivilegeSets( + List> sufficientPrivileges, Runnable action, Runnable cleanupAction) { + doTestSufficientPrivilegeSets(sufficientPrivileges, action, cleanupAction, PRINCIPAL_NAME); + } + /** * @param sufficientPrivileges each set of concurrent privileges expected to be sufficient * together. @@ -123,6 +139,11 @@ private void doTestInsufficientPrivileges( doTestInsufficientPrivileges(insufficientPrivileges, PRINCIPAL_NAME, action); } + private void doTestInsufficientPrivilegeSets( + List> insufficientPrivilegesSets, Runnable action) { + doTestInsufficientPrivilegeSets(insufficientPrivilegesSets, PRINCIPAL_NAME, action); + } + /** * Tests each "insufficient" privilege individually using CATALOG_ROLE1 by granting at the * CATALOG_NAME level, ensuring the action fails, then revoking after each test case. @@ -139,6 +160,20 @@ private void doTestInsufficientPrivileges( adminService.revokePrivilegeOnCatalogFromRole(CATALOG_NAME, CATALOG_ROLE1, privilege)); } + private void doTestInsufficientPrivilegeSets( + List> insufficientPrivilegeSets, + String principalName, + Runnable action) { + doTestInsufficientPrivilegeSets( + insufficientPrivilegeSets, + principalName, + action, + (privilege) -> + adminService.grantPrivilegeOnCatalogToRole(CATALOG_NAME, CATALOG_ROLE1, privilege), + (privilege) -> + adminService.revokePrivilegeOnCatalogFromRole(CATALOG_NAME, CATALOG_ROLE1, privilege)); + } + @Test public void testListPoliciesAllSufficientPrivileges() { doTestSufficientPrivileges( @@ -301,4 +336,390 @@ public void testDropPolicyInsufficientPrivileges() { PolarisPrivilege.POLICY_WRITE), () -> newWrapper().dropPolicy(POLICY_NS1_1, true)); } + + @Test + public void testAttachPolicyToCatalogSufficientPrivileges() { + Assertions.assertThat( + adminService.grantPrivilegeOnCatalogToRole( + CATALOG_NAME, CATALOG_ROLE2, PolarisPrivilege.POLICY_DETACH)) + .isTrue(); + Assertions.assertThat( + adminService.grantPrivilegeOnCatalogToRole( + CATALOG_NAME, CATALOG_ROLE2, PolarisPrivilege.CATALOG_DETACH_POLICY)) + .isTrue(); + PolicyAttachmentTarget namespaceTarget = + PolicyAttachmentTarget.builder().setType(PolicyAttachmentTarget.TypeEnum.CATALOG).build(); + AttachPolicyRequest attachPolicyRequest = + AttachPolicyRequest.builder().setTarget(namespaceTarget).build(); + DetachPolicyRequest detachPolicyRequest = + DetachPolicyRequest.builder().setTarget(namespaceTarget).build(); + + doTestSufficientPrivilegeSets( + List.of( + Set.of(PolarisPrivilege.POLICY_ATTACH, PolarisPrivilege.CATALOG_ATTACH_POLICY), + Set.of(PolarisPrivilege.CATALOG_MANAGE_CONTENT)), + () -> newWrapper(Set.of(PRINCIPAL_ROLE1)).attachPolicy(POLICY_NS1_1, attachPolicyRequest), + () -> newWrapper(Set.of(PRINCIPAL_ROLE2)).detachPolicy(POLICY_NS1_1, detachPolicyRequest), + PRINCIPAL_NAME); + } + + @Test + public void testAttachPolicyToCatalogInsufficientPrivileges() { + PolicyAttachmentTarget namespaceTarget = + PolicyAttachmentTarget.builder().setType(PolicyAttachmentTarget.TypeEnum.CATALOG).build(); + AttachPolicyRequest attachPolicyRequest = + AttachPolicyRequest.builder().setTarget(namespaceTarget).build(); + + doTestInsufficientPrivilegeSets( + List.of( + Set.of(PolarisPrivilege.POLICY_ATTACH, PolarisPrivilege.NAMESPACE_ATTACH_POLICY), + Set.of(PolarisPrivilege.POLICY_ATTACH, PolarisPrivilege.TABLE_ATTACH_POLICY), + Set.of(PolarisPrivilege.POLICY_ATTACH), + Set.of(PolarisPrivilege.CATALOG_ATTACH_POLICY)), + () -> newWrapper(Set.of(PRINCIPAL_ROLE1)).attachPolicy(POLICY_NS1_1, attachPolicyRequest)); + } + + @Test + public void testAttachPolicyToNamespaceSufficientPrivileges() { + Assertions.assertThat( + adminService.grantPrivilegeOnCatalogToRole( + CATALOG_NAME, CATALOG_ROLE2, PolarisPrivilege.POLICY_DETACH)) + .isTrue(); + Assertions.assertThat( + adminService.grantPrivilegeOnCatalogToRole( + CATALOG_NAME, CATALOG_ROLE2, PolarisPrivilege.NAMESPACE_DETACH_POLICY)) + .isTrue(); + + PolicyAttachmentTarget namespaceTarget = + PolicyAttachmentTarget.builder() + .setType(PolicyAttachmentTarget.TypeEnum.NAMESPACE) + .setPath(Arrays.asList(NS2.levels())) + .build(); + AttachPolicyRequest attachPolicyRequest = + AttachPolicyRequest.builder().setTarget(namespaceTarget).build(); + DetachPolicyRequest detachPolicyRequest = + DetachPolicyRequest.builder().setTarget(namespaceTarget).build(); + + doTestSufficientPrivilegeSets( + List.of( + Set.of(PolarisPrivilege.POLICY_ATTACH, PolarisPrivilege.NAMESPACE_ATTACH_POLICY), + Set.of(PolarisPrivilege.CATALOG_MANAGE_CONTENT)), + () -> newWrapper(Set.of(PRINCIPAL_ROLE1)).attachPolicy(POLICY_NS1_1, attachPolicyRequest), + () -> newWrapper(Set.of(PRINCIPAL_ROLE2)).detachPolicy(POLICY_NS1_1, detachPolicyRequest)); + } + + @Test + public void testAttachPolicyToNamespaceInsufficientPrivileges() { + PolicyAttachmentTarget namespaceTarget = + PolicyAttachmentTarget.builder() + .setType(PolicyAttachmentTarget.TypeEnum.NAMESPACE) + .setPath(Arrays.asList(NS2.levels())) + .build(); + AttachPolicyRequest attachPolicyRequest = + AttachPolicyRequest.builder().setTarget(namespaceTarget).build(); + + doTestInsufficientPrivilegeSets( + List.of( + Set.of(PolarisPrivilege.POLICY_ATTACH, PolarisPrivilege.CATALOG_ATTACH_POLICY), + Set.of(PolarisPrivilege.POLICY_ATTACH, PolarisPrivilege.TABLE_ATTACH_POLICY), + Set.of(PolarisPrivilege.POLICY_ATTACH), + Set.of(PolarisPrivilege.NAMESPACE_ATTACH_POLICY)), + () -> newWrapper(Set.of(PRINCIPAL_ROLE1)).attachPolicy(POLICY_NS1_1, attachPolicyRequest)); + } + + @Test + public void testAttachPolicyToTableSufficientPrivileges() { + Assertions.assertThat( + adminService.grantPrivilegeOnCatalogToRole( + CATALOG_NAME, CATALOG_ROLE2, PolarisPrivilege.POLICY_DETACH)) + .isTrue(); + Assertions.assertThat( + adminService.grantPrivilegeOnCatalogToRole( + CATALOG_NAME, CATALOG_ROLE2, PolarisPrivilege.TABLE_DETACH_POLICY)) + .isTrue(); + + PolicyAttachmentTarget tableTarget = + PolicyAttachmentTarget.builder() + .setType(PolicyAttachmentTarget.TypeEnum.TABLE_LIKE) + .setPath(PolarisCatalogHelpers.tableIdentifierToList(TABLE_NS2_1)) + .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_CONTENT)), + () -> newWrapper(Set.of(PRINCIPAL_ROLE1)).attachPolicy(POLICY_NS1_1, attachPolicyRequest), + () -> newWrapper(Set.of(PRINCIPAL_ROLE2)).detachPolicy(POLICY_NS1_1, detachPolicyRequest)); + } + + @Test + public void testAttachPolicyToTableInsufficientPrivileges() { + PolicyAttachmentTarget tableTarget = + PolicyAttachmentTarget.builder() + .setType(PolicyAttachmentTarget.TypeEnum.TABLE_LIKE) + .setPath(PolarisCatalogHelpers.tableIdentifierToList(TABLE_NS2_1)) + .build(); + AttachPolicyRequest attachPolicyRequest = + AttachPolicyRequest.builder().setTarget(tableTarget).build(); + + doTestInsufficientPrivilegeSets( + List.of( + Set.of(PolarisPrivilege.POLICY_ATTACH, PolarisPrivilege.CATALOG_ATTACH_POLICY), + Set.of(PolarisPrivilege.POLICY_ATTACH, PolarisPrivilege.NAMESPACE_ATTACH_POLICY), + Set.of(PolarisPrivilege.POLICY_ATTACH), + Set.of(PolarisPrivilege.TABLE_ATTACH_POLICY)), + () -> newWrapper(Set.of(PRINCIPAL_ROLE1)).attachPolicy(POLICY_NS1_1, attachPolicyRequest)); + } + + @Test + public void testDetachPolicyFromCatalogSufficientPrivileges() { + Assertions.assertThat( + adminService.grantPrivilegeOnCatalogToRole( + CATALOG_NAME, CATALOG_ROLE2, PolarisPrivilege.POLICY_ATTACH)) + .isTrue(); + Assertions.assertThat( + adminService.grantPrivilegeOnCatalogToRole( + CATALOG_NAME, CATALOG_ROLE2, PolarisPrivilege.CATALOG_ATTACH_POLICY)) + .isTrue(); + Assertions.assertThat( + adminService.grantPrivilegeOnCatalogToRole( + CATALOG_NAME, CATALOG_ROLE2, PolarisPrivilege.POLICY_DETACH)) + .isTrue(); + Assertions.assertThat( + adminService.grantPrivilegeOnCatalogToRole( + CATALOG_NAME, CATALOG_ROLE2, PolarisPrivilege.CATALOG_DETACH_POLICY)) + .isTrue(); + PolicyAttachmentTarget catalogTarget = + PolicyAttachmentTarget.builder().setType(PolicyAttachmentTarget.TypeEnum.CATALOG).build(); + AttachPolicyRequest attachPolicyRequest = + AttachPolicyRequest.builder().setTarget(catalogTarget).build(); + DetachPolicyRequest detachPolicyRequest = + DetachPolicyRequest.builder().setTarget(catalogTarget).build(); + + newWrapper(Set.of(PRINCIPAL_ROLE2)).attachPolicy(POLICY_NS1_1, attachPolicyRequest); + + doTestSufficientPrivilegeSets( + List.of( + Set.of(PolarisPrivilege.POLICY_DETACH, PolarisPrivilege.CATALOG_DETACH_POLICY), + Set.of(PolarisPrivilege.CATALOG_MANAGE_CONTENT)), + () -> newWrapper(Set.of(PRINCIPAL_ROLE1)).detachPolicy(POLICY_NS1_1, detachPolicyRequest), + () -> + newWrapper(Set.of(PRINCIPAL_ROLE2)) + .attachPolicy(POLICY_NS1_1, attachPolicyRequest) /* cleanupAction */); + + newWrapper(Set.of(PRINCIPAL_ROLE2)).detachPolicy(POLICY_NS1_1, detachPolicyRequest); + } + + @Test + public void testDetachPolicyFromCatalogInsufficientPrivileges() { + Assertions.assertThat( + adminService.grantPrivilegeOnCatalogToRole( + CATALOG_NAME, CATALOG_ROLE2, PolarisPrivilege.POLICY_ATTACH)) + .isTrue(); + Assertions.assertThat( + adminService.grantPrivilegeOnCatalogToRole( + CATALOG_NAME, CATALOG_ROLE2, PolarisPrivilege.CATALOG_ATTACH_POLICY)) + .isTrue(); + Assertions.assertThat( + adminService.grantPrivilegeOnCatalogToRole( + CATALOG_NAME, CATALOG_ROLE2, PolarisPrivilege.POLICY_DETACH)) + .isTrue(); + Assertions.assertThat( + adminService.grantPrivilegeOnCatalogToRole( + CATALOG_NAME, CATALOG_ROLE2, PolarisPrivilege.CATALOG_DETACH_POLICY)) + .isTrue(); + + PolicyAttachmentTarget catalogTarget = + PolicyAttachmentTarget.builder().setType(PolicyAttachmentTarget.TypeEnum.CATALOG).build(); + AttachPolicyRequest attachPolicyRequest = + AttachPolicyRequest.builder().setTarget(catalogTarget).build(); + DetachPolicyRequest detachPolicyRequest = + DetachPolicyRequest.builder().setTarget(catalogTarget).build(); + + newWrapper(Set.of(PRINCIPAL_ROLE2)).attachPolicy(POLICY_NS1_1, attachPolicyRequest); + + doTestInsufficientPrivilegeSets( + List.of( + Set.of(PolarisPrivilege.POLICY_DETACH, PolarisPrivilege.NAMESPACE_DETACH_POLICY), + Set.of(PolarisPrivilege.POLICY_DETACH, PolarisPrivilege.TABLE_DETACH_POLICY), + Set.of(PolarisPrivilege.POLICY_DETACH), + Set.of(PolarisPrivilege.CATALOG_DETACH_POLICY)), + () -> newWrapper(Set.of(PRINCIPAL_ROLE1)).detachPolicy(POLICY_NS1_1, detachPolicyRequest)); + + newWrapper(Set.of(PRINCIPAL_ROLE2)).detachPolicy(POLICY_NS1_1, detachPolicyRequest); + } + + @Test + public void testDetachPolicyFromNamespaceSufficientPrivileges() { + Assertions.assertThat( + adminService.grantPrivilegeOnCatalogToRole( + CATALOG_NAME, CATALOG_ROLE2, PolarisPrivilege.POLICY_ATTACH)) + .isTrue(); + Assertions.assertThat( + adminService.grantPrivilegeOnCatalogToRole( + CATALOG_NAME, CATALOG_ROLE2, PolarisPrivilege.NAMESPACE_ATTACH_POLICY)) + .isTrue(); + Assertions.assertThat( + adminService.grantPrivilegeOnCatalogToRole( + CATALOG_NAME, CATALOG_ROLE2, PolarisPrivilege.POLICY_DETACH)) + .isTrue(); + Assertions.assertThat( + adminService.grantPrivilegeOnCatalogToRole( + CATALOG_NAME, CATALOG_ROLE2, PolarisPrivilege.NAMESPACE_DETACH_POLICY)) + .isTrue(); + + PolicyAttachmentTarget namespaceTarget = + PolicyAttachmentTarget.builder() + .setType(PolicyAttachmentTarget.TypeEnum.NAMESPACE) + .setPath(Arrays.asList(NS2.levels())) + .build(); + AttachPolicyRequest attachPolicyRequest = + AttachPolicyRequest.builder().setTarget(namespaceTarget).build(); + DetachPolicyRequest detachPolicyRequest = + DetachPolicyRequest.builder().setTarget(namespaceTarget).build(); + + newWrapper(Set.of(PRINCIPAL_ROLE2)).attachPolicy(POLICY_NS1_1, attachPolicyRequest); + + doTestSufficientPrivilegeSets( + List.of( + Set.of(PolarisPrivilege.POLICY_DETACH, PolarisPrivilege.NAMESPACE_DETACH_POLICY), + Set.of(PolarisPrivilege.CATALOG_MANAGE_CONTENT)), + () -> newWrapper(Set.of(PRINCIPAL_ROLE1)).detachPolicy(POLICY_NS1_1, detachPolicyRequest), + () -> + newWrapper(Set.of(PRINCIPAL_ROLE2)) + .attachPolicy(POLICY_NS1_1, attachPolicyRequest) /* cleanupAction */); + + newWrapper(Set.of(PRINCIPAL_ROLE2)).detachPolicy(POLICY_NS1_1, detachPolicyRequest); + } + + @Test + public void testDetachPolicyFromNamespaceInsufficientPrivilege() { + Assertions.assertThat( + adminService.grantPrivilegeOnCatalogToRole( + CATALOG_NAME, CATALOG_ROLE2, PolarisPrivilege.POLICY_ATTACH)) + .isTrue(); + Assertions.assertThat( + adminService.grantPrivilegeOnCatalogToRole( + CATALOG_NAME, CATALOG_ROLE2, PolarisPrivilege.NAMESPACE_ATTACH_POLICY)) + .isTrue(); + Assertions.assertThat( + adminService.grantPrivilegeOnCatalogToRole( + CATALOG_NAME, CATALOG_ROLE2, PolarisPrivilege.POLICY_DETACH)) + .isTrue(); + Assertions.assertThat( + adminService.grantPrivilegeOnCatalogToRole( + CATALOG_NAME, CATALOG_ROLE2, PolarisPrivilege.NAMESPACE_DETACH_POLICY)) + .isTrue(); + + PolicyAttachmentTarget namespaceTarget = + PolicyAttachmentTarget.builder() + .setType(PolicyAttachmentTarget.TypeEnum.NAMESPACE) + .setPath(Arrays.asList(NS2.levels())) + .build(); + AttachPolicyRequest attachPolicyRequest = + AttachPolicyRequest.builder().setTarget(namespaceTarget).build(); + DetachPolicyRequest detachPolicyRequest = + DetachPolicyRequest.builder().setTarget(namespaceTarget).build(); + + newWrapper(Set.of(PRINCIPAL_ROLE2)).attachPolicy(POLICY_NS1_1, attachPolicyRequest); + + doTestInsufficientPrivilegeSets( + List.of( + Set.of(PolarisPrivilege.POLICY_DETACH, PolarisPrivilege.CATALOG_DETACH_POLICY), + Set.of(PolarisPrivilege.POLICY_DETACH, PolarisPrivilege.TABLE_DETACH_POLICY), + Set.of(PolarisPrivilege.POLICY_DETACH), + Set.of(PolarisPrivilege.NAMESPACE_DETACH_POLICY)), + () -> newWrapper(Set.of(PRINCIPAL_ROLE1)).detachPolicy(POLICY_NS1_1, detachPolicyRequest)); + + newWrapper(Set.of(PRINCIPAL_ROLE2)).detachPolicy(POLICY_NS1_1, detachPolicyRequest); + } + + @Test + public void testDetachPolicyFromTableSufficientPrivileges() { + Assertions.assertThat( + adminService.grantPrivilegeOnCatalogToRole( + CATALOG_NAME, CATALOG_ROLE2, PolarisPrivilege.POLICY_ATTACH)) + .isTrue(); + Assertions.assertThat( + adminService.grantPrivilegeOnCatalogToRole( + CATALOG_NAME, CATALOG_ROLE2, PolarisPrivilege.TABLE_ATTACH_POLICY)) + .isTrue(); + Assertions.assertThat( + adminService.grantPrivilegeOnCatalogToRole( + CATALOG_NAME, CATALOG_ROLE2, PolarisPrivilege.POLICY_DETACH)) + .isTrue(); + Assertions.assertThat( + adminService.grantPrivilegeOnCatalogToRole( + CATALOG_NAME, CATALOG_ROLE2, PolarisPrivilege.TABLE_DETACH_POLICY)) + .isTrue(); + + PolicyAttachmentTarget tableTarget = + PolicyAttachmentTarget.builder() + .setType(PolicyAttachmentTarget.TypeEnum.TABLE_LIKE) + .setPath(PolarisCatalogHelpers.tableIdentifierToList(TABLE_NS2_1)) + .build(); + AttachPolicyRequest attachPolicyRequest = + AttachPolicyRequest.builder().setTarget(tableTarget).build(); + DetachPolicyRequest detachPolicyRequest = + DetachPolicyRequest.builder().setTarget(tableTarget).build(); + + newWrapper(Set.of(PRINCIPAL_ROLE2)).attachPolicy(POLICY_NS1_1, attachPolicyRequest); + + doTestSufficientPrivilegeSets( + List.of( + Set.of(PolarisPrivilege.POLICY_DETACH, PolarisPrivilege.TABLE_DETACH_POLICY), + Set.of(PolarisPrivilege.CATALOG_MANAGE_CONTENT)), + () -> newWrapper(Set.of(PRINCIPAL_ROLE1)).detachPolicy(POLICY_NS1_1, detachPolicyRequest), + () -> + newWrapper(Set.of(PRINCIPAL_ROLE2)) + .attachPolicy(POLICY_NS1_1, attachPolicyRequest) /* cleanupAction */); + + newWrapper(Set.of(PRINCIPAL_ROLE2)).detachPolicy(POLICY_NS1_1, detachPolicyRequest); + } + + @Test + public void testDetachFromPolicyInsufficientPrivileges() { + Assertions.assertThat( + adminService.grantPrivilegeOnCatalogToRole( + CATALOG_NAME, CATALOG_ROLE2, PolarisPrivilege.POLICY_ATTACH)) + .isTrue(); + Assertions.assertThat( + adminService.grantPrivilegeOnCatalogToRole( + CATALOG_NAME, CATALOG_ROLE2, PolarisPrivilege.TABLE_ATTACH_POLICY)) + .isTrue(); + Assertions.assertThat( + adminService.grantPrivilegeOnCatalogToRole( + CATALOG_NAME, CATALOG_ROLE2, PolarisPrivilege.POLICY_DETACH)) + .isTrue(); + Assertions.assertThat( + adminService.grantPrivilegeOnCatalogToRole( + CATALOG_NAME, CATALOG_ROLE2, PolarisPrivilege.TABLE_DETACH_POLICY)) + .isTrue(); + + PolicyAttachmentTarget tableTarget = + PolicyAttachmentTarget.builder() + .setType(PolicyAttachmentTarget.TypeEnum.TABLE_LIKE) + .setPath(PolarisCatalogHelpers.tableIdentifierToList(TABLE_NS2_1)) + .build(); + AttachPolicyRequest attachPolicyRequest = + AttachPolicyRequest.builder().setTarget(tableTarget).build(); + DetachPolicyRequest detachPolicyRequest = + DetachPolicyRequest.builder().setTarget(tableTarget).build(); + + newWrapper(Set.of(PRINCIPAL_ROLE2)).attachPolicy(POLICY_NS1_1, attachPolicyRequest); + + doTestInsufficientPrivilegeSets( + List.of( + Set.of(PolarisPrivilege.POLICY_DETACH, PolarisPrivilege.CATALOG_DETACH_POLICY), + Set.of(PolarisPrivilege.POLICY_DETACH, PolarisPrivilege.NAMESPACE_DETACH_POLICY), + Set.of(PolarisPrivilege.POLICY_DETACH), + Set.of(PolarisPrivilege.TABLE_DETACH_POLICY)), + () -> newWrapper(Set.of(PRINCIPAL_ROLE1)).detachPolicy(POLICY_NS1_1, detachPolicyRequest)); + + newWrapper(Set.of(PRINCIPAL_ROLE2)).detachPolicy(POLICY_NS1_1, detachPolicyRequest); + } } diff --git a/service/common/src/main/java/org/apache/polaris/service/catalog/policy/PolicyCatalogHandler.java b/service/common/src/main/java/org/apache/polaris/service/catalog/policy/PolicyCatalogHandler.java index 1eceb362d2..fb0c71f2e7 100644 --- a/service/common/src/main/java/org/apache/polaris/service/catalog/policy/PolicyCatalogHandler.java +++ b/service/common/src/main/java/org/apache/polaris/service/catalog/policy/PolicyCatalogHandler.java @@ -19,24 +19,33 @@ package org.apache.polaris.service.catalog.policy; import jakarta.ws.rs.core.SecurityContext; +import java.util.Arrays; import java.util.HashSet; import java.util.List; import org.apache.iceberg.catalog.Namespace; +import org.apache.iceberg.catalog.TableIdentifier; +import org.apache.iceberg.exceptions.NoSuchNamespaceException; +import org.apache.iceberg.exceptions.NoSuchTableException; import org.apache.polaris.core.auth.PolarisAuthorizableOperation; import org.apache.polaris.core.auth.PolarisAuthorizer; import org.apache.polaris.core.catalog.PolarisCatalogHelpers; import org.apache.polaris.core.context.CallContext; +import org.apache.polaris.core.entity.PolarisEntitySubType; import org.apache.polaris.core.entity.PolarisEntityType; import org.apache.polaris.core.persistence.PolarisEntityManager; import org.apache.polaris.core.persistence.PolarisMetaStoreManager; import org.apache.polaris.core.persistence.PolarisResolvedPathWrapper; import org.apache.polaris.core.persistence.resolver.ResolverPath; +import org.apache.polaris.core.persistence.resolver.ResolverStatus; import org.apache.polaris.core.policy.PolicyType; import org.apache.polaris.core.policy.exceptions.NoSuchPolicyException; import org.apache.polaris.service.catalog.common.CatalogHandler; +import org.apache.polaris.service.types.AttachPolicyRequest; import org.apache.polaris.service.types.CreatePolicyRequest; +import org.apache.polaris.service.types.DetachPolicyRequest; import org.apache.polaris.service.types.ListPoliciesResponse; import org.apache.polaris.service.types.LoadPolicyResponse; +import org.apache.polaris.service.types.PolicyAttachmentTarget; import org.apache.polaris.service.types.PolicyIdentifier; import org.apache.polaris.service.types.UpdatePolicyRequest; @@ -115,6 +124,16 @@ public boolean dropPolicy(PolicyIdentifier identifier, boolean detachAll) { return policyCatalog.dropPolicy(identifier, detachAll); } + public boolean attachPolicy(PolicyIdentifier identifier, AttachPolicyRequest request) { + authorizePolicyMappingOperationOrThrow(identifier, request.getTarget(), true); + return policyCatalog.attachPolicy(identifier, request.getTarget(), request.getParameters()); + } + + public boolean detachPolicy(PolicyIdentifier identifier, DetachPolicyRequest request) { + authorizePolicyMappingOperationOrThrow(identifier, request.getTarget(), false); + return policyCatalog.detachPolicy(identifier, request.getTarget()); + } + private void authorizeBasicPolicyOperationOrThrow( PolarisAuthorizableOperation op, PolicyIdentifier identifier) { resolutionManifest = @@ -141,4 +160,107 @@ private void authorizeBasicPolicyOperationOrThrow( initializeCatalog(); } + + private void authorizePolicyMappingOperationOrThrow( + PolicyIdentifier identifier, PolicyAttachmentTarget target, boolean isAttach) { + resolutionManifest = + entityManager.prepareResolutionManifest(callContext, securityContext, catalogName); + resolutionManifest.addPassthroughPath( + new ResolverPath( + PolarisCatalogHelpers.identifierToList(identifier.getNamespace(), identifier.getName()), + PolarisEntityType.POLICY, + true /* optional */), + identifier); + + switch (target.getType()) { + case CATALOG -> {} + case NAMESPACE -> { + Namespace targetNamespace = Namespace.of(target.getPath().toArray(new String[0])); + resolutionManifest.addPath( + new ResolverPath(Arrays.asList(targetNamespace.levels()), PolarisEntityType.NAMESPACE), + targetNamespace); + } + case TABLE_LIKE -> { + TableIdentifier targetIdentifier = + TableIdentifier.of(target.getPath().toArray(new String[0])); + resolutionManifest.addPath( + new ResolverPath( + PolarisCatalogHelpers.tableIdentifierToList(targetIdentifier), + PolarisEntityType.TABLE_LIKE), + targetIdentifier); + } + default -> throw new IllegalArgumentException("Unsupported target type: " + target.getType()); + } + + ResolverStatus status = resolutionManifest.resolveAll(); + + throwNotFoundExceptionIfFailToResolve(status, identifier); + + PolarisResolvedPathWrapper policyWrapper = + resolutionManifest.getPassthroughResolvedPath( + identifier, PolarisEntityType.POLICY, PolarisEntitySubType.NULL_SUBTYPE); + if (policyWrapper == null) { + throw new NoSuchPolicyException(String.format("Policy does not exist: %s", identifier)); + } + + PolarisResolvedPathWrapper targetWrapper = + PolicyCatalogUtils.getResolvedPathWrapper(resolutionManifest, target); + + PolarisAuthorizableOperation op = + determinePolicyMappingOperation(target, targetWrapper, isAttach); + + authorizer.authorizeOrThrow( + authenticatedPrincipal, + resolutionManifest.getAllActivatedCatalogRoleAndPrincipalRoles(), + op, + policyWrapper, + targetWrapper); + + initializeCatalog(); + } + + private PolarisAuthorizableOperation determinePolicyMappingOperation( + PolicyAttachmentTarget target, PolarisResolvedPathWrapper targetWrapper, boolean isAttach) { + return switch (targetWrapper.getRawLeafEntity().getType()) { + case CATALOG -> + isAttach + ? PolarisAuthorizableOperation.ATTACH_POLICY_TO_CATALOG + : PolarisAuthorizableOperation.DETACH_POLICY_FROM_CATALOG; + case NAMESPACE -> + isAttach + ? PolarisAuthorizableOperation.ATTACH_POLICY_TO_NAMESPACE + : PolarisAuthorizableOperation.DETACH_POLICY_FROM_NAMESPACE; + case TABLE_LIKE -> { + PolarisEntitySubType subType = targetWrapper.getRawLeafEntity().getSubType(); + if (subType == PolarisEntitySubType.ICEBERG_TABLE) { + yield isAttach + ? PolarisAuthorizableOperation.ATTACH_POLICY_TO_TABLE + : PolarisAuthorizableOperation.DETACH_POLICY_FROM_TABLE; + } + throw new IllegalArgumentException("Unsupported table-like subtype: " + subType); + } + default -> throw new IllegalArgumentException("Unsupported target type: " + target.getType()); + }; + } + + private void throwNotFoundExceptionIfFailToResolve( + ResolverStatus status, PolicyIdentifier identifier) { + if ((status.getStatus() == ResolverStatus.StatusEnum.PATH_COULD_NOT_BE_FULLY_RESOLVED)) { + switch (status.getFailedToResolvePath().getLastEntityType()) { + case PolarisEntityType.TABLE_LIKE -> + throw new NoSuchTableException( + "Table or view does not exist: %s", + PolarisCatalogHelpers.listToTableIdentifier( + status.getFailedToResolvePath().getEntityNames())); + case PolarisEntityType.NAMESPACE -> + throw new NoSuchNamespaceException( + "Namespace does not exist: %s", + Namespace.of( + status.getFailedToResolvePath().getEntityNames().toArray(new String[0]))); + case PolarisEntityType.POLICY -> + throw new NoSuchPolicyException(String.format("Policy does not exist: %s", identifier)); + default -> throw new IllegalStateException("Cannot resolve"); + } + } + } } diff --git a/service/common/src/main/java/org/apache/polaris/service/catalog/policy/PolicyCatalogUtils.java b/service/common/src/main/java/org/apache/polaris/service/catalog/policy/PolicyCatalogUtils.java new file mode 100644 index 0000000000..42364a3e45 --- /dev/null +++ b/service/common/src/main/java/org/apache/polaris/service/catalog/policy/PolicyCatalogUtils.java @@ -0,0 +1,62 @@ +/* + * 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.service.catalog.policy; + +import jakarta.annotation.Nonnull; +import org.apache.iceberg.catalog.Namespace; +import org.apache.iceberg.catalog.TableIdentifier; +import org.apache.iceberg.exceptions.NoSuchNamespaceException; +import org.apache.iceberg.exceptions.NoSuchTableException; +import org.apache.polaris.core.entity.PolarisEntitySubType; +import org.apache.polaris.core.entity.PolarisEntityType; +import org.apache.polaris.core.persistence.PolarisResolvedPathWrapper; +import org.apache.polaris.core.persistence.resolver.PolarisResolutionManifest; +import org.apache.polaris.service.types.PolicyAttachmentTarget; + +public class PolicyCatalogUtils { + + public static PolarisResolvedPathWrapper getResolvedPathWrapper( + @Nonnull PolarisResolutionManifest resolutionManifest, + @Nonnull PolicyAttachmentTarget target) { + return switch (target.getType()) { + // get the current catalog entity, since policy cannot apply across catalog at this moment + case CATALOG -> resolutionManifest.getResolvedReferenceCatalogEntity(); + case NAMESPACE -> { + var namespace = Namespace.of(target.getPath().toArray(new String[0])); + var resolvedTargetEntity = resolutionManifest.getResolvedPath(namespace); + if (resolvedTargetEntity == null) { + throw new NoSuchNamespaceException("Namespace does not exist: %s", namespace); + } + yield resolvedTargetEntity; + } + case TABLE_LIKE -> { + var tableIdentifier = TableIdentifier.of(target.getPath().toArray(new String[0])); + // only Iceberg tables are supported + var resolvedTableEntity = + resolutionManifest.getResolvedPath( + tableIdentifier, PolarisEntityType.TABLE_LIKE, PolarisEntitySubType.ICEBERG_TABLE); + if (resolvedTableEntity == null) { + throw new NoSuchTableException("Iceberg Table does not exist: %s", tableIdentifier); + } + yield resolvedTableEntity; + } + default -> throw new IllegalArgumentException("Unsupported target type: " + target.getType()); + }; + } +}