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 411755126 Implement Finer Grained Operations and Privileges For Update
Table (#2697)
411755126 is described below
commit 41175512655cdb5561b8952a3bf93588185bc536
Author: Travis Bowen <[email protected]>
AuthorDate: Tue Oct 7 08:04:32 2025 -0700
Implement Finer Grained Operations and Privileges For Update Table (#2697)
This implements finer grained operations and privileges for update table in
a backwards compatible way as discussed on the mailing list.
The idea is that all the existing privileges and operations will work and
continue to work even after this change. (i.e. TABLE_WRITE_PROPERTIES will
still ensure update table is authorized even after these changes).
However, because Polaris will now be able to identify each operation within
an UpdateTable request and has a privilege model with inheritance that maps to
each operation, users will now have the option of restricting permissions at a
finer level if desired.
---
.../core/auth/PolarisAuthorizableOperation.java | 36 +-
.../polaris/core/auth/PolarisAuthorizerImpl.java | 195 +++++++
.../polaris/core/config/FeatureConfiguration.java | 9 +
.../polaris/core/entity/PolarisPrivilege.java | 90 ++++
.../polaris/core/entity/PolarisPrivilegeTest.java | 20 +-
regtests/t_pyspark/src/conftest.py | 110 ++++
regtests/t_pyspark/src/iceberg_spark.py | 16 +-
.../src/test_spark_sql_fine_grained_authz.py | 572 +++++++++++++++++++++
.../service/catalog/common/CatalogHandler.java | 52 +-
.../catalog/iceberg/IcebergCatalogHandler.java | 80 ++-
.../iceberg/IcebergCatalogHandlerAuthzTest.java | 374 +++++++++++++-
...ebergCatalogHandlerFineGrainedDisabledTest.java | 121 +++++
spec/polaris-management-service.yml | 54 ++
13 files changed, 1683 insertions(+), 46 deletions(-)
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 2013b4f28..9d12cc148 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
@@ -71,6 +71,11 @@ import static
org.apache.polaris.core.entity.PolarisPrivilege.PRINCIPAL_ROLE_WRI
import static
org.apache.polaris.core.entity.PolarisPrivilege.PRINCIPAL_ROTATE_CREDENTIALS;
import static
org.apache.polaris.core.entity.PolarisPrivilege.PRINCIPAL_WRITE_PROPERTIES;
import static
org.apache.polaris.core.entity.PolarisPrivilege.SERVICE_MANAGE_ACCESS;
+import static
org.apache.polaris.core.entity.PolarisPrivilege.TABLE_ADD_PARTITION_SPEC;
+import static org.apache.polaris.core.entity.PolarisPrivilege.TABLE_ADD_SCHEMA;
+import static
org.apache.polaris.core.entity.PolarisPrivilege.TABLE_ADD_SNAPSHOT;
+import static
org.apache.polaris.core.entity.PolarisPrivilege.TABLE_ADD_SORT_ORDER;
+import static
org.apache.polaris.core.entity.PolarisPrivilege.TABLE_ASSIGN_UUID;
import static
org.apache.polaris.core.entity.PolarisPrivilege.TABLE_ATTACH_POLICY;
import static org.apache.polaris.core.entity.PolarisPrivilege.TABLE_CREATE;
import static
org.apache.polaris.core.entity.PolarisPrivilege.TABLE_DETACH_POLICY;
@@ -80,6 +85,18 @@ import static
org.apache.polaris.core.entity.PolarisPrivilege.TABLE_LIST_GRANTS;
import static
org.apache.polaris.core.entity.PolarisPrivilege.TABLE_MANAGE_GRANTS_ON_SECURABLE;
import static org.apache.polaris.core.entity.PolarisPrivilege.TABLE_READ_DATA;
import static
org.apache.polaris.core.entity.PolarisPrivilege.TABLE_READ_PROPERTIES;
+import static
org.apache.polaris.core.entity.PolarisPrivilege.TABLE_REMOVE_PARTITION_SPECS;
+import static
org.apache.polaris.core.entity.PolarisPrivilege.TABLE_REMOVE_PROPERTIES;
+import static
org.apache.polaris.core.entity.PolarisPrivilege.TABLE_REMOVE_SNAPSHOTS;
+import static
org.apache.polaris.core.entity.PolarisPrivilege.TABLE_REMOVE_SNAPSHOT_REF;
+import static
org.apache.polaris.core.entity.PolarisPrivilege.TABLE_REMOVE_STATISTICS;
+import static
org.apache.polaris.core.entity.PolarisPrivilege.TABLE_SET_CURRENT_SCHEMA;
+import static
org.apache.polaris.core.entity.PolarisPrivilege.TABLE_SET_DEFAULT_SORT_ORDER;
+import static
org.apache.polaris.core.entity.PolarisPrivilege.TABLE_SET_LOCATION;
+import static
org.apache.polaris.core.entity.PolarisPrivilege.TABLE_SET_PROPERTIES;
+import static
org.apache.polaris.core.entity.PolarisPrivilege.TABLE_SET_SNAPSHOT_REF;
+import static
org.apache.polaris.core.entity.PolarisPrivilege.TABLE_SET_STATISTICS;
+import static
org.apache.polaris.core.entity.PolarisPrivilege.TABLE_UPGRADE_FORMAT_VERSION;
import static org.apache.polaris.core.entity.PolarisPrivilege.TABLE_WRITE_DATA;
import static
org.apache.polaris.core.entity.PolarisPrivilege.TABLE_WRITE_PROPERTIES;
import static org.apache.polaris.core.entity.PolarisPrivilege.VIEW_CREATE;
@@ -212,7 +229,24 @@ public enum PolarisAuthorizableOperation {
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);
+ POLICY_MANAGE_GRANTS_ON_SECURABLE,
CATALOG_ROLE_MANAGE_GRANTS_FOR_GRANTEE),
+ ASSIGN_TABLE_UUID(TABLE_ASSIGN_UUID),
+ UPGRADE_TABLE_FORMAT_VERSION(TABLE_UPGRADE_FORMAT_VERSION),
+ ADD_TABLE_SCHEMA(TABLE_ADD_SCHEMA),
+ SET_TABLE_CURRENT_SCHEMA(TABLE_SET_CURRENT_SCHEMA),
+ ADD_TABLE_PARTITION_SPEC(TABLE_ADD_PARTITION_SPEC),
+ ADD_TABLE_SORT_ORDER(TABLE_ADD_SORT_ORDER),
+ SET_TABLE_DEFAULT_SORT_ORDER(TABLE_SET_DEFAULT_SORT_ORDER),
+ ADD_TABLE_SNAPSHOT(TABLE_ADD_SNAPSHOT),
+ SET_TABLE_SNAPSHOT_REF(TABLE_SET_SNAPSHOT_REF),
+ REMOVE_TABLE_SNAPSHOTS(TABLE_REMOVE_SNAPSHOTS),
+ REMOVE_TABLE_SNAPSHOT_REF(TABLE_REMOVE_SNAPSHOT_REF),
+ SET_TABLE_LOCATION(TABLE_SET_LOCATION),
+ SET_TABLE_PROPERTIES(TABLE_SET_PROPERTIES),
+ REMOVE_TABLE_PROPERTIES(TABLE_REMOVE_PROPERTIES),
+ SET_TABLE_STATISTICS(TABLE_SET_STATISTICS),
+ REMOVE_TABLE_STATISTICS(TABLE_REMOVE_STATISTICS),
+ REMOVE_TABLE_PARTITION_SPECS(TABLE_REMOVE_PARTITION_SPECS);
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 5091f4b82..9d943823a 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
@@ -83,6 +83,11 @@ import static
org.apache.polaris.core.entity.PolarisPrivilege.PRINCIPAL_ROLE_WRI
import static
org.apache.polaris.core.entity.PolarisPrivilege.PRINCIPAL_ROTATE_CREDENTIALS;
import static
org.apache.polaris.core.entity.PolarisPrivilege.PRINCIPAL_WRITE_PROPERTIES;
import static
org.apache.polaris.core.entity.PolarisPrivilege.SERVICE_MANAGE_ACCESS;
+import static
org.apache.polaris.core.entity.PolarisPrivilege.TABLE_ADD_PARTITION_SPEC;
+import static org.apache.polaris.core.entity.PolarisPrivilege.TABLE_ADD_SCHEMA;
+import static
org.apache.polaris.core.entity.PolarisPrivilege.TABLE_ADD_SNAPSHOT;
+import static
org.apache.polaris.core.entity.PolarisPrivilege.TABLE_ADD_SORT_ORDER;
+import static
org.apache.polaris.core.entity.PolarisPrivilege.TABLE_ASSIGN_UUID;
import static
org.apache.polaris.core.entity.PolarisPrivilege.TABLE_ATTACH_POLICY;
import static org.apache.polaris.core.entity.PolarisPrivilege.TABLE_CREATE;
import static
org.apache.polaris.core.entity.PolarisPrivilege.TABLE_DETACH_POLICY;
@@ -91,8 +96,21 @@ import static
org.apache.polaris.core.entity.PolarisPrivilege.TABLE_FULL_METADAT
import static org.apache.polaris.core.entity.PolarisPrivilege.TABLE_LIST;
import static
org.apache.polaris.core.entity.PolarisPrivilege.TABLE_LIST_GRANTS;
import static
org.apache.polaris.core.entity.PolarisPrivilege.TABLE_MANAGE_GRANTS_ON_SECURABLE;
+import static
org.apache.polaris.core.entity.PolarisPrivilege.TABLE_MANAGE_STRUCTURE;
import static org.apache.polaris.core.entity.PolarisPrivilege.TABLE_READ_DATA;
import static
org.apache.polaris.core.entity.PolarisPrivilege.TABLE_READ_PROPERTIES;
+import static
org.apache.polaris.core.entity.PolarisPrivilege.TABLE_REMOVE_PARTITION_SPECS;
+import static
org.apache.polaris.core.entity.PolarisPrivilege.TABLE_REMOVE_PROPERTIES;
+import static
org.apache.polaris.core.entity.PolarisPrivilege.TABLE_REMOVE_SNAPSHOTS;
+import static
org.apache.polaris.core.entity.PolarisPrivilege.TABLE_REMOVE_SNAPSHOT_REF;
+import static
org.apache.polaris.core.entity.PolarisPrivilege.TABLE_REMOVE_STATISTICS;
+import static
org.apache.polaris.core.entity.PolarisPrivilege.TABLE_SET_CURRENT_SCHEMA;
+import static
org.apache.polaris.core.entity.PolarisPrivilege.TABLE_SET_DEFAULT_SORT_ORDER;
+import static
org.apache.polaris.core.entity.PolarisPrivilege.TABLE_SET_LOCATION;
+import static
org.apache.polaris.core.entity.PolarisPrivilege.TABLE_SET_PROPERTIES;
+import static
org.apache.polaris.core.entity.PolarisPrivilege.TABLE_SET_SNAPSHOT_REF;
+import static
org.apache.polaris.core.entity.PolarisPrivilege.TABLE_SET_STATISTICS;
+import static
org.apache.polaris.core.entity.PolarisPrivilege.TABLE_UPGRADE_FORMAT_VERSION;
import static org.apache.polaris.core.entity.PolarisPrivilege.TABLE_WRITE_DATA;
import static
org.apache.polaris.core.entity.PolarisPrivilege.TABLE_WRITE_PROPERTIES;
import static org.apache.polaris.core.entity.PolarisPrivilege.VIEW_CREATE;
@@ -248,6 +266,183 @@ public class PolarisAuthorizerImpl implements
PolarisAuthorizer {
TABLE_FULL_METADATA,
TABLE_WRITE_DATA,
TABLE_WRITE_PROPERTIES));
+ SUPER_PRIVILEGES.putAll(
+ TABLE_ASSIGN_UUID,
+ List.of(
+ CATALOG_MANAGE_CONTENT,
+ CATALOG_MANAGE_METADATA,
+ TABLE_FULL_METADATA,
+ TABLE_WRITE_DATA,
+ TABLE_WRITE_PROPERTIES,
+ TABLE_MANAGE_STRUCTURE,
+ TABLE_ASSIGN_UUID));
+ SUPER_PRIVILEGES.putAll(
+ TABLE_UPGRADE_FORMAT_VERSION,
+ List.of(
+ CATALOG_MANAGE_CONTENT,
+ CATALOG_MANAGE_METADATA,
+ TABLE_FULL_METADATA,
+ TABLE_WRITE_DATA,
+ TABLE_WRITE_PROPERTIES,
+ TABLE_MANAGE_STRUCTURE,
+ TABLE_UPGRADE_FORMAT_VERSION));
+ SUPER_PRIVILEGES.putAll(
+ TABLE_ADD_SCHEMA,
+ List.of(
+ CATALOG_MANAGE_CONTENT,
+ CATALOG_MANAGE_METADATA,
+ TABLE_FULL_METADATA,
+ TABLE_WRITE_DATA,
+ TABLE_WRITE_PROPERTIES,
+ TABLE_MANAGE_STRUCTURE,
+ TABLE_ADD_SCHEMA));
+ SUPER_PRIVILEGES.putAll(
+ TABLE_SET_CURRENT_SCHEMA,
+ List.of(
+ CATALOG_MANAGE_CONTENT,
+ CATALOG_MANAGE_METADATA,
+ TABLE_FULL_METADATA,
+ TABLE_WRITE_DATA,
+ TABLE_WRITE_PROPERTIES,
+ TABLE_MANAGE_STRUCTURE,
+ TABLE_SET_CURRENT_SCHEMA));
+ SUPER_PRIVILEGES.putAll(
+ TABLE_ADD_PARTITION_SPEC,
+ List.of(
+ CATALOG_MANAGE_CONTENT,
+ CATALOG_MANAGE_METADATA,
+ TABLE_FULL_METADATA,
+ TABLE_WRITE_DATA,
+ TABLE_WRITE_PROPERTIES,
+ TABLE_MANAGE_STRUCTURE,
+ TABLE_ADD_PARTITION_SPEC));
+ SUPER_PRIVILEGES.putAll(
+ TABLE_ADD_SORT_ORDER,
+ List.of(
+ CATALOG_MANAGE_CONTENT,
+ CATALOG_MANAGE_METADATA,
+ TABLE_FULL_METADATA,
+ TABLE_WRITE_DATA,
+ TABLE_WRITE_PROPERTIES,
+ TABLE_MANAGE_STRUCTURE,
+ TABLE_ADD_SORT_ORDER));
+ SUPER_PRIVILEGES.putAll(
+ TABLE_SET_DEFAULT_SORT_ORDER,
+ List.of(
+ CATALOG_MANAGE_CONTENT,
+ CATALOG_MANAGE_METADATA,
+ TABLE_FULL_METADATA,
+ TABLE_WRITE_DATA,
+ TABLE_WRITE_PROPERTIES,
+ TABLE_MANAGE_STRUCTURE,
+ TABLE_SET_DEFAULT_SORT_ORDER));
+ SUPER_PRIVILEGES.putAll(
+ TABLE_ADD_SNAPSHOT,
+ List.of(
+ CATALOG_MANAGE_CONTENT,
+ CATALOG_MANAGE_METADATA,
+ TABLE_FULL_METADATA,
+ TABLE_WRITE_DATA,
+ TABLE_WRITE_PROPERTIES,
+ TABLE_ADD_SNAPSHOT));
+ SUPER_PRIVILEGES.putAll(
+ TABLE_SET_SNAPSHOT_REF,
+ List.of(
+ CATALOG_MANAGE_CONTENT,
+ CATALOG_MANAGE_METADATA,
+ TABLE_FULL_METADATA,
+ TABLE_WRITE_DATA,
+ TABLE_WRITE_PROPERTIES,
+ TABLE_SET_SNAPSHOT_REF));
+ SUPER_PRIVILEGES.putAll(
+ TABLE_REMOVE_SNAPSHOTS,
+ List.of(
+ CATALOG_MANAGE_CONTENT,
+ CATALOG_MANAGE_METADATA,
+ TABLE_FULL_METADATA,
+ TABLE_WRITE_DATA,
+ TABLE_WRITE_PROPERTIES,
+ TABLE_MANAGE_STRUCTURE,
+ TABLE_REMOVE_SNAPSHOTS));
+ SUPER_PRIVILEGES.putAll(
+ TABLE_REMOVE_SNAPSHOT_REF,
+ List.of(
+ CATALOG_MANAGE_CONTENT,
+ CATALOG_MANAGE_METADATA,
+ TABLE_FULL_METADATA,
+ TABLE_WRITE_DATA,
+ TABLE_WRITE_PROPERTIES,
+ TABLE_MANAGE_STRUCTURE,
+ TABLE_REMOVE_SNAPSHOT_REF));
+ SUPER_PRIVILEGES.putAll(
+ TABLE_SET_LOCATION,
+ List.of(
+ CATALOG_MANAGE_CONTENT,
+ CATALOG_MANAGE_METADATA,
+ TABLE_FULL_METADATA,
+ TABLE_WRITE_DATA,
+ TABLE_WRITE_PROPERTIES,
+ TABLE_MANAGE_STRUCTURE,
+ TABLE_SET_LOCATION));
+ SUPER_PRIVILEGES.putAll(
+ TABLE_SET_PROPERTIES,
+ List.of(
+ CATALOG_MANAGE_CONTENT,
+ CATALOG_MANAGE_METADATA,
+ TABLE_FULL_METADATA,
+ TABLE_WRITE_DATA,
+ TABLE_WRITE_PROPERTIES,
+ TABLE_MANAGE_STRUCTURE,
+ TABLE_SET_PROPERTIES));
+ SUPER_PRIVILEGES.putAll(
+ TABLE_REMOVE_PROPERTIES,
+ List.of(
+ CATALOG_MANAGE_CONTENT,
+ CATALOG_MANAGE_METADATA,
+ TABLE_FULL_METADATA,
+ TABLE_WRITE_DATA,
+ TABLE_WRITE_PROPERTIES,
+ TABLE_MANAGE_STRUCTURE,
+ TABLE_REMOVE_PROPERTIES));
+ SUPER_PRIVILEGES.putAll(
+ TABLE_SET_STATISTICS,
+ List.of(
+ CATALOG_MANAGE_CONTENT,
+ CATALOG_MANAGE_METADATA,
+ TABLE_FULL_METADATA,
+ TABLE_WRITE_DATA,
+ TABLE_WRITE_PROPERTIES,
+ TABLE_MANAGE_STRUCTURE,
+ TABLE_SET_STATISTICS));
+ SUPER_PRIVILEGES.putAll(
+ TABLE_REMOVE_STATISTICS,
+ List.of(
+ CATALOG_MANAGE_CONTENT,
+ CATALOG_MANAGE_METADATA,
+ TABLE_FULL_METADATA,
+ TABLE_WRITE_DATA,
+ TABLE_WRITE_PROPERTIES,
+ TABLE_MANAGE_STRUCTURE,
+ TABLE_REMOVE_STATISTICS));
+ SUPER_PRIVILEGES.putAll(
+ TABLE_REMOVE_PARTITION_SPECS,
+ List.of(
+ CATALOG_MANAGE_CONTENT,
+ CATALOG_MANAGE_METADATA,
+ TABLE_FULL_METADATA,
+ TABLE_WRITE_DATA,
+ TABLE_WRITE_PROPERTIES,
+ TABLE_MANAGE_STRUCTURE,
+ TABLE_REMOVE_PARTITION_SPECS));
+ SUPER_PRIVILEGES.putAll(
+ TABLE_MANAGE_STRUCTURE,
+ List.of(
+ CATALOG_MANAGE_CONTENT,
+ CATALOG_MANAGE_METADATA,
+ TABLE_FULL_METADATA,
+ TABLE_WRITE_DATA,
+ TABLE_WRITE_PROPERTIES,
+ TABLE_MANAGE_STRUCTURE));
SUPER_PRIVILEGES.putAll(
VIEW_WRITE_PROPERTIES,
List.of(
diff --git
a/polaris-core/src/main/java/org/apache/polaris/core/config/FeatureConfiguration.java
b/polaris-core/src/main/java/org/apache/polaris/core/config/FeatureConfiguration.java
index 5bd9f7257..5d81c79f1 100644
---
a/polaris-core/src/main/java/org/apache/polaris/core/config/FeatureConfiguration.java
+++
b/polaris-core/src/main/java/org/apache/polaris/core/config/FeatureConfiguration.java
@@ -420,4 +420,13 @@ public class FeatureConfiguration<T> extends
PolarisConfiguration<T> {
.catalogConfig("polaris.config.allow-dropping-non-empty-passthrough-facade-catalog")
.defaultValue(false)
.buildFeatureConfiguration();
+
+ public static final FeatureConfiguration<Boolean>
ENABLE_FINE_GRAINED_UPDATE_TABLE_PRIVILEGES =
+ PolarisConfiguration.<Boolean>builder()
+ .key("ENABLE_FINE_GRAINED_UPDATE_TABLE_PRIVILEGES")
+
.catalogConfig("polaris.config.enable-fine-grained-update-table-privileges")
+ .description(
+ "When true, enables finer grained update table privileges which
are passed to the authorizer for update table operations")
+ .defaultValue(true)
+ .buildFeatureConfiguration();
}
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 b7a51565b..d76a6d457 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
@@ -155,6 +155,96 @@ public enum PolarisPrivilege {
PolarisEntityType.POLICY,
PolarisEntitySubType.NULL_SUBTYPE,
PolarisEntityType.CATALOG_ROLE),
+ TABLE_ASSIGN_UUID(
+ 85,
+ PolarisEntityType.TABLE_LIKE,
+ List.of(PolarisEntitySubType.ICEBERG_TABLE,
PolarisEntitySubType.GENERIC_TABLE),
+ PolarisEntityType.CATALOG_ROLE),
+ TABLE_UPGRADE_FORMAT_VERSION(
+ 86,
+ PolarisEntityType.TABLE_LIKE,
+ List.of(PolarisEntitySubType.ICEBERG_TABLE,
PolarisEntitySubType.GENERIC_TABLE),
+ PolarisEntityType.CATALOG_ROLE),
+ TABLE_ADD_SCHEMA(
+ 87,
+ PolarisEntityType.TABLE_LIKE,
+ List.of(PolarisEntitySubType.ICEBERG_TABLE,
PolarisEntitySubType.GENERIC_TABLE),
+ PolarisEntityType.CATALOG_ROLE),
+ TABLE_SET_CURRENT_SCHEMA(
+ 88,
+ PolarisEntityType.TABLE_LIKE,
+ List.of(PolarisEntitySubType.ICEBERG_TABLE,
PolarisEntitySubType.GENERIC_TABLE),
+ PolarisEntityType.CATALOG_ROLE),
+ TABLE_ADD_PARTITION_SPEC(
+ 89,
+ PolarisEntityType.TABLE_LIKE,
+ List.of(PolarisEntitySubType.ICEBERG_TABLE,
PolarisEntitySubType.GENERIC_TABLE),
+ PolarisEntityType.CATALOG_ROLE),
+ TABLE_ADD_SORT_ORDER(
+ 90,
+ PolarisEntityType.TABLE_LIKE,
+ List.of(PolarisEntitySubType.ICEBERG_TABLE,
PolarisEntitySubType.GENERIC_TABLE),
+ PolarisEntityType.CATALOG_ROLE),
+ TABLE_SET_DEFAULT_SORT_ORDER(
+ 91,
+ PolarisEntityType.TABLE_LIKE,
+ List.of(PolarisEntitySubType.ICEBERG_TABLE,
PolarisEntitySubType.GENERIC_TABLE),
+ PolarisEntityType.CATALOG_ROLE),
+ TABLE_ADD_SNAPSHOT(
+ 92,
+ PolarisEntityType.TABLE_LIKE,
+ List.of(PolarisEntitySubType.ICEBERG_TABLE,
PolarisEntitySubType.GENERIC_TABLE),
+ PolarisEntityType.CATALOG_ROLE),
+ TABLE_SET_SNAPSHOT_REF(
+ 93,
+ PolarisEntityType.TABLE_LIKE,
+ List.of(PolarisEntitySubType.ICEBERG_TABLE,
PolarisEntitySubType.GENERIC_TABLE),
+ PolarisEntityType.CATALOG_ROLE),
+ TABLE_REMOVE_SNAPSHOTS(
+ 94,
+ PolarisEntityType.TABLE_LIKE,
+ List.of(PolarisEntitySubType.ICEBERG_TABLE,
PolarisEntitySubType.GENERIC_TABLE),
+ PolarisEntityType.CATALOG_ROLE),
+ TABLE_REMOVE_SNAPSHOT_REF(
+ 95,
+ PolarisEntityType.TABLE_LIKE,
+ List.of(PolarisEntitySubType.ICEBERG_TABLE,
PolarisEntitySubType.GENERIC_TABLE),
+ PolarisEntityType.CATALOG_ROLE),
+ TABLE_SET_LOCATION(
+ 96,
+ PolarisEntityType.TABLE_LIKE,
+ List.of(PolarisEntitySubType.ICEBERG_TABLE,
PolarisEntitySubType.GENERIC_TABLE),
+ PolarisEntityType.CATALOG_ROLE),
+ TABLE_SET_PROPERTIES(
+ 97,
+ PolarisEntityType.TABLE_LIKE,
+ List.of(PolarisEntitySubType.ICEBERG_TABLE,
PolarisEntitySubType.GENERIC_TABLE),
+ PolarisEntityType.CATALOG_ROLE),
+ TABLE_REMOVE_PROPERTIES(
+ 98,
+ PolarisEntityType.TABLE_LIKE,
+ List.of(PolarisEntitySubType.ICEBERG_TABLE,
PolarisEntitySubType.GENERIC_TABLE),
+ PolarisEntityType.CATALOG_ROLE),
+ TABLE_SET_STATISTICS(
+ 99,
+ PolarisEntityType.TABLE_LIKE,
+ List.of(PolarisEntitySubType.ICEBERG_TABLE,
PolarisEntitySubType.GENERIC_TABLE),
+ PolarisEntityType.CATALOG_ROLE),
+ TABLE_REMOVE_STATISTICS(
+ 100,
+ PolarisEntityType.TABLE_LIKE,
+ List.of(PolarisEntitySubType.ICEBERG_TABLE,
PolarisEntitySubType.GENERIC_TABLE),
+ PolarisEntityType.CATALOG_ROLE),
+ TABLE_REMOVE_PARTITION_SPECS(
+ 101,
+ PolarisEntityType.TABLE_LIKE,
+ List.of(PolarisEntitySubType.ICEBERG_TABLE,
PolarisEntitySubType.GENERIC_TABLE),
+ PolarisEntityType.CATALOG_ROLE),
+ TABLE_MANAGE_STRUCTURE(
+ 102,
+ PolarisEntityType.TABLE_LIKE,
+ List.of(PolarisEntitySubType.ICEBERG_TABLE,
PolarisEntitySubType.GENERIC_TABLE),
+ PolarisEntityType.CATALOG_ROLE),
;
/**
diff --git
a/polaris-core/src/test/java/org/apache/polaris/core/entity/PolarisPrivilegeTest.java
b/polaris-core/src/test/java/org/apache/polaris/core/entity/PolarisPrivilegeTest.java
index 70c52e911..14596911f 100644
---
a/polaris-core/src/test/java/org/apache/polaris/core/entity/PolarisPrivilegeTest.java
+++
b/polaris-core/src/test/java/org/apache/polaris/core/entity/PolarisPrivilegeTest.java
@@ -113,7 +113,25 @@ public class PolarisPrivilegeTest {
Arguments.of(82, PolarisPrivilege.NAMESPACE_DETACH_POLICY),
Arguments.of(83, PolarisPrivilege.TABLE_DETACH_POLICY),
Arguments.of(84, PolarisPrivilege.POLICY_MANAGE_GRANTS_ON_SECURABLE),
- Arguments.of(85, null));
+ Arguments.of(85, PolarisPrivilege.TABLE_ASSIGN_UUID),
+ Arguments.of(86, PolarisPrivilege.TABLE_UPGRADE_FORMAT_VERSION),
+ Arguments.of(87, PolarisPrivilege.TABLE_ADD_SCHEMA),
+ Arguments.of(88, PolarisPrivilege.TABLE_SET_CURRENT_SCHEMA),
+ Arguments.of(89, PolarisPrivilege.TABLE_ADD_PARTITION_SPEC),
+ Arguments.of(90, PolarisPrivilege.TABLE_ADD_SORT_ORDER),
+ Arguments.of(91, PolarisPrivilege.TABLE_SET_DEFAULT_SORT_ORDER),
+ Arguments.of(92, PolarisPrivilege.TABLE_ADD_SNAPSHOT),
+ Arguments.of(93, PolarisPrivilege.TABLE_SET_SNAPSHOT_REF),
+ Arguments.of(94, PolarisPrivilege.TABLE_REMOVE_SNAPSHOTS),
+ Arguments.of(95, PolarisPrivilege.TABLE_REMOVE_SNAPSHOT_REF),
+ Arguments.of(96, PolarisPrivilege.TABLE_SET_LOCATION),
+ Arguments.of(97, PolarisPrivilege.TABLE_SET_PROPERTIES),
+ Arguments.of(98, PolarisPrivilege.TABLE_REMOVE_PROPERTIES),
+ Arguments.of(99, PolarisPrivilege.TABLE_SET_STATISTICS),
+ Arguments.of(100, PolarisPrivilege.TABLE_REMOVE_STATISTICS),
+ Arguments.of(101, PolarisPrivilege.TABLE_REMOVE_PARTITION_SPECS),
+ Arguments.of(102, PolarisPrivilege.TABLE_MANAGE_STRUCTURE),
+ Arguments.of(103, null));
}
@ParameterizedTest
diff --git a/regtests/t_pyspark/src/conftest.py
b/regtests/t_pyspark/src/conftest.py
index a250257af..d45363f6c 100644
--- a/regtests/t_pyspark/src/conftest.py
+++ b/regtests/t_pyspark/src/conftest.py
@@ -76,6 +76,7 @@ def test_bucket():
def aws_role_arn():
return os.getenv('AWS_ROLE_ARN')
+
@pytest.fixture
def aws_bucket_base_location_prefix():
"""
@@ -178,3 +179,112 @@ def root_client(polaris_host, polaris_url):
host=polaris_url))
api = PolarisDefaultApi(client)
return api
+
+# Helper function to create catalog with specific storage configuration
+def _create_catalog_with_storage(root_client, catalog_client, catalog_name,
storage_config_info, base_location):
+ """
+ Internal helper to create a catalog with specific storage configuration.
+
+ Args:
+ root_client: Management API client
+ catalog_client: Catalog API client
+ catalog_name: Name for the catalog
+ storage_config_info: Storage configuration (S3 or FILE)
+ base_location: Base location for the catalog
+ """
+ from polaris.management import AwsStorageConfigInfo
+
+ # Build properties dict
+ catalog_properties = {
+ "default-base-location": base_location,
+ "polaris.config.drop-with-purge.enabled": "true"
+ }
+
+ # Add AWS-specific properties if using S3 storage
+ if isinstance(storage_config_info, AwsStorageConfigInfo):
+ catalog_properties["client.credentials-provider"] =
"software.amazon.awssdk.auth.credentials.SystemPropertyCredentialsProvider"
+
+ catalog = Catalog(name=catalog_name, type='INTERNAL',
+ properties=CatalogProperties.from_dict(catalog_properties),
+ storage_config_info=storage_config_info)
+
+ try:
+
root_client.create_catalog(create_catalog_request=CreateCatalogRequest(catalog=catalog))
+ resp = root_client.get_catalog(catalog_name=catalog.name)
+
+ # Set up basic catalog role with admin privileges
+ root_client.assign_catalog_role_to_principal_role(
+ principal_role_name='service_admin',
+ catalog_name=catalog_name,
+ grant_catalog_role_request=GrantCatalogRoleRequest(
+ catalog_role=CatalogRole(name='catalog_admin')
+ )
+ )
+
+ writer_catalog_role = create_catalog_role(root_client, resp,
'admin_writer')
+ root_client.add_grant_to_catalog_role(
+ catalog_name, writer_catalog_role.name,
+ AddGrantRequest(grant=CatalogGrant(
+ catalog_name=catalog_name,
+ type='catalog',
+ privilege=CatalogPrivilege.CATALOG_MANAGE_CONTENT
+ ))
+ )
+
+ root_client.assign_catalog_role_to_principal_role(
+ principal_role_name='service_admin',
+ catalog_name=catalog_name,
+
grant_catalog_role_request=GrantCatalogRoleRequest(catalog_role=writer_catalog_role)
+ )
+
+ yield resp
+ finally:
+ # Cleanup
+ namespaces = catalog_client.list_namespaces(catalog_name)
+ for n in namespaces.namespaces:
+ clear_namespace(catalog_name, catalog_client, n)
+ catalog_roles = root_client.list_catalog_roles(catalog_name)
+ for r in catalog_roles.roles:
+ if r.name != 'catalog_admin':
+ root_client.delete_catalog_role(catalog_name, r.name)
+ root_client.delete_catalog(catalog_name=catalog_name)
+
+
[email protected]
+def file_catalog(root_client, catalog_client):
+ """
+ Catalog that always uses FILE storage for local testing.
+ This fixture runs in any environment without external dependencies.
+ """
+ from polaris.management import FileStorageConfigInfo
+
+ catalog_name = f'file_catalog_{str(uuid.uuid4())[-10:]}'
+ storage_config = FileStorageConfigInfo(storage_type="FILE",
allowed_locations=["file:///tmp"])
+ base_location = "file:///tmp/polaris"
+
+ yield from _create_catalog_with_storage(
+ root_client, catalog_client, catalog_name, storage_config, base_location
+ )
+
+
[email protected]
+def s3_catalog(root_client, catalog_client, test_bucket, aws_role_arn,
aws_bucket_base_location_prefix):
+ """
+ Catalog that always uses S3 storage for AWS testing.
+ Tests using this fixture should include @pytest.mark.skipif for
AWS_TEST_ENABLED.
+ """
+ from polaris.management import AwsStorageConfigInfo
+
+ catalog_name = f's3_catalog_{str(uuid.uuid4())[-10:]}'
+ storage_config = AwsStorageConfigInfo(
+ storage_type="S3",
+
allowed_locations=[f"s3://{test_bucket}/{aws_bucket_base_location_prefix}/"],
+ role_arn=aws_role_arn
+ )
+ base_location =
f"s3://{test_bucket}/{aws_bucket_base_location_prefix}/s3_catalog"
+
+ yield from _create_catalog_with_storage(
+ root_client, catalog_client, catalog_name, storage_config, base_location
+ )
+
+
diff --git a/regtests/t_pyspark/src/iceberg_spark.py
b/regtests/t_pyspark/src/iceberg_spark.py
index 7d866bde2..fb430d48a 100644
--- a/regtests/t_pyspark/src/iceberg_spark.py
+++ b/regtests/t_pyspark/src/iceberg_spark.py
@@ -46,7 +46,8 @@ class IcebergSparkSession:
aws_region: str = None,
catalog_name: str = None,
polaris_url: str = None,
- realm: str = 'POLARIS'
+ realm: str = 'POLARIS',
+ use_vended_credentials: bool = True
):
"""Constructor for Iceberg Spark session. Sets the member variables."""
self.bearer_token = bearer_token
@@ -56,6 +57,7 @@ class IcebergSparkSession:
self.catalog_name = catalog_name
self.polaris_url = polaris_url
self.realm = realm
+ self.use_vended_credentials = use_vended_credentials
def get_catalog_name(self):
"""Get the catalog name of this spark session based on catalog_type."""
@@ -101,7 +103,6 @@ class IcebergSparkSession:
.config(
f"spark.sql.catalog.{catalog_name}",
"org.apache.iceberg.spark.SparkCatalog"
)
-
.config(f"spark.sql.catalog.{catalog_name}.header.X-Iceberg-Access-Delegation",
"vended-credentials")
.config(f"spark.sql.catalog.{catalog_name}.type", "rest")
.config(f"spark.sql.catalog.{catalog_name}.uri", self.polaris_url)
.config(f"spark.sql.catalog.{catalog_name}.warehouse", self.catalog_name)
@@ -112,6 +113,17 @@ class IcebergSparkSession:
.config("spark.ui.showConsoleProgress", False)
)
+ # Conditionally add vended credentials header
+ if self.use_vended_credentials:
+ spark_session_builder = spark_session_builder.config(
+
f"spark.sql.catalog.{catalog_name}.header.X-Iceberg-Access-Delegation",
"vended-credentials"
+ )
+ else:
+ # Explicitly remove the header if it was set globally
+ spark_session_builder = spark_session_builder.config(
+
f"spark.sql.catalog.{catalog_name}.header.X-Iceberg-Access-Delegation", ""
+ )
+
self.spark_session = spark_session_builder.getOrCreate()
self.quiet_logs(self.spark_session.sparkContext)
return self
diff --git a/regtests/t_pyspark/src/test_spark_sql_fine_grained_authz.py
b/regtests/t_pyspark/src/test_spark_sql_fine_grained_authz.py
new file mode 100644
index 000000000..b6b47203c
--- /dev/null
+++ b/regtests/t_pyspark/src/test_spark_sql_fine_grained_authz.py
@@ -0,0 +1,572 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+"""
+Fine-grained authorization tests for Polaris.
+
+These tests validate that fine-grained table metadata update privileges work
correctly.
+
+The authorization logic is storage-agnostic, so testing with a catalog using
FILE storage
+"""
+
+import os
+import pytest
+import uuid
+from py4j.protocol import Py4JJavaError
+
+from iceberg_spark import IcebergSparkSession
+from polaris.management import PrincipalRole, CatalogRole, CatalogGrant,
CatalogPrivilege, \
+ AddGrantRequest, GrantCatalogRoleRequest, GrantPrincipalRoleRequest
+
+# Import existing helper functions instead of copying them
+from conftest import create_catalog_role
+from test_spark_sql_s3_with_privileges import create_principal,
create_principal_role
+
+
[email protected]
+def fine_grained_authz_test_catalog(root_client, catalog_client):
+ """
+ Catalog specifically for fine-grained authorization testing.
+ Does NOT assign catalog_admin to service_admin to avoid privilege
inheritance issues.
+ """
+ from polaris.management import FileStorageConfigInfo, Catalog,
CatalogProperties, CreateCatalogRequest
+ from conftest import create_catalog_role
+
+ catalog_name = f'fine_grained_authz_test_catalog_{str(uuid.uuid4())[-10:]}'
+ storage_config = FileStorageConfigInfo(storage_type="FILE",
allowed_locations=["file:///tmp"])
+ base_location = "file:///tmp/polaris"
+
+ # Build properties dict with fine-grained authorization enabled
+ catalog_properties = {
+ "default-base-location": base_location,
+ "polaris.config.drop-with-purge.enabled": "true",
+ "polaris.config.enable-fine-grained-update-table-privileges": "true"
+ }
+
+ catalog = Catalog(name=catalog_name, type='INTERNAL',
+
properties=CatalogProperties.from_dict(catalog_properties),
+ storage_config_info=storage_config)
+
+ try:
+
root_client.create_catalog(create_catalog_request=CreateCatalogRequest(catalog=catalog))
+ resp = root_client.get_catalog(catalog_name=catalog.name)
+
+ # IMPORTANT: We do NOT assign catalog_admin to service_admin here!
+ # This ensures fine-grained tests have only the privileges explicitly
granted
+
+ # However, we need to grant cleanup privileges to service_admin for
fixture teardown
+ cleanup_catalog_role = create_catalog_role(root_client, resp,
'cleanup_role')
+ cleanup_privileges = [
+ CatalogPrivilege.TABLE_DROP,
+ CatalogPrivilege.TABLE_WRITE_DATA, # Needed for
DROP_TABLE_WITH_PURGE
+ CatalogPrivilege.NAMESPACE_DROP
+ ]
+
+ for privilege in cleanup_privileges:
+ root_client.add_grant_to_catalog_role(
+ catalog_name, cleanup_catalog_role.name,
+ AddGrantRequest(grant=CatalogGrant(
+ catalog_name=catalog_name,
+ type='catalog',
+ privilege=privilege
+ ))
+ )
+
+ root_client.assign_catalog_role_to_principal_role(
+ principal_role_name='service_admin',
+ catalog_name=catalog_name,
+
grant_catalog_role_request=GrantCatalogRoleRequest(catalog_role=cleanup_catalog_role)
+ )
+
+ yield resp
+ finally:
+ # Cleanup
+ from conftest import clear_namespace
+ namespaces = catalog_client.list_namespaces(catalog_name)
+ for n in namespaces.namespaces:
+ clear_namespace(catalog_name, catalog_client, n)
+ catalog_roles = root_client.list_catalog_roles(catalog_name)
+ for r in catalog_roles.roles:
+ if r.name not in ['catalog_admin', 'cleanup_role']:
+ root_client.delete_catalog_role(catalog_name, r.name)
+ # Delete cleanup_role last
+ try:
+ root_client.delete_catalog_role(catalog_name, 'cleanup_role')
+ except:
+ pass
+ root_client.delete_catalog(catalog_name=catalog_name)
+
+
+
+def test_coarse_grained_table_write_properties(polaris_url,
polaris_catalog_url, root_client, fine_grained_authz_test_catalog):
+ """Test that coarse-grained TABLE_WRITE_PROPERTIES privilege allows all
metadata operations"""
+
+ catalog_name = fine_grained_authz_test_catalog.name
+
+ # Create a single principal with TABLE_WRITE_PROPERTIES (coarse-grained
privilege)
+ principal_name = f"coarse_grained_user_{str(uuid.uuid4())[-10:]}"
+ principal_role_name = f"coarse_grained_role_{str(uuid.uuid4())[-10:]}"
+ catalog_role_name = f"coarse_grained_cat_role_{str(uuid.uuid4())[-10:]}"
+
+ try:
+ # Create principal with coarse-grained privileges
+ principal = create_principal(polaris_url, polaris_catalog_url,
root_client, principal_name)
+ principal_role = create_principal_role(root_client,
principal_role_name)
+ catalog_role = create_catalog_role(root_client,
fine_grained_authz_test_catalog, catalog_role_name)
+
+ root_client.assign_catalog_role_to_principal_role(
+ principal_role_name=principal_role.name,
+ catalog_name=catalog_name,
+
grant_catalog_role_request=GrantCatalogRoleRequest(catalog_role=catalog_role)
+ )
+
+ # Grant coarse-grained privileges (including TABLE_WRITE_PROPERTIES
super-privilege)
+ coarse_grained_privileges = [
+ CatalogPrivilege.NAMESPACE_FULL_METADATA,
+ CatalogPrivilege.TABLE_CREATE,
+ CatalogPrivilege.TABLE_LIST,
+ CatalogPrivilege.TABLE_READ_PROPERTIES,
+ CatalogPrivilege.TABLE_READ_DATA,
+ CatalogPrivilege.TABLE_DROP,
+ CatalogPrivilege.TABLE_WRITE_DATA,
+ CatalogPrivilege.TABLE_WRITE_PROPERTIES # This should allow both
SET and UNSET
+ ]
+
+ for privilege in coarse_grained_privileges:
+ root_client.add_grant_to_catalog_role(
+ catalog_name,
+ catalog_role_name,
+ AddGrantRequest(grant=CatalogGrant(
+ catalog_name=catalog_name,
+ type='catalog',
+ privilege=privilege
+ ))
+ )
+
+ root_client.assign_principal_role(
+ principal.principal.name,
+
grant_principal_role_request=GrantPrincipalRoleRequest(principal_role=principal_role)
+ )
+
+ # Test with coarse-grained privilege - should work for both SET and
UNSET operations
+ with IcebergSparkSession(
+
credentials=f'{principal.principal.client_id}:{principal.credentials.client_secret.get_secret_value()}',
+ catalog_name=catalog_name,
+ polaris_url=polaris_catalog_url
+ ) as spark:
+ spark.sql(f'USE {catalog_name}')
+ spark.sql('CREATE NAMESPACE db1')
+ spark.sql('CREATE TABLE db1.test_table (col1 int, col2 string)')
+
+ # Both operations should work with TABLE_WRITE_PROPERTIES
+ spark.sql("ALTER TABLE db1.test_table SET TBLPROPERTIES
('test.property' = 'test.value')")
+ spark.sql("ALTER TABLE db1.test_table UNSET TBLPROPERTIES
('test.property')")
+
+ finally:
+ # Cleanup principal and roles
+ try:
+ root_client.delete_principal(principal_name)
+ root_client.delete_principal_role(principal_role_name)
+ root_client.delete_catalog_role(catalog_name, catalog_role_name)
+ except:
+ pass
+
+
+def test_fine_grained_table_set_properties(polaris_url, polaris_catalog_url,
root_client, fine_grained_authz_test_catalog):
+ """Test fine-grained TABLE_SET_PROPERTIES privilege allows SET operations
but not UNSET"""
+
+ catalog_name = fine_grained_authz_test_catalog.name
+
+ # Create setup principal (for table creation)
+ setup_principal_name = f"setup_user_{str(uuid.uuid4())[-10:]}"
+ setup_principal_role_name = f"setup_role_{str(uuid.uuid4())[-10:]}"
+ setup_catalog_role_name = f"setup_cat_role_{str(uuid.uuid4())[-10:]}"
+
+ # Create test principal (for fine-grained testing)
+ test_principal_name = f"test_user_{str(uuid.uuid4())[-10:]}"
+ test_principal_role_name = f"test_role_{str(uuid.uuid4())[-10:]}"
+ test_catalog_role_name = f"test_cat_role_{str(uuid.uuid4())[-10:]}"
+
+ try:
+ # Create setup principal with full privileges
+ setup_principal = create_principal(polaris_url, polaris_catalog_url,
root_client, setup_principal_name)
+ setup_principal_role = create_principal_role(root_client,
setup_principal_role_name)
+ setup_catalog_role = create_catalog_role(root_client,
fine_grained_authz_test_catalog, setup_catalog_role_name)
+
+ root_client.assign_catalog_role_to_principal_role(
+ principal_role_name=setup_principal_role.name,
+ catalog_name=catalog_name,
+
grant_catalog_role_request=GrantCatalogRoleRequest(catalog_role=setup_catalog_role)
+ )
+
+ # Grant setup privileges (including super-privileges)
+ setup_privileges = [
+ CatalogPrivilege.NAMESPACE_FULL_METADATA,
+ CatalogPrivilege.TABLE_CREATE,
+ CatalogPrivilege.TABLE_LIST,
+ CatalogPrivilege.TABLE_READ_PROPERTIES,
+ CatalogPrivilege.TABLE_DROP,
+ CatalogPrivilege.TABLE_WRITE_DATA
+ ]
+
+ for privilege in setup_privileges:
+ root_client.add_grant_to_catalog_role(
+ catalog_name,
+ setup_catalog_role_name,
+ AddGrantRequest(grant=CatalogGrant(
+ catalog_name=catalog_name,
+ type='catalog',
+ privilege=privilege
+ ))
+ )
+
+ root_client.assign_principal_role(
+ setup_principal.principal.name,
+
grant_principal_role_request=GrantPrincipalRoleRequest(principal_role=setup_principal_role)
+ )
+
+ # Create test principal with only fine-grained privileges
+ test_principal = create_principal(polaris_url, polaris_catalog_url,
root_client, test_principal_name)
+ test_principal_role = create_principal_role(root_client,
test_principal_role_name)
+ test_catalog_role = create_catalog_role(root_client,
fine_grained_authz_test_catalog, test_catalog_role_name)
+
+ root_client.assign_catalog_role_to_principal_role(
+ principal_role_name=test_principal_role.name,
+ catalog_name=catalog_name,
+
grant_catalog_role_request=GrantCatalogRoleRequest(catalog_role=test_catalog_role)
+ )
+
+ # Grant only basic privileges to test role
+ test_basic_privileges = [
+ CatalogPrivilege.TABLE_READ_PROPERTIES,
+ CatalogPrivilege.TABLE_SET_PROPERTIES # The specific privilege
we're testing
+ ]
+
+ for privilege in test_basic_privileges:
+ root_client.add_grant_to_catalog_role(
+ catalog_name,
+ test_catalog_role_name,
+ AddGrantRequest(grant=CatalogGrant(
+ catalog_name=catalog_name,
+ type='catalog',
+ privilege=privilege
+ ))
+ )
+
+ root_client.assign_principal_role(
+ test_principal.principal.name,
+
grant_principal_role_request=GrantPrincipalRoleRequest(principal_role=test_principal_role)
+ )
+
+ # Create table using the setup principal
+ with IcebergSparkSession(
+
credentials=f'{setup_principal.principal.client_id}:{setup_principal.credentials.client_secret.get_secret_value()}',
+ catalog_name=catalog_name,
+ polaris_url=polaris_catalog_url
+ ) as spark:
+ spark.sql(f'USE {catalog_name}')
+ spark.sql('CREATE NAMESPACE db1')
+ spark.sql('CREATE TABLE db1.test_table (col1 int, col2 string)')
+
+ # Test fine-grained operations using the test principal
+ with IcebergSparkSession(
+
credentials=f'{test_principal.principal.client_id}:{test_principal.credentials.client_secret.get_secret_value()}',
+ catalog_name=catalog_name,
+ polaris_url=polaris_catalog_url,
+ use_vended_credentials=False # Not needed for file storage type
+ ) as spark:
+ spark.sql(f'USE {catalog_name}')
+
+ # SET operation should work with TABLE_SET_PROPERTIES
+ spark.sql("ALTER TABLE db1.test_table SET TBLPROPERTIES
('test.property' = 'test.value')")
+
+ # UNSET operation should fail without TABLE_REMOVE_PROPERTIES
+ with pytest.raises(Py4JJavaError) as exc_info:
+ spark.sql("ALTER TABLE db1.test_table UNSET TBLPROPERTIES
('test.property')")
+
+ # Verify the error is related to authorization
+ error_str = str(exc_info.value).lower()
+ assert "forbidden" in error_str or "not authorized" in error_str,
f"Unexpected error: {exc_info.value}"
+ finally:
+ # Cleanup principals and roles
+ try:
+ root_client.delete_principal(setup_principal_name)
+ root_client.delete_principal_role(setup_principal_role_name)
+ root_client.delete_catalog_role(catalog_name,
setup_catalog_role_name)
+ root_client.delete_principal(test_principal_name)
+ root_client.delete_principal_role(test_principal_role_name)
+ root_client.delete_catalog_role(catalog_name,
test_catalog_role_name)
+ except:
+ pass
+
+
+def test_fine_grained_table_remove_properties(polaris_url,
polaris_catalog_url, root_client, fine_grained_authz_test_catalog):
+ """Test that fine-grained TABLE_REMOVE_PROPERTIES privilege allows UNSET
operations but not SET"""
+
+ catalog_name = fine_grained_authz_test_catalog.name
+
+ # Create setup principal (for table creation)
+ setup_principal_name = f"setup_user_{str(uuid.uuid4())[-10:]}"
+ setup_principal_role_name = f"setup_role_{str(uuid.uuid4())[-10:]}"
+ setup_catalog_role_name = f"setup_cat_role_{str(uuid.uuid4())[-10:]}"
+
+ # Create test principal (for fine-grained testing)
+ test_principal_name = f"test_user_{str(uuid.uuid4())[-10:]}"
+ test_principal_role_name = f"test_role_{str(uuid.uuid4())[-10:]}"
+ test_catalog_role_name = f"test_cat_role_{str(uuid.uuid4())[-10:]}"
+
+ try:
+ # Create setup principal with full privileges
+ setup_principal = create_principal(polaris_url, polaris_catalog_url,
root_client, setup_principal_name)
+ setup_principal_role = create_principal_role(root_client,
setup_principal_role_name)
+ setup_catalog_role = create_catalog_role(root_client,
fine_grained_authz_test_catalog, setup_catalog_role_name)
+
+ root_client.assign_catalog_role_to_principal_role(
+ principal_role_name=setup_principal_role.name,
+ catalog_name=catalog_name,
+
grant_catalog_role_request=GrantCatalogRoleRequest(catalog_role=setup_catalog_role)
+ )
+
+ # Grant setup privileges (including super-privileges)
+ setup_privileges = [
+ CatalogPrivilege.NAMESPACE_FULL_METADATA,
+ CatalogPrivilege.TABLE_CREATE,
+ CatalogPrivilege.TABLE_LIST,
+ CatalogPrivilege.TABLE_READ_PROPERTIES,
+ CatalogPrivilege.TABLE_DROP,
+ CatalogPrivilege.TABLE_WRITE_DATA
+ ]
+
+ for privilege in setup_privileges:
+ root_client.add_grant_to_catalog_role(
+ catalog_name,
+ setup_catalog_role_name,
+ AddGrantRequest(grant=CatalogGrant(
+ catalog_name=catalog_name,
+ type='catalog',
+ privilege=privilege
+ ))
+ )
+
+ root_client.assign_principal_role(
+ setup_principal.principal.name,
+
grant_principal_role_request=GrantPrincipalRoleRequest(principal_role=setup_principal_role)
+ )
+
+ # Create test principal with only fine-grained privileges
+ test_principal = create_principal(polaris_url, polaris_catalog_url,
root_client, test_principal_name)
+ test_principal_role = create_principal_role(root_client,
test_principal_role_name)
+ test_catalog_role = create_catalog_role(root_client,
fine_grained_authz_test_catalog, test_catalog_role_name)
+
+ root_client.assign_catalog_role_to_principal_role(
+ principal_role_name=test_principal_role.name,
+ catalog_name=catalog_name,
+
grant_catalog_role_request=GrantCatalogRoleRequest(catalog_role=test_catalog_role)
+ )
+
+ # Grant only TABLE_REMOVE_PROPERTIES (not SET_PROPERTIES)
+ test_basic_privileges = [
+ CatalogPrivilege.TABLE_READ_PROPERTIES,
+ CatalogPrivilege.TABLE_REMOVE_PROPERTIES # The specific privilege
we're testing
+ ]
+
+ for privilege in test_basic_privileges:
+ root_client.add_grant_to_catalog_role(
+ catalog_name,
+ test_catalog_role_name,
+ AddGrantRequest(grant=CatalogGrant(
+ catalog_name=catalog_name,
+ type='catalog',
+ privilege=privilege
+ ))
+ )
+
+ root_client.assign_principal_role(
+ test_principal.principal.name,
+
grant_principal_role_request=GrantPrincipalRoleRequest(principal_role=test_principal_role)
+ )
+
+ # Create table using the setup principal and set a property to remove
later
+ with IcebergSparkSession(
+
credentials=f'{setup_principal.principal.client_id}:{setup_principal.credentials.client_secret.get_secret_value()}',
+ catalog_name=catalog_name,
+ polaris_url=polaris_catalog_url
+ ) as spark:
+ spark.sql(f'USE {catalog_name}')
+ spark.sql('CREATE NAMESPACE db1')
+ spark.sql('CREATE TABLE db1.test_table (col1 int, col2 string)')
+ # Set a property first so we can remove it
+ spark.sql("ALTER TABLE db1.test_table SET TBLPROPERTIES
('test.property' = 'test.value')")
+
+ # Test fine-grained operations using the test principal
+ with IcebergSparkSession(
+
credentials=f'{test_principal.principal.client_id}:{test_principal.credentials.client_secret.get_secret_value()}',
+ catalog_name=catalog_name,
+ polaris_url=polaris_catalog_url,
+ use_vended_credentials=False # Not needed for file storage type
+ ) as spark:
+ spark.sql(f'USE {catalog_name}')
+
+ # UNSET operation should work with TABLE_REMOVE_PROPERTIES
+ spark.sql("ALTER TABLE db1.test_table UNSET TBLPROPERTIES
('test.property')")
+
+ # SET operation should fail without TABLE_SET_PROPERTIES
+ with pytest.raises(Py4JJavaError) as exc_info:
+ spark.sql("ALTER TABLE db1.test_table SET TBLPROPERTIES
('test.property2' = 'test.value2')")
+
+ # Verify the error is related to authorization
+ error_str = str(exc_info.value).lower()
+ assert "not authorized" in error_str or "forbidden" in error_str,
f"Unexpected error: {exc_info.value}"
+
+ finally:
+ # Cleanup principals and roles
+ try:
+ root_client.delete_principal(setup_principal_name)
+ root_client.delete_principal_role(setup_principal_role_name)
+ root_client.delete_catalog_role(catalog_name,
setup_catalog_role_name)
+ root_client.delete_principal(test_principal_name)
+ root_client.delete_principal_role(test_principal_role_name)
+ root_client.delete_catalog_role(catalog_name,
test_catalog_role_name)
+ except:
+ pass
+
+
+def test_multiple_fine_grained_privileges_together(polaris_url,
polaris_catalog_url, root_client, fine_grained_authz_test_catalog):
+ """Test that multiple fine-grained privileges work together correctly"""
+
+ catalog_name = fine_grained_authz_test_catalog.name
+
+ # Create setup principal (for table creation)
+ setup_principal_name = f"setup_user_{str(uuid.uuid4())[-10:]}"
+ setup_principal_role_name = f"setup_role_{str(uuid.uuid4())[-10:]}"
+ setup_catalog_role_name = f"setup_cat_role_{str(uuid.uuid4())[-10:]}"
+
+ # Create test principal (for fine-grained testing)
+ test_principal_name = f"test_user_{str(uuid.uuid4())[-10:]}"
+ test_principal_role_name = f"test_role_{str(uuid.uuid4())[-10:]}"
+ test_catalog_role_name = f"test_cat_role_{str(uuid.uuid4())[-10:]}"
+
+ try:
+ # Create setup principal with full privileges
+ setup_principal = create_principal(polaris_url, polaris_catalog_url,
root_client, setup_principal_name)
+ setup_principal_role = create_principal_role(root_client,
setup_principal_role_name)
+ setup_catalog_role = create_catalog_role(root_client,
fine_grained_authz_test_catalog, setup_catalog_role_name)
+
+ root_client.assign_catalog_role_to_principal_role(
+ principal_role_name=setup_principal_role.name,
+ catalog_name=catalog_name,
+
grant_catalog_role_request=GrantCatalogRoleRequest(catalog_role=setup_catalog_role)
+ )
+
+ # Grant setup privileges (including super-privileges)
+ setup_privileges = [
+ CatalogPrivilege.NAMESPACE_FULL_METADATA,
+ CatalogPrivilege.TABLE_CREATE,
+ CatalogPrivilege.TABLE_LIST,
+ CatalogPrivilege.TABLE_READ_PROPERTIES,
+ CatalogPrivilege.TABLE_DROP,
+ CatalogPrivilege.TABLE_WRITE_DATA
+ ]
+
+ for privilege in setup_privileges:
+ root_client.add_grant_to_catalog_role(
+ catalog_name,
+ setup_catalog_role_name,
+ AddGrantRequest(grant=CatalogGrant(
+ catalog_name=catalog_name,
+ type='catalog',
+ privilege=privilege
+ ))
+ )
+
+ root_client.assign_principal_role(
+ setup_principal.principal.name,
+
grant_principal_role_request=GrantPrincipalRoleRequest(principal_role=setup_principal_role)
+ )
+
+ # Create test principal with multiple fine-grained privileges
+ test_principal = create_principal(polaris_url, polaris_catalog_url,
root_client, test_principal_name)
+ test_principal_role = create_principal_role(root_client,
test_principal_role_name)
+ test_catalog_role = create_catalog_role(root_client,
fine_grained_authz_test_catalog, test_catalog_role_name)
+
+ root_client.assign_catalog_role_to_principal_role(
+ principal_role_name=test_principal_role.name,
+ catalog_name=catalog_name,
+
grant_catalog_role_request=GrantCatalogRoleRequest(catalog_role=test_catalog_role)
+ )
+
+ # Grant both SET and REMOVE properties privileges
+ test_privileges = [
+ CatalogPrivilege.TABLE_READ_PROPERTIES,
+ CatalogPrivilege.TABLE_SET_PROPERTIES, # For SET operations
+ CatalogPrivilege.TABLE_REMOVE_PROPERTIES # For UNSET operations
+ ]
+
+ for privilege in test_privileges:
+ root_client.add_grant_to_catalog_role(
+ catalog_name,
+ test_catalog_role_name,
+ AddGrantRequest(grant=CatalogGrant(
+ catalog_name=catalog_name,
+ type='catalog',
+ privilege=privilege
+ ))
+ )
+
+ root_client.assign_principal_role(
+ test_principal.principal.name,
+
grant_principal_role_request=GrantPrincipalRoleRequest(principal_role=test_principal_role)
+ )
+
+ # Create table using the setup principal
+ with IcebergSparkSession(
+
credentials=f'{setup_principal.principal.client_id}:{setup_principal.credentials.client_secret.get_secret_value()}',
+ catalog_name=catalog_name,
+ polaris_url=polaris_catalog_url
+ ) as spark:
+ spark.sql(f'USE {catalog_name}')
+ spark.sql('CREATE NAMESPACE db1')
+ spark.sql('CREATE TABLE db1.test_table (col1 int, col2 string)')
+
+ # Test multiple fine-grained operations using the test principal
+ with IcebergSparkSession(
+
credentials=f'{test_principal.principal.client_id}:{test_principal.credentials.client_secret.get_secret_value()}',
+ catalog_name=catalog_name,
+ polaris_url=polaris_catalog_url,
+ use_vended_credentials=False # Not needed for file storage type
+ ) as spark:
+ spark.sql(f'USE {catalog_name}')
+
+ # Multiple operations in sequence - all should work with both
privileges
+ spark.sql("ALTER TABLE db1.test_table SET TBLPROPERTIES ('prop1' =
'value1', 'prop2' = 'value2')")
+ spark.sql("ALTER TABLE db1.test_table UNSET TBLPROPERTIES
('prop1')")
+ spark.sql("ALTER TABLE db1.test_table SET TBLPROPERTIES ('prop3' =
'value3')")
+ spark.sql("ALTER TABLE db1.test_table UNSET TBLPROPERTIES
('prop2', 'prop3')")
+
+ finally:
+ # Cleanup principals and roles
+ try:
+ root_client.delete_principal(setup_principal_name)
+ root_client.delete_principal_role(setup_principal_role_name)
+ root_client.delete_catalog_role(catalog_name,
setup_catalog_role_name)
+ root_client.delete_principal(test_principal_name)
+ root_client.delete_principal_role(test_principal_role_name)
+ root_client.delete_catalog_role(catalog_name,
test_catalog_role_name)
+ except:
+ pass
diff --git
a/runtime/service/src/main/java/org/apache/polaris/service/catalog/common/CatalogHandler.java
b/runtime/service/src/main/java/org/apache/polaris/service/catalog/common/CatalogHandler.java
index cdee75213..8919aeb2a 100644
---
a/runtime/service/src/main/java/org/apache/polaris/service/catalog/common/CatalogHandler.java
+++
b/runtime/service/src/main/java/org/apache/polaris/service/catalog/common/CatalogHandler.java
@@ -23,6 +23,7 @@ import static
org.apache.polaris.core.entity.PolarisEntitySubType.ICEBERG_TABLE;
import jakarta.enterprise.inject.Instance;
import jakarta.ws.rs.core.SecurityContext;
import java.util.Arrays;
+import java.util.EnumSet;
import java.util.List;
import java.util.Optional;
import org.apache.iceberg.catalog.Namespace;
@@ -238,29 +239,50 @@ public abstract class CatalogHandler {
initializeCatalog();
}
+ /**
+ * Ensures resolution manifest is initialized for a table identifier. This
allows checking
+ * catalog-level feature flags or other resolved entities before
authorization. If already
+ * initialized, this is a no-op.
+ */
+ protected void ensureResolutionManifestForTable(TableIdentifier identifier) {
+ if (resolutionManifest == null) {
+ resolutionManifest = newResolutionManifest();
+
+ // The underlying Catalog is also allowed to fetch "fresh" versions of
the target entity.
+ resolutionManifest.addPassthroughPath(
+ new ResolverPath(
+ PolarisCatalogHelpers.tableIdentifierToList(identifier),
+ PolarisEntityType.TABLE_LIKE,
+ true /* optional */),
+ identifier);
+ resolutionManifest.resolveAll();
+ }
+ }
+
protected void authorizeBasicTableLikeOperationOrThrow(
PolarisAuthorizableOperation op, PolarisEntitySubType subType,
TableIdentifier identifier) {
- resolutionManifest = newResolutionManifest();
+ authorizeBasicTableLikeOperationsOrThrow(EnumSet.of(op), subType,
identifier);
+ }
- // The underlying Catalog is also allowed to fetch "fresh" versions of the
target entity.
- resolutionManifest.addPassthroughPath(
- new ResolverPath(
- PolarisCatalogHelpers.tableIdentifierToList(identifier),
- PolarisEntityType.TABLE_LIKE,
- true /* optional */),
- identifier);
- resolutionManifest.resolveAll();
+ protected void authorizeBasicTableLikeOperationsOrThrow(
+ EnumSet<PolarisAuthorizableOperation> ops,
+ PolarisEntitySubType subType,
+ TableIdentifier identifier) {
+ ensureResolutionManifestForTable(identifier);
PolarisResolvedPathWrapper target =
resolutionManifest.getResolvedPath(identifier,
PolarisEntityType.TABLE_LIKE, subType, true);
if (target == null) {
throwNotFoundExceptionForTableLikeEntity(identifier, List.of(subType));
}
- authorizer.authorizeOrThrow(
- polarisPrincipal,
- resolutionManifest.getAllActivatedCatalogRoleAndPrincipalRoles(),
- op,
- target,
- null /* secondary */);
+
+ for (PolarisAuthorizableOperation op : ops) {
+ authorizer.authorizeOrThrow(
+ polarisPrincipal,
+ resolutionManifest.getAllActivatedCatalogRoleAndPrincipalRoles(),
+ op,
+ target,
+ null /* secondary */);
+ }
initializeCatalog();
}
diff --git
a/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogHandler.java
b/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogHandler.java
index 0154dd3ca..03a5881c8 100644
---
a/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogHandler.java
+++
b/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogHandler.java
@@ -852,9 +852,16 @@ public class IcebergCatalogHandler extends CatalogHandler
implements AutoCloseab
public LoadTableResponse updateTable(
TableIdentifier tableIdentifier, UpdateTableRequest request) {
- PolarisAuthorizableOperation op =
PolarisAuthorizableOperation.UPDATE_TABLE;
- authorizeBasicTableLikeOperationOrThrow(
- op, PolarisEntitySubType.ICEBERG_TABLE, tableIdentifier);
+
+ // Ensure resolution manifest is initialized so we can determine whether
+ // fine grained authz model is enabled at the catalog level
+ ensureResolutionManifestForTable(tableIdentifier);
+
+ EnumSet<PolarisAuthorizableOperation> authorizableOperations =
+ getUpdateTableAuthorizableOperations(request);
+
+ authorizeBasicTableLikeOperationsOrThrow(
+ authorizableOperations, PolarisEntitySubType.ICEBERG_TABLE,
tableIdentifier);
CatalogEntity catalog = getResolvedCatalogEntity();
if (catalog.isStaticFacade()) {
@@ -1122,6 +1129,73 @@ public class IcebergCatalogHandler extends
CatalogHandler implements AutoCloseab
}
}
+ private EnumSet<PolarisAuthorizableOperation>
getUpdateTableAuthorizableOperations(
+ UpdateTableRequest request) {
+ boolean useFineGrainedOperations =
+ realmConfig.getConfig(
+ FeatureConfiguration.ENABLE_FINE_GRAINED_UPDATE_TABLE_PRIVILEGES,
+ getResolvedCatalogEntity());
+
+ if (useFineGrainedOperations) {
+ EnumSet<PolarisAuthorizableOperation> actions =
+ request.updates().stream()
+ .map(
+ update ->
+ switch (update) {
+ case MetadataUpdate.AssignUUID assignUuid ->
+ PolarisAuthorizableOperation.ASSIGN_TABLE_UUID;
+ case MetadataUpdate.UpgradeFormatVersion upgradeFormat
->
+
PolarisAuthorizableOperation.UPGRADE_TABLE_FORMAT_VERSION;
+ case MetadataUpdate.AddSchema addSchema ->
+ PolarisAuthorizableOperation.ADD_TABLE_SCHEMA;
+ case MetadataUpdate.SetCurrentSchema setCurrentSchema
->
+
PolarisAuthorizableOperation.SET_TABLE_CURRENT_SCHEMA;
+ case MetadataUpdate.AddPartitionSpec addPartitionSpec
->
+
PolarisAuthorizableOperation.ADD_TABLE_PARTITION_SPEC;
+ case MetadataUpdate.AddSortOrder addSortOrder ->
+ PolarisAuthorizableOperation.ADD_TABLE_SORT_ORDER;
+ case MetadataUpdate.SetDefaultSortOrder
setDefaultSortOrder ->
+
PolarisAuthorizableOperation.SET_TABLE_DEFAULT_SORT_ORDER;
+ case MetadataUpdate.AddSnapshot addSnapshot ->
+ PolarisAuthorizableOperation.ADD_TABLE_SNAPSHOT;
+ case MetadataUpdate.SetSnapshotRef setSnapshotRef ->
+
PolarisAuthorizableOperation.SET_TABLE_SNAPSHOT_REF;
+ case MetadataUpdate.RemoveSnapshots removeSnapshots ->
+
PolarisAuthorizableOperation.REMOVE_TABLE_SNAPSHOTS;
+ case MetadataUpdate.RemoveSnapshotRef
removeSnapshotRef ->
+
PolarisAuthorizableOperation.REMOVE_TABLE_SNAPSHOT_REF;
+ case MetadataUpdate.SetLocation setLocation ->
+ PolarisAuthorizableOperation.SET_TABLE_LOCATION;
+ case MetadataUpdate.SetProperties setProperties ->
+ PolarisAuthorizableOperation.SET_TABLE_PROPERTIES;
+ case MetadataUpdate.RemoveProperties removeProperties
->
+
PolarisAuthorizableOperation.REMOVE_TABLE_PROPERTIES;
+ case MetadataUpdate.SetStatistics setStatistics ->
+ PolarisAuthorizableOperation.SET_TABLE_STATISTICS;
+ case MetadataUpdate.RemoveStatistics removeStatistics
->
+
PolarisAuthorizableOperation.REMOVE_TABLE_STATISTICS;
+ case MetadataUpdate.RemovePartitionSpecs
removePartitionSpecs ->
+
PolarisAuthorizableOperation.REMOVE_TABLE_PARTITION_SPECS;
+ default ->
+ PolarisAuthorizableOperation
+ .UPDATE_TABLE; // Fallback for unknown update
types
+ })
+ .collect(
+ () -> EnumSet.noneOf(PolarisAuthorizableOperation.class),
+ EnumSet::add,
+ EnumSet::addAll);
+
+ // If there are no MetadataUpdates, then default to the UPDATE_TABLE
operation.
+ if (actions.isEmpty()) {
+ actions.add(PolarisAuthorizableOperation.UPDATE_TABLE);
+ }
+
+ return actions;
+ } else {
+ return EnumSet.of(PolarisAuthorizableOperation.UPDATE_TABLE);
+ }
+ }
+
@Override
public void close() throws Exception {
if (baseCatalog instanceof Closeable closeable) {
diff --git
a/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogHandlerAuthzTest.java
b/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogHandlerAuthzTest.java
index f9827fddf..0a5f063e2 100644
---
a/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogHandlerAuthzTest.java
+++
b/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogHandlerAuthzTest.java
@@ -22,6 +22,7 @@ import com.google.common.collect.ImmutableMap;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.junit.TestProfile;
import jakarta.enterprise.inject.Instance;
+import jakarta.inject.Inject;
import jakarta.ws.rs.core.SecurityContext;
import java.time.Instant;
import java.util.List;
@@ -31,6 +32,7 @@ import java.util.Set;
import java.util.UUID;
import org.apache.iceberg.CatalogProperties;
import org.apache.iceberg.CatalogUtil;
+import org.apache.iceberg.MetadataUpdate;
import org.apache.iceberg.PartitionSpec;
import org.apache.iceberg.SortOrder;
import org.apache.iceberg.TableMetadata;
@@ -57,6 +59,9 @@ import
org.apache.polaris.core.admin.model.PrincipalWithCredentialsCredentials;
import org.apache.polaris.core.admin.model.StorageConfigInfo;
import org.apache.polaris.core.auth.PolarisPrincipal;
import org.apache.polaris.core.catalog.ExternalCatalogFactory;
+import org.apache.polaris.core.config.FeatureConfiguration;
+import org.apache.polaris.core.config.PolarisConfiguration;
+import org.apache.polaris.core.config.RealmConfig;
import org.apache.polaris.core.context.CallContext;
import org.apache.polaris.core.entity.CatalogEntity;
import org.apache.polaris.core.entity.CatalogRoleEntity;
@@ -76,10 +81,26 @@ import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
+/**
+ * Authorization test class for IcebergCatalogHandler. Runs with the default
value for
+ * ENABLE_FINE_GRAINED_UPDATE_TABLE_PRIVILEGES (currently true).
+ *
+ * <p>This class tests:
+ *
+ * <ul>
+ * <li>Standard authorization behavior for all catalog operations
+ * <li>Fine-grained authorization for table metadata update operations
+ * <li>Coarse-grained fallback behavior
+ * <li>Super-privilege behavior (e.g., TABLE_MANAGE_STRUCTURE)
+ * </ul>
+ */
@QuarkusTest
@TestProfile(PolarisAuthzTestBase.Profile.class)
public class IcebergCatalogHandlerAuthzTest extends PolarisAuthzTestBase {
+ @Inject CallContextCatalogFactory callContextCatalogFactory;
+ @Inject Instance<ExternalCatalogFactory> externalCatalogFactories;
+
@SuppressWarnings("unchecked")
private static Instance<ExternalCatalogFactory>
emptyExternalCatalogFactory() {
Instance<ExternalCatalogFactory> mock = Mockito.mock(Instance.class);
@@ -88,7 +109,7 @@ public class IcebergCatalogHandlerAuthzTest extends
PolarisAuthzTestBase {
return mock;
}
- private IcebergCatalogHandler newWrapper() {
+ protected IcebergCatalogHandler newWrapper() {
return newWrapper(Set.of());
}
@@ -116,6 +137,27 @@ public class IcebergCatalogHandlerAuthzTest extends
PolarisAuthzTestBase {
polarisEventListener);
}
+ protected void doTestInsufficientPrivileges(
+ List<PolarisPrivilege> insufficientPrivileges, Runnable action) {
+ doTestInsufficientPrivileges(insufficientPrivileges, PRINCIPAL_NAME,
action);
+ }
+
+ /**
+ * Tests each "insufficient" privilege individually using CATALOG_ROLE1 by
granting at the
+ * CATALOG_NAME level, ensuring the action fails, then revoking after each
test case.
+ */
+ private void doTestInsufficientPrivileges(
+ List<PolarisPrivilege> insufficientPrivileges, String principalName,
Runnable action) {
+ doTestInsufficientPrivileges(
+ insufficientPrivileges,
+ principalName,
+ action,
+ (privilege) ->
+ adminService.grantPrivilegeOnCatalogToRole(CATALOG_NAME,
CATALOG_ROLE1, privilege),
+ (privilege) ->
+ adminService.revokePrivilegeOnCatalogFromRole(CATALOG_NAME,
CATALOG_ROLE1, privilege));
+ }
+
/**
* Tests each "sufficient" privilege individually using CATALOG_ROLE1 by
granting at the
* CATALOG_NAME level, revoking after each test, and also ensuring that the
request fails after
@@ -130,7 +172,7 @@ public class IcebergCatalogHandlerAuthzTest extends
PolarisAuthzTestBase {
* either the cleanup privileges must be latent, or the cleanup action
could be run with
* PRINCIPAL_ROLE2 while runnint {@code action} with PRINCIPAL_ROLE1.
*/
- private void doTestSufficientPrivileges(
+ protected void doTestSufficientPrivileges(
List<PolarisPrivilege> sufficientPrivileges, Runnable action, Runnable
cleanupAction) {
doTestSufficientPrivilegeSets(
sufficientPrivileges.stream().map(Set::of).toList(), action,
cleanupAction, PRINCIPAL_NAME);
@@ -160,7 +202,7 @@ public class IcebergCatalogHandlerAuthzTest extends
PolarisAuthzTestBase {
* @param principalName
* @param catalogName
*/
- private void doTestSufficientPrivilegeSets(
+ protected void doTestSufficientPrivilegeSets(
List<Set<PolarisPrivilege>> sufficientPrivileges,
Runnable action,
Runnable cleanupAction,
@@ -177,27 +219,6 @@ public class IcebergCatalogHandlerAuthzTest extends
PolarisAuthzTestBase {
adminService.revokePrivilegeOnCatalogFromRole(catalogName,
CATALOG_ROLE1, privilege));
}
- private void doTestInsufficientPrivileges(
- List<PolarisPrivilege> insufficientPrivileges, Runnable action) {
- doTestInsufficientPrivileges(insufficientPrivileges, PRINCIPAL_NAME,
action);
- }
-
- /**
- * Tests each "insufficient" privilege individually using CATALOG_ROLE1 by
granting at the
- * CATALOG_NAME level, ensuring the action fails, then revoking after each
test case.
- */
- private void doTestInsufficientPrivileges(
- List<PolarisPrivilege> insufficientPrivileges, String principalName,
Runnable action) {
- doTestInsufficientPrivileges(
- insufficientPrivileges,
- principalName,
- action,
- (privilege) ->
- adminService.grantPrivilegeOnCatalogToRole(CATALOG_NAME,
CATALOG_ROLE1, privilege),
- (privilege) ->
- adminService.revokePrivilegeOnCatalogFromRole(CATALOG_NAME,
CATALOG_ROLE1, privilege));
- }
-
@Test
public void testListNamespacesAllSufficientPrivileges() {
doTestSufficientPrivileges(
@@ -1069,6 +1090,108 @@ public class IcebergCatalogHandlerAuthzTest extends
PolarisAuthzTestBase {
() -> newWrapper().updateTableForStagedCreate(TABLE_NS1A_2, new
UpdateTableRequest()));
}
+ @Test
+ public void testUpdateTableFallbackToCoarseGrainedWhenFeatureDisabled() {
+ // Test that when fine-grained authorization is disabled, it falls back to
+ // TABLE_WRITE_PROPERTIES
+ // This test validates that the feature flag works correctly by testing
the negative case
+ UpdateTableRequest request =
+ UpdateTableRequest.create(
+ TABLE_NS1A_2,
+ List.of(), // no requirements
+ List.of(new
MetadataUpdate.AssignUUID(UUID.randomUUID().toString())));
+
+ // With fine-grained authorization disabled, TABLE_WRITE_PROPERTIES should
work
+ // even for operations that would require specific privileges when enabled
+ doTestSufficientPrivileges(
+ List.of(
+ PolarisPrivilege.TABLE_WRITE_PROPERTIES,
+ PolarisPrivilege.TABLE_WRITE_DATA,
+ PolarisPrivilege.TABLE_FULL_METADATA,
+ PolarisPrivilege.CATALOG_MANAGE_CONTENT),
+ () ->
newWrapperWithFineGrainedAuthzDisabled().updateTable(TABLE_NS1A_2, request),
+ null /* cleanupAction */);
+ }
+
+ /**
+ * Creates a wrapper with fine-grained authorization explicitly disabled for
testing the fallback
+ * behavior to coarse-grained authorization.
+ */
+ private IcebergCatalogHandler newWrapperWithFineGrainedAuthzDisabled() {
+ // Create a custom CallContextCatalogFactory that mocks the configuration
+ CallContextCatalogFactory mockFactory =
Mockito.mock(CallContextCatalogFactory.class);
+
+ // Mock the catalog factory to return our regular catalog but with mocked
config
+ Mockito.when(
+ mockFactory.createCallContextCatalog(
+ Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any()))
+ .thenReturn(baseCatalog);
+
+ return newWrapperWithFineLevelAuthDisabled(Set.of(), CATALOG_NAME,
mockFactory, false);
+ }
+
+ private IcebergCatalogHandler newWrapperWithFineLevelAuthDisabled(
+ Set<String> activatedPrincipalRoles,
+ String catalogName,
+ CallContextCatalogFactory factory,
+ boolean fineGrainedAuthzEnabled) {
+
+ PolarisPrincipal authenticatedPrincipal =
+ PolarisPrincipal.of(principalEntity, activatedPrincipalRoles);
+
+ // Create a custom CallContext that returns a custom RealmConfig
+ CallContext mockCallContext = Mockito.mock(CallContext.class);
+
+ // Create a simple RealmConfig implementation that overrides just what we
need
+ RealmConfig customRealmConfig =
+ new RealmConfig() {
+ @Override
+ public <T> T getConfig(String configName) {
+ return realmConfig.getConfig(configName);
+ }
+
+ @Override
+ public <T> T getConfig(String configName, T defaultValue) {
+ return realmConfig.getConfig(configName, defaultValue);
+ }
+
+ @Override
+ public <T> T getConfig(PolarisConfiguration<T> config) {
+ return realmConfig.getConfig(config);
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public <T> T getConfig(PolarisConfiguration<T> config, CatalogEntity
catalogEntity) {
+ // Override the specific configuration we want to test
+ if
(config.equals(FeatureConfiguration.ENABLE_FINE_GRAINED_UPDATE_TABLE_PRIVILEGES))
{
+ return (T) Boolean.valueOf(fineGrainedAuthzEnabled);
+ }
+ return realmConfig.getConfig(config, catalogEntity);
+ }
+ };
+
+ // Mock the regular CallContext calls
+
Mockito.when(mockCallContext.getRealmConfig()).thenReturn(customRealmConfig);
+ Mockito.when(mockCallContext.getPolarisCallContext())
+ .thenReturn(callContext.getPolarisCallContext());
+
+ return new IcebergCatalogHandler(
+ diagServices,
+ mockCallContext,
+ resolutionManifestFactory,
+ metaStoreManager,
+ userSecretsManager,
+ securityContext(authenticatedPrincipal),
+ factory,
+ catalogName,
+ polarisAuthorizer,
+ reservedProperties,
+ catalogHandlerUtils,
+ emptyExternalCatalogFactory(),
+ polarisEventListener);
+ }
+
@Test
public void testDropTableWithoutPurgeAllSufficientPrivileges() {
assertSuccess(
@@ -1906,4 +2029,207 @@ public class IcebergCatalogHandlerAuthzTest extends
PolarisAuthzTestBase {
newWrapper(Set.of(PRINCIPAL_ROLE1)).sendNotification(table, request);
});
}
+
+ @Test
+ public void testUpdateTableWith_AssignUuid_Privilege() {
+ // Test that TABLE_ASSIGN_UUID privilege is required for AssignUUID
MetadataUpdate
+ UpdateTableRequest request =
+ UpdateTableRequest.create(
+ TABLE_NS1A_2,
+ List.of(), // no requirements
+ List.of(new
MetadataUpdate.AssignUUID(UUID.randomUUID().toString())));
+
+ doTestSufficientPrivileges(
+ List.of(
+ PolarisPrivilege.TABLE_ASSIGN_UUID,
+ PolarisPrivilege.TABLE_WRITE_PROPERTIES, // Should also work with
broader privilege
+ PolarisPrivilege.TABLE_FULL_METADATA,
+ PolarisPrivilege.CATALOG_MANAGE_CONTENT),
+ () -> newWrapper().updateTable(TABLE_NS1A_2, request),
+ null /* cleanupAction */);
+ }
+
+ @Test
+ public void testUpdateTableWith_AssignUuidInsufficientPermissions() {
+ UpdateTableRequest request =
+ UpdateTableRequest.create(
+ TABLE_NS1A_2,
+ List.of(), // no requirements
+ List.of(new
MetadataUpdate.AssignUUID(UUID.randomUUID().toString())));
+
+ doTestInsufficientPrivileges(
+ List.of(
+ PolarisPrivilege.NAMESPACE_FULL_METADATA,
+ PolarisPrivilege.VIEW_FULL_METADATA,
+ PolarisPrivilege.TABLE_READ_PROPERTIES,
+ PolarisPrivilege.TABLE_READ_DATA,
+ PolarisPrivilege.TABLE_CREATE,
+ PolarisPrivilege.TABLE_LIST,
+ PolarisPrivilege.TABLE_DROP,
+ // Test that other fine-grained privileges don't work
+ PolarisPrivilege.TABLE_ADD_SCHEMA,
+ PolarisPrivilege.TABLE_SET_LOCATION),
+ () -> newWrapper().updateTable(TABLE_NS1A_2, request));
+ }
+
+ @Test
+ public void testUpdateTableWith_UpgradeFormatVersionPrivilege() {
+ // Test that TABLE_UPGRADE_FORMAT_VERSION privilege is required for
UpgradeFormatVersion
+ // MetadataUpdate
+ UpdateTableRequest request =
+ UpdateTableRequest.create(
+ TABLE_NS1A_2,
+ List.of(), // no requirements
+ List.of(new MetadataUpdate.UpgradeFormatVersion(2)));
+
+ doTestSufficientPrivileges(
+ List.of(
+ PolarisPrivilege.TABLE_UPGRADE_FORMAT_VERSION,
+ PolarisPrivilege.TABLE_WRITE_PROPERTIES, // Should also work with
broader privilege
+ PolarisPrivilege.TABLE_FULL_METADATA,
+ PolarisPrivilege.CATALOG_MANAGE_CONTENT),
+ () -> newWrapper().updateTable(TABLE_NS1A_2, request),
+ null /* cleanupAction */);
+ }
+
+ @Test
+ public void testUpdateTableWith_SetPropertiesPrivilege() {
+ // Test that TABLE_SET_PROPERTIES privilege is required for SetProperties
MetadataUpdate
+ UpdateTableRequest request =
+ UpdateTableRequest.create(
+ TABLE_NS1A_2,
+ List.of(), // no requirements
+ List.of(new MetadataUpdate.SetProperties(Map.of("test.property",
"test.value"))));
+
+ doTestSufficientPrivileges(
+ List.of(
+ PolarisPrivilege.TABLE_SET_PROPERTIES,
+ PolarisPrivilege.TABLE_WRITE_PROPERTIES, // Should also work with
broader privilege
+ PolarisPrivilege.TABLE_FULL_METADATA,
+ PolarisPrivilege.CATALOG_MANAGE_CONTENT),
+ () -> newWrapper().updateTable(TABLE_NS1A_2, request),
+ null /* cleanupAction */);
+ }
+
+ @Test
+ public void testUpdateTableWith_RemoveProperties_Privilege() {
+ // Test that TABLE_REMOVE_PROPERTIES privilege is required for
RemoveProperties MetadataUpdate
+ UpdateTableRequest request =
+ UpdateTableRequest.create(
+ TABLE_NS1A_2,
+ List.of(), // no requirements
+ List.of(new
MetadataUpdate.RemoveProperties(Set.of("property.to.remove"))));
+
+ doTestSufficientPrivileges(
+ List.of(
+ PolarisPrivilege.TABLE_REMOVE_PROPERTIES,
+ PolarisPrivilege.TABLE_WRITE_PROPERTIES, // Should also work with
broader privilege
+ PolarisPrivilege.TABLE_FULL_METADATA,
+ PolarisPrivilege.CATALOG_MANAGE_CONTENT),
+ () -> newWrapper().updateTable(TABLE_NS1A_2, request),
+ null /* cleanupAction */);
+ }
+
+ @Test
+ public void testUpdateTableWith_MultipleUpdates_Privilege() {
+ // Test that multiple MetadataUpdate types require multiple specific
privileges
+ UpdateTableRequest request =
+ UpdateTableRequest.create(
+ TABLE_NS1A_2,
+ List.of(), // no requirements
+ List.of(
+ new MetadataUpdate.UpgradeFormatVersion(2),
+ new MetadataUpdate.SetProperties(Map.of("test.prop",
"test.val"))));
+
+ // Test that having both specific privileges works
+ doTestSufficientPrivilegeSets(
+ List.of(
+ Set.of(
+ PolarisPrivilege.TABLE_UPGRADE_FORMAT_VERSION,
+ PolarisPrivilege.TABLE_SET_PROPERTIES),
+ Set.of(PolarisPrivilege.TABLE_WRITE_PROPERTIES), // Broader
privilege should work
+ Set.of(PolarisPrivilege.TABLE_FULL_METADATA),
+ Set.of(PolarisPrivilege.CATALOG_MANAGE_CONTENT)),
+ () -> newWrapper().updateTable(TABLE_NS1A_2, request),
+ null /* cleanupAction */,
+ PRINCIPAL_NAME,
+ CATALOG_NAME);
+ }
+
+ @Test
+ public void testUpdateTableWith_MultipleUpdatesInsufficientPermissions() {
+ // Test that having only one of the required privileges fails
+ UpdateTableRequest request =
+ UpdateTableRequest.create(
+ TABLE_NS1A_2,
+ List.of(), // no requirements
+ List.of(
+ new MetadataUpdate.UpgradeFormatVersion(2),
+ new MetadataUpdate.SetProperties(Map.of("test.prop",
"test.val"))));
+
+ // Test that having only one specific privilege fails (need both)
+ doTestInsufficientPrivileges(
+ List.of(
+ PolarisPrivilege.TABLE_UPGRADE_FORMAT_VERSION, // Only one of the
two needed
+ PolarisPrivilege.TABLE_SET_PROPERTIES, // Only one of the two
needed
+ PolarisPrivilege.TABLE_ASSIGN_UUID, // Wrong privilege
+ PolarisPrivilege.TABLE_READ_PROPERTIES,
+ PolarisPrivilege.TABLE_CREATE),
+ () -> newWrapper().updateTable(TABLE_NS1A_2, request));
+ }
+
+ @Test
+ public void testUpdateTableWith_TableManageStructureSuperPrivilege() {
+ // Test that TABLE_MANAGE_STRUCTURE works as a super privilege for
structural operations
+ // (but NOT for snapshot operations like TABLE_ADD_SNAPSHOT)
+
+ // Test structural operations that should work with TABLE_MANAGE_STRUCTURE
+ UpdateTableRequest structuralRequest =
+ UpdateTableRequest.create(
+ TABLE_NS1A_2,
+ List.of(), // no requirements
+ List.of(
+ new MetadataUpdate.AssignUUID(UUID.randomUUID().toString()),
+ new MetadataUpdate.UpgradeFormatVersion(2),
+ new MetadataUpdate.SetProperties(Map.of("test.property",
"test.value")),
+ new
MetadataUpdate.RemoveProperties(Set.of("property.to.remove"))));
+
+ doTestSufficientPrivileges(
+ List.of(
+ PolarisPrivilege.TABLE_MANAGE_STRUCTURE, // Should work for all
structural operations
+ PolarisPrivilege.TABLE_WRITE_PROPERTIES, // Should also work with
broader privilege
+ PolarisPrivilege.TABLE_FULL_METADATA,
+ PolarisPrivilege.CATALOG_MANAGE_CONTENT),
+ () -> newWrapper().updateTable(TABLE_NS1A_2, structuralRequest),
+ null /* cleanupAction */);
+ }
+
+ @Test
+ public void
testUpdateTableWith_TableManageStructureDoesNotIncludeSnapshots() {
+ // Verify that TABLE_MANAGE_STRUCTURE does NOT grant access to snapshot
operations
+ // This test verifies that TABLE_ADD_SNAPSHOT and TABLE_SET_SNAPSHOT_REF
were correctly
+ // excluded from the TABLE_MANAGE_STRUCTURE super privilege mapping
+
+ // Test that TABLE_MANAGE_STRUCTURE works for non-snapshot structural
operations
+ UpdateTableRequest nonSnapshotRequest =
+ UpdateTableRequest.create(
+ TABLE_NS1A_2,
+ List.of(), // no requirements
+ List.of(
+ new MetadataUpdate.AssignUUID(UUID.randomUUID().toString()),
+ new MetadataUpdate.SetProperties(Map.of("structure.test",
"value"))));
+
+ doTestSufficientPrivileges(
+ List.of(PolarisPrivilege.TABLE_MANAGE_STRUCTURE),
+ () -> newWrapper().updateTable(TABLE_NS1A_2, nonSnapshotRequest),
+ null /* cleanupAction */);
+
+ // Test that TABLE_MANAGE_STRUCTURE is insufficient for operations that
require
+ // different privilege categories (like read operations)
+ doTestInsufficientPrivileges(
+ List.of(PolarisPrivilege.TABLE_MANAGE_STRUCTURE),
+ () ->
+ newWrapper()
+ .loadTable(TABLE_NS1A_2, "all")); // Load table requires
different privileges
+ }
}
diff --git
a/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogHandlerFineGrainedDisabledTest.java
b/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogHandlerFineGrainedDisabledTest.java
new file mode 100644
index 000000000..0e9719794
--- /dev/null
+++
b/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogHandlerFineGrainedDisabledTest.java
@@ -0,0 +1,121 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.polaris.service.catalog.iceberg;
+
+import com.google.common.collect.ImmutableMap;
+import io.quarkus.test.junit.QuarkusTest;
+import io.quarkus.test.junit.TestProfile;
+import jakarta.enterprise.inject.Instance;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+import org.apache.iceberg.MetadataUpdate;
+import org.apache.iceberg.rest.requests.UpdateTableRequest;
+import org.apache.polaris.core.auth.PolarisPrincipal;
+import org.apache.polaris.core.entity.PolarisPrivilege;
+import org.apache.polaris.service.admin.PolarisAuthzTestBase;
+import org.apache.polaris.service.context.catalog.CallContextCatalogFactory;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+/**
+ * Test class specifically for testing fine-grained authorization when the
feature is DISABLED. This
+ * ensures that fine-grained privileges are properly ignored when the feature
flag is off.
+ */
+@QuarkusTest
+@TestProfile(IcebergCatalogHandlerFineGrainedDisabledTest.Profile.class)
+public class IcebergCatalogHandlerFineGrainedDisabledTest extends
PolarisAuthzTestBase {
+
+ @jakarta.inject.Inject CallContextCatalogFactory callContextCatalogFactory;
+
+ @SuppressWarnings("unchecked")
+ private static
Instance<org.apache.polaris.core.catalog.ExternalCatalogFactory>
+ emptyExternalCatalogFactory() {
+ Instance<org.apache.polaris.core.catalog.ExternalCatalogFactory> mock =
+ Mockito.mock(Instance.class);
+ Mockito.when(mock.select(Mockito.any())).thenReturn(mock);
+ Mockito.when(mock.isUnsatisfied()).thenReturn(true);
+ return mock;
+ }
+
+ private IcebergCatalogHandler newWrapper() {
+ PolarisPrincipal authenticatedPrincipal =
PolarisPrincipal.of(principalEntity, Set.of());
+ return new IcebergCatalogHandler(
+ diagServices,
+ callContext,
+ resolutionManifestFactory,
+ metaStoreManager,
+ userSecretsManager,
+ securityContext(authenticatedPrincipal),
+ callContextCatalogFactory,
+ CATALOG_NAME,
+ polarisAuthorizer,
+ reservedProperties,
+ catalogHandlerUtils,
+ emptyExternalCatalogFactory(),
+ polarisEventListener);
+ }
+
+ public static class Profile extends PolarisAuthzTestBase.Profile {
+ @Override
+ public Map<String, String> getConfigOverrides() {
+ return ImmutableMap.<String, String>builder()
+ .putAll(super.getConfigOverrides())
+
.put("polaris.features.\"ENABLE_FINE_GRAINED_UPDATE_TABLE_PRIVILEGES\"",
"false")
+ .build();
+ }
+ }
+
+ @Test
+ public void testUpdateTableFineGrainedPrivilegesIgnoredWhenFeatureDisabled()
{
+ // Test that when fine-grained authorization is disabled, fine-grained
privileges alone are
+ // insufficient
+ // This ensures the feature flag properly controls behavior and
fine-grained privileges don't
+ // "leak through"
+ UpdateTableRequest request =
+ UpdateTableRequest.create(
+ TABLE_NS1A_2,
+ List.of(), // no requirements
+ List.of(new
MetadataUpdate.AssignUUID(UUID.randomUUID().toString())));
+
+ // With fine-grained authorization disabled, even having the specific
fine-grained privilege
+ // should be insufficient - the system should require the broader
privileges
+ doTestInsufficientPrivileges(
+ List.of(
+ PolarisPrivilege
+ .TABLE_ASSIGN_UUID, // This alone should be insufficient when
feature disabled
+ PolarisPrivilege.TABLE_UPGRADE_FORMAT_VERSION,
+ PolarisPrivilege.TABLE_SET_PROPERTIES,
+ PolarisPrivilege.TABLE_REMOVE_PROPERTIES,
+ PolarisPrivilege.TABLE_ADD_SCHEMA,
+ PolarisPrivilege.TABLE_SET_LOCATION,
+ PolarisPrivilege.TABLE_READ_PROPERTIES,
+ PolarisPrivilege.TABLE_READ_DATA,
+ PolarisPrivilege.TABLE_CREATE,
+ PolarisPrivilege.TABLE_LIST,
+ PolarisPrivilege.TABLE_DROP),
+ PRINCIPAL_NAME,
+ () -> newWrapper().updateTable(TABLE_NS1A_2, request),
+ (privilege) ->
+ adminService.grantPrivilegeOnCatalogToRole(CATALOG_NAME,
CATALOG_ROLE1, privilege),
+ (privilege) ->
+ adminService.revokePrivilegeOnCatalogFromRole(CATALOG_NAME,
CATALOG_ROLE1, privilege));
+ }
+}
diff --git a/spec/polaris-management-service.yml
b/spec/polaris-management-service.yml
index 84546bbf4..59baaf99d 100644
--- a/spec/polaris-management-service.yml
+++ b/spec/polaris-management-service.yml
@@ -1468,6 +1468,24 @@ components:
- TABLE_FULL_METADATA
- TABLE_ATTACH_POLICY
- TABLE_DETACH_POLICY
+ - TABLE_ASSIGN_UUID
+ - TABLE_UPGRADE_FORMAT_VERSION
+ - TABLE_ADD_SCHEMA
+ - TABLE_SET_CURRENT_SCHEMA
+ - TABLE_ADD_PARTITION_SPEC
+ - TABLE_ADD_SORT_ORDER
+ - TABLE_SET_DEFAULT_SORT_ORDER
+ - TABLE_ADD_SNAPSHOT
+ - TABLE_SET_SNAPSHOT_REF
+ - TABLE_REMOVE_SNAPSHOTS
+ - TABLE_REMOVE_SNAPSHOT_REF
+ - TABLE_SET_LOCATION
+ - TABLE_SET_PROPERTIES
+ - TABLE_REMOVE_PROPERTIES
+ - TABLE_SET_STATISTICS
+ - TABLE_REMOVE_STATISTICS
+ - TABLE_REMOVE_PARTITION_SPECS
+ - TABLE_MANAGE_STRUCTURE
PolicyPrivilege:
type: string
@@ -1515,6 +1533,24 @@ components:
- POLICY_FULL_METADATA
- NAMESPACE_ATTACH_POLICY
- NAMESPACE_DETACH_POLICY
+ - TABLE_ASSIGN_UUID
+ - TABLE_UPGRADE_FORMAT_VERSION
+ - TABLE_ADD_SCHEMA
+ - TABLE_SET_CURRENT_SCHEMA
+ - TABLE_ADD_PARTITION_SPEC
+ - TABLE_ADD_SORT_ORDER
+ - TABLE_SET_DEFAULT_SORT_ORDER
+ - TABLE_ADD_SNAPSHOT
+ - TABLE_SET_SNAPSHOT_REF
+ - TABLE_REMOVE_SNAPSHOTS
+ - TABLE_REMOVE_SNAPSHOT_REF
+ - TABLE_SET_LOCATION
+ - TABLE_SET_PROPERTIES
+ - TABLE_REMOVE_PROPERTIES
+ - TABLE_SET_STATISTICS
+ - TABLE_REMOVE_STATISTICS
+ - TABLE_REMOVE_PARTITION_SPECS
+ - TABLE_MANAGE_STRUCTURE
CatalogPrivilege:
type: string
@@ -1552,6 +1588,24 @@ components:
- POLICY_FULL_METADATA
- CATALOG_ATTACH_POLICY
- CATALOG_DETACH_POLICY
+ - TABLE_ASSIGN_UUID
+ - TABLE_UPGRADE_FORMAT_VERSION
+ - TABLE_ADD_SCHEMA
+ - TABLE_SET_CURRENT_SCHEMA
+ - TABLE_ADD_PARTITION_SPEC
+ - TABLE_ADD_SORT_ORDER
+ - TABLE_SET_DEFAULT_SORT_ORDER
+ - TABLE_ADD_SNAPSHOT
+ - TABLE_SET_SNAPSHOT_REF
+ - TABLE_REMOVE_SNAPSHOTS
+ - TABLE_REMOVE_SNAPSHOT_REF
+ - TABLE_SET_LOCATION
+ - TABLE_SET_PROPERTIES
+ - TABLE_REMOVE_PROPERTIES
+ - TABLE_SET_STATISTICS
+ - TABLE_REMOVE_STATISTICS
+ - TABLE_REMOVE_PARTITION_SPECS
+ - TABLE_MANAGE_STRUCTURE
AddGrantRequest:
type: object