This is an automated email from the ASF dual-hosted git repository.

honahx 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 5c3f0c2a4 [Policy Store | Management Spec] Add policy privileges to 
spec and update admin service impl (#1529)
5c3f0c2a4 is described below

commit 5c3f0c2a40c332b10b3624981f7b162350a13859
Author: Honah (Jonas) J. <[email protected]>
AuthorDate: Tue May 20 12:41:41 2025 -0500

    [Policy Store | Management Spec] Add policy privileges to spec and update 
admin service impl (#1529)
    
    This PR adds new policy related privileges to polaris-management-api.yml 
and update PolarisAdminService to allow granting new privileges
---
 .../polaris/service/it/env/ManagementApi.java      |  10 ++
 .../test/PolarisPolicyServiceIntegrationTest.java  | 189 ++++++++++++++++++++-
 .../core/auth/PolarisAuthorizableOperation.java    |   6 +-
 .../polaris/core/auth/PolarisAuthorizerImpl.java   |   6 +-
 .../polaris/core/entity/PolarisPrivilege.java      |   5 +
 .../admin/PolarisAdminServiceAuthzTest.java        |  59 +++++++
 .../polaris/service/admin/PolarisAdminService.java | 144 ++++++++++++++++
 .../polaris/service/admin/PolarisServiceImpl.java  |  28 +++
 spec/polaris-management-service.yml                |  50 ++++++
 9 files changed, 492 insertions(+), 5 deletions(-)

diff --git 
a/integration-tests/src/main/java/org/apache/polaris/service/it/env/ManagementApi.java
 
b/integration-tests/src/main/java/org/apache/polaris/service/it/env/ManagementApi.java
index 83ec02038..fb3019c3e 100644
--- 
a/integration-tests/src/main/java/org/apache/polaris/service/it/env/ManagementApi.java
+++ 
b/integration-tests/src/main/java/org/apache/polaris/service/it/env/ManagementApi.java
@@ -115,6 +115,16 @@ public class ManagementApi extends RestApi {
     }
   }
 
+  public void revokeGrant(String catalogName, String catalogRoleName, 
GrantResource grant) {
+    try (Response response =
+        request(
+                "v1/catalogs/{cat}/catalog-roles/{role}/grants",
+                Map.of("cat", catalogName, "role", catalogRoleName))
+            .post(Entity.json(grant))) {
+      assertThat(response).returns(CREATED.getStatusCode(), 
Response::getStatus);
+    }
+  }
+
   public void grantCatalogRoleToPrincipalRole(
       String principalRoleName, String catalogName, CatalogRole catalogRole) {
     try (Response response =
diff --git 
a/integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisPolicyServiceIntegrationTest.java
 
b/integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisPolicyServiceIntegrationTest.java
index 45a9893d0..c610f13ff 100644
--- 
a/integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisPolicyServiceIntegrationTest.java
+++ 
b/integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisPolicyServiceIntegrationTest.java
@@ -18,7 +18,9 @@
  */
 package org.apache.polaris.service.it.test;
 
+import static jakarta.ws.rs.core.Response.Status.NOT_FOUND;
 import static org.apache.polaris.service.it.env.PolarisClient.polarisClient;
+import static org.assertj.core.api.Assertions.assertThat;
 
 import com.google.common.collect.ImmutableMap;
 import jakarta.ws.rs.client.Entity;
@@ -33,6 +35,7 @@ import java.util.List;
 import java.util.Map;
 import java.util.Optional;
 import java.util.UUID;
+import java.util.stream.Stream;
 import org.apache.iceberg.Schema;
 import org.apache.iceberg.catalog.Namespace;
 import org.apache.iceberg.catalog.TableIdentifier;
@@ -47,9 +50,16 @@ 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.GrantResources;
+import org.apache.polaris.core.admin.model.NamespaceGrant;
+import org.apache.polaris.core.admin.model.NamespacePrivilege;
 import org.apache.polaris.core.admin.model.PolarisCatalog;
+import org.apache.polaris.core.admin.model.PolicyGrant;
+import org.apache.polaris.core.admin.model.PolicyPrivilege;
 import org.apache.polaris.core.admin.model.PrincipalWithCredentials;
 import org.apache.polaris.core.admin.model.StorageConfigInfo;
+import org.apache.polaris.core.admin.model.TableGrant;
+import org.apache.polaris.core.admin.model.TablePrivilege;
 import org.apache.polaris.core.catalog.PolarisCatalogHelpers;
 import org.apache.polaris.core.entity.CatalogEntity;
 import org.apache.polaris.core.policy.PredefinedPolicyTypes;
@@ -68,6 +78,7 @@ 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.assertj.core.api.InstanceOfAssertFactories;
 import org.junit.jupiter.api.AfterAll;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeAll;
@@ -86,6 +97,8 @@ public class PolarisPolicyServiceIntegrationTest {
       Optional.ofNullable(System.getenv("INTEGRATION_TEST_ROLE_ARN"))
           .orElse("arn:aws:iam::123456789012:role/my-role");
 
+  private static final String CATALOG_ROLE_1 = "catalogrole1";
+  private static final String CATALOG_ROLE_2 = "catalogrole2";
   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");
@@ -225,9 +238,9 @@ public class PolarisPolicyServiceIntegrationTest {
             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.createCatalogRole(currentCatalogName, CATALOG_ROLE_1);
+    managementApi.addGrant(currentCatalogName, CATALOG_ROLE_1, catalogGrant);
+    CatalogRole catalogRole = managementApi.getCatalogRole(currentCatalogName, 
CATALOG_ROLE_1);
     managementApi.grantCatalogRoleToPrincipalRole(
         principalRoleName, currentCatalogName, catalogRole);
 
@@ -487,6 +500,176 @@ public class PolarisPolicyServiceIntegrationTest {
     restCatalog.dropTable(NS2_T1);
   }
 
+  @Test
+  public void testGrantsOnPolicy() {
+    restCatalog.createNamespace(NS1);
+    try {
+      policyApi.createPolicy(
+          currentCatalogName,
+          NS1_P1,
+          PredefinedPolicyTypes.DATA_COMPACTION,
+          EXAMPLE_TABLE_MAINTENANCE_POLICY_CONTENT,
+          "test policy");
+      managementApi.createCatalogRole(currentCatalogName, CATALOG_ROLE_2);
+      Stream<PolicyGrant> policyGrants =
+          Arrays.stream(PolicyPrivilege.values())
+              .map(
+                  p ->
+                      new PolicyGrant(
+                          Arrays.asList(NS1.levels()),
+                          NS1_P1.getName(),
+                          p,
+                          GrantResource.TypeEnum.POLICY));
+      policyGrants.forEach(g -> managementApi.addGrant(currentCatalogName, 
CATALOG_ROLE_2, g));
+
+      Assertions.assertThat(managementApi.listGrants(currentCatalogName, 
CATALOG_ROLE_2))
+          .extracting(GrantResources::getGrants)
+          .asInstanceOf(InstanceOfAssertFactories.list(GrantResource.class))
+          .map(gr -> ((PolicyGrant) gr).getPrivilege())
+          .containsExactlyInAnyOrder(PolicyPrivilege.values());
+
+      PolicyGrant policyReadGrant =
+          new PolicyGrant(
+              Arrays.asList(NS1.levels()),
+              NS1_P1.getName(),
+              PolicyPrivilege.POLICY_READ,
+              GrantResource.TypeEnum.POLICY);
+      managementApi.revokeGrant(currentCatalogName, CATALOG_ROLE_2, 
policyReadGrant);
+
+      Assertions.assertThat(managementApi.listGrants(currentCatalogName, 
CATALOG_ROLE_2))
+          .extracting(GrantResources::getGrants)
+          .asInstanceOf(InstanceOfAssertFactories.list(GrantResource.class))
+          .map(gr -> ((PolicyGrant) gr).getPrivilege())
+          .doesNotContain(PolicyPrivilege.POLICY_READ);
+    } finally {
+      policyApi.purge(currentCatalogName, NS1);
+    }
+  }
+
+  @Test
+  public void testGrantsOnNonExistingPolicy() {
+    restCatalog.createNamespace(NS1);
+
+    try {
+      managementApi.createCatalogRole(currentCatalogName, CATALOG_ROLE_2);
+      Stream<PolicyGrant> policyGrants =
+          Arrays.stream(PolicyPrivilege.values())
+              .map(
+                  p ->
+                      new PolicyGrant(
+                          Arrays.asList(NS1.levels()),
+                          NS1_P1.getName(),
+                          p,
+                          GrantResource.TypeEnum.POLICY));
+      policyGrants.forEach(
+          g -> {
+            try (Response response =
+                managementApi
+                    .request(
+                        "v1/catalogs/{cat}/catalog-roles/{role}/grants",
+                        Map.of("cat", currentCatalogName, "role", 
"catalogrole2"))
+                    .put(Entity.json(g))) {
+
+              
assertThat(response.getStatus()).isEqualTo(NOT_FOUND.getStatusCode());
+            }
+          });
+    } finally {
+      policyApi.purge(currentCatalogName, NS1);
+    }
+  }
+
+  @Test
+  public void testGrantsOnNamespace() {
+    restCatalog.createNamespace(NS1);
+    try {
+      managementApi.createCatalogRole(currentCatalogName, CATALOG_ROLE_2);
+      List<NamespacePrivilege> policyPrivilegesOnNamespace =
+          List.of(
+              NamespacePrivilege.POLICY_LIST,
+              NamespacePrivilege.POLICY_CREATE,
+              NamespacePrivilege.POLICY_DROP,
+              NamespacePrivilege.POLICY_WRITE,
+              NamespacePrivilege.POLICY_READ,
+              NamespacePrivilege.POLICY_FULL_METADATA,
+              NamespacePrivilege.NAMESPACE_ATTACH_POLICY,
+              NamespacePrivilege.NAMESPACE_DETACH_POLICY);
+      Stream<NamespaceGrant> namespaceGrants =
+          policyPrivilegesOnNamespace.stream()
+              .map(
+                  p ->
+                      new NamespaceGrant(
+                          Arrays.asList(NS1.levels()), p, 
GrantResource.TypeEnum.NAMESPACE));
+      namespaceGrants.forEach(g -> managementApi.addGrant(currentCatalogName, 
CATALOG_ROLE_2, g));
+
+      Assertions.assertThat(managementApi.listGrants(currentCatalogName, 
CATALOG_ROLE_2))
+          .extracting(GrantResources::getGrants)
+          .asInstanceOf(InstanceOfAssertFactories.list(GrantResource.class))
+          .map(gr -> ((NamespaceGrant) gr).getPrivilege())
+          .containsExactlyInAnyOrderElementsOf(policyPrivilegesOnNamespace);
+    } finally {
+      policyApi.purge(currentCatalogName, NS1);
+    }
+  }
+
+  @Test
+  public void testGrantsOnCatalog() {
+    managementApi.createCatalogRole(currentCatalogName, CATALOG_ROLE_2);
+    List<CatalogPrivilege> policyPrivilegesOnCatalog =
+        List.of(
+            CatalogPrivilege.POLICY_LIST,
+            CatalogPrivilege.POLICY_CREATE,
+            CatalogPrivilege.POLICY_DROP,
+            CatalogPrivilege.POLICY_WRITE,
+            CatalogPrivilege.POLICY_READ,
+            CatalogPrivilege.POLICY_FULL_METADATA,
+            CatalogPrivilege.CATALOG_ATTACH_POLICY,
+            CatalogPrivilege.CATALOG_DETACH_POLICY);
+    Stream<CatalogGrant> catalogGrants =
+        policyPrivilegesOnCatalog.stream()
+            .map(p -> new CatalogGrant(p, GrantResource.TypeEnum.CATALOG));
+    catalogGrants.forEach(g -> managementApi.addGrant(currentCatalogName, 
CATALOG_ROLE_2, g));
+
+    Assertions.assertThat(managementApi.listGrants(currentCatalogName, 
CATALOG_ROLE_2))
+        .extracting(GrantResources::getGrants)
+        .asInstanceOf(InstanceOfAssertFactories.list(GrantResource.class))
+        .map(gr -> ((CatalogGrant) gr).getPrivilege())
+        .containsExactlyInAnyOrderElementsOf(policyPrivilegesOnCatalog);
+  }
+
+  @Test
+  public void testGrantsOnTable() {
+    restCatalog.createNamespace(NS2);
+    try {
+      managementApi.createCatalogRole(currentCatalogName, CATALOG_ROLE_2);
+      restCatalog
+          .buildTable(
+              NS2_T1, new Schema(Types.NestedField.optional(1, "string", 
Types.StringType.get())))
+          .create();
+
+      List<TablePrivilege> policyPrivilegesOnTable =
+          List.of(TablePrivilege.TABLE_ATTACH_POLICY, 
TablePrivilege.TABLE_DETACH_POLICY);
+
+      Stream<TableGrant> tableGrants =
+          policyPrivilegesOnTable.stream()
+              .map(
+                  p ->
+                      new TableGrant(
+                          Arrays.asList(NS2.levels()),
+                          NS2_T1.name(),
+                          p,
+                          GrantResource.TypeEnum.TABLE));
+      tableGrants.forEach(g -> managementApi.addGrant(currentCatalogName, 
CATALOG_ROLE_2, g));
+
+      Assertions.assertThat(managementApi.listGrants(currentCatalogName, 
CATALOG_ROLE_2))
+          .extracting(GrantResources::getGrants)
+          .asInstanceOf(InstanceOfAssertFactories.list(GrantResource.class))
+          .map(gr -> ((TableGrant) gr).getPrivilege())
+          .containsExactlyInAnyOrderElementsOf(policyPrivilegesOnTable);
+    } finally {
+      policyApi.purge(currentCatalogName, NS2);
+    }
+  }
+
   private static ApplicablePolicy policyToApplicablePolicy(
       Policy policy, boolean inherited, Namespace parent) {
     return new ApplicablePolicy(
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 e7f5a6cc0..2013b4f28 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
@@ -49,6 +49,7 @@ 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_MANAGE_GRANTS_ON_SECURABLE;
 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;
@@ -208,7 +209,10 @@ public enum PolarisAuthorizableOperation {
   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_TABLE(TABLE_READ_PROPERTIES),
+  ADD_POLICY_GRANT_TO_CATALOG_ROLE(POLICY_MANAGE_GRANTS_ON_SECURABLE),
+  REVOKE_POLICY_GRANT_FROM_CATALOG_ROLE(
+      POLICY_MANAGE_GRANTS_ON_SECURABLE, 
CATALOG_ROLE_MANAGE_GRANTS_FOR_GRANTEE);
 
   private final EnumSet<PolarisPrivilege> privilegesOnTarget;
   private final EnumSet<PolarisPrivilege> privilegesOnSecondary;
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 8a73a151f..265853949 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
@@ -57,6 +57,7 @@ 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;
+import static 
org.apache.polaris.core.entity.PolarisPrivilege.POLICY_MANAGE_GRANTS_ON_SECURABLE;
 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;
@@ -342,7 +343,7 @@ public class PolarisAuthorizerImpl implements 
PolarisAuthorizer {
         VIEW_LIST_GRANTS,
         List.of(VIEW_LIST_GRANTS, VIEW_MANAGE_GRANTS_ON_SECURABLE, 
CATALOG_MANAGE_ACCESS));
 
-    // _MANAGE_GRANTS_ON_SECURABLE for CATALOG, NAMESPACE, TABLE, VIEW
+    // _MANAGE_GRANTS_ON_SECURABLE for CATALOG, NAMESPACE, TABLE, VIEW, POLICY
     SUPER_PRIVILEGES.putAll(
         CATALOG_MANAGE_GRANTS_ON_SECURABLE,
         List.of(CATALOG_MANAGE_GRANTS_ON_SECURABLE, CATALOG_MANAGE_ACCESS));
@@ -355,6 +356,9 @@ public class PolarisAuthorizerImpl implements 
PolarisAuthorizer {
     SUPER_PRIVILEGES.putAll(
         VIEW_MANAGE_GRANTS_ON_SECURABLE,
         List.of(VIEW_MANAGE_GRANTS_ON_SECURABLE, CATALOG_MANAGE_ACCESS));
+    SUPER_PRIVILEGES.putAll(
+        POLICY_MANAGE_GRANTS_ON_SECURABLE,
+        List.of(POLICY_MANAGE_GRANTS_ON_SECURABLE, CATALOG_MANAGE_ACCESS));
 
     // PRINCIPAL CRUDL
     SUPER_PRIVILEGES.putAll(
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 71b7b0df8..88cf6083b 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
@@ -150,6 +150,11 @@ public enum PolarisPrivilege {
   CATALOG_DETACH_POLICY(81, PolarisEntityType.CATALOG),
   NAMESPACE_DETACH_POLICY(82, PolarisEntityType.NAMESPACE),
   TABLE_DETACH_POLICY(83, PolarisEntityType.TABLE_LIKE, 
PolarisEntitySubType.ICEBERG_TABLE),
+  POLICY_MANAGE_GRANTS_ON_SECURABLE(
+      84,
+      PolarisEntityType.POLICY,
+      PolarisEntitySubType.NULL_SUBTYPE,
+      PolarisEntityType.CATALOG_ROLE),
   ;
 
   /**
diff --git 
a/quarkus/service/src/test/java/org/apache/polaris/service/quarkus/admin/PolarisAdminServiceAuthzTest.java
 
b/quarkus/service/src/test/java/org/apache/polaris/service/quarkus/admin/PolarisAdminServiceAuthzTest.java
index f5a6460b1..d74447b53 100644
--- 
a/quarkus/service/src/test/java/org/apache/polaris/service/quarkus/admin/PolarisAdminServiceAuthzTest.java
+++ 
b/quarkus/service/src/test/java/org/apache/polaris/service/quarkus/admin/PolarisAdminServiceAuthzTest.java
@@ -1853,4 +1853,63 @@ public class PolarisAdminServiceAuthzTest extends 
PolarisAuthzTestBase {
         (privilege) ->
             adminService.revokePrivilegeOnCatalogFromRole(CATALOG_NAME, 
CATALOG_ROLE1, privilege));
   }
+
+  @Test
+  public void testGrantPrivilegeOnPolicyToRoleSufficientPrivileges() {
+    doTestSufficientPrivileges(
+        List.of(
+            PolarisPrivilege.CATALOG_MANAGE_ACCESS,
+            PolarisPrivilege.POLICY_MANAGE_GRANTS_ON_SECURABLE),
+        () ->
+            newTestAdminService(Set.of(PRINCIPAL_ROLE1))
+                .grantPrivilegeOnPolicyToRole(
+                    CATALOG_NAME,
+                    CATALOG_ROLE2,
+                    POLICY_NS1_1,
+                    PolarisPrivilege.CATALOG_MANAGE_ACCESS),
+        null, // cleanupAction
+        (privilege) ->
+            adminService.grantPrivilegeOnCatalogToRole(CATALOG_NAME, 
CATALOG_ROLE1, privilege),
+        (privilege) ->
+            adminService.revokePrivilegeOnCatalogFromRole(CATALOG_NAME, 
CATALOG_ROLE1, privilege));
+  }
+
+  @Test
+  public void testGrantPrivilegeOnPolicyToRoleInsufficientPrivileges() {
+    doTestInsufficientPrivileges(
+        List.of(
+            PolarisPrivilege.PRINCIPAL_MANAGE_GRANTS_FOR_GRANTEE,
+            PolarisPrivilege.PRINCIPAL_MANAGE_GRANTS_ON_SECURABLE,
+            PolarisPrivilege.PRINCIPAL_LIST_GRANTS,
+            PolarisPrivilege.PRINCIPAL_ROLE_MANAGE_GRANTS_FOR_GRANTEE,
+            PolarisPrivilege.PRINCIPAL_ROLE_MANAGE_GRANTS_ON_SECURABLE,
+            PolarisPrivilege.PRINCIPAL_ROLE_LIST_GRANTS,
+            PolarisPrivilege.CATALOG_ROLE_MANAGE_GRANTS_FOR_GRANTEE,
+            PolarisPrivilege.CATALOG_ROLE_MANAGE_GRANTS_ON_SECURABLE,
+            PolarisPrivilege.CATALOG_ROLE_LIST_GRANTS,
+            PolarisPrivilege.CATALOG_MANAGE_GRANTS_ON_SECURABLE,
+            PolarisPrivilege.CATALOG_LIST_GRANTS,
+            PolarisPrivilege.NAMESPACE_MANAGE_GRANTS_ON_SECURABLE,
+            PolarisPrivilege.NAMESPACE_LIST_GRANTS,
+            PolarisPrivilege.TABLE_MANAGE_GRANTS_ON_SECURABLE,
+            PolarisPrivilege.TABLE_LIST_GRANTS,
+            PolarisPrivilege.VIEW_MANAGE_GRANTS_ON_SECURABLE,
+            PolarisPrivilege.VIEW_LIST_GRANTS,
+            PolarisPrivilege.PRINCIPAL_FULL_METADATA,
+            PolarisPrivilege.PRINCIPAL_ROLE_FULL_METADATA,
+            PolarisPrivilege.CATALOG_FULL_METADATA,
+            PolarisPrivilege.CATALOG_MANAGE_CONTENT,
+            PolarisPrivilege.SERVICE_MANAGE_ACCESS),
+        () ->
+            newTestAdminService(Set.of(PRINCIPAL_ROLE1))
+                .grantPrivilegeOnPolicyToRole(
+                    CATALOG_NAME,
+                    CATALOG_ROLE2,
+                    POLICY_NS1_1,
+                    PolarisPrivilege.CATALOG_MANAGE_ACCESS),
+        (privilege) ->
+            adminService.grantPrivilegeOnCatalogToRole(CATALOG_NAME, 
CATALOG_ROLE1, privilege),
+        (privilege) ->
+            adminService.revokePrivilegeOnCatalogFromRole(CATALOG_NAME, 
CATALOG_ROLE1, privilege));
+  }
 }
diff --git 
a/service/common/src/main/java/org/apache/polaris/service/admin/PolarisAdminService.java
 
b/service/common/src/main/java/org/apache/polaris/service/admin/PolarisAdminService.java
index 3e7bf9226..cd0f124ce 100644
--- 
a/service/common/src/main/java/org/apache/polaris/service/admin/PolarisAdminService.java
+++ 
b/service/common/src/main/java/org/apache/polaris/service/admin/PolarisAdminService.java
@@ -57,6 +57,8 @@ import org.apache.polaris.core.admin.model.GrantResource;
 import org.apache.polaris.core.admin.model.NamespaceGrant;
 import org.apache.polaris.core.admin.model.NamespacePrivilege;
 import org.apache.polaris.core.admin.model.OAuthClientCredentialsParameters;
+import org.apache.polaris.core.admin.model.PolicyGrant;
+import org.apache.polaris.core.admin.model.PolicyPrivilege;
 import org.apache.polaris.core.admin.model.PrincipalWithCredentials;
 import org.apache.polaris.core.admin.model.PrincipalWithCredentialsCredentials;
 import org.apache.polaris.core.admin.model.TableGrant;
@@ -100,6 +102,8 @@ import 
org.apache.polaris.core.persistence.pagination.PageToken;
 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.core.policy.PolicyEntity;
+import org.apache.polaris.core.policy.exceptions.NoSuchPolicyException;
 import org.apache.polaris.core.secrets.UserSecretReference;
 import org.apache.polaris.core.secrets.UserSecretsManager;
 import org.apache.polaris.core.storage.PolarisStorageConfigurationInfo;
@@ -108,6 +112,7 @@ import 
org.apache.polaris.core.storage.aws.AwsStorageConfigurationInfo;
 import org.apache.polaris.core.storage.azure.AzureStorageConfigurationInfo;
 import org.apache.polaris.service.catalog.common.CatalogHandler;
 import org.apache.polaris.service.config.ReservedProperties;
+import org.apache.polaris.service.types.PolicyIdentifier;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -529,6 +534,45 @@ public class PolarisAdminService {
         catalogRoleWrapper);
   }
 
+  private void authorizeGrantOnPolicyOperationOrThrow(
+      PolarisAuthorizableOperation op,
+      String catalogName,
+      PolicyIdentifier identifier,
+      String catalogRoleName) {
+    resolutionManifest =
+        entityManager.prepareResolutionManifest(callContext, securityContext, 
catalogName);
+    resolutionManifest.addPath(
+        new ResolverPath(
+            PolarisCatalogHelpers.identifierToList(identifier.getNamespace(), 
identifier.getName()),
+            PolarisEntityType.POLICY),
+        identifier);
+    resolutionManifest.addPath(
+        new ResolverPath(List.of(catalogRoleName), 
PolarisEntityType.CATALOG_ROLE),
+        catalogRoleName);
+    ResolverStatus status = resolutionManifest.resolveAll();
+    if (status.getStatus() == 
ResolverStatus.StatusEnum.ENTITY_COULD_NOT_BE_RESOLVED) {
+      throw new NotFoundException("Catalog not found: %s", catalogName);
+    } else if (status.getStatus() == 
ResolverStatus.StatusEnum.PATH_COULD_NOT_BE_FULLY_RESOLVED) {
+      if (status.getFailedToResolvePath().getLastEntityType() == 
PolarisEntityType.POLICY) {
+        throw new NoSuchPolicyException(String.format("Policy does not exist: 
%s", identifier));
+      } else {
+        throw new NotFoundException("CatalogRole not found: %s.%s", 
catalogName, catalogRoleName);
+      }
+    }
+
+    PolarisResolvedPathWrapper policyWrapper = 
resolutionManifest.getResolvedPath(identifier, true);
+    PolarisResolvedPathWrapper catalogRoleWrapper =
+        resolutionManifest.getResolvedPath(catalogRoleName, true);
+
+    authorizer.authorizeOrThrow(
+        callContext,
+        authenticatedPrincipal,
+        resolutionManifest.getAllActivatedCatalogRoleAndPrincipalRoles(),
+        op,
+        policyWrapper,
+        catalogRoleWrapper);
+  }
+
   /** Get all locations where data for a `CatalogEntity` may be stored */
   private Set<String> getCatalogLocations(CatalogEntity catalogEntity) {
     HashSet<String> catalogLocations = new HashSet<>();
@@ -1744,6 +1788,32 @@ public class PolarisAdminService {
         privilege);
   }
 
+  public boolean grantPrivilegeOnPolicyToRole(
+      String catalogName,
+      String catalogRoleName,
+      PolicyIdentifier identifier,
+      PolarisPrivilege privilege) {
+    PolarisAuthorizableOperation op = 
PolarisAuthorizableOperation.ADD_POLICY_GRANT_TO_CATALOG_ROLE;
+
+    authorizeGrantOnPolicyOperationOrThrow(op, catalogName, identifier, 
catalogRoleName);
+
+    return grantPrivilegeOnPolicyEntityToRole(catalogName, catalogRoleName, 
identifier, privilege);
+  }
+
+  public boolean revokePrivilegeOnPolicyFromRole(
+      String catalogName,
+      String catalogRoleName,
+      PolicyIdentifier identifier,
+      PolarisPrivilege privilege) {
+    PolarisAuthorizableOperation op =
+        PolarisAuthorizableOperation.REVOKE_POLICY_GRANT_FROM_CATALOG_ROLE;
+
+    authorizeGrantOnPolicyOperationOrThrow(op, catalogName, identifier, 
catalogRoleName);
+
+    return revokePrivilegeOnPolicyEntityFromRole(
+        catalogName, catalogRoleName, identifier, privilege);
+  }
+
   public List<PolarisEntity> listAssigneePrincipalRolesForCatalogRole(
       String catalogName, String catalogRoleName) {
     PolarisAuthorizableOperation op =
@@ -1778,6 +1848,7 @@ public class PolarisAdminService {
     List<NamespaceGrant> namespaceGrants = new ArrayList<>();
     List<TableGrant> tableGrants = new ArrayList<>();
     List<ViewGrant> viewGrants = new ArrayList<>();
+    List<PolicyGrant> policyGrants = new ArrayList<>();
     Map<Long, PolarisBaseEntity> entityMap = grantList.getEntitiesAsMap();
     for (PolarisGrantRecord record : grantList.getGrantRecords()) {
       PolarisPrivilege privilege = 
PolarisPrivilege.fromCode(record.getPrivilegeCode());
@@ -1832,6 +1903,18 @@ public class PolarisAdminService {
               }
               break;
             }
+          case POLICY:
+            {
+              PolicyEntity policyEntity = PolicyEntity.of(baseEntity);
+              PolicyGrant grant =
+                  new PolicyGrant(
+                      
Arrays.asList(policyEntity.getParentNamespace().levels()),
+                      policyEntity.getName(),
+                      PolicyPrivilege.valueOf(privilege.toString()),
+                      GrantResource.TypeEnum.POLICY);
+              policyGrants.add(grant);
+              break;
+            }
           default:
             throw new IllegalArgumentException(
                 String.format(
@@ -1846,6 +1929,7 @@ public class PolarisAdminService {
     allGrants.addAll(namespaceGrants);
     allGrants.addAll(tableGrants);
     allGrants.addAll(viewGrants);
+    allGrants.addAll(policyGrants);
     return allGrants;
   }
 
@@ -1961,4 +2045,64 @@ public class PolarisAdminService {
             privilege)
         .isSuccess();
   }
+
+  private boolean grantPrivilegeOnPolicyEntityToRole(
+      String catalogName,
+      String catalogRoleName,
+      PolicyIdentifier identifier,
+      PolarisPrivilege privilege) {
+    if (findCatalogByName(catalogName).isEmpty()) {
+      throw new NotFoundException("Parent catalog %s not found", catalogName);
+    }
+    PolarisEntity catalogRoleEntity =
+        findCatalogRoleByName(catalogName, catalogRoleName)
+            .orElseThrow(() -> new NotFoundException("CatalogRole %s not 
found", catalogRoleName));
+
+    PolarisResolvedPathWrapper resolvedPathWrapper = 
resolutionManifest.getResolvedPath(identifier);
+    if (resolvedPathWrapper == null) {
+      throw new NoSuchPolicyException(String.format("Policy not exists: %s", 
identifier));
+    }
+
+    List<PolarisEntity> catalogPath = resolvedPathWrapper.getRawParentPath();
+    PolarisEntity policyEntity = resolvedPathWrapper.getRawLeafEntity();
+
+    return metaStoreManager
+        .grantPrivilegeOnSecurableToRole(
+            getCurrentPolarisContext(),
+            catalogRoleEntity,
+            PolarisEntity.toCoreList(catalogPath),
+            policyEntity,
+            privilege)
+        .isSuccess();
+  }
+
+  private boolean revokePrivilegeOnPolicyEntityFromRole(
+      String catalogName,
+      String catalogRoleName,
+      PolicyIdentifier identifier,
+      PolarisPrivilege privilege) {
+    if (findCatalogByName(catalogName).isEmpty()) {
+      throw new NotFoundException("Parent catalog %s not found", catalogName);
+    }
+    PolarisEntity catalogRoleEntity =
+        findCatalogRoleByName(catalogName, catalogRoleName)
+            .orElseThrow(() -> new NotFoundException("CatalogRole %s not 
found", catalogRoleName));
+
+    PolarisResolvedPathWrapper resolvedPathWrapper = 
resolutionManifest.getResolvedPath(identifier);
+    if (resolvedPathWrapper == null) {
+      throw new NoSuchPolicyException(String.format("Policy not exists: %s", 
identifier));
+    }
+
+    List<PolarisEntity> catalogPath = resolvedPathWrapper.getRawParentPath();
+    PolarisEntity policyEntity = resolvedPathWrapper.getRawLeafEntity();
+
+    return metaStoreManager
+        .revokePrivilegeOnSecurableFromRole(
+            getCurrentPolarisContext(),
+            catalogRoleEntity,
+            PolarisEntity.toCoreList(catalogPath),
+            policyEntity,
+            privilege)
+        .isSuccess();
+  }
 }
diff --git 
a/service/common/src/main/java/org/apache/polaris/service/admin/PolarisServiceImpl.java
 
b/service/common/src/main/java/org/apache/polaris/service/admin/PolarisServiceImpl.java
index e4e351a99..431bbe6fc 100644
--- 
a/service/common/src/main/java/org/apache/polaris/service/admin/PolarisServiceImpl.java
+++ 
b/service/common/src/main/java/org/apache/polaris/service/admin/PolarisServiceImpl.java
@@ -44,6 +44,7 @@ import 
org.apache.polaris.core.admin.model.GrantPrincipalRoleRequest;
 import org.apache.polaris.core.admin.model.GrantResource;
 import org.apache.polaris.core.admin.model.GrantResources;
 import org.apache.polaris.core.admin.model.NamespaceGrant;
+import org.apache.polaris.core.admin.model.PolicyGrant;
 import org.apache.polaris.core.admin.model.Principal;
 import org.apache.polaris.core.admin.model.PrincipalRole;
 import org.apache.polaris.core.admin.model.PrincipalRoles;
@@ -77,6 +78,7 @@ import 
org.apache.polaris.service.admin.api.PolarisPrincipalRolesApiService;
 import org.apache.polaris.service.admin.api.PolarisPrincipalsApiService;
 import org.apache.polaris.service.config.RealmEntityManagerFactory;
 import org.apache.polaris.service.config.ReservedProperties;
+import org.apache.polaris.service.types.PolicyIdentifier;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -621,6 +623,19 @@ public class PolarisServiceImpl
           adminService.grantPrivilegeOnCatalogToRole(catalogName, 
catalogRoleName, privilege);
           break;
         }
+      case PolicyGrant policyGrant:
+        {
+          PolarisPrivilege privilege =
+              PolarisPrivilege.valueOf(policyGrant.getPrivilege().toString());
+          String policyName = policyGrant.getPolicyName();
+          String[] namespaceParts = policyGrant.getNamespace().toArray(new 
String[0]);
+          adminService.grantPrivilegeOnPolicyToRole(
+              catalogName,
+              catalogRoleName,
+              new PolicyIdentifier(Namespace.of(namespaceParts), policyName),
+              privilege);
+          break;
+        }
       default:
         LOGGER
             .atWarn()
@@ -697,6 +712,19 @@ public class PolarisServiceImpl
           adminService.revokePrivilegeOnCatalogFromRole(catalogName, 
catalogRoleName, privilege);
           break;
         }
+      case PolicyGrant policyGrant:
+        {
+          PolarisPrivilege privilege =
+              PolarisPrivilege.valueOf(policyGrant.getPrivilege().toString());
+          String policyName = policyGrant.getPolicyName();
+          String[] namespaceParts = policyGrant.getNamespace().toArray(new 
String[0]);
+          adminService.revokePrivilegeOnPolicyFromRole(
+              catalogName,
+              catalogRoleName,
+              new PolicyIdentifier(Namespace.of(namespaceParts), policyName),
+              privilege);
+          break;
+        }
       default:
         LOGGER
             .atWarn()
diff --git a/spec/polaris-management-service.yml 
b/spec/polaris-management-service.yml
index 0f0a4fc0b..6bba8b57f 100644
--- a/spec/polaris-management-service.yml
+++ b/spec/polaris-management-service.yml
@@ -1282,6 +1282,20 @@ components:
         - TABLE_READ_DATA
         - TABLE_WRITE_DATA
         - TABLE_FULL_METADATA
+        - TABLE_ATTACH_POLICY
+        - TABLE_DETACH_POLICY
+
+    PolicyPrivilege:
+      type: string
+      enum:
+        - CATALOG_MANAGE_ACCESS
+        - POLICY_READ
+        - POLICY_DROP
+        - POLICY_WRITE
+        - POLICY_LIST
+        - POLICY_FULL_METADATA
+        - POLICY_ATTACH
+        - POLICY_DETACH
 
     NamespacePrivilege:
       type: string
@@ -1309,6 +1323,14 @@ components:
         - NAMESPACE_FULL_METADATA
         - TABLE_FULL_METADATA
         - VIEW_FULL_METADATA
+        - POLICY_CREATE
+        - POLICY_WRITE
+        - POLICY_READ
+        - POLICY_DROP
+        - POLICY_LIST
+        - POLICY_FULL_METADATA
+        - NAMESPACE_ATTACH_POLICY
+        - NAMESPACE_DETACH_POLICY
 
     CatalogPrivilege:
       type: string
@@ -1338,6 +1360,14 @@ components:
         - NAMESPACE_FULL_METADATA
         - TABLE_FULL_METADATA
         - VIEW_FULL_METADATA
+        - POLICY_CREATE
+        - POLICY_WRITE
+        - POLICY_READ
+        - POLICY_DROP
+        - POLICY_LIST
+        - POLICY_FULL_METADATA
+        - CATALOG_ATTACH_POLICY
+        - CATALOG_DETACH_POLICY
 
     AddGrantRequest:
       type: object
@@ -1391,6 +1421,24 @@ components:
             - tableName
             - privilege
 
+    PolicyGrant:
+      allOf:
+        - $ref: '#/components/schemas/GrantResource'
+        - type: object
+          properties:
+            namespace:
+              type: array
+              items:
+                type: string
+            policyName:
+              type: string
+            privilege:
+              $ref: '#/components/schemas/PolicyPrivilege'
+          required:
+            - namespace
+            - policyName
+            - privilege
+
     NamespaceGrant:
       allOf:
         - $ref: '#/components/schemas/GrantResource'
@@ -1426,6 +1474,7 @@ components:
           namespace: '#/components/schemas/NamespaceGrant'
           table: '#/components/schemas/TableGrant'
           view: '#/components/schemas/ViewGrant'
+          policy: '#/components/schemas/PolicyGrant'
       properties:
         type:
           type: string
@@ -1434,6 +1483,7 @@ components:
             - namespace
             - table
             - view
+            - policy
       required:
         - type
 


Reply via email to