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 9b285c1bb0..0957a4e9db 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 @@ -40,6 +40,11 @@ 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_CREATE; +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; +import static org.apache.polaris.core.entity.PolarisPrivilege.POLICY_WRITE; import static org.apache.polaris.core.entity.PolarisPrivilege.PRINCIPAL_CREATE; import static org.apache.polaris.core.entity.PolarisPrivilege.PRINCIPAL_DROP; import static org.apache.polaris.core.entity.PolarisPrivilege.PRINCIPAL_LIST; @@ -182,6 +187,11 @@ public enum PolarisAuthorizableOperation { REVOKE_VIEW_GRANT_FROM_CATALOG_ROLE( VIEW_MANAGE_GRANTS_ON_SECURABLE, CATALOG_ROLE_MANAGE_GRANTS_FOR_GRANTEE), LIST_GRANTS_ON_VIEW(VIEW_LIST_GRANTS), + CREATE_POLICY(POLICY_CREATE), + LOAD_POLICY(POLICY_READ), + DROP_POLICY(POLICY_DROP), + UPDATE_POLICY(POLICY_WRITE), + LIST_POLICY(POLICY_LIST), ; 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 564be49dac..abeefa2b6a 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 @@ -47,6 +47,12 @@ 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_CREATE; +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; +import static org.apache.polaris.core.entity.PolarisPrivilege.POLICY_READ; +import static org.apache.polaris.core.entity.PolarisPrivilege.POLICY_WRITE; import static org.apache.polaris.core.entity.PolarisPrivilege.PRINCIPAL_CREATE; import static org.apache.polaris.core.entity.PolarisPrivilege.PRINCIPAL_DROP; import static org.apache.polaris.core.entity.PolarisPrivilege.PRINCIPAL_FULL_METADATA; @@ -457,6 +463,38 @@ public class PolarisAuthorizerImpl implements PolarisAuthorizer { SUPER_PRIVILEGES.putAll( CATALOG_ROLE_MANAGE_GRANTS_FOR_GRANTEE, List.of(CATALOG_ROLE_MANAGE_GRANTS_FOR_GRANTEE, CATALOG_MANAGE_ACCESS)); + + // Policy privileges + SUPER_PRIVILEGES.putAll( + POLICY_CREATE, + List.of( + POLICY_CREATE, POLICY_FULL_METADATA, CATALOG_MANAGE_METADATA, CATALOG_MANAGE_CONTENT)); + SUPER_PRIVILEGES.putAll( + POLICY_WRITE, + List.of( + POLICY_WRITE, POLICY_FULL_METADATA, CATALOG_MANAGE_METADATA, CATALOG_MANAGE_CONTENT)); + SUPER_PRIVILEGES.putAll( + POLICY_DROP, + List.of( + POLICY_DROP, POLICY_FULL_METADATA, CATALOG_MANAGE_METADATA, CATALOG_MANAGE_CONTENT)); + SUPER_PRIVILEGES.putAll( + POLICY_READ, + List.of( + POLICY_READ, + POLICY_WRITE, + POLICY_FULL_METADATA, + CATALOG_MANAGE_METADATA, + CATALOG_MANAGE_CONTENT)); + SUPER_PRIVILEGES.putAll( + POLICY_LIST, + List.of( + POLICY_LIST, + POLICY_CREATE, + POLICY_READ, + POLICY_WRITE, + POLICY_FULL_METADATA, + CATALOG_MANAGE_METADATA, + 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 6a2464a069..03585790b9 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 @@ -136,6 +136,12 @@ public enum PolarisPrivilege { CATALOG_ROLE_FULL_METADATA(67, PolarisEntityType.CATALOG_ROLE), CATALOG_ROLE_MANAGE_GRANTS_ON_SECURABLE(68, PolarisEntityType.CATALOG_ROLE), CATALOG_ROLE_MANAGE_GRANTS_FOR_GRANTEE(69, PolarisEntityType.CATALOG_ROLE), + POLICY_CREATE(70, PolarisEntityType.NAMESPACE), + POLICY_READ(71, PolarisEntityType.POLICY), + POLICY_DROP(72, PolarisEntityType.POLICY), + POLICY_WRITE(73, PolarisEntityType.POLICY), + POLICY_LIST(74, PolarisEntityType.NAMESPACE), + POLICY_FULL_METADATA(75, PolarisEntityType.POLICY), ; /** 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 4b0037eed0..10972b602c 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 @@ -75,6 +75,7 @@ import org.apache.polaris.core.persistence.dao.entity.EntityResult; import org.apache.polaris.core.persistence.resolver.PolarisResolutionManifest; import org.apache.polaris.core.persistence.transactional.TransactionalPersistence; +import org.apache.polaris.core.policy.PredefinedPolicyTypes; import org.apache.polaris.core.secrets.UserSecretsManager; import org.apache.polaris.core.secrets.UserSecretsManagerFactory; import org.apache.polaris.service.admin.PolarisAdminService; @@ -82,12 +83,14 @@ import org.apache.polaris.service.catalog.generic.GenericTableCatalog; import org.apache.polaris.service.catalog.iceberg.IcebergCatalog; import org.apache.polaris.service.catalog.io.FileIOFactory; +import org.apache.polaris.service.catalog.policy.PolicyCatalog; import org.apache.polaris.service.config.DefaultConfigurationStore; import org.apache.polaris.service.config.RealmEntityManagerFactory; import org.apache.polaris.service.context.CallContextCatalogFactory; import org.apache.polaris.service.context.PolarisCallContextCatalogFactory; import org.apache.polaris.service.storage.PolarisStorageIntegrationProviderImpl; import org.apache.polaris.service.task.TaskExecutor; +import org.apache.polaris.service.types.PolicyIdentifier; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; @@ -139,6 +142,9 @@ public Map getConfigOverrides() { protected static final TableIdentifier TABLE_NS1_1_GENERIC = TableIdentifier.of(NS1, "layer1_table_generic"); + // A policy directly under ns1 + protected static final PolicyIdentifier POLICY_NS1_1 = new PolicyIdentifier(NS1, "layer1_policy"); + // Two tables under ns1a protected static final TableIdentifier TABLE_NS1A_1 = TableIdentifier.of(NS1A, "table1"); protected static final TableIdentifier TABLE_NS1A_2 = TableIdentifier.of(NS1A, "table2"); @@ -185,6 +191,7 @@ public Map getConfigOverrides() { protected IcebergCatalog baseCatalog; protected GenericTableCatalog genericTableCatalog; + protected PolicyCatalog policyCatalog; protected PolarisAdminService adminService; protected PolarisEntityManager entityManager; protected PolarisMetaStoreManager metaStoreManager; @@ -321,6 +328,12 @@ public void before(TestInfo testInfo) { genericTableCatalog.createGenericTable(TABLE_NS1_1_GENERIC, "format", "doc", Map.of()); + policyCatalog.createPolicy( + POLICY_NS1_1, + PredefinedPolicyTypes.DATA_COMPACTION.getName(), + "test_policy", + "{\"enable\": false}"); + baseCatalog .buildView(VIEW_NS1_1) .withSchema(SCHEMA) @@ -463,6 +476,7 @@ private void initBaseCatalog() { CatalogProperties.FILE_IO_IMPL, "org.apache.iceberg.inmemory.InMemoryFileIO")); this.genericTableCatalog = new GenericTableCatalog(metaStoreManager, callContext, passthroughView); + this.policyCatalog = new PolicyCatalog(metaStoreManager, callContext, passthroughView); } @Alternative 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 new file mode 100644 index 0000000000..74a54b807c --- /dev/null +++ b/quarkus/service/src/test/java/org/apache/polaris/service/quarkus/catalog/PolicyCatalogHandlerAuthzTest.java @@ -0,0 +1,304 @@ +/* + * 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.quarkus.catalog; + +import io.quarkus.test.junit.QuarkusTest; +import java.util.List; +import java.util.Set; +import org.apache.polaris.core.auth.AuthenticatedPolarisPrincipal; +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.CreatePolicyRequest; +import org.apache.polaris.service.types.PolicyIdentifier; +import org.apache.polaris.service.types.UpdatePolicyRequest; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; + +@QuarkusTest +public class PolicyCatalogHandlerAuthzTest extends PolarisAuthzTestBase { + private PolicyCatalogHandler newWrapper() { + return newWrapper(Set.of()); + } + + private PolicyCatalogHandler newWrapper(Set activatedPrincipalRoles) { + return newWrapper(activatedPrincipalRoles, CATALOG_NAME); + } + + private PolicyCatalogHandler newWrapper(Set activatedPrincipalRoles, String catalogName) { + final AuthenticatedPolarisPrincipal authenticatedPrincipal = + new AuthenticatedPolarisPrincipal(principalEntity, activatedPrincipalRoles); + return new PolicyCatalogHandler( + callContext, + entityManager, + metaStoreManager, + securityContext(authenticatedPrincipal, activatedPrincipalRoles), + catalogName, + polarisAuthorizer); + } + + /** + * Tests each "sufficient" privilege individually using CATALOG_ROLE1 by granting at the + * CATALOG_NAME level, revoking after each test, and also ensuring that the request fails after + * revocation. + * + * @param sufficientPrivileges List of privileges that should be sufficient each in isolation for + * {@code action} to succeed. + * @param action The operation being tested; could also be multiple operations that should all + * succeed with the sufficient privilege + * @param cleanupAction If non-null, additional action to run to "undo" a previous success action + * in case the action has side effects. Called before revoking the sufficient privilege; + * either the cleanup privileges must be latent, or the cleanup action could be run with + * PRINCIPAL_ROLE2 while runnint {@code action} with PRINCIPAL_ROLE1. + */ + private void doTestSufficientPrivileges( + List sufficientPrivileges, Runnable action, Runnable cleanupAction) { + doTestSufficientPrivilegeSets( + sufficientPrivileges.stream().map(priv -> Set.of(priv)).toList(), + action, + cleanupAction, + PRINCIPAL_NAME); + } + + /** + * @param sufficientPrivileges each set of concurrent privileges expected to be sufficient + * together. + * @param action + * @param cleanupAction + * @param principalName + */ + private void doTestSufficientPrivilegeSets( + List> sufficientPrivileges, + Runnable action, + Runnable cleanupAction, + String principalName) { + doTestSufficientPrivilegeSets( + sufficientPrivileges, action, cleanupAction, principalName, CATALOG_NAME); + } + + /** + * @param sufficientPrivileges each set of concurrent privileges expected to be sufficient + * together. + * @param action + * @param cleanupAction + * @param principalName + * @param catalogName + */ + private void doTestSufficientPrivilegeSets( + List> sufficientPrivileges, + Runnable action, + Runnable cleanupAction, + String principalName, + String catalogName) { + doTestSufficientPrivilegeSets( + sufficientPrivileges, + action, + cleanupAction, + principalName, + (privilege) -> + adminService.grantPrivilegeOnCatalogToRole(catalogName, CATALOG_ROLE1, privilege), + (privilege) -> + adminService.revokePrivilegeOnCatalogFromRole(catalogName, CATALOG_ROLE1, privilege)); + } + + private void doTestInsufficientPrivileges( + List insufficientPrivileges, Runnable action) { + doTestInsufficientPrivileges(insufficientPrivileges, 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. + */ + private void doTestInsufficientPrivileges( + List insufficientPrivileges, String principalName, Runnable action) { + doTestInsufficientPrivileges( + insufficientPrivileges, + principalName, + action, + (privilege) -> + adminService.grantPrivilegeOnCatalogToRole(CATALOG_NAME, CATALOG_ROLE1, privilege), + (privilege) -> + adminService.revokePrivilegeOnCatalogFromRole(CATALOG_NAME, CATALOG_ROLE1, privilege)); + } + + @Test + public void testListPoliciesAllSufficientPrivileges() { + doTestSufficientPrivileges( + List.of( + PolarisPrivilege.POLICY_LIST, + PolarisPrivilege.POLICY_CREATE, + PolarisPrivilege.POLICY_WRITE, + PolarisPrivilege.POLICY_READ, + PolarisPrivilege.POLICY_FULL_METADATA, + PolarisPrivilege.CATALOG_MANAGE_CONTENT), + () -> newWrapper().listPolicies(NS1, null), + null /* cleanupAction */); + } + + @Test + public void testListPoliciesInsufficientPrivileges() { + doTestInsufficientPrivileges( + List.of(PolarisPrivilege.NAMESPACE_FULL_METADATA, PolarisPrivilege.POLICY_DROP), + () -> newWrapper().listPolicies(NS1, null)); + } + + @Test + public void testCreatePolicyAllSufficientPrivileges() { + Assertions.assertThat( + adminService.grantPrivilegeOnCatalogToRole( + CATALOG_NAME, CATALOG_ROLE2, PolarisPrivilege.POLICY_DROP)) + .isTrue(); + + final PolicyIdentifier newPolicy = new PolicyIdentifier(NS2, "newPolicy"); + final CreatePolicyRequest createPolicyRequest = + CreatePolicyRequest.builder() + .setName(newPolicy.getName()) + .setType(PredefinedPolicyTypes.DATA_COMPACTION.getName()) + .setContent("{\"enable\": false}") + .build(); + + doTestSufficientPrivileges( + List.of( + PolarisPrivilege.POLICY_CREATE, + PolarisPrivilege.CATALOG_MANAGE_CONTENT, + PolarisPrivilege.POLICY_FULL_METADATA), + () -> newWrapper(Set.of(PRINCIPAL_ROLE1)).createPolicy(NS2, createPolicyRequest), + () -> newWrapper(Set.of(PRINCIPAL_ROLE2)).dropPolicy(newPolicy, true)); + } + + @Test + public void testCreatePolicyInsufficientPrivileges() { + final PolicyIdentifier newPolicy = new PolicyIdentifier(NS2, "newPolicy"); + final CreatePolicyRequest createPolicyRequest = + CreatePolicyRequest.builder() + .setName(newPolicy.getName()) + .setType(PredefinedPolicyTypes.DATA_COMPACTION.getName()) + .setContent("{\"enable\": false}") + .build(); + + doTestInsufficientPrivileges( + List.of( + PolarisPrivilege.POLICY_READ, + PolarisPrivilege.POLICY_LIST, + PolarisPrivilege.POLICY_DROP, + PolarisPrivilege.POLICY_LIST, + PolarisPrivilege.POLICY_WRITE), + () -> newWrapper().createPolicy(NS2, createPolicyRequest)); + } + + @Test + public void testLoadPolicyAllSufficientPrivileges() { + doTestSufficientPrivileges( + List.of( + PolarisPrivilege.POLICY_READ, + PolarisPrivilege.POLICY_WRITE, + PolarisPrivilege.POLICY_FULL_METADATA, + PolarisPrivilege.CATALOG_MANAGE_CONTENT), + () -> newWrapper().loadPolicy(POLICY_NS1_1), + null /* cleanupAction */); + } + + @Test + public void testLoadPolicyInsufficientPrivileges() { + doTestInsufficientPrivileges( + List.of( + PolarisPrivilege.POLICY_LIST, + PolarisPrivilege.POLICY_DROP, + PolarisPrivilege.POLICY_CREATE), + () -> newWrapper().loadPolicy(POLICY_NS1_1)); + } + + @Test + public void testUpdatePolicyAllSufficientPrivileges() { + doTestSufficientPrivileges( + List.of( + PolarisPrivilege.POLICY_WRITE, + PolarisPrivilege.POLICY_FULL_METADATA, + PolarisPrivilege.CATALOG_MANAGE_CONTENT), + () -> + newWrapper() + .updatePolicy( + POLICY_NS1_1, + UpdatePolicyRequest.builder() + .setCurrentPolicyVersion(0) + .setDescription("test_policy") + .setContent("{\"enable\": false}") + .build()), + null /* cleanupAction */); + } + + @Test + public void testUpdatePolicyInsufficientPrivileges() { + doTestInsufficientPrivileges( + List.of( + PolarisPrivilege.POLICY_LIST, + PolarisPrivilege.POLICY_DROP, + PolarisPrivilege.POLICY_CREATE, + PolarisPrivilege.POLICY_READ), + () -> + newWrapper() + .updatePolicy( + POLICY_NS1_1, + UpdatePolicyRequest.builder() + .setCurrentPolicyVersion(0) + .setDescription("test_policy") + .setContent("{\"enable\": false}") + .build())); + } + + @Test + public void testDropPolicyAllSufficientPrivileges() { + Assertions.assertThat( + adminService.grantPrivilegeOnCatalogToRole( + CATALOG_NAME, CATALOG_ROLE2, PolarisPrivilege.POLICY_CREATE)) + .isTrue(); + + final CreatePolicyRequest createPolicyRequest = + CreatePolicyRequest.builder() + .setName(POLICY_NS1_1.getName()) + .setType(PredefinedPolicyTypes.DATA_COMPACTION.getName()) + .setDescription("test_policy") + .setContent("{\"enable\": false}") + .build(); + + doTestSufficientPrivileges( + List.of( + PolarisPrivilege.POLICY_DROP, + PolarisPrivilege.POLICY_FULL_METADATA, + PolarisPrivilege.CATALOG_MANAGE_CONTENT), + () -> newWrapper(Set.of(PRINCIPAL_ROLE1)).dropPolicy(POLICY_NS1_1, true), + () -> + newWrapper(Set.of(PRINCIPAL_ROLE2)) + .createPolicy( + POLICY_NS1_1.getNamespace(), createPolicyRequest) /* cleanupAction */); + } + + @Test + public void testDropPolicyInsufficientPrivileges() { + doTestInsufficientPrivileges( + List.of( + PolarisPrivilege.POLICY_READ, + PolarisPrivilege.POLICY_LIST, + PolarisPrivilege.POLICY_CREATE, + PolarisPrivilege.POLICY_WRITE), + () -> newWrapper().dropPolicy(POLICY_NS1_1, true)); + } +} diff --git a/service/common/src/main/java/org/apache/polaris/service/catalog/common/CatalogHandler.java b/service/common/src/main/java/org/apache/polaris/service/catalog/common/CatalogHandler.java index 3a902c8edb..9c8f1913f3 100644 --- a/service/common/src/main/java/org/apache/polaris/service/catalog/common/CatalogHandler.java +++ b/service/common/src/main/java/org/apache/polaris/service/catalog/common/CatalogHandler.java @@ -43,6 +43,7 @@ import org.apache.polaris.core.persistence.resolver.PolarisResolutionManifest; import org.apache.polaris.core.persistence.resolver.ResolverPath; import org.apache.polaris.core.persistence.resolver.ResolverStatus; +import org.apache.polaris.service.types.PolicyIdentifier; /** * An ABC for catalog wrappers which provides authorize methods that should be called before a @@ -54,9 +55,9 @@ public abstract class CatalogHandler { // Initialized in the authorize methods. protected PolarisResolutionManifest resolutionManifest = null; - private final PolarisEntityManager entityManager; - private final String catalogName; - private final PolarisAuthorizer authorizer; + protected final PolarisEntityManager entityManager; + protected final String catalogName; + protected final PolarisAuthorizer authorizer; protected final CallContext callContext; protected final AuthenticatedPolarisPrincipal authenticatedPrincipal; @@ -89,14 +90,15 @@ public CatalogHandler( protected void authorizeBasicNamespaceOperationOrThrow( PolarisAuthorizableOperation op, Namespace namespace) { - authorizeBasicNamespaceOperationOrThrow(op, namespace, null, null); + authorizeBasicNamespaceOperationOrThrow(op, namespace, null, null, null); } protected void authorizeBasicNamespaceOperationOrThrow( PolarisAuthorizableOperation op, Namespace namespace, List extraPassthroughNamespaces, - List extraPassthroughTableLikes) { + List extraPassthroughTableLikes, + List extraPassThroughPolicies) { resolutionManifest = entityManager.prepareResolutionManifest(callContext, securityContext, catalogName); resolutionManifest.addPath( @@ -121,6 +123,18 @@ protected void authorizeBasicNamespaceOperationOrThrow( id); } } + + if (extraPassThroughPolicies != null) { + for (PolicyIdentifier id : extraPassThroughPolicies) { + resolutionManifest.addPassthroughPath( + new ResolverPath( + PolarisCatalogHelpers.identifierToList(id.getNamespace(), id.getName()), + PolarisEntityType.POLICY, + true /* optional */), + id); + } + } + resolutionManifest.resolveAll(); PolarisResolvedPathWrapper target = resolutionManifest.getResolvedPath(namespace, true); if (target == null) { diff --git a/service/common/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogHandler.java b/service/common/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogHandler.java index 716d68fdbc..c4105ddba8 100644 --- a/service/common/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogHandler.java +++ b/service/common/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogHandler.java @@ -506,7 +506,7 @@ public boolean sendNotification(TableIdentifier identifier, NotificationRequest extraPassthroughNamespaces.add(nsLevel); } authorizeBasicNamespaceOperationOrThrow( - op, Namespace.empty(), extraPassthroughNamespaces, extraPassthroughTableLikes); + op, Namespace.empty(), extraPassthroughNamespaces, extraPassthroughTableLikes, null); CatalogEntity catalog = CatalogEntity.of( 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 718b4ef9e0..195d0e128d 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 @@ -27,6 +27,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import org.apache.iceberg.catalog.Namespace; import org.apache.iceberg.catalog.TableIdentifier; @@ -213,7 +214,8 @@ public Policy updatePolicy( currentPolicyVersion, policyVersion)); } - if (newDescription.equals(policy.getDescription()) && newContent.equals(policy.getContent())) { + if (Objects.equals(newDescription, policy.getDescription()) + && Objects.equals(newContent, policy.getContent())) { // No need to update the policy if the new description and content are the same as the current return constructPolicy(policy); } 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 new file mode 100644 index 0000000000..1eceb362d2 --- /dev/null +++ b/service/common/src/main/java/org/apache/polaris/service/catalog/policy/PolicyCatalogHandler.java @@ -0,0 +1,144 @@ +/* + * 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.ws.rs.core.SecurityContext; +import java.util.HashSet; +import java.util.List; +import org.apache.iceberg.catalog.Namespace; +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.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.policy.PolicyType; +import org.apache.polaris.core.policy.exceptions.NoSuchPolicyException; +import org.apache.polaris.service.catalog.common.CatalogHandler; +import org.apache.polaris.service.types.CreatePolicyRequest; +import org.apache.polaris.service.types.ListPoliciesResponse; +import org.apache.polaris.service.types.LoadPolicyResponse; +import org.apache.polaris.service.types.PolicyIdentifier; +import org.apache.polaris.service.types.UpdatePolicyRequest; + +public class PolicyCatalogHandler extends CatalogHandler { + + private PolarisMetaStoreManager metaStoreManager; + + private PolicyCatalog policyCatalog; + + public PolicyCatalogHandler( + CallContext callContext, + PolarisEntityManager entityManager, + PolarisMetaStoreManager metaStoreManager, + SecurityContext securityContext, + String catalogName, + PolarisAuthorizer authorizer) { + super(callContext, entityManager, securityContext, catalogName, authorizer); + this.metaStoreManager = metaStoreManager; + } + + @Override + protected void initializeCatalog() { + this.policyCatalog = new PolicyCatalog(metaStoreManager, callContext, this.resolutionManifest); + } + + public ListPoliciesResponse listPolicies(Namespace parent, PolicyType policyType) { + PolarisAuthorizableOperation op = PolarisAuthorizableOperation.LIST_POLICY; + authorizeBasicNamespaceOperationOrThrow(op, parent); + + return ListPoliciesResponse.builder() + .setIdentifiers(new HashSet<>(policyCatalog.listPolicies(parent, policyType))) + .build(); + } + + public LoadPolicyResponse createPolicy(Namespace namespace, CreatePolicyRequest request) { + PolarisAuthorizableOperation op = PolarisAuthorizableOperation.CREATE_POLICY; + PolicyIdentifier identifier = + PolicyIdentifier.builder().setNamespace(namespace).setName(request.getName()).build(); + + // authorize the creating policy under namespace operation + authorizeBasicNamespaceOperationOrThrow( + op, identifier.getNamespace(), null, null, List.of(identifier)); + + return LoadPolicyResponse.builder() + .setPolicy( + policyCatalog.createPolicy( + identifier, request.getType(), request.getDescription(), request.getContent())) + .build(); + } + + public LoadPolicyResponse loadPolicy(PolicyIdentifier identifier) { + PolarisAuthorizableOperation op = PolarisAuthorizableOperation.LOAD_POLICY; + authorizeBasicPolicyOperationOrThrow(op, identifier); + + return LoadPolicyResponse.builder().setPolicy(policyCatalog.loadPolicy(identifier)).build(); + } + + public LoadPolicyResponse updatePolicy(PolicyIdentifier identifier, UpdatePolicyRequest request) { + PolarisAuthorizableOperation op = PolarisAuthorizableOperation.UPDATE_POLICY; + authorizeBasicPolicyOperationOrThrow(op, identifier); + + return LoadPolicyResponse.builder() + .setPolicy( + policyCatalog.updatePolicy( + identifier, + request.getDescription(), + request.getContent(), + request.getCurrentPolicyVersion())) + .build(); + } + + public boolean dropPolicy(PolicyIdentifier identifier, boolean detachAll) { + PolarisAuthorizableOperation op = PolarisAuthorizableOperation.DROP_POLICY; + authorizeBasicPolicyOperationOrThrow(op, identifier); + + return policyCatalog.dropPolicy(identifier, detachAll); + } + + private void authorizeBasicPolicyOperationOrThrow( + PolarisAuthorizableOperation op, PolicyIdentifier identifier) { + resolutionManifest = + entityManager.prepareResolutionManifest(callContext, securityContext, catalogName); + resolutionManifest.addPassthroughPath( + new ResolverPath( + PolarisCatalogHelpers.identifierToList(identifier.getNamespace(), identifier.getName()), + PolarisEntityType.POLICY, + true /* optional */), + identifier); + resolutionManifest.resolveAll(); + + PolarisResolvedPathWrapper target = resolutionManifest.getResolvedPath(identifier, true); + if (target == null) { + throw new NoSuchPolicyException(String.format("Policy does not exist: %s", identifier)); + } + + authorizer.authorizeOrThrow( + authenticatedPrincipal, + resolutionManifest.getAllActivatedCatalogRoleAndPrincipalRoles(), + op, + target, + null /* secondary */); + + initializeCatalog(); + } +}