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


Reply via email to