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 83ec020388..fb3019c3e2 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 void addGrant(String catalogName, String catalogRoleName, GrantResource g } } + 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 45a9893d08..c610f13ffa 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.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.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.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 void before(TestInfo testInfo) { 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 @@ NS2_T1, new Schema(Types.NestedField.optional(1, "string", Types.StringType.get( 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 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 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 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 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 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 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 policyPrivilegesOnTable = + List.of(TablePrivilege.TABLE_ATTACH_POLICY, TablePrivilege.TABLE_DETACH_POLICY); + + Stream 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 e7f5a6cc00..2013b4f28e 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_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 privilegesOnTarget; private final EnumSet 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 8a73a151fc..2658539491 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_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 71b7b0df83..88cf6083bf 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 f5a6460b15..d74447b538 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 void testRevokePrivilegeOnViewFromRoleInsufficientPrivileges() { (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 3e7bf92263..cd0f124ce2 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.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.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.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 @@ private void authorizeGrantOnTableLikeOperationOrThrow( 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 getCatalogLocations(CatalogEntity catalogEntity) { HashSet catalogLocations = new HashSet<>(); @@ -1744,6 +1788,32 @@ public boolean revokePrivilegeOnViewFromRole( 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 listAssigneePrincipalRolesForCatalogRole( String catalogName, String catalogRoleName) { PolarisAuthorizableOperation op = @@ -1778,6 +1848,7 @@ public List listGrantsForCatalogRole(String catalogName, String c List namespaceGrants = new ArrayList<>(); List tableGrants = new ArrayList<>(); List viewGrants = new ArrayList<>(); + List policyGrants = new ArrayList<>(); Map entityMap = grantList.getEntitiesAsMap(); for (PolarisGrantRecord record : grantList.getGrantRecords()) { PolarisPrivilege privilege = PolarisPrivilege.fromCode(record.getPrivilegeCode()); @@ -1832,6 +1903,18 @@ public List listGrantsForCatalogRole(String catalogName, String c } 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 List listGrantsForCatalogRole(String catalogName, String c allGrants.addAll(namespaceGrants); allGrants.addAll(tableGrants); allGrants.addAll(viewGrants); + allGrants.addAll(policyGrants); return allGrants; } @@ -1961,4 +2045,64 @@ private boolean revokePrivilegeOnTableLikeFromRole( 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 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 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 e4e351a99c..431bbe6fc9 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.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.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 Response addGrantToCatalogRole( 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 Response revokeGrantFromCatalogRole( 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 0f0a4fc0b0..6bba8b57f8 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