This is an automated email from the ASF dual-hosted git repository.
collado 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 ae1004bd72 Remove PRINCIPAL_ROLE_MANAGE_GRANTS_FOR_GRANTEE requirement
from REVOKE_CATALOG_ROLE_FROM_PRINCIPAL_ROLE (#3906)
ae1004bd72 is described below
commit ae1004bd72080eca297210e4bcc1f4ad23f81a0c
Author: Michael Collado <[email protected]>
AuthorDate: Tue Mar 10 14:05:08 2026 -0700
Remove PRINCIPAL_ROLE_MANAGE_GRANTS_FOR_GRANTEE requirement from
REVOKE_CATALOG_ROLE_FROM_PRINCIPAL_ROLE (#3906)
* Remove PRINCIPAL_ROLE_MANAGE_GRANTS_FOR_GRANTEE requirement from
REVOKE_CATALOG_ROLE_FROM_PRINCIPAL_ROLE
This change simplifies the authorization model for revoking catalog roles
from principal roles by removing the requirement to have privileges on the
grantee (principal role). After this change, only privileges on the
securable
(catalog role) are required, making revoke symmetric with the assign
operation.
This allows catalog administrators to fully manage catalog role assignments
without requiring elevated privileges on principal roles.
Changes:
- PolarisAuthorizableOperation: Remove
PRINCIPAL_ROLE_MANAGE_GRANTS_FOR_GRANTEE
from REVOKE_CATALOG_ROLE_FROM_PRINCIPAL_ROLE operation definition
- PolarisAdminServiceAuthzTest: Update test to validate only
CATALOG_ROLE_MANAGE_GRANTS_ON_SECURABLE privilege is required
- PolarisManagementServiceIntegrationTest: Update integration test to expect
catalog admin revoke to succeed and handle re-granting for service admin
test
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* Add CHANGELOG entry for REVOKE_CATALOG_ROLE_FROM_PRINCIPAL_ROLE
authorization change
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* Fix test name collisions in ManagementServiceIntegrationTest
Multiple tests were using the same entity name hints ("mypr33" and
"principal1"),
causing 409 CONFLICT errors when tests run in parallel. This change makes
each
test use unique entity names based on the test function name.
Fixed tests:
- testCatalogAdminGrantAndRevokeCatalogRoles
- testServiceAdminCanTransferCatalogAdmin
- testCatalogAdminGrantAndRevokeCatalogRolesFromWrongCatalog
- testTableManageAccessCanGrantAndRevokeFromCatalogRoles
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* Fixed name collisions in integration test
---------
Co-authored-by: Claude Opus 4.6 <[email protected]>
---
CHANGELOG.md | 1 +
.../PolarisManagementServiceIntegrationTest.java | 65 ++++++++++++----------
.../core/auth/PolarisAuthorizableOperation.java | 3 +-
.../admin/PolarisAdminServiceAuthzTest.java | 13 +----
4 files changed, 41 insertions(+), 41 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c525dca1a5..5fa87f5ec3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -43,6 +43,7 @@ request adding CHANGELOG notes for breaking (!) changes and
possibly other secti
- The `PolarisMetricsReporter.reportMetric()` method signature has been
extended to include a `receivedTimestamp` parameter of type `java.time.Instant`.
- The `ExternalCatalogFactory.createCatalog()` and `createGenericCatalog()`
method signatures have been extended to include a `catalogProperties` parameter
of type `Map<String, String>` for passing through proxy and timeout settings to
federated catalog HTTP clients.
- Metrics reporting now requires the `TABLE_READ_DATA` privilege on the target
table for read (scan) metrics and `TABLE_WRITE_DATA` for write (commit) metrics.
+- The `REVOKE_CATALOG_ROLE_FROM_PRINCIPAL_ROLE` operation no longer requires
the `PRINCIPAL_ROLE_MANAGE_GRANTS_FOR_GRANTEE` privilege. Only
`CATALOG_ROLE_MANAGE_GRANTS_ON_SECURABLE` on the catalog role is now required,
making revoke symmetric with assign. This allows catalog administrators to
fully manage catalog role assignments without requiring elevated privileges on
principal roles.
### New Features
diff --git
a/integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisManagementServiceIntegrationTest.java
b/integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisManagementServiceIntegrationTest.java
index cd3f7f15d8..822fd5c04d 100644
---
a/integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisManagementServiceIntegrationTest.java
+++
b/integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisManagementServiceIntegrationTest.java
@@ -1724,7 +1724,7 @@ public class PolarisManagementServiceIntegrationTest {
public void testCatalogAdminGrantAndRevokeCatalogRoles() {
// Create a PrincipalRole and a new catalog. Grant the catalog_admin role
to the new principal
// role
- String principalRoleName = client.newEntityName("mypr33");
+ String principalRoleName =
client.newEntityName("mypr_catalog_admin_grant_revoke");
managementApi.createPrincipalRole(principalRoleName);
String catalogName = client.newEntityName("myuniquetestcatalog");
@@ -1743,7 +1743,7 @@ public class PolarisManagementServiceIntegrationTest {
managementApi.grantCatalogRoleToPrincipalRole(principalRoleName,
catalogName, catalogAdminRole);
PrincipalWithCredentials catalogAdminPrincipal =
- managementApi.createPrincipal(client.newEntityName("principal1"));
+
managementApi.createPrincipal(client.newEntityName("principal_catalog_admin_grant_revoke"));
managementApi.assignPrincipalRole(
catalogAdminPrincipal.getPrincipal().getName(), principalRoleName);
@@ -1752,12 +1752,12 @@ public class PolarisManagementServiceIntegrationTest {
// Create a second principal role. Use the catalog admin principal to list
principal roles and
// grant a catalog role to the new principal role
- String principalRoleName2 = "mypr2";
+ String principalRoleName2 =
client.newEntityName("mypr2_catalog_admin_grant_revoke");
PrincipalRole principalRole2 = new PrincipalRole(principalRoleName2);
managementApi.createPrincipalRole(principalRole2);
// create a catalog role and grant it manage_content privilege
- String catalogRoleName = "mycr1";
+ String catalogRoleName =
client.newEntityName("mycr1_catalog_admin_grant_revoke");
client.managementApi(catalogAdminToken).createCatalogRole(catalogName,
catalogRoleName);
CatalogPrivilege privilege = CatalogPrivilege.CATALOG_MANAGE_CONTENT;
@@ -1774,8 +1774,8 @@ public class PolarisManagementServiceIntegrationTest {
.grantCatalogRoleToPrincipalRole(
principalRoleName2, catalogName, new CatalogRole(catalogRoleName));
- // But the catalog admin cannot revoke the role because it requires
- // PRINCIPAL_ROLE_MANAGE_GRANTS_FOR_GRANTEE
+ // The catalog admin can now revoke the role since it has
+ // CATALOG_ROLE_MANAGE_GRANTS_ON_SECURABLE
try (Response response =
client
.managementApi(catalogAdminToken)
@@ -1787,11 +1787,17 @@ public class PolarisManagementServiceIntegrationTest {
+ "/"
+ catalogRoleName)
.delete()) {
- assertThat(response).returns(FORBIDDEN.getStatusCode(),
Response::getStatus);
+ assertThat(response).returns(Response.Status.NO_CONTENT.getStatusCode(),
Response::getStatus);
}
- // The service admin can revoke the role because it has the
- // PRINCIPAL_ROLE_MANAGE_GRANTS_FOR_GRANTEE privilege
+ // Re-grant the role to test that service admin can also revoke
+ client
+ .managementApi(catalogAdminToken)
+ .grantCatalogRoleToPrincipalRole(
+ principalRoleName2, catalogName, new CatalogRole(catalogRoleName));
+
+ // The service admin can also revoke the role (has
CATALOG_ROLE_MANAGE_GRANTS_ON_SECURABLE
+ // via catalog-scoped privileges)
try (Response response =
managementApi
.request(
@@ -1828,7 +1834,7 @@ public class PolarisManagementServiceIntegrationTest {
public void testServiceAdminCanTransferCatalogAdmin() {
// Create a PrincipalRole and a new catalog. Grant the catalog_admin role
to the new principal
// role
- String principalRoleName = client.newEntityName("mypr33");
+ String principalRoleName =
client.newEntityName("mypr_service_admin_transfer");
PrincipalRole principalRole1 = new PrincipalRole(principalRoleName);
managementApi.createPrincipalRole(principalRole1);
@@ -1848,14 +1854,14 @@ public class PolarisManagementServiceIntegrationTest {
managementApi.grantCatalogRoleToPrincipalRole(principalRoleName,
catalogName, catalogAdminRole);
PrincipalWithCredentials catalogAdminPrincipal =
- managementApi.createPrincipal(client.newEntityName("principal1"));
+
managementApi.createPrincipal(client.newEntityName("principal_service_admin_transfer"));
managementApi.assignPrincipalRole(
catalogAdminPrincipal.getPrincipal().getName(),
principalRole1.getName());
String catalogAdminToken = client.obtainToken(catalogAdminPrincipal);
- // service_admin revokes the catalog_admin privilege from its principal
role
+ // service_admin revokes the catalog_admin privilege from its own
principal role
try {
try (Response response =
managementApi
@@ -1868,10 +1874,11 @@ public class PolarisManagementServiceIntegrationTest {
.returns(Response.Status.NO_CONTENT.getStatusCode(),
Response::getStatus);
}
- // the service_admin can not revoke the catalog_admin privilege from the
new principal role
+ // After revoking its own catalog_admin, service_admin can no longer
revoke the
+ // catalog_admin privilege from the new principal role because it no
longer has
+ // CATALOG_ROLE_MANAGE_GRANTS_ON_SECURABLE (which comes from having
catalog_admin)
try (Response response =
- client
- .managementApi(catalogAdminToken)
+ managementApi
.request(
"v1/principal-roles/"
+ principalRoleName
@@ -1896,7 +1903,7 @@ public class PolarisManagementServiceIntegrationTest {
public void testCatalogAdminGrantAndRevokeCatalogRolesFromWrongCatalog() {
// Create a PrincipalRole and a new catalog. Grant the catalog_admin role
to the new principal
// role
- String principalRoleName = client.newEntityName("mypr33");
+ String principalRoleName = client.newEntityName("mypr_wrong_catalog");
PrincipalRole principalRole1 = new PrincipalRole(principalRoleName);
managementApi.createPrincipalRole(principalRole1);
@@ -1923,7 +1930,7 @@ public class PolarisManagementServiceIntegrationTest {
managementApi.createCatalog(catalog2);
// create a catalog role *in the second catalog* and grant it
manage_content privilege
- String catalogRoleName = "mycr1";
+ String catalogRoleName = client.newEntityName("mycr1_wrong_catalog");
managementApi.createCatalogRole(catalogName2, catalogRoleName);
// Get the catalog admin role from the *first* catalog and grant that role
to the principal role
@@ -1934,7 +1941,7 @@ public class PolarisManagementServiceIntegrationTest {
// Create a principal and grant the principal role to it
PrincipalWithCredentials catalogAdminPrincipal =
- managementApi.createPrincipal(client.newEntityName("principal1"));
+
managementApi.createPrincipal(client.newEntityName("principal_wrong_catalog"));
managementApi.assignPrincipalRole(
catalogAdminPrincipal.getPrincipal().getName(),
principalRole1.getName());
@@ -1959,7 +1966,7 @@ public class PolarisManagementServiceIntegrationTest {
@Test
public void testTableManageAccessCanGrantAndRevokeFromCatalogRoles() {
// Create a PrincipalRole and a new catalog.
- String principalRoleName = client.newEntityName("mypr33");
+ String principalRoleName = client.newEntityName("mypr_table_manage");
PrincipalRole principalRole1 = new PrincipalRole(principalRoleName);
managementApi.createPrincipalRole(principalRole1);
@@ -1975,7 +1982,8 @@ public class PolarisManagementServiceIntegrationTest {
managementApi.createCatalog(catalog);
// create a valid target CatalogRole in this catalog
- managementApi.createCatalogRole(catalogName, "target_catalog_role");
+ String targetCatalogRoleName = client.newEntityName("target_catalog_role");
+ managementApi.createCatalogRole(catalogName, targetCatalogRoleName);
// create a second catalog
String catalogName2 = client.newEntityName("anothertablemanagecatalog");
@@ -1989,17 +1997,18 @@ public class PolarisManagementServiceIntegrationTest {
managementApi.createCatalog(catalog2);
// create an *invalid* target CatalogRole in second catalog
- managementApi.createCatalogRole(catalogName2,
"invalid_target_catalog_role");
+ String invalidTargetCatalogRoleName =
client.newEntityName("invalid_target_catalog_role");
+ managementApi.createCatalogRole(catalogName2,
invalidTargetCatalogRoleName);
// create the namespace "c" in *both* namespaces
- String namespaceName = "c";
+ String namespaceName = client.newEntityName("c");
catalogApi.createNamespace(catalogName, namespaceName);
catalogApi.createNamespace(catalogName2, namespaceName);
// create a catalog role *in the first catalog* and grant it
manage_content privilege at the
// namespace level
// grant that role to the PrincipalRole
- String catalogRoleName = "ns_manage_access_role";
+ String catalogRoleName = client.newEntityName("ns_manage_access_role");
managementApi.createCatalogRole(catalogName, catalogRoleName);
managementApi.addGrant(
catalogName,
@@ -2028,7 +2037,7 @@ public class PolarisManagementServiceIntegrationTest {
.managementApi(manageAccessUserToken)
.addGrant(
catalogName,
- "target_catalog_role",
+ targetCatalogRoleName,
new NamespaceGrant(
List.of(namespaceName),
NamespacePrivilege.TABLE_CREATE,
@@ -2044,7 +2053,7 @@ public class PolarisManagementServiceIntegrationTest {
.managementApi(manageAccessUserToken)
.request("v1/principal-roles/" + principalRoleName +
"/catalog-roles/" + catalogName)
.put(
- Entity.json(new GrantCatalogRoleRequest(new
CatalogRole("target_catalog_role"))))) {
+ Entity.json(new GrantCatalogRoleRequest(new
CatalogRole(targetCatalogRoleName))))) {
assertThat(response).returns(FORBIDDEN.getStatusCode(),
Response::getStatus);
}
@@ -2054,7 +2063,7 @@ public class PolarisManagementServiceIntegrationTest {
.managementApi(manageAccessUserToken)
.request(
"v1/catalogs/{cat}/catalog-roles/{role}/grants",
- Map.of("cat", catalogName, "role", "target_catalog_role"))
+ Map.of("cat", catalogName, "role", targetCatalogRoleName))
.put(
Entity.json(
new CatalogGrant(
@@ -2070,7 +2079,7 @@ public class PolarisManagementServiceIntegrationTest {
.managementApi(manageAccessUserToken)
.request(
"v1/catalogs/{cat}/catalog-roles/{role}/grants",
- Map.of("cat", catalogName2, "role",
"invalid_target_catalog_role"))
+ Map.of("cat", catalogName2, "role",
invalidTargetCatalogRoleName))
.put(
Entity.json(
new NamespaceGrant(
@@ -2086,7 +2095,7 @@ public class PolarisManagementServiceIntegrationTest {
.managementApi(manageAccessUserToken)
.request(
"v1/catalogs/{cat}/catalog-roles/{role}/grants",
- Map.of("cat", catalogName2, "role",
"invalid_target_catalog_role"))
+ Map.of("cat", catalogName2, "role",
invalidTargetCatalogRoleName))
.put(
Entity.json(
new CatalogGrant(
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 ee373a2325..318481709b 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
@@ -173,8 +173,7 @@ public enum PolarisAuthorizableOperation {
LIST_ASSIGNEE_PRINCIPALS_FOR_PRINCIPAL_ROLE(PRINCIPAL_ROLE_LIST_GRANTS),
LIST_CATALOG_ROLES_FOR_PRINCIPAL_ROLE(PRINCIPAL_ROLE_LIST_GRANTS),
ASSIGN_CATALOG_ROLE_TO_PRINCIPAL_ROLE(CATALOG_ROLE_MANAGE_GRANTS_ON_SECURABLE),
- REVOKE_CATALOG_ROLE_FROM_PRINCIPAL_ROLE(
- CATALOG_ROLE_MANAGE_GRANTS_ON_SECURABLE,
PRINCIPAL_ROLE_MANAGE_GRANTS_FOR_GRANTEE),
+
REVOKE_CATALOG_ROLE_FROM_PRINCIPAL_ROLE(CATALOG_ROLE_MANAGE_GRANTS_ON_SECURABLE),
LIST_CATALOG_ROLES(CATALOG_ROLE_LIST),
CREATE_CATALOG_ROLE(CATALOG_ROLE_CREATE),
GET_CATALOG_ROLE(CATALOG_ROLE_READ_PROPERTIES),
diff --git
a/runtime/service/src/test/java/org/apache/polaris/service/admin/PolarisAdminServiceAuthzTest.java
b/runtime/service/src/test/java/org/apache/polaris/service/admin/PolarisAdminServiceAuthzTest.java
index 6c9fe81724..33545e1852 100644
---
a/runtime/service/src/test/java/org/apache/polaris/service/admin/PolarisAdminServiceAuthzTest.java
+++
b/runtime/service/src/test/java/org/apache/polaris/service/admin/PolarisAdminServiceAuthzTest.java
@@ -563,9 +563,7 @@ public class PolarisAdminServiceAuthzTest extends
PolarisAuthzTestBase {
@TestFactory
Stream<DynamicNode> testRevokeCatalogRoleFromPrincipalRolePrivileges() {
- // Revoke requires privileges both on the "securable" (CatalogRole) as
well as the "grantee"
- // (PrincipalRole); neither CATALOG_MANAGE_ACCESS nor
SERVICE_MANAGE_ACCESS alone are
- // sufficient.
+ // Revoke only requires privileges on the securable (CatalogRole).
return authzTestsBuilder("revokeCatalogRoleFromPrincipalRole")
.action(
() ->
@@ -580,14 +578,7 @@ public class PolarisAdminServiceAuthzTest extends
PolarisAuthzTestBase {
privilege ->
adminService.revokePrivilegeOnRootContainerFromPrincipalRole(
PRINCIPAL_ROLE1, privilege))
- .shouldPassWith(
- PolarisPrivilege.CATALOG_MANAGE_ACCESS,
PolarisPrivilege.SERVICE_MANAGE_ACCESS)
- .shouldPassWith(
- PolarisPrivilege.CATALOG_MANAGE_ACCESS,
- PolarisPrivilege.PRINCIPAL_ROLE_MANAGE_GRANTS_FOR_GRANTEE)
- .shouldPassWith(
- PolarisPrivilege.CATALOG_ROLE_MANAGE_GRANTS_ON_SECURABLE,
- PolarisPrivilege.PRINCIPAL_ROLE_MANAGE_GRANTS_FOR_GRANTEE)
+
.shouldPassWith(PolarisPrivilege.CATALOG_ROLE_MANAGE_GRANTS_ON_SECURABLE)
.createTests();
}