Skip to content

[Policy Store | Management Spec] Add policy privileges to spec and update admin service impl #1529

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
May 20, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -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");
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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<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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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));
Expand All @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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),
;

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
}
Loading