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