This is an automated email from the ASF dual-hosted git repository. yufei pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/polaris.git
The following commit(s) were added to refs/heads/main by this push: new 81ddf32c2 Implement PolicyCatalogAdapter (#1438) 81ddf32c2 is described below commit 81ddf32c2ad298fed07eb589e7d5f917f8c4e604 Author: Honah (Jonas) J. <hon...@apache.org> AuthorDate: Fri Apr 25 14:01:55 2025 -0500 Implement PolicyCatalogAdapter (#1438) --- .../polaris/service/it/env/PolarisClient.java | 9 + .../apache/polaris/service/it/env/PolicyApi.java | 194 ++++++++++ .../test/PolarisPolicyServiceIntegrationTest.java | 428 +++++++++++++++++++++ plugins/spark/v3.5/regtests/spark_sql.ref | 2 +- .../polaris/core/config/FeatureConfiguration.java | 7 + .../apache/polaris/core/rest/PolarisEndpoints.java | 45 +++ .../polaris/core/rest/PolarisResourcePaths.java | 8 + .../service/quarkus/it/QuarkusPolicyServiceIT.java | 10 +- .../it/QuarkusPolicyServiceIntegrationTest.java | 10 +- regtests/t_spark_sql/ref/spark_sql_basic.sh.ref | 2 +- regtests/t_spark_sql/ref/spark_sql_views.sh.ref | 2 +- .../catalog/iceberg/IcebergCatalogAdapter.java | 1 + .../catalog/policy/PolicyCatalogAdapter.java | 213 ++++++++++ .../catalog/policy/PolicyCatalogHandler.java | 3 + 14 files changed, 921 insertions(+), 13 deletions(-) diff --git a/integration-tests/src/main/java/org/apache/polaris/service/it/env/PolarisClient.java b/integration-tests/src/main/java/org/apache/polaris/service/it/env/PolarisClient.java index e18f8736b..baec590f9 100644 --- a/integration-tests/src/main/java/org/apache/polaris/service/it/env/PolarisClient.java +++ b/integration-tests/src/main/java/org/apache/polaris/service/it/env/PolarisClient.java @@ -116,6 +116,15 @@ public final class PolarisClient implements AutoCloseable { client, endpoints, obtainToken(credentials), endpoints.catalogApiEndpoint()); } + public PolicyApi policyApi(PrincipalWithCredentials principal) { + return new PolicyApi(client, endpoints, obtainToken(principal), endpoints.catalogApiEndpoint()); + } + + public PolicyApi policyApi(ClientCredentials credentials) { + return new PolicyApi( + client, endpoints, obtainToken(credentials), endpoints.catalogApiEndpoint()); + } + /** * Requests an access token from the Polaris server for the client ID/secret pair that is part of * the given principal data object. diff --git a/integration-tests/src/main/java/org/apache/polaris/service/it/env/PolicyApi.java b/integration-tests/src/main/java/org/apache/polaris/service/it/env/PolicyApi.java new file mode 100644 index 000000000..7597aaef3 --- /dev/null +++ b/integration-tests/src/main/java/org/apache/polaris/service/it/env/PolicyApi.java @@ -0,0 +1,194 @@ +/* + * 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.it.env; + +import jakarta.ws.rs.client.Client; +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.core.Response; +import java.net.URI; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.apache.iceberg.catalog.Namespace; +import org.apache.iceberg.rest.RESTUtil; +import org.apache.polaris.core.policy.PolicyType; +import org.apache.polaris.service.types.ApplicablePolicy; +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.GetApplicablePoliciesResponse; +import org.apache.polaris.service.types.ListPoliciesResponse; +import org.apache.polaris.service.types.LoadPolicyResponse; +import org.apache.polaris.service.types.Policy; +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; + +public class PolicyApi extends RestApi { + PolicyApi(Client client, PolarisApiEndpoints endpoints, String authToken, URI uri) { + super(client, endpoints, authToken, uri); + } + + public void purge(String catalog, Namespace ns) { + listPolicies(catalog, ns).forEach(t -> dropPolicy(catalog, t)); + } + + public List<PolicyIdentifier> listPolicies(String catalog, Namespace namespace) { + return listPolicies(catalog, namespace, null); + } + + public List<PolicyIdentifier> listPolicies(String catalog, Namespace namespace, PolicyType type) { + String ns = RESTUtil.encodeNamespace(namespace); + Map<String, String> queryParams = new HashMap<>(); + if (type != null) { + queryParams.put("policyType", type.getName()); + } + try (Response res = + request( + "polaris/v1/{cat}/namespaces/{ns}/policies", + Map.of("cat", catalog, "ns", ns), + queryParams) + .get()) { + Assertions.assertThat(res.getStatus()).isEqualTo(Response.Status.OK.getStatusCode()); + return res.readEntity(ListPoliciesResponse.class).getIdentifiers().stream().toList(); + } + } + + public void dropPolicy(String catalog, PolicyIdentifier policyIdentifier) { + String ns = RESTUtil.encodeNamespace(policyIdentifier.getNamespace()); + try (Response res = + request( + "polaris/v1/{cat}/namespaces/{ns}/policies/{policy}", + Map.of("cat", catalog, "ns", ns, "policy", policyIdentifier.getName())) + .delete()) { + Assertions.assertThat(res.getStatus()).isEqualTo(Response.Status.NO_CONTENT.getStatusCode()); + } + } + + public Policy loadPolicy(String catalog, PolicyIdentifier policyIdentifier) { + String ns = RESTUtil.encodeNamespace(policyIdentifier.getNamespace()); + try (Response res = + request( + "polaris/v1/{cat}/namespaces/{ns}/policies/{policy}", + Map.of("cat", catalog, "ns", ns, "policy", policyIdentifier.getName())) + .get()) { + Assertions.assertThat(res.getStatus()).isEqualTo(Response.Status.OK.getStatusCode()); + return res.readEntity(LoadPolicyResponse.class).getPolicy(); + } + } + + public Policy createPolicy( + String catalog, + PolicyIdentifier policyIdentifier, + PolicyType policyType, + String content, + String description) { + String ns = RESTUtil.encodeNamespace(policyIdentifier.getNamespace()); + CreatePolicyRequest request = + CreatePolicyRequest.builder() + .setType(policyType.getName()) + .setName(policyIdentifier.getName()) + .setDescription(description) + .setContent(content) + .build(); + try (Response res = + request("polaris/v1/{cat}/namespaces/{ns}/policies", Map.of("cat", catalog, "ns", ns)) + .post(Entity.json(request))) { + Assertions.assertThat(res.getStatus()).isEqualTo(Response.Status.OK.getStatusCode()); + return res.readEntity(LoadPolicyResponse.class).getPolicy(); + } + } + + public Policy updatePolicy( + String catalog, + PolicyIdentifier policyIdentifier, + String newContent, + String newDescription, + int currentPolicyVersion) { + String ns = RESTUtil.encodeNamespace(policyIdentifier.getNamespace()); + UpdatePolicyRequest request = + UpdatePolicyRequest.builder() + .setContent(newContent) + .setDescription(newDescription) + .setCurrentPolicyVersion(currentPolicyVersion) + .build(); + try (Response res = + request( + "polaris/v1/{cat}/namespaces/{ns}/policies/{policy}", + Map.of("cat", catalog, "ns", ns, "policy", policyIdentifier.getName())) + .put(Entity.json(request))) { + Assertions.assertThat(res.getStatus()).isEqualTo(Response.Status.OK.getStatusCode()); + return res.readEntity(LoadPolicyResponse.class).getPolicy(); + } + } + + public void attachPolicy( + String catalog, + PolicyIdentifier policyIdentifier, + PolicyAttachmentTarget target, + Map<String, String> parameters) { + String ns = RESTUtil.encodeNamespace(policyIdentifier.getNamespace()); + AttachPolicyRequest request = + AttachPolicyRequest.builder().setTarget(target).setParameters(parameters).build(); + try (Response res = + request( + "polaris/v1/{cat}/namespaces/{ns}/policies/{policy}/mappings", + Map.of("cat", catalog, "ns", ns, "policy", policyIdentifier.getName())) + .put(Entity.json(request))) { + Assertions.assertThat(res.getStatus()).isEqualTo(Response.Status.NO_CONTENT.getStatusCode()); + } + } + + public void detachPolicy( + String catalog, PolicyIdentifier policyIdentifier, PolicyAttachmentTarget target) { + String ns = RESTUtil.encodeNamespace(policyIdentifier.getNamespace()); + DetachPolicyRequest request = DetachPolicyRequest.builder().setTarget(target).build(); + try (Response res = + request( + "polaris/v1/{cat}/namespaces/{ns}/policies/{policy}/mappings", + Map.of("cat", catalog, "ns", ns, "policy", policyIdentifier.getName())) + .post(Entity.json(request))) { + Assertions.assertThat(res.getStatus()).isEqualTo(Response.Status.NO_CONTENT.getStatusCode()); + } + } + + public List<ApplicablePolicy> getApplicablePolicies( + String catalog, Namespace namespace, String targetName, PolicyType policyType) { + String ns = namespace != null ? RESTUtil.encodeNamespace(namespace) : null; + Map<String, String> queryParams = new HashMap<>(); + if (ns != null) { + queryParams.put("namespace", ns); + } + if (targetName != null) { + queryParams.put("target-name", targetName); + } + if (policyType != null) { + queryParams.put("policyType", policyType.getName()); + } + + try (Response res = + request("polaris/v1/{cat}/applicable-policies", Map.of("cat", catalog), queryParams) + .get()) { + Assertions.assertThat(res.getStatus()).isEqualTo(Response.Status.OK.getStatusCode()); + return res.readEntity(GetApplicablePoliciesResponse.class).getApplicablePolicies().stream() + .toList(); + } + } +} diff --git a/integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisPolicyServiceIntegrationTest.java b/integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisPolicyServiceIntegrationTest.java new file mode 100644 index 000000000..055fa411c --- /dev/null +++ b/integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisPolicyServiceIntegrationTest.java @@ -0,0 +1,428 @@ +/* + * 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.it.test; + +import static org.apache.polaris.service.it.env.PolarisClient.polarisClient; + +import com.google.common.collect.ImmutableMap; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.reflect.Method; +import java.net.URI; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import org.apache.iceberg.Schema; +import org.apache.iceberg.catalog.Namespace; +import org.apache.iceberg.catalog.TableIdentifier; +import org.apache.iceberg.rest.RESTCatalog; +import org.apache.iceberg.types.Types; +import org.apache.polaris.core.admin.model.AwsStorageConfigInfo; +import org.apache.polaris.core.admin.model.Catalog; +import org.apache.polaris.core.admin.model.CatalogGrant; +import org.apache.polaris.core.admin.model.CatalogPrivilege; +import org.apache.polaris.core.admin.model.CatalogProperties; +import org.apache.polaris.core.admin.model.CatalogRole; +import org.apache.polaris.core.admin.model.FileStorageConfigInfo; +import org.apache.polaris.core.admin.model.GrantResource; +import org.apache.polaris.core.admin.model.PolarisCatalog; +import org.apache.polaris.core.admin.model.PrincipalWithCredentials; +import org.apache.polaris.core.admin.model.StorageConfigInfo; +import org.apache.polaris.core.catalog.PolarisCatalogHelpers; +import org.apache.polaris.core.entity.CatalogEntity; +import org.apache.polaris.core.policy.PredefinedPolicyTypes; +import org.apache.polaris.service.it.env.ClientCredentials; +import org.apache.polaris.service.it.env.IcebergHelper; +import org.apache.polaris.service.it.env.IntegrationTestsHelper; +import org.apache.polaris.service.it.env.ManagementApi; +import org.apache.polaris.service.it.env.PolarisApiEndpoints; +import org.apache.polaris.service.it.env.PolarisClient; +import org.apache.polaris.service.it.env.PolicyApi; +import org.apache.polaris.service.it.ext.PolarisIntegrationTestExtension; +import org.apache.polaris.service.types.ApplicablePolicy; +import org.apache.polaris.service.types.Policy; +import org.apache.polaris.service.types.PolicyAttachmentTarget; +import org.apache.polaris.service.types.PolicyIdentifier; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInfo; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.io.TempDir; + +@ExtendWith(PolarisIntegrationTestExtension.class) +public class PolarisPolicyServiceIntegrationTest { + + private static final String TEST_ROLE_ARN = + Optional.ofNullable(System.getenv("INTEGRATION_TEST_ROLE_ARN")) + .orElse("arn:aws:iam::123456789012:role/my-role"); + + private static final String EXAMPLE_TABLE_MAINTENANCE_POLICY_CONTENT = "{\"enable\":true}"; + private static final Namespace NS1 = Namespace.of("NS1"); + private static final Namespace NS2 = Namespace.of("NS2"); + private static final PolicyIdentifier NS1_P1 = new PolicyIdentifier(NS1, "P1"); + private static final PolicyIdentifier NS1_P2 = new PolicyIdentifier(NS1, "P2"); + private static final PolicyIdentifier NS1_P3 = new PolicyIdentifier(NS1, "P3"); + private static final TableIdentifier NS2_T1 = TableIdentifier.of(NS2, "T1"); + + private static URI s3BucketBase; + private static String principalRoleName; + private static ClientCredentials adminCredentials; + private static PrincipalWithCredentials principalCredentials; + private static PolarisApiEndpoints endpoints; + private static PolarisClient client; + private static ManagementApi managementApi; + private static PolicyApi policyApi; + + private RESTCatalog restCatalog; + private String currentCatalogName; + + private final String catalogBaseLocation = + s3BucketBase + "/" + System.getenv("USER") + "/path/to/data"; + + private static final String[] DEFAULT_CATALOG_PROPERTIES = { + "allow.unstructured.table.location", "true", + "allow.external.table.location", "true" + }; + + @Retention(RetentionPolicy.RUNTIME) + private @interface CatalogConfig { + Catalog.TypeEnum value() default Catalog.TypeEnum.INTERNAL; + + String[] properties() default { + "allow.unstructured.table.location", "true", + "allow.external.table.location", "true" + }; + } + + @Retention(RetentionPolicy.RUNTIME) + private @interface RestCatalogConfig { + String[] value() default {}; + } + + @BeforeAll + public static void setup( + PolarisApiEndpoints apiEndpoints, ClientCredentials credentials, @TempDir Path tempDir) { + adminCredentials = credentials; + endpoints = apiEndpoints; + client = polarisClient(endpoints); + managementApi = client.managementApi(credentials); + String principalName = client.newEntityName("snowman-rest"); + principalRoleName = client.newEntityName("rest-admin"); + principalCredentials = managementApi.createPrincipalWithRole(principalName, principalRoleName); + URI testRootUri = IntegrationTestsHelper.getTemporaryDirectory(tempDir); + s3BucketBase = testRootUri.resolve("my-bucket"); + + policyApi = client.policyApi(principalCredentials); + } + + @AfterAll + public static void close() throws Exception { + client.close(); + } + + @BeforeEach + public void before(TestInfo testInfo) { + String principalName = "snowman-rest-" + UUID.randomUUID(); + principalRoleName = "rest-admin-" + UUID.randomUUID(); + PrincipalWithCredentials principalCredentials = + managementApi.createPrincipalWithRole(principalName, principalRoleName); + + Method method = testInfo.getTestMethod().orElseThrow(); + currentCatalogName = client.newEntityName(method.getName()); + AwsStorageConfigInfo awsConfigModel = + AwsStorageConfigInfo.builder() + .setRoleArn(TEST_ROLE_ARN) + .setExternalId("externalId") + .setUserArn("a:user:arn") + .setStorageType(StorageConfigInfo.StorageTypeEnum.S3) + .setAllowedLocations(List.of("s3://my-old-bucket/path/to/data")) + .build(); + Optional<PolarisPolicyServiceIntegrationTest.CatalogConfig> catalogConfig = + Optional.ofNullable( + method.getAnnotation(PolarisPolicyServiceIntegrationTest.CatalogConfig.class)); + + CatalogProperties.Builder catalogPropsBuilder = CatalogProperties.builder(catalogBaseLocation); + String[] properties = + catalogConfig + .map(PolarisPolicyServiceIntegrationTest.CatalogConfig::properties) + .orElse(DEFAULT_CATALOG_PROPERTIES); + for (int i = 0; i < properties.length; i += 2) { + catalogPropsBuilder.addProperty(properties[i], properties[i + 1]); + } + if (!s3BucketBase.getScheme().equals("file")) { + catalogPropsBuilder.addProperty( + CatalogEntity.REPLACE_NEW_LOCATION_PREFIX_WITH_CATALOG_DEFAULT_KEY, "file:"); + } + Catalog catalog = + PolarisCatalog.builder() + .setType( + catalogConfig + .map(PolarisPolicyServiceIntegrationTest.CatalogConfig::value) + .orElse(Catalog.TypeEnum.INTERNAL)) + .setName(currentCatalogName) + .setProperties(catalogPropsBuilder.build()) + .setStorageConfigInfo( + s3BucketBase.getScheme().equals("file") + ? new FileStorageConfigInfo( + StorageConfigInfo.StorageTypeEnum.FILE, List.of("file://")) + : awsConfigModel) + .build(); + + managementApi.createCatalog(principalRoleName, catalog); + + Optional<PolarisPolicyServiceIntegrationTest.RestCatalogConfig> restCatalogConfig = + testInfo + .getTestMethod() + .flatMap( + m -> + Optional.ofNullable( + m.getAnnotation( + PolarisPolicyServiceIntegrationTest.RestCatalogConfig.class))); + ImmutableMap.Builder<String, String> extraPropertiesBuilder = ImmutableMap.builder(); + restCatalogConfig.ifPresent( + config -> { + for (int i = 0; i < config.value().length; i += 2) { + extraPropertiesBuilder.put(config.value()[i], config.value()[i + 1]); + } + }); + + restCatalog = + IcebergHelper.restCatalog( + client, + endpoints, + principalCredentials, + currentCatalogName, + extraPropertiesBuilder.build()); + CatalogGrant catalogGrant = + new CatalogGrant(CatalogPrivilege.CATALOG_MANAGE_CONTENT, GrantResource.TypeEnum.CATALOG); + managementApi.createCatalogRole(currentCatalogName, "catalogrole1"); + managementApi.addGrant(currentCatalogName, "catalogrole1", catalogGrant); + CatalogRole catalogRole = managementApi.getCatalogRole(currentCatalogName, "catalogrole1"); + managementApi.grantCatalogRoleToPrincipalRole( + principalRoleName, currentCatalogName, catalogRole); + + policyApi = client.policyApi(principalCredentials); + } + + @AfterEach + public void cleanUp() { + client.cleanUp(adminCredentials); + } + + @Test + public void testCreatePolicy() { + restCatalog.createNamespace(NS1); + Policy policy = + policyApi.createPolicy( + currentCatalogName, + NS1_P1, + PredefinedPolicyTypes.DATA_COMPACTION, + EXAMPLE_TABLE_MAINTENANCE_POLICY_CONTENT, + "test policy"); + + Assertions.assertThat(policy).isNotNull(); + Assertions.assertThat(policy.getName()).isEqualTo("P1"); + Assertions.assertThat(policy.getDescription()).isEqualTo("test policy"); + Assertions.assertThat(policy.getPolicyType()) + .isEqualTo(PredefinedPolicyTypes.DATA_COMPACTION.getName()); + Assertions.assertThat(policy.getContent()).isEqualTo(EXAMPLE_TABLE_MAINTENANCE_POLICY_CONTENT); + Assertions.assertThat(policy.getInheritable()) + .isEqualTo(PredefinedPolicyTypes.DATA_COMPACTION.isInheritable()); + Assertions.assertThat(policy.getVersion()).isEqualTo(0); + + Policy loadedPolicy = policyApi.loadPolicy(currentCatalogName, NS1_P1); + Assertions.assertThat(loadedPolicy).isEqualTo(policy); + + policyApi.dropPolicy(currentCatalogName, NS1_P1); + } + + @Test + public void testDropPolicy() { + restCatalog.createNamespace(NS1); + policyApi.createPolicy( + currentCatalogName, + NS1_P1, + PredefinedPolicyTypes.DATA_COMPACTION, + EXAMPLE_TABLE_MAINTENANCE_POLICY_CONTENT, + "test policy"); + policyApi.dropPolicy(currentCatalogName, NS1_P1); + Assertions.assertThat(policyApi.listPolicies(currentCatalogName, NS1)).hasSize(0); + } + + @Test + public void testUpdatePolicy() { + restCatalog.createNamespace(NS1); + policyApi.createPolicy( + currentCatalogName, + NS1_P1, + PredefinedPolicyTypes.DATA_COMPACTION, + EXAMPLE_TABLE_MAINTENANCE_POLICY_CONTENT, + "test policy"); + + String updatedContent = "{\"enable\":false}"; + String updatedDescription = "updated test policy"; + Policy updatedPolicy = + policyApi.updatePolicy(currentCatalogName, NS1_P1, updatedContent, updatedDescription, 0); + + Assertions.assertThat(updatedPolicy).isNotNull(); + Assertions.assertThat(updatedPolicy.getName()).isEqualTo("P1"); + Assertions.assertThat(updatedPolicy.getDescription()).isEqualTo(updatedDescription); + Assertions.assertThat(updatedPolicy.getPolicyType()) + .isEqualTo(PredefinedPolicyTypes.DATA_COMPACTION.getName()); + Assertions.assertThat(updatedPolicy.getContent()).isEqualTo(updatedContent); + Assertions.assertThat(updatedPolicy.getInheritable()) + .isEqualTo(PredefinedPolicyTypes.DATA_COMPACTION.isInheritable()); + Assertions.assertThat(updatedPolicy.getVersion()).isEqualTo(1); + + policyApi.dropPolicy(currentCatalogName, NS1_P1); + } + + @Test + public void testListPolicies() { + restCatalog.createNamespace(NS1); + policyApi.createPolicy( + currentCatalogName, + NS1_P1, + PredefinedPolicyTypes.DATA_COMPACTION, + EXAMPLE_TABLE_MAINTENANCE_POLICY_CONTENT, + "test policy"); + policyApi.createPolicy( + currentCatalogName, + NS1_P2, + PredefinedPolicyTypes.METADATA_COMPACTION, + EXAMPLE_TABLE_MAINTENANCE_POLICY_CONTENT, + "test policy"); + + Assertions.assertThat(policyApi.listPolicies(currentCatalogName, NS1)) + .containsExactlyInAnyOrder(NS1_P1, NS1_P2); + Assertions.assertThat( + policyApi.listPolicies(currentCatalogName, NS1, PredefinedPolicyTypes.DATA_COMPACTION)) + .containsExactly(NS1_P1); + Assertions.assertThat( + policyApi.listPolicies( + currentCatalogName, NS1, PredefinedPolicyTypes.METADATA_COMPACTION)) + .containsExactly(NS1_P2); + + policyApi.dropPolicy(currentCatalogName, NS1_P1); + policyApi.dropPolicy(currentCatalogName, NS1_P2); + } + + @Test + public void testPolicyMapping() { + restCatalog.createNamespace(NS1); + restCatalog.createNamespace(NS2); + Policy p1 = + policyApi.createPolicy( + currentCatalogName, + NS1_P1, + PredefinedPolicyTypes.DATA_COMPACTION, + EXAMPLE_TABLE_MAINTENANCE_POLICY_CONTENT, + "test policy"); + Policy p2 = + policyApi.createPolicy( + currentCatalogName, + NS1_P2, + PredefinedPolicyTypes.METADATA_COMPACTION, + EXAMPLE_TABLE_MAINTENANCE_POLICY_CONTENT, + "test policy"); + Policy p3 = + policyApi.createPolicy( + currentCatalogName, + NS1_P3, + PredefinedPolicyTypes.ORPHAN_FILE_REMOVAL, + EXAMPLE_TABLE_MAINTENANCE_POLICY_CONTENT, + "test policy"); + + restCatalog + .buildTable( + NS2_T1, new Schema(Types.NestedField.of(1, true, "string", Types.StringType.get()))) + .create(); + + PolicyAttachmentTarget catalogTarget = + PolicyAttachmentTarget.builder().setType(PolicyAttachmentTarget.TypeEnum.CATALOG).build(); + PolicyAttachmentTarget namespaceTarget = + PolicyAttachmentTarget.builder() + .setType(PolicyAttachmentTarget.TypeEnum.NAMESPACE) + .setPath(Arrays.asList(NS2.levels())) + .build(); + PolicyAttachmentTarget tableTarget = + PolicyAttachmentTarget.builder() + .setType(PolicyAttachmentTarget.TypeEnum.TABLE_LIKE) + .setPath(PolarisCatalogHelpers.tableIdentifierToList(NS2_T1)) + .build(); + + policyApi.attachPolicy(currentCatalogName, NS1_P1, catalogTarget, Map.of()); + policyApi.attachPolicy(currentCatalogName, NS1_P2, namespaceTarget, Map.of()); + policyApi.attachPolicy(currentCatalogName, NS1_P3, tableTarget, Map.of()); + + List<ApplicablePolicy> applicablePoliciesOnCatalog = + policyApi.getApplicablePolicies(currentCatalogName, null, null, null); + Assertions.assertThat(applicablePoliciesOnCatalog) + .containsExactly(policyToApplicablePolicy(p1, false, NS1)); + + List<ApplicablePolicy> applicablePoliciesOnNamespace = + policyApi.getApplicablePolicies(currentCatalogName, NS2, null, null); + Assertions.assertThat(applicablePoliciesOnNamespace) + .containsExactlyInAnyOrder( + policyToApplicablePolicy(p1, true, NS1), policyToApplicablePolicy(p2, false, NS1)); + + List<ApplicablePolicy> applicablePoliciesOnTable = + policyApi.getApplicablePolicies(currentCatalogName, NS2, NS2_T1.name(), null); + Assertions.assertThat(applicablePoliciesOnTable) + .containsExactlyInAnyOrder( + policyToApplicablePolicy(p1, true, NS1), + policyToApplicablePolicy(p2, true, NS1), + policyToApplicablePolicy(p3, false, NS1)); + + Assertions.assertThat( + policyApi.getApplicablePolicies( + currentCatalogName, NS2, NS2_T1.name(), PredefinedPolicyTypes.METADATA_COMPACTION)) + .containsExactlyInAnyOrder(policyToApplicablePolicy(p2, true, NS1)); + + policyApi.detachPolicy(currentCatalogName, NS1_P1, catalogTarget); + policyApi.detachPolicy(currentCatalogName, NS1_P2, namespaceTarget); + policyApi.detachPolicy(currentCatalogName, NS1_P3, tableTarget); + + policyApi.dropPolicy(currentCatalogName, NS1_P1); + policyApi.dropPolicy(currentCatalogName, NS1_P2); + policyApi.dropPolicy(currentCatalogName, NS1_P3); + + restCatalog.dropTable(NS2_T1); + } + + private static ApplicablePolicy policyToApplicablePolicy( + Policy policy, boolean inherited, Namespace parent) { + return new ApplicablePolicy( + policy.getPolicyType(), + policy.getInheritable(), + policy.getName(), + policy.getDescription(), + policy.getContent(), + policy.getVersion(), + inherited, + Arrays.asList(parent.levels())); + } +} diff --git a/plugins/spark/v3.5/regtests/spark_sql.ref b/plugins/spark/v3.5/regtests/spark_sql.ref index 5825d0931..525a705ae 100755 --- a/plugins/spark/v3.5/regtests/spark_sql.ref +++ b/plugins/spark/v3.5/regtests/spark_sql.ref @@ -1,4 +1,4 @@ -{"defaults":{"default-base-location":"file:///tmp/spark_catalog"},"overrides":{"prefix":"spark_sql_catalog"},"endpoints":["GET /v1/{prefix}/namespaces","GET /v1/{prefix}/namespaces/{namespace}","HEAD /v1/{prefix}/namespaces/{namespace}","POST /v1/{prefix}/namespaces","POST /v1/{prefix}/namespaces/{namespace}/properties","DELETE /v1/{prefix}/namespaces/{namespace}","GET /v1/{prefix}/namespaces/{namespace}/tables","GET /v1/{prefix}/namespaces/{namespace}/tables/{table}","HEAD /v1/{prefix}/ [...] +{"defaults":{"default-base-location":"file:///tmp/spark_catalog"},"overrides":{"prefix":"spark_sql_catalog"},"endpoints":["GET /v1/{prefix}/namespaces","GET /v1/{prefix}/namespaces/{namespace}","HEAD /v1/{prefix}/namespaces/{namespace}","POST /v1/{prefix}/namespaces","POST /v1/{prefix}/namespaces/{namespace}/properties","DELETE /v1/{prefix}/namespaces/{namespace}","GET /v1/{prefix}/namespaces/{namespace}/tables","GET /v1/{prefix}/namespaces/{namespace}/tables/{table}","HEAD /v1/{prefix}/ [...] Catalog created spark-sql (default)> use polaris; spark-sql ()> create namespace db1; diff --git a/polaris-core/src/main/java/org/apache/polaris/core/config/FeatureConfiguration.java b/polaris-core/src/main/java/org/apache/polaris/core/config/FeatureConfiguration.java index f857d03ac..ee663dbb1 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/config/FeatureConfiguration.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/config/FeatureConfiguration.java @@ -227,4 +227,11 @@ public class FeatureConfiguration<T> extends PolarisConfiguration<T> { + " to perform federation to remote catalogs.") .defaultValue(false) .buildFeatureConfiguration(); + + public static final FeatureConfiguration<Boolean> ENABLE_POLICY_STORE = + PolarisConfiguration.<Boolean>builder() + .key("ENABLE_POLICY_STORE") + .description("If true, the policy-store endpoints are enabled") + .defaultValue(true) + .buildFeatureConfiguration(); } diff --git a/polaris-core/src/main/java/org/apache/polaris/core/rest/PolarisEndpoints.java b/polaris-core/src/main/java/org/apache/polaris/core/rest/PolarisEndpoints.java index edbfb1414..1fd91395b 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/rest/PolarisEndpoints.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/rest/PolarisEndpoints.java @@ -25,6 +25,7 @@ import org.apache.polaris.core.config.FeatureConfiguration; import org.apache.polaris.core.context.CallContext; public class PolarisEndpoints { + // Generic table endpoints public static final Endpoint V1_LIST_GENERIC_TABLES = Endpoint.create("GET", PolarisResourcePaths.V1_GENERIC_TABLES); public static final Endpoint V1_LOAD_GENERIC_TABLE = @@ -42,6 +43,36 @@ public class PolarisEndpoints { .add(V1_LOAD_GENERIC_TABLE) .build(); + // Policy store endpoints + public static final Endpoint V1_LIST_POLICIES = + Endpoint.create("GET", PolarisResourcePaths.V1_POLICIES); + public static final Endpoint V1_CREATE_POLICY = + Endpoint.create("POST", PolarisResourcePaths.V1_POLICIES); + public static final Endpoint V1_LOAD_POLICY = + Endpoint.create("GET", PolarisResourcePaths.V1_POLICY); + public static final Endpoint V1_UPDATE_POLICY = + Endpoint.create("PUT", PolarisResourcePaths.V1_POLICY); + public static final Endpoint V1_DROP_POLICY = + Endpoint.create("DELETE", PolarisResourcePaths.V1_POLICY); + public static final Endpoint V1_ATTACH_POLICY = + Endpoint.create("PUT", PolarisResourcePaths.V1_POLICY_MAPPINGS); + public static final Endpoint V1_DETACH_POLICY = + Endpoint.create("POST", PolarisResourcePaths.V1_POLICY_MAPPINGS); + public static final Endpoint V1_GET_APPLICABLE_POLICIES = + Endpoint.create("GET", PolarisResourcePaths.V1_APPLICABLE_POLICIES); + + public static final Set<Endpoint> POLICY_STORE_ENDPOINTS = + ImmutableSet.<Endpoint>builder() + .add(V1_LIST_POLICIES) + .add(V1_CREATE_POLICY) + .add(V1_LOAD_POLICY) + .add(V1_UPDATE_POLICY) + .add(V1_DROP_POLICY) + .add(V1_ATTACH_POLICY) + .add(V1_DETACH_POLICY) + .add(V1_GET_APPLICABLE_POLICIES) + .build(); + /** * Get the generic table endpoints. Returns GENERIC_TABLE_ENDPOINTS if ENABLE_GENERIC_TABLES is * set to true, otherwise, returns an empty set. @@ -57,4 +88,18 @@ public class PolarisEndpoints { return genericTableEnabled ? GENERIC_TABLE_ENDPOINTS : ImmutableSet.of(); } + + /** + * Get the policy store endpoints. Returns POLICY_ENDPOINTS if ENABLE_POLICY_STORE is set to true, + * otherwise, returns an empty set + */ + public static Set<Endpoint> getSupportedPolicyEndpoints(CallContext callContext) { + boolean policyStoreEnabled = + callContext + .getPolarisCallContext() + .getConfigurationStore() + .getConfiguration( + callContext.getPolarisCallContext(), FeatureConfiguration.ENABLE_POLICY_STORE); + return policyStoreEnabled ? POLICY_STORE_ENDPOINTS : ImmutableSet.of(); + } } diff --git a/polaris-core/src/main/java/org/apache/polaris/core/rest/PolarisResourcePaths.java b/polaris-core/src/main/java/org/apache/polaris/core/rest/PolarisResourcePaths.java index 159a1a014..8a30d7962 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/rest/PolarisResourcePaths.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/rest/PolarisResourcePaths.java @@ -34,6 +34,14 @@ public class PolarisResourcePaths { public static final String V1_GENERIC_TABLE = "polaris/v1/{prefix}/namespaces/{namespace}/generic-tables/{generic-table}"; + // Policy Store endpoints + public static final String V1_POLICIES = "/polaris/v1/{prefix}/namespaces/{namespace}/policies"; + public static final String V1_POLICY = + "/polaris/v1/{prefix}/namespaces/{namespace}/policies/{policy-name}"; + public static final String V1_POLICY_MAPPINGS = + "/polaris/v1/{prefix}/namespaces/{namespace}/policies/{policy-name}/mappings"; + public static final String V1_APPLICABLE_POLICIES = "/polaris/v1/{prefix}/applicable-policies"; + private final String prefix; public PolarisResourcePaths(String prefix) { diff --git a/service/common/src/main/java/org/apache/polaris/service/catalog/policy/PolicyServiceImpl.java b/quarkus/service/src/intTest/java/org/apache/polaris/service/quarkus/it/QuarkusPolicyServiceIT.java similarity index 73% copy from service/common/src/main/java/org/apache/polaris/service/catalog/policy/PolicyServiceImpl.java copy to quarkus/service/src/intTest/java/org/apache/polaris/service/quarkus/it/QuarkusPolicyServiceIT.java index 55bb18000..68280a672 100644 --- a/service/common/src/main/java/org/apache/polaris/service/catalog/policy/PolicyServiceImpl.java +++ b/quarkus/service/src/intTest/java/org/apache/polaris/service/quarkus/it/QuarkusPolicyServiceIT.java @@ -16,10 +16,10 @@ * specific language governing permissions and limitations * under the License. */ -package org.apache.polaris.service.catalog.policy; +package org.apache.polaris.service.quarkus.it; -import jakarta.enterprise.context.RequestScoped; -import org.apache.polaris.service.catalog.api.PolarisCatalogPolicyApiService; +import io.quarkus.test.junit.QuarkusIntegrationTest; +import org.apache.polaris.service.it.test.PolarisPolicyServiceIntegrationTest; -@RequestScoped -public class PolicyServiceImpl implements PolarisCatalogPolicyApiService {} +@QuarkusIntegrationTest +public class QuarkusPolicyServiceIT extends PolarisPolicyServiceIntegrationTest {} diff --git a/service/common/src/main/java/org/apache/polaris/service/catalog/policy/PolicyServiceImpl.java b/quarkus/service/src/test/java/org/apache/polaris/service/quarkus/it/QuarkusPolicyServiceIntegrationTest.java similarity index 74% rename from service/common/src/main/java/org/apache/polaris/service/catalog/policy/PolicyServiceImpl.java rename to quarkus/service/src/test/java/org/apache/polaris/service/quarkus/it/QuarkusPolicyServiceIntegrationTest.java index 55bb18000..a668e92e5 100644 --- a/service/common/src/main/java/org/apache/polaris/service/catalog/policy/PolicyServiceImpl.java +++ b/quarkus/service/src/test/java/org/apache/polaris/service/quarkus/it/QuarkusPolicyServiceIntegrationTest.java @@ -16,10 +16,10 @@ * specific language governing permissions and limitations * under the License. */ -package org.apache.polaris.service.catalog.policy; +package org.apache.polaris.service.quarkus.it; -import jakarta.enterprise.context.RequestScoped; -import org.apache.polaris.service.catalog.api.PolarisCatalogPolicyApiService; +import io.quarkus.test.junit.QuarkusTest; +import org.apache.polaris.service.it.test.PolarisPolicyServiceIntegrationTest; -@RequestScoped -public class PolicyServiceImpl implements PolarisCatalogPolicyApiService {} +@QuarkusTest +public class QuarkusPolicyServiceIntegrationTest extends PolarisPolicyServiceIntegrationTest {} diff --git a/regtests/t_spark_sql/ref/spark_sql_basic.sh.ref b/regtests/t_spark_sql/ref/spark_sql_basic.sh.ref index eaf0e18a8..79b317fcf 100755 --- a/regtests/t_spark_sql/ref/spark_sql_basic.sh.ref +++ b/regtests/t_spark_sql/ref/spark_sql_basic.sh.ref @@ -1,4 +1,4 @@ -{"defaults":{"default-base-location":"file:///tmp/spark_sql_s3_catalog"},"overrides":{"prefix":"spark_sql_basic_catalog"},"endpoints":["GET /v1/{prefix}/namespaces","GET /v1/{prefix}/namespaces/{namespace}","HEAD /v1/{prefix}/namespaces/{namespace}","POST /v1/{prefix}/namespaces","POST /v1/{prefix}/namespaces/{namespace}/properties","DELETE /v1/{prefix}/namespaces/{namespace}","GET /v1/{prefix}/namespaces/{namespace}/tables","GET /v1/{prefix}/namespaces/{namespace}/tables/{table}","HEAD [...] +{"defaults":{"default-base-location":"file:///tmp/spark_sql_s3_catalog"},"overrides":{"prefix":"spark_sql_basic_catalog"},"endpoints":["GET /v1/{prefix}/namespaces","GET /v1/{prefix}/namespaces/{namespace}","HEAD /v1/{prefix}/namespaces/{namespace}","POST /v1/{prefix}/namespaces","POST /v1/{prefix}/namespaces/{namespace}/properties","DELETE /v1/{prefix}/namespaces/{namespace}","GET /v1/{prefix}/namespaces/{namespace}/tables","GET /v1/{prefix}/namespaces/{namespace}/tables/{table}","HEAD [...] Catalog created spark-sql (default)> use polaris; spark-sql ()> show namespaces; diff --git a/regtests/t_spark_sql/ref/spark_sql_views.sh.ref b/regtests/t_spark_sql/ref/spark_sql_views.sh.ref index ffae79311..9bb78d644 100755 --- a/regtests/t_spark_sql/ref/spark_sql_views.sh.ref +++ b/regtests/t_spark_sql/ref/spark_sql_views.sh.ref @@ -1,4 +1,4 @@ -{"defaults":{"default-base-location":"file:///tmp/spark_sql_s3_catalog"},"overrides":{"prefix":"spark_sql_views_catalog"},"endpoints":["GET /v1/{prefix}/namespaces","GET /v1/{prefix}/namespaces/{namespace}","HEAD /v1/{prefix}/namespaces/{namespace}","POST /v1/{prefix}/namespaces","POST /v1/{prefix}/namespaces/{namespace}/properties","DELETE /v1/{prefix}/namespaces/{namespace}","GET /v1/{prefix}/namespaces/{namespace}/tables","GET /v1/{prefix}/namespaces/{namespace}/tables/{table}","HEAD [...] +{"defaults":{"default-base-location":"file:///tmp/spark_sql_s3_catalog"},"overrides":{"prefix":"spark_sql_views_catalog"},"endpoints":["GET /v1/{prefix}/namespaces","GET /v1/{prefix}/namespaces/{namespace}","HEAD /v1/{prefix}/namespaces/{namespace}","POST /v1/{prefix}/namespaces","POST /v1/{prefix}/namespaces/{namespace}/properties","DELETE /v1/{prefix}/namespaces/{namespace}","GET /v1/{prefix}/namespaces/{namespace}/tables","GET /v1/{prefix}/namespaces/{namespace}/tables/{table}","HEAD [...] Catalog created spark-sql (default)> use polaris; spark-sql ()> show namespaces; diff --git a/service/common/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogAdapter.java b/service/common/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogAdapter.java index 689535139..e2dcefc0b 100644 --- a/service/common/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogAdapter.java +++ b/service/common/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogAdapter.java @@ -724,6 +724,7 @@ public class IcebergCatalogAdapter .addAll(VIEW_ENDPOINTS) .addAll(COMMIT_ENDPOINT) .addAll(PolarisEndpoints.getSupportedGenericTableEndpoints(callContext)) + .addAll(PolarisEndpoints.getSupportedPolicyEndpoints(callContext)) .build()) .build()) .build(); diff --git a/service/common/src/main/java/org/apache/polaris/service/catalog/policy/PolicyCatalogAdapter.java b/service/common/src/main/java/org/apache/polaris/service/catalog/policy/PolicyCatalogAdapter.java new file mode 100644 index 000000000..fe70d00a8 --- /dev/null +++ b/service/common/src/main/java/org/apache/polaris/service/catalog/policy/PolicyCatalogAdapter.java @@ -0,0 +1,213 @@ +/* + * 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.enterprise.context.RequestScoped; +import jakarta.inject.Inject; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.SecurityContext; +import org.apache.iceberg.catalog.Namespace; +import org.apache.iceberg.exceptions.NotAuthorizedException; +import org.apache.iceberg.rest.RESTUtil; +import org.apache.polaris.core.auth.AuthenticatedPolarisPrincipal; +import org.apache.polaris.core.auth.PolarisAuthorizer; +import org.apache.polaris.core.context.CallContext; +import org.apache.polaris.core.context.RealmContext; +import org.apache.polaris.core.persistence.PolarisEntityManager; +import org.apache.polaris.core.persistence.PolarisMetaStoreManager; +import org.apache.polaris.core.policy.PolicyType; +import org.apache.polaris.service.catalog.CatalogPrefixParser; +import org.apache.polaris.service.catalog.api.PolarisCatalogPolicyApiService; +import org.apache.polaris.service.catalog.common.CatalogAdapter; +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.GetApplicablePoliciesResponse; +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; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@RequestScoped +public class PolicyCatalogAdapter implements PolarisCatalogPolicyApiService, CatalogAdapter { + private static final Logger LOGGER = LoggerFactory.getLogger(PolicyCatalogAdapter.class); + + private final RealmContext realmContext; + private final CallContext callContext; + private final PolarisEntityManager entityManager; + private final PolarisMetaStoreManager metaStoreManager; + private final PolarisAuthorizer polarisAuthorizer; + private final CatalogPrefixParser prefixParser; + + @Inject + public PolicyCatalogAdapter( + RealmContext realmContext, + CallContext callContext, + PolarisEntityManager entityManager, + PolarisMetaStoreManager metaStoreManager, + PolarisAuthorizer polarisAuthorizer, + CatalogPrefixParser prefixParser) { + this.realmContext = realmContext; + this.callContext = callContext; + this.entityManager = entityManager; + this.metaStoreManager = metaStoreManager; + this.polarisAuthorizer = polarisAuthorizer; + this.prefixParser = prefixParser; + } + + private PolicyCatalogHandler newHandlerWrapper(SecurityContext securityContext, String prefix) { + var authenticatedPrincipal = (AuthenticatedPolarisPrincipal) securityContext.getUserPrincipal(); + if (authenticatedPrincipal == null) { + throw new NotAuthorizedException("Failed to find authenticatedPrincipal in SecurityContext"); + } + + return new PolicyCatalogHandler( + callContext, + entityManager, + metaStoreManager, + securityContext, + prefixParser.prefixToCatalogName(realmContext, prefix), + polarisAuthorizer); + } + + @Override + public Response createPolicy( + String prefix, + String namespace, + CreatePolicyRequest createPolicyRequest, + RealmContext realmContext, + SecurityContext securityContext) { + Namespace ns = decodeNamespace(namespace); + PolicyCatalogHandler handler = newHandlerWrapper(securityContext, prefix); + LoadPolicyResponse response = handler.createPolicy(ns, createPolicyRequest); + return Response.ok(response).build(); + } + + @Override + public Response listPolicies( + String prefix, + String namespace, + String pageToken, + Integer pageSize, + String policyType, + RealmContext realmContext, + SecurityContext securityContext) { + Namespace ns = decodeNamespace(namespace); + PolicyType type = + policyType != null ? PolicyType.fromName(RESTUtil.decodeString(policyType)) : null; + PolicyCatalogHandler handler = newHandlerWrapper(securityContext, prefix); + ListPoliciesResponse response = handler.listPolicies(ns, type); + return Response.ok(response).build(); + } + + @Override + public Response loadPolicy( + String prefix, + String namespace, + String policyName, + RealmContext realmContext, + SecurityContext securityContext) { + Namespace ns = decodeNamespace(namespace); + PolicyIdentifier identifier = new PolicyIdentifier(ns, RESTUtil.decodeString(policyName)); + PolicyCatalogHandler handler = newHandlerWrapper(securityContext, prefix); + LoadPolicyResponse response = handler.loadPolicy(identifier); + return Response.ok(response).build(); + } + + @Override + public Response updatePolicy( + String prefix, + String namespace, + String policyName, + UpdatePolicyRequest updatePolicyRequest, + RealmContext realmContext, + SecurityContext securityContext) { + Namespace ns = decodeNamespace(namespace); + PolicyIdentifier identifier = new PolicyIdentifier(ns, RESTUtil.decodeString(policyName)); + PolicyCatalogHandler handler = newHandlerWrapper(securityContext, prefix); + LoadPolicyResponse response = handler.updatePolicy(identifier, updatePolicyRequest); + return Response.ok(response).build(); + } + + @Override + public Response dropPolicy( + String prefix, + String namespace, + String policyName, + Boolean detachAll, + RealmContext realmContext, + SecurityContext securityContext) { + Namespace ns = decodeNamespace(namespace); + PolicyIdentifier identifier = new PolicyIdentifier(ns, RESTUtil.decodeString(policyName)); + PolicyCatalogHandler handler = newHandlerWrapper(securityContext, prefix); + handler.dropPolicy(identifier, detachAll != null && detachAll); + return Response.noContent().build(); + } + + @Override + public Response attachPolicy( + String prefix, + String namespace, + String policyName, + AttachPolicyRequest attachPolicyRequest, + RealmContext realmContext, + SecurityContext securityContext) { + Namespace ns = decodeNamespace(namespace); + PolicyIdentifier identifier = new PolicyIdentifier(ns, RESTUtil.decodeString(policyName)); + PolicyCatalogHandler handler = newHandlerWrapper(securityContext, prefix); + handler.attachPolicy(identifier, attachPolicyRequest); + return Response.noContent().build(); + } + + @Override + public Response detachPolicy( + String prefix, + String namespace, + String policyName, + DetachPolicyRequest detachPolicyRequest, + RealmContext realmContext, + SecurityContext securityContext) { + Namespace ns = decodeNamespace(namespace); + PolicyIdentifier identifier = new PolicyIdentifier(ns, RESTUtil.decodeString(policyName)); + PolicyCatalogHandler handler = newHandlerWrapper(securityContext, prefix); + handler.detachPolicy(identifier, detachPolicyRequest); + return Response.noContent().build(); + } + + @Override + public Response getApplicablePolicies( + String prefix, + String pageToken, + Integer pageSize, + String namespace, + String targetName, + String policyType, + RealmContext realmContext, + SecurityContext securityContext) { + Namespace ns = namespace != null ? decodeNamespace(namespace) : null; + String target = targetName != null ? RESTUtil.decodeString(targetName) : null; + PolicyType type = + policyType != null ? PolicyType.fromName(RESTUtil.decodeString(policyType)) : null; + PolicyCatalogHandler handler = newHandlerWrapper(securityContext, prefix); + GetApplicablePoliciesResponse response = handler.getApplicablePolicies(ns, target, type); + return Response.ok(response).build(); + } +} 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 f4dea27b4..8273256ba 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 @@ -32,6 +32,7 @@ import org.apache.iceberg.exceptions.NotFoundException; 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.config.FeatureConfiguration; import org.apache.polaris.core.context.CallContext; import org.apache.polaris.core.entity.PolarisEntitySubType; import org.apache.polaris.core.entity.PolarisEntityType; @@ -72,6 +73,8 @@ public class PolicyCatalogHandler extends CatalogHandler { @Override protected void initializeCatalog() { + FeatureConfiguration.enforceFeatureEnabledOrThrow( + callContext, FeatureConfiguration.ENABLE_POLICY_STORE); this.policyCatalog = new PolicyCatalog(metaStoreManager, callContext, this.resolutionManifest); }