You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@polaris.apache.org by ad...@apache.org on 2026/06/05 11:41:31 UTC

(polaris) branch main updated: Support access delegation in registerTable (#3734)

This is an automated email from the ASF dual-hosted git repository.

adutra 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 4c6bffbc19 Support access delegation in registerTable (#3734)
4c6bffbc19 is described below

commit 4c6bffbc19afc70d5dd6e3940d949a06c2c5b140
Author: Alexandre Dutra <ad...@apache.org>
AuthorDate: Fri Jun 5 13:41:26 2026 +0200

    Support access delegation in registerTable (#3734)
    
    This PR introduces support for access delegation in the `registerTable` endpoint.
    
    Four new authorizable operations are introduced:
    
    - `REGISTER_TABLE_WITH_READ_DELEGATION`: requires `TABLE_CREATE` + `TABLE_READ_DATA`
    - `REGISTER_TABLE_WITH_WRITE_DELEGATION`: requires `TABLE_CREATE` + `TABLE_WRITE_DATA`
    - `REGISTER_TABLE_OVERWRITE_WITH_READ_DELEGATION`: requires `TABLE_FULL_METADATA`+ `TABLE_READ_DATA`
    - `REGISTER_TABLE_OVERWRITE_WITH_WRITE_DELEGATION`: requires `TABLE_FULL_METADATA` + `TABLE_WRITE_DATA`
    
    The implementation is inspired by the `loadTable` endpoint; the authorization checks are very similar.
---
 .../ranger/RangerPolarisOperationSemantics.java    |  16 +
 .../authz_tests/tests_authz_namespace.json         |   8 +
 .../resources/authz_tests/tests_authz_table.json   |   8 +
 .../impl/src/test/resources/logback-test.xml       |  32 ++
 .../apache/polaris/service/it/env/CatalogApi.java  |  60 ++++
 .../it/test/PolarisRestCatalogIntegrationBase.java | 339 +++++++++++++++-----
 .../core/auth/PolarisAuthorizableOperation.java    |   4 +
 .../polaris/core/auth/RbacOperationSemantics.java  |  12 +
 .../service/it/PolarisRestCatalogMinIOIT.java      |  12 -
 .../service/catalog/common/CatalogHandler.java     |  13 +-
 .../catalog/iceberg/IcebergCatalogAdapter.java     |  12 +-
 .../catalog/iceberg/IcebergCatalogHandler.java     | 113 +++++--
 .../AbstractIcebergCatalogHandlerAuthzTest.java    | 345 +++++++++++++++++----
 13 files changed, 787 insertions(+), 187 deletions(-)

diff --git a/extensions/auth/ranger/impl/src/main/java/org/apache/polaris/extension/auth/ranger/RangerPolarisOperationSemantics.java b/extensions/auth/ranger/impl/src/main/java/org/apache/polaris/extension/auth/ranger/RangerPolarisOperationSemantics.java
index 30362bf5cd..f8436d9024 100644
--- a/extensions/auth/ranger/impl/src/main/java/org/apache/polaris/extension/auth/ranger/RangerPolarisOperationSemantics.java
+++ b/extensions/auth/ranger/impl/src/main/java/org/apache/polaris/extension/auth/ranger/RangerPolarisOperationSemantics.java
@@ -170,6 +170,22 @@ public record RangerPolarisOperationSemantics(
         PolarisAuthorizableOperation.REGISTER_TABLE_OVERWRITE,
         new RangerPolarisOperationSemantics(
             toSet(TABLE_METADATA_FULL), null, ResolvedPathRooting.ROOT));
+    RBAC_SEMANTICS_BY_OPERATION.put(
+        PolarisAuthorizableOperation.REGISTER_TABLE_WITH_READ_DELEGATION,
+        new RangerPolarisOperationSemantics(
+            toSet(TABLE_CREATE, TABLE_READ_DATA), null, ResolvedPathRooting.ROOT));
+    RBAC_SEMANTICS_BY_OPERATION.put(
+        PolarisAuthorizableOperation.REGISTER_TABLE_WITH_WRITE_DELEGATION,
+        new RangerPolarisOperationSemantics(
+            toSet(TABLE_CREATE, TABLE_WRITE_DATA), null, ResolvedPathRooting.ROOT));
+    RBAC_SEMANTICS_BY_OPERATION.put(
+        PolarisAuthorizableOperation.REGISTER_TABLE_OVERWRITE_WITH_READ_DELEGATION,
+        new RangerPolarisOperationSemantics(
+            toSet(TABLE_METADATA_FULL, TABLE_READ_DATA), null, ResolvedPathRooting.ROOT));
+    RBAC_SEMANTICS_BY_OPERATION.put(
+        PolarisAuthorizableOperation.REGISTER_TABLE_OVERWRITE_WITH_WRITE_DELEGATION,
+        new RangerPolarisOperationSemantics(
+            toSet(TABLE_METADATA_FULL, TABLE_WRITE_DATA), null, ResolvedPathRooting.ROOT));
     RBAC_SEMANTICS_BY_OPERATION.put(
         PolarisAuthorizableOperation.LOAD_TABLE,
         new RangerPolarisOperationSemantics(
diff --git a/extensions/auth/ranger/impl/src/test/resources/authz_tests/tests_authz_namespace.json b/extensions/auth/ranger/impl/src/test/resources/authz_tests/tests_authz_namespace.json
index 7ceba2389c..ea21e43e67 100644
--- a/extensions/auth/ranger/impl/src/test/resources/authz_tests/tests_authz_namespace.json
+++ b/extensions/auth/ranger/impl/src/test/resources/authz_tests/tests_authz_namespace.json
@@ -44,6 +44,14 @@
       "request": { "authzOp": "REGISTER_TABLE", "principal": { "name": "admin1" }, "target": "NAMESPACE:POLARIS/catalog1/namespace1" },
       "result":  { "isAllowed": true }
     },
+    {
+      "request": { "authzOp": "REGISTER_TABLE_WITH_READ_DELEGATION", "principal": { "name": "admin1" }, "target": "NAMESPACE:POLARIS/catalog1/namespace1" },
+      "result":  { "isAllowed": true }
+    },
+    {
+      "request": { "authzOp": "REGISTER_TABLE_WITH_WRITE_DELEGATION", "principal": { "name": "admin1" }, "target": "NAMESPACE:POLARIS/catalog1/namespace1" },
+      "result":  { "isAllowed": true }
+    },
     {
       "request": { "authzOp": "UPDATE_TABLE_FOR_STAGED_CREATE", "principal": { "name": "admin1" }, "target": "NAMESPACE:POLARIS/catalog1/namespace1" },
       "result":  { "isAllowed": true }
diff --git a/extensions/auth/ranger/impl/src/test/resources/authz_tests/tests_authz_table.json b/extensions/auth/ranger/impl/src/test/resources/authz_tests/tests_authz_table.json
index cf6e353236..90ebaa2b0b 100644
--- a/extensions/auth/ranger/impl/src/test/resources/authz_tests/tests_authz_table.json
+++ b/extensions/auth/ranger/impl/src/test/resources/authz_tests/tests_authz_table.json
@@ -120,6 +120,14 @@
       "request": { "authzOp": "REGISTER_TABLE_OVERWRITE", "principal": { "name": "admin1" }, "target": "TABLE_LIKE:POLARIS/catalog1/namespace1/table1" },
       "result":  { "isAllowed": true }
     },
+    {
+      "request": { "authzOp": "REGISTER_TABLE_OVERWRITE_WITH_READ_DELEGATION", "principal": { "name": "admin1" }, "target": "TABLE_LIKE:POLARIS/catalog1/namespace1/table1" },
+      "result":  { "isAllowed": true }
+    },
+    {
+      "request": { "authzOp": "REGISTER_TABLE_OVERWRITE_WITH_WRITE_DELEGATION", "principal": { "name": "admin1" }, "target": "TABLE_LIKE:POLARIS/catalog1/namespace1/table1" },
+      "result":  { "isAllowed": true }
+    },
     {
       "request": { "authzOp": "DROP_VIEW", "principal": { "name": "admin1" }, "target": "TABLE_LIKE:POLARIS/catalog1/namespace1/view1" },
       "result":  { "isAllowed": true }
diff --git a/extensions/auth/ranger/impl/src/test/resources/logback-test.xml b/extensions/auth/ranger/impl/src/test/resources/logback-test.xml
new file mode 100644
index 0000000000..b7e97bb826
--- /dev/null
+++ b/extensions/auth/ranger/impl/src/test/resources/logback-test.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!--
+
+ 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.
+
+-->
+<configuration debug="false">
+  <contextListener class="ch.qos.logback.classic.jul.LevelChangePropagator"/>
+  <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
+    <encoder>
+      <pattern>%date{ISO8601} [%thread] %-5level %logger{36} - %msg%n</pattern>
+    </encoder>
+  </appender>
+  <root level="${test.log.level:-ERROR}">
+    <appender-ref ref="console"/>
+  </root>
+</configuration>
diff --git a/integration-tests/src/main/java/org/apache/polaris/service/it/env/CatalogApi.java b/integration-tests/src/main/java/org/apache/polaris/service/it/env/CatalogApi.java
index 6546798be6..d589e0d889 100644
--- a/integration-tests/src/main/java/org/apache/polaris/service/it/env/CatalogApi.java
+++ b/integration-tests/src/main/java/org/apache/polaris/service/it/env/CatalogApi.java
@@ -178,6 +178,66 @@ public class CatalogApi extends PolarisRestApi {
     }
   }
 
+  public LoadTableResponse registerTable(
+      String catalog,
+      Namespace namespace,
+      String tableName,
+      String metadataLocation,
+      boolean overwrite) {
+    return registerTable(catalog, namespace, tableName, metadataLocation, overwrite, Map.of());
+  }
+
+  public LoadTableResponse registerTableWithAccessDelegation(
+      String catalog,
+      Namespace namespace,
+      String tableName,
+      String metadataLocation,
+      boolean overwrite) {
+    return registerTable(
+        catalog,
+        namespace,
+        tableName,
+        metadataLocation,
+        overwrite,
+        Map.of("X-Iceberg-Access-Delegation", "vended-credentials"));
+  }
+
+  public LoadTableResponse registerTable(
+      String catalog,
+      Namespace namespace,
+      String tableName,
+      String metadataLocation,
+      boolean overwrite,
+      Map<String, String> headers) {
+    HashMap<String, String> allHeaders = new HashMap<>(defaultHeaders());
+    allHeaders.putAll(headers);
+
+    String ns = NamespaceUtils.joinNamespace(namespace, NamespaceUtils.DEFAULT_NAMESPACE_SEPARATOR);
+    try (Response res =
+        request(
+                "v1/{cat}/namespaces/{ns}/register",
+                Map.of("cat", catalog, "ns", ns),
+                Map.of(),
+                allHeaders)
+            .post(
+                Entity.json(
+                    Map.of(
+                        "name",
+                        tableName,
+                        "metadata-location",
+                        metadataLocation,
+                        "overwrite",
+                        overwrite)))) {
+      if (res.getStatus() == Response.Status.OK.getStatusCode()) {
+        return res.readEntity(LoadTableResponse.class);
+      }
+      throw new RESTException(
+          "Unhandled error: %s",
+          ((ErrorHandler) ErrorHandlers.defaultErrorHandler())
+              .parseResponse(res.getStatus(), res.readEntity(String.class)));
+    }
+  }
+
   public LoadTableResponse loadTable(String catalog, TableIdentifier id, String snapshots) {
     return loadTable(catalog, id, snapshots, Map.of());
   }
diff --git a/integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisRestCatalogIntegrationBase.java b/integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisRestCatalogIntegrationBase.java
index 6895d087c2..ee99b07684 100644
--- a/integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisRestCatalogIntegrationBase.java
+++ b/integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisRestCatalogIntegrationBase.java
@@ -60,6 +60,7 @@ import org.apache.iceberg.catalog.CatalogTests;
 import org.apache.iceberg.catalog.Namespace;
 import org.apache.iceberg.catalog.TableCommit;
 import org.apache.iceberg.catalog.TableIdentifier;
+import org.apache.iceberg.exceptions.BadRequestException;
 import org.apache.iceberg.exceptions.CommitFailedException;
 import org.apache.iceberg.exceptions.ForbiddenException;
 import org.apache.iceberg.exceptions.RESTException;
@@ -746,7 +747,9 @@ public abstract class PolarisRestCatalogIntegrationBase extends CatalogTests<RES
       resolvingFileIO.setConf(new Configuration());
       String fileLocation = externalCatalogBaseLocation + "/ns1/my_table/metadata/v1.metadata.json";
       TableMetadataParser.write(tableMetadata, resolvingFileIO.newOutputFile(fileLocation));
-      restCatalog.registerTable(TableIdentifier.of(ns1, "my_table"), fileLocation);
+      // Use the REST API directly to bypass requesting vended credentials, otherwise the
+      // register-table operation would fail like the load-table one below.
+      catalogApi.registerTable(currentCatalogName, ns1, "my_table", fileLocation, false);
       try {
         Assertions.assertThatThrownBy(
                 () -> restCatalog.loadTable(TableIdentifier.of(ns1, "my_table")))
@@ -941,13 +944,15 @@ public abstract class PolarisRestCatalogIntegrationBase extends CatalogTests<RES
    * existing table metadata. Verifies that the overwrite is applied and the catalog reflects the
    * updated metadata.
    */
+  @RestCatalogConfig({"header.X-Iceberg-Access-Delegation", ""})
   @Test
-  public void testRegisterTableOverwriteViaRest() {
+  public void testRegisterTableOverwrite() {
     Namespace ns = Namespace.of("ns_overwrite_rest");
     restCatalog.createNamespace(ns);
 
     // Create a source table and capture its metadata location
-    Table source = restCatalog.buildTable(TableIdentifier.of(ns, "source_table"), SCHEMA).create();
+    TableIdentifier ident = TableIdentifier.of(ns, "source_table");
+    Table source = restCatalog.buildTable(ident, SCHEMA).create();
     String currentMetadataLocation =
         ((BaseTable) source).operations().current().metadataFileLocation();
     String metadataDir =
@@ -963,31 +968,18 @@ public abstract class PolarisRestCatalogIntegrationBase extends CatalogTests<RES
           ((BaseTable) source).operations().current(),
           resolvingFileIO.newOutputFile(newMetadataLocation));
 
-      // Invoke REST register with overwrite=true
-      Invocation registerInvocation =
-          catalogApi
-              .request("v1/" + currentCatalogName + "/namespaces/ns_overwrite_rest/register")
-              .buildPost(
-                  Entity.json(
-                      Map.of(
-                          "name",
-                          "source_table",
-                          "metadata-location",
-                          newMetadataLocation,
-                          "overwrite",
-                          true)));
+      try {
+        // register with overwrite=true
+        restCatalog.registerTable(ident, newMetadataLocation, true);
 
-      try (Response registerResponse = registerInvocation.invoke()) {
-        assertThat(registerResponse.getStatus()).isEqualTo(Response.Status.OK.getStatusCode());
+        // Reload the table via REST client and verify metadata-location updated
+        Table loaded = restCatalog.loadTable(ident);
+        assertThat(((BaseTable) loaded).operations().current().metadataFileLocation())
+            .isEqualTo(newMetadataLocation);
+      } finally {
+        // Clean up the old metadata file
+        resolvingFileIO.deleteFile(currentMetadataLocation);
       }
-
-      // Reload the table via REST client and verify metadata-location updated
-      Table loaded = restCatalog.loadTable(TableIdentifier.of(ns, "source_table"));
-      assertThat(((BaseTable) loaded).operations().current().metadataFileLocation())
-          .isEqualTo(newMetadataLocation);
-
-      // Clean up the old metadata file
-      resolvingFileIO.deleteFile(currentMetadataLocation);
     }
   }
 
@@ -997,6 +989,7 @@ public abstract class PolarisRestCatalogIntegrationBase extends CatalogTests<RES
    * must receive a 400 Bad Request.
    */
   @CatalogConfig(Catalog.TypeEnum.EXTERNAL)
+  @RestCatalogConfig({"header.X-Iceberg-Access-Delegation", ""})
   @Test
   public void testRegisterTableOverwriteRejectedForExternalCatalog() {
     Namespace ns = Namespace.of("ns_overwrite_ext");
@@ -1014,28 +1007,234 @@ public abstract class PolarisRestCatalogIntegrationBase extends CatalogTests<RES
 
       String fileLocation =
           externalCatalogBaseLocation + "/ns_overwrite_ext/my_table/metadata/v1.metadata.json";
+      try {
+        TableMetadataParser.write(tableMetadata, resolvingFileIO.newOutputFile(fileLocation));
+
+        // Register the table without overwrite first (must succeed)
+        restCatalog.registerTable(TableIdentifier.of(ns, "my_table"), fileLocation);
+
+        // Now attempt to register again with overwrite=true — must be rejected with 400
+        assertThatThrownBy(
+                () ->
+                    restCatalog.registerTable(
+                        TableIdentifier.of(ns, "my_table"), fileLocation, true))
+            .isInstanceOf(BadRequestException.class)
+            .hasMessageContaining(
+                "Register table overwrite is only supported for internal Polaris catalogs");
+      } finally {
+        resolvingFileIO.deleteFile(fileLocation);
+      }
+    }
+  }
+
+  /**
+   * Create an INTERNAL catalog. Register a table WITH access delegation and verify that the
+   * registerTable response contains vended credentials (when supported by the storage type).
+   */
+  @RestCatalogConfig({"header.X-Iceberg-Access-Delegation", "vended-credentials"})
+  @Test
+  public void testRegisterTableWithAccessDelegation() {
+    Namespace ns1 = Namespace.of("ns1");
+    restCatalog.createNamespace(ns1);
+    TableMetadata tableMetadata =
+        TableMetadata.newTableMetadata(
+            new Schema(List.of(Types.NestedField.required(1, "col1", new Types.StringType()))),
+            PartitionSpec.unpartitioned(),
+            catalogBaseLocation + "/ns1/my_table",
+            Map.of());
+    try (ResolvingFileIO resolvingFileIO = new ResolvingFileIO()) {
+      initializeClientFileIO(resolvingFileIO);
+      resolvingFileIO.setConf(new Configuration());
+      String fileLocation = catalogBaseLocation + "/ns1/my_table/metadata/v1.metadata.json";
       TableMetadataParser.write(tableMetadata, resolvingFileIO.newOutputFile(fileLocation));
+      try {
+        LoadTableResponse registerResponse =
+            catalogApi.registerTableWithAccessDelegation(
+                currentCatalogName, ns1, "my_table", fileLocation, false);
+
+        assertThat(registerResponse).isNotNull();
+        assertThat(registerResponse.tableMetadata()).isNotNull();
+        assertThat(registerResponse.tableMetadata().location()).isEqualTo(tableMetadata.location());
+        assertThat(registerResponse.metadataLocation()).isEqualTo(fileLocation);
+
+        if (getStorageConfigInfo().getStorageType() != StorageConfigInfo.StorageTypeEnum.FILE) {
+          assertThat(registerResponse.credentials())
+              .as("Cloud storage should vend credentials")
+              .isNotEmpty();
+        }
+      } finally {
+        resolvingFileIO.deleteFile(fileLocation);
+      }
+    }
+  }
 
-      // Register the table without overwrite first (must succeed)
-      restCatalog.registerTable(TableIdentifier.of(ns, "my_table"), fileLocation);
+  /**
+   * Create an EXTERNAL catalog with credential vending enabled. Register a table WITH access
+   * delegation and verify that the registerTable response contains vended credentials (when
+   * supported by the storage type).
+   */
+  @CatalogConfig(
+      value = Catalog.TypeEnum.EXTERNAL,
+      properties = {"polaris.config.enable.credential.vending", "true"})
+  @RestCatalogConfig({"header.X-Iceberg-Access-Delegation", "vended-credentials"})
+  @Test
+  public void testRegisterTableWithAccessDelegationForExternalCatalog() {
+    Namespace ns1 = Namespace.of("ns1");
+    restCatalog.createNamespace(ns1);
+    TableMetadata tableMetadata =
+        TableMetadata.newTableMetadata(
+            new Schema(List.of(Types.NestedField.required(1, "col1", new Types.StringType()))),
+            PartitionSpec.unpartitioned(),
+            externalCatalogBaseLocation + "/ns1/my_table",
+            Map.of());
+    try (ResolvingFileIO resolvingFileIO = new ResolvingFileIO()) {
+      initializeClientFileIO(resolvingFileIO);
+      resolvingFileIO.setConf(new Configuration());
+      String fileLocation = externalCatalogBaseLocation + "/ns1/my_table/metadata/v1.metadata.json";
+      TableMetadataParser.write(tableMetadata, resolvingFileIO.newOutputFile(fileLocation));
+      try {
+        LoadTableResponse registerResponse =
+            catalogApi.registerTableWithAccessDelegation(
+                currentCatalogName, ns1, "my_table", fileLocation, false);
+
+        assertThat(registerResponse).isNotNull();
+        assertThat(registerResponse.tableMetadata()).isNotNull();
+        assertThat(registerResponse.tableMetadata().location()).isEqualTo(tableMetadata.location());
+        assertThat(registerResponse.metadataLocation()).isEqualTo(fileLocation);
+
+        if (getStorageConfigInfo().getStorageType() != StorageConfigInfo.StorageTypeEnum.FILE) {
+          assertThat(registerResponse.credentials())
+              .as("Cloud storage should vend credentials")
+              .isNotEmpty();
+        }
+      } finally {
+        resolvingFileIO.deleteFile(fileLocation);
+      }
+    }
+  }
 
-      // Now attempt to register again with overwrite=true — must be rejected with 400
-      Invocation overwriteInvocation =
-          catalogApi
-              .request("v1/" + currentCatalogName + "/namespaces/ns_overwrite_ext/register")
-              .buildPost(
-                  Entity.json(
-                      Map.of(
-                          "name",
-                          "my_table",
-                          "metadata-location",
-                          fileLocation,
-                          "overwrite",
-                          true)));
-
-      try (Response overwriteResponse = overwriteInvocation.invoke()) {
-        assertThat(overwriteResponse.getStatus())
-            .isEqualTo(Response.Status.BAD_REQUEST.getStatusCode());
+  /**
+   * Create an EXTERNAL catalog. The test configuration, by default, disables access delegation for
+   * EXTERNAL catalogs, so try to register a table with the REST client configured to try to fetch
+   * vended credentials. Expect an IllegalArgumentException (HTTP 400 Bad Request).
+   */
+  @CatalogConfig(Catalog.TypeEnum.EXTERNAL)
+  @RestCatalogConfig({"header.X-Iceberg-Access-Delegation", "vended-credentials"})
+  @Test
+  public void testRegisterTableWithAccessDelegationForExternalCatalogWithConfigDisabled() {
+    Namespace ns1 = Namespace.of("ns1");
+    restCatalog.createNamespace(ns1);
+    TableMetadata tableMetadata =
+        TableMetadata.newTableMetadata(
+            new Schema(List.of(Types.NestedField.required(1, "col1", new Types.StringType()))),
+            PartitionSpec.unpartitioned(),
+            externalCatalogBaseLocation + "/ns1/my_table",
+            Map.of());
+    try (ResolvingFileIO resolvingFileIO = new ResolvingFileIO()) {
+      initializeClientFileIO(resolvingFileIO);
+      resolvingFileIO.setConf(new Configuration());
+      String fileLocation = externalCatalogBaseLocation + "/ns1/my_table/metadata/v1.metadata.json";
+      TableMetadataParser.write(tableMetadata, resolvingFileIO.newOutputFile(fileLocation));
+      try {
+        assertThatThrownBy(
+                () ->
+                    restCatalog.registerTable(
+                        TableIdentifier.of(ns1, "my_table"), fileLocation, false))
+            .isInstanceOf(IllegalArgumentException.class)
+            .hasMessageContaining("is not enabled for this external catalog")
+            .hasMessageContaining(
+                FeatureConfiguration.ALLOW_EXTERNAL_CATALOG_CREDENTIAL_VENDING.catalogConfig());
+      } finally {
+        resolvingFileIO.deleteFile(fileLocation);
+      }
+    }
+  }
+
+  /**
+   * Create an INTERNAL catalog. Register a table WITH access delegation + overwrite and verify that
+   * the registerTable response contains vended credentials (when supported by the storage type).
+   */
+  @RestCatalogConfig({"header.X-Iceberg-Access-Delegation", "vended-credentials"})
+  @Test
+  public void testRegisterTableOverwriteWithAccessDelegation() {
+    Namespace ns1 = Namespace.of("ns1");
+    restCatalog.createNamespace(ns1);
+
+    // Create a source table and capture its metadata location
+    TableIdentifier tableIdentifier = TableIdentifier.of(ns1, "tbl1");
+    Table source = restCatalog.createTable(tableIdentifier, SCHEMA);
+    assertThat(source.location()).isNotNull();
+    TableMetadata tableMetadata = ((BaseTable) source).operations().current();
+    String currentMetadataLocation =
+        ((BaseTable) source).operations().current().metadataFileLocation();
+    String metadataDir =
+        currentMetadataLocation.substring(0, currentMetadataLocation.lastIndexOf('/') + 1);
+    String newMetadataLocation = metadataDir + "overwrite-v1.metadata.json";
+
+    try (ResolvingFileIO resolvingFileIO = new ResolvingFileIO()) {
+      initializeClientFileIO(resolvingFileIO);
+      resolvingFileIO.setConf(new Configuration());
+
+      // Write a new metadata file (re-using the current metadata contents)
+      TableMetadataParser.write(
+          ((BaseTable) source).operations().current(),
+          resolvingFileIO.newOutputFile(newMetadataLocation));
+
+      try {
+
+        LoadTableResponse registerResponse =
+            catalogApi.registerTableWithAccessDelegation(
+                currentCatalogName, ns1, "tbl1", tableMetadata.metadataFileLocation(), true);
+
+        assertThat(registerResponse).isNotNull();
+        assertThat(registerResponse.tableMetadata()).isNotNull();
+        assertThat(registerResponse.tableMetadata().location()).isEqualTo(tableMetadata.location());
+        assertThat(registerResponse.metadataLocation())
+            .isEqualTo(tableMetadata.metadataFileLocation());
+
+        if (getStorageConfigInfo().getStorageType() != StorageConfigInfo.StorageTypeEnum.FILE) {
+          assertThat(registerResponse.credentials())
+              .as("Cloud storage should vend credentials")
+              .isNotEmpty();
+        }
+
+      } finally {
+        // Clean up the old metadata file
+        resolvingFileIO.deleteFile(currentMetadataLocation);
+      }
+    }
+  }
+
+  /**
+   * Create an EXTERNAL catalog. Try to register with overwrite a table and request fetch vended
+   * credentials. Expect an IllegalArgumentException (HTTP 400 Bad Request) because overwrite is not
+   * enabled for external catalogs.
+   */
+  @CatalogConfig(Catalog.TypeEnum.EXTERNAL)
+  @RestCatalogConfig({"header.X-Iceberg-Access-Delegation", "vended-credentials"})
+  @Test
+  public void testRegisterTableOverwriteWithAccessDelegationForExternalCatalogForbidden() {
+    Namespace ns1 = Namespace.of("ns1");
+    restCatalog.createNamespace(ns1);
+    TableMetadata tableMetadata =
+        TableMetadata.newTableMetadata(
+            new Schema(List.of(Types.NestedField.required(1, "col1", new Types.StringType()))),
+            PartitionSpec.unpartitioned(),
+            externalCatalogBaseLocation + "/ns1/my_table",
+            Map.of());
+    try (ResolvingFileIO resolvingFileIO = new ResolvingFileIO()) {
+      initializeClientFileIO(resolvingFileIO);
+      resolvingFileIO.setConf(new Configuration());
+      String fileLocation = externalCatalogBaseLocation + "/ns1/my_table/metadata/v1.metadata.json";
+      TableMetadataParser.write(tableMetadata, resolvingFileIO.newOutputFile(fileLocation));
+      try {
+        assertThatThrownBy(
+                () ->
+                    restCatalog.registerTable(
+                        TableIdentifier.of(ns1, "my_table"), fileLocation, true))
+            .isInstanceOf(BadRequestException.class)
+            .hasMessageContaining(
+                "Register table overwrite is only supported for internal Polaris catalogs");
       } finally {
         resolvingFileIO.deleteFile(fileLocation);
       }
@@ -1600,7 +1799,7 @@ public abstract class PolarisRestCatalogIntegrationBase extends CatalogTests<RES
                 "v1/{cat}/namespaces/{ns}/views/{view}",
                 Map.of("cat", currentCatalogName, "ns", namespace.toString(), "view", viewName))
             .head()) {
-      assertThat(response).returns(Response.Status.NOT_FOUND.getStatusCode(), Response::getStatus);
+      assertThat(response).returns(NOT_FOUND.getStatusCode(), Response::getStatus);
     }
 
     // New view should exist
@@ -1643,7 +1842,7 @@ public abstract class PolarisRestCatalogIntegrationBase extends CatalogTests<RES
       GenericTable createResponse =
           genericTableApi.createGenericTable(
               currentCatalogName, tableIdentifier, "format", Map.of());
-      Assertions.assertThat(createResponse.getFormat()).isEqualTo("format");
+      assertThat(createResponse.getFormat()).isEqualTo("format");
     } finally {
       genericTableApi.purge(currentCatalogName, namespace);
     }
@@ -1660,7 +1859,7 @@ public abstract class PolarisRestCatalogIntegrationBase extends CatalogTests<RES
 
       GenericTable loadResponse =
           genericTableApi.getGenericTable(currentCatalogName, tableIdentifier);
-      Assertions.assertThat(loadResponse.getFormat()).isEqualTo("format");
+      assertThat(loadResponse.getFormat()).isEqualTo("format");
 
     } finally {
       genericTableApi.purge(currentCatalogName, namespace);
@@ -1681,9 +1880,8 @@ public abstract class PolarisRestCatalogIntegrationBase extends CatalogTests<RES
       List<TableIdentifier> identifiers =
           genericTableApi.listGenericTables(currentCatalogName, namespace);
 
-      Assertions.assertThat(identifiers).hasSize(2);
-      Assertions.assertThat(identifiers)
-          .containsExactlyInAnyOrder(tableIdentifier1, tableIdentifier2);
+      assertThat(identifiers).hasSize(2);
+      assertThat(identifiers).containsExactlyInAnyOrder(tableIdentifier1, tableIdentifier2);
     } finally {
       genericTableApi.purge(currentCatalogName, namespace);
     }
@@ -1700,7 +1898,7 @@ public abstract class PolarisRestCatalogIntegrationBase extends CatalogTests<RES
 
       GenericTable loadResponse =
           genericTableApi.getGenericTable(currentCatalogName, tableIdentifier);
-      Assertions.assertThat(loadResponse.getFormat()).isEqualTo("format");
+      assertThat(loadResponse.getFormat()).isEqualTo("format");
 
       genericTableApi.dropGenericTable(currentCatalogName, tableIdentifier);
 
@@ -1875,8 +2073,8 @@ public abstract class PolarisRestCatalogIntegrationBase extends CatalogTests<RES
                         .setDoc("doc")
                         .setProperties(Map.of("polaris.reserved", "true"))
                         .build()))) {
-      Assertions.assertThat(res.getStatus()).isEqualTo(Response.Status.BAD_REQUEST.getStatusCode());
-      Assertions.assertThat(res.readEntity(String.class)).contains("reserved prefix");
+      assertThat(res.getStatus()).isEqualTo(Response.Status.BAD_REQUEST.getStatusCode());
+      assertThat(res.readEntity(String.class)).contains("reserved prefix");
     }
 
     genericTableApi.purge(currentCatalogName, namespace);
@@ -1889,7 +2087,7 @@ public abstract class PolarisRestCatalogIntegrationBase extends CatalogTests<RES
             () -> {
               restCatalog.createNamespace(namespace, ImmutableMap.of("polaris.reserved", "true"));
             })
-        .isInstanceOf(org.apache.iceberg.exceptions.BadRequestException.class)
+        .isInstanceOf(BadRequestException.class)
         .hasMessageContaining("reserved prefix");
   }
 
@@ -1898,11 +2096,11 @@ public abstract class PolarisRestCatalogIntegrationBase extends CatalogTests<RES
     Namespace namespace = Namespace.of("ns1");
     restCatalog.createNamespace(namespace, ImmutableMap.of("a", "b"));
     restCatalog.setProperties(namespace, ImmutableMap.of("c", "d"));
-    Assertions.assertThatCode(
+    assertThatCode(
             () -> {
               restCatalog.setProperties(namespace, ImmutableMap.of("polaris.reserved", "true"));
             })
-        .isInstanceOf(org.apache.iceberg.exceptions.BadRequestException.class)
+        .isInstanceOf(BadRequestException.class)
         .hasMessageContaining("reserved prefix");
     genericTableApi.purge(currentCatalogName, namespace);
   }
@@ -1912,11 +2110,11 @@ public abstract class PolarisRestCatalogIntegrationBase extends CatalogTests<RES
     Namespace namespace = Namespace.of("ns1");
     restCatalog.createNamespace(namespace, ImmutableMap.of("a", "b"));
     restCatalog.removeProperties(namespace, Sets.newHashSet("a"));
-    Assertions.assertThatCode(
+    assertThatCode(
             () -> {
               restCatalog.removeProperties(namespace, Sets.newHashSet("polaris.reserved"));
             })
-        .isInstanceOf(org.apache.iceberg.exceptions.BadRequestException.class)
+        .isInstanceOf(BadRequestException.class)
         .hasMessageContaining("reserved prefix");
     genericTableApi.purge(currentCatalogName, namespace);
   }
@@ -1926,7 +2124,7 @@ public abstract class PolarisRestCatalogIntegrationBase extends CatalogTests<RES
     Namespace namespace = Namespace.of("ns1");
     restCatalog.createNamespace(namespace);
     TableIdentifier identifier = TableIdentifier.of(namespace, "t1");
-    Assertions.assertThatCode(
+    assertThatCode(
             () -> {
               restCatalog.createTable(
                   identifier,
@@ -1945,7 +2143,7 @@ public abstract class PolarisRestCatalogIntegrationBase extends CatalogTests<RES
     restCatalog.createNamespace(namespace);
     TableIdentifier identifier = TableIdentifier.of(namespace, "t1");
     restCatalog.createTable(identifier, SCHEMA);
-    Assertions.assertThatCode(
+    assertThatCode(
             () -> {
               var txn =
                   restCatalog.newReplaceTableTransaction(
@@ -2072,7 +2270,7 @@ public abstract class PolarisRestCatalogIntegrationBase extends CatalogTests<RES
 
     try (Response response = invocation.invoke()) {
       // Should return 404 Not Found regardless of If-None-Match header
-      assertThat(response.getStatus()).isEqualTo(Response.Status.NOT_FOUND.getStatusCode());
+      assertThat(response.getStatus()).isEqualTo(NOT_FOUND.getStatusCode());
     }
   }
 
@@ -2440,8 +2638,7 @@ public abstract class PolarisRestCatalogIntegrationBase extends CatalogTests<RES
     }
 
     try {
-      Assertions.assertThat(catalogApi.listNamespaces(currentCatalogName, Namespace.empty()))
-          .hasSize(20);
+      assertThat(catalogApi.listNamespaces(currentCatalogName, Namespace.empty())).hasSize(20);
       for (var pageSize : List.of(1, 2, 3, 9, 10, 11, 19, 20, 21, 2000)) {
         int total = 0;
         String pageToken = null;
@@ -2449,13 +2646,11 @@ public abstract class PolarisRestCatalogIntegrationBase extends CatalogTests<RES
           ListNamespacesResponse response =
               catalogApi.listNamespaces(
                   currentCatalogName, Namespace.empty(), pageToken, String.valueOf(pageSize));
-          Assertions.assertThat(response.namespaces().size()).isLessThanOrEqualTo(pageSize);
+          assertThat(response.namespaces().size()).isLessThanOrEqualTo(pageSize);
           total += response.namespaces().size();
           pageToken = response.nextPageToken();
         } while (pageToken != null);
-        Assertions.assertThat(total)
-            .as("Total paginated results for pageSize = " + pageSize)
-            .isEqualTo(20);
+        assertThat(total).as("Total paginated results for pageSize = " + pageSize).isEqualTo(20);
       }
     } finally {
       for (int i = 0; i < 20; i++) {
@@ -2475,7 +2670,7 @@ public abstract class PolarisRestCatalogIntegrationBase extends CatalogTests<RES
     }
 
     try {
-      Assertions.assertThat(catalogApi.listTables(currentCatalogName, namespace)).hasSize(20);
+      assertThat(catalogApi.listTables(currentCatalogName, namespace)).hasSize(20);
       for (var pageSize : List.of(1, 2, 3, 9, 10, 11, 19, 20, 21, 2000)) {
         int total = 0;
         String pageToken = null;
@@ -2483,13 +2678,11 @@ public abstract class PolarisRestCatalogIntegrationBase extends CatalogTests<RES
           ListTablesResponse response =
               catalogApi.listTables(
                   currentCatalogName, namespace, pageToken, String.valueOf(pageSize));
-          Assertions.assertThat(response.identifiers().size()).isLessThanOrEqualTo(pageSize);
+          assertThat(response.identifiers().size()).isLessThanOrEqualTo(pageSize);
           total += response.identifiers().size();
           pageToken = response.nextPageToken();
         } while (pageToken != null);
-        Assertions.assertThat(total)
-            .as("Total paginated results for pageSize = " + pageSize)
-            .isEqualTo(20);
+        assertThat(total).as("Total paginated results for pageSize = " + pageSize).isEqualTo(20);
       }
     } finally {
       for (int i = 0; i < 20; i++) {
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 8f6dedb13f..c016749459 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
@@ -33,6 +33,10 @@ public enum PolarisAuthorizableOperation {
   CREATE_TABLE_STAGED_WITH_WRITE_DELEGATION,
   REGISTER_TABLE,
   REGISTER_TABLE_OVERWRITE,
+  REGISTER_TABLE_WITH_READ_DELEGATION,
+  REGISTER_TABLE_WITH_WRITE_DELEGATION,
+  REGISTER_TABLE_OVERWRITE_WITH_READ_DELEGATION,
+  REGISTER_TABLE_OVERWRITE_WITH_WRITE_DELEGATION,
   LOAD_TABLE,
   LOAD_TABLE_WITH_READ_DELEGATION,
   LOAD_TABLE_WITH_WRITE_DELEGATION,
diff --git a/polaris-core/src/main/java/org/apache/polaris/core/auth/RbacOperationSemantics.java b/polaris-core/src/main/java/org/apache/polaris/core/auth/RbacOperationSemantics.java
index b0c66efbc2..f63999959d 100644
--- a/polaris-core/src/main/java/org/apache/polaris/core/auth/RbacOperationSemantics.java
+++ b/polaris-core/src/main/java/org/apache/polaris/core/auth/RbacOperationSemantics.java
@@ -87,6 +87,10 @@ import static org.apache.polaris.core.auth.PolarisAuthorizableOperation.LOAD_VIE
 import static org.apache.polaris.core.auth.PolarisAuthorizableOperation.NAMESPACE_EXISTS;
 import static org.apache.polaris.core.auth.PolarisAuthorizableOperation.REGISTER_TABLE;
 import static org.apache.polaris.core.auth.PolarisAuthorizableOperation.REGISTER_TABLE_OVERWRITE;
+import static org.apache.polaris.core.auth.PolarisAuthorizableOperation.REGISTER_TABLE_OVERWRITE_WITH_READ_DELEGATION;
+import static org.apache.polaris.core.auth.PolarisAuthorizableOperation.REGISTER_TABLE_OVERWRITE_WITH_WRITE_DELEGATION;
+import static org.apache.polaris.core.auth.PolarisAuthorizableOperation.REGISTER_TABLE_WITH_READ_DELEGATION;
+import static org.apache.polaris.core.auth.PolarisAuthorizableOperation.REGISTER_TABLE_WITH_WRITE_DELEGATION;
 import static org.apache.polaris.core.auth.PolarisAuthorizableOperation.REGISTER_VIEW;
 import static org.apache.polaris.core.auth.PolarisAuthorizableOperation.REMOVE_TABLE_PARTITION_SPECS;
 import static org.apache.polaris.core.auth.PolarisAuthorizableOperation.REMOVE_TABLE_PROPERTIES;
@@ -290,6 +294,14 @@ record RbacOperationSemantics(
     register(CREATE_TABLE_STAGED_WITH_WRITE_DELEGATION, EnumSet.of(TABLE_CREATE, TABLE_WRITE_DATA));
     register(REGISTER_TABLE, TABLE_CREATE);
     register(REGISTER_TABLE_OVERWRITE, TABLE_FULL_METADATA);
+    register(REGISTER_TABLE_WITH_READ_DELEGATION, EnumSet.of(TABLE_CREATE, TABLE_READ_DATA));
+    register(REGISTER_TABLE_WITH_WRITE_DELEGATION, EnumSet.of(TABLE_CREATE, TABLE_WRITE_DATA));
+    register(
+        REGISTER_TABLE_OVERWRITE_WITH_READ_DELEGATION,
+        EnumSet.of(TABLE_FULL_METADATA, TABLE_READ_DATA));
+    register(
+        REGISTER_TABLE_OVERWRITE_WITH_WRITE_DELEGATION,
+        EnumSet.of(TABLE_FULL_METADATA, TABLE_WRITE_DATA));
     register(LOAD_TABLE, TABLE_READ_PROPERTIES);
     register(LOAD_TABLE_WITH_READ_DELEGATION, TABLE_READ_DATA);
     register(LOAD_TABLE_WITH_WRITE_DELEGATION, TABLE_WRITE_DATA);
diff --git a/runtime/service/src/intTest/java/org/apache/polaris/service/it/PolarisRestCatalogMinIOIT.java b/runtime/service/src/intTest/java/org/apache/polaris/service/it/PolarisRestCatalogMinIOIT.java
index 76c95dac11..fc61854b6b 100644
--- a/runtime/service/src/intTest/java/org/apache/polaris/service/it/PolarisRestCatalogMinIOIT.java
+++ b/runtime/service/src/intTest/java/org/apache/polaris/service/it/PolarisRestCatalogMinIOIT.java
@@ -35,7 +35,6 @@ import org.apache.polaris.test.minio.Minio;
 import org.apache.polaris.test.minio.MinioAccess;
 import org.apache.polaris.test.minio.MinioExtension;
 import org.junit.jupiter.api.BeforeAll;
-import org.junit.jupiter.api.TestInfo;
 import org.junit.jupiter.api.extension.ExtendWith;
 
 @QuarkusIntegrationTest
@@ -95,15 +94,4 @@ public class PolarisRestCatalogMinIOIT extends PolarisRestCatalogIntegrationBase
 
     return storageConfig.build();
   }
-
-  @Override
-  protected Map<String, String> extraCatalogProperties(TestInfo testInfo) {
-    if (testInfo.getTestMethod().orElseThrow().getName().equals("testRegisterTable")) {
-      // This test registers a table – operation that doesn't support access delegation –
-      // then attempts to use the table's FileIO. This can only work if the client has its
-      // own S3 credentials.
-      return s3Properties;
-    }
-    return Map.of();
-  }
 }
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 fbdfa549a3..c9cf763bd5 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
@@ -218,10 +218,13 @@ public abstract class CatalogHandler {
 
   /**
    * Authorizes a register-table-with-overwrite operation. If the table already exists, {@code
-   * REGISTER_TABLE_OVERWRITE} is authorized against the table entity. If the table does not exist,
-   * {@code REGISTER_TABLE} is authorized against the parent namespace.
+   * overwriteOp} is authorized against the table entity. If the table does not exist, {@code
+   * fallbackOp} is authorized against the parent namespace.
    */
-  protected void authorizeRegisterTableOverwriteOrThrow(TableIdentifier identifier) {
+  protected void authorizeRegisterTableOverwriteOrThrow(
+      PolarisAuthorizableOperation overwriteOp,
+      PolarisAuthorizableOperation fallbackOp,
+      TableIdentifier identifier) {
     Namespace namespace = identifier.namespace();
     resolutionManifest = newResolutionManifest();
     resolutionManifest.addPath(
@@ -252,7 +255,7 @@ public abstract class CatalogHandler {
           .authorizeOrThrow(
               polarisPrincipal(),
               resolutionManifest.getAllActivatedCatalogRoleAndPrincipalRoles(),
-              PolarisAuthorizableOperation.REGISTER_TABLE_OVERWRITE,
+              overwriteOp,
               tableTarget,
               null /* secondary */);
     } else {
@@ -265,7 +268,7 @@ public abstract class CatalogHandler {
           .authorizeOrThrow(
               polarisPrincipal(),
               resolutionManifest.getAllActivatedCatalogRoleAndPrincipalRoles(),
-              PolarisAuthorizableOperation.REGISTER_TABLE, // normal register-table operation
+              fallbackOp, // normal register-table operation
               namespaceTarget,
               null /* secondary */);
     }
diff --git a/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogAdapter.java b/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogAdapter.java
index 4a6ceb1c1d..bff88f9639 100644
--- a/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogAdapter.java
+++ b/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogAdapter.java
@@ -433,12 +433,20 @@ public class IcebergCatalogAdapter
     registerTableRequest.validate();
     Namespace ns =
         NamespaceUtils.splitNamespace(namespace, NamespaceUtils.DEFAULT_NAMESPACE_SEPARATOR);
-    EntityNameValidator.validateIdentifier(TableIdentifier.of(ns, registerTableRequest.name()));
+    TableIdentifier tableIdentifier = TableIdentifier.of(ns, registerTableRequest.name());
+    EntityNameValidator.validateIdentifier(tableIdentifier);
+    EnumSet<AccessDelegationMode> delegationModes =
+        parseAccessDelegationModes(accessDelegationMode);
     return withCatalog(
         securityContext,
         prefix,
         catalog -> {
-          LoadTableResponse response = catalog.registerTable(ns, registerTableRequest);
+          LoadTableResponse response =
+              catalog.registerTable(
+                  ns,
+                  registerTableRequest,
+                  delegationModes,
+                  getRefreshCredentialsEndpoint(delegationModes, prefix, tableIdentifier));
           return tryInsertETagHeader(
                   Response.ok(response), response, namespace, registerTableRequest.name())
               .build();
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 80edc5ca1a..7e778c08fd 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
@@ -613,46 +613,48 @@ public abstract class IcebergCatalogHandler extends CatalogHandler implements Au
    *
    * @param namespace The namespace to register the table in
    * @param request the register table request
+   * @param delegationModes the access delegation modes to use
+   * @param refreshCredentialsEndpoint the refresh credentials endpoint to use
    * @return ETagged {@link LoadTableResponse} to uniquely identify the table metadata
    */
-  public LoadTableResponse registerTable(Namespace namespace, RegisterTableRequest request) {
+  public LoadTableResponse registerTable(
+      Namespace namespace,
+      RegisterTableRequest request,
+      EnumSet<AccessDelegationMode> delegationModes,
+      Optional<String> refreshCredentialsEndpoint) {
+
+    request.validate();
     TableIdentifier identifier = TableIdentifier.of(namespace, request.name());
+    boolean overwrite = request.overwrite();
 
-    if (request.overwrite()) {
-      authorizeRegisterTableOverwriteOrThrow(identifier);
-      return registerTableWithOverwrite(identifier, request);
-    }
+    Set<PolarisStorageActions> actionsRequested =
+        authorizeRegisterTable(identifier, delegationModes, overwrite);
 
-    authorizeCreateTableLikeUnderNamespaceOperationOrThrow(
-        PolarisAuthorizableOperation.REGISTER_TABLE, identifier);
-    return catalogHandlerUtils().registerTable(baseCatalog, namespace, request);
-  }
+    if (overwrite) {
+      // For non-Polaris/federated catalogs, reject overwrite until this is
+      // supported by a common catalog contract.
+      CatalogEntity catalogEntity = getResolvedCatalogEntity();
+      if (catalogEntity.isExternal()) {
+        throw new BadRequestException(
+            "Register table overwrite is only supported for internal Polaris catalogs");
+      }
+    }
 
-  private LoadTableResponse registerTableWithOverwrite(
-      TableIdentifier identifier, RegisterTableRequest request) {
+    // Resolve the mode before registering the table to avoid registering a table and then failing
+    // to return credentials if the mode is invalid
+    Optional<AccessDelegationMode> resolvedMode = resolveAccessDelegationModes(delegationModes);
 
-    // For non-Polaris/federated catalogs, reject overwrite until this is
-    // supported by a common catalog contract.
-    CatalogEntity catalogEntity = getResolvedCatalogEntity();
-    if (catalogEntity.isExternal()) {
-      throw new BadRequestException(
-          "Register table overwrite is only supported for internal Polaris catalogs");
-    }
+    Table table = baseCatalog.registerTable(identifier, request.metadataLocation(), overwrite);
 
-    // Handle Polaris-specific overwrite logic.
-    if (baseCatalog instanceof IcebergCatalog icebergCatalog) {
-      // Use the overwrite-capable registration path for IcebergCatalog
-      Table table = icebergCatalog.registerTable(identifier, request.metadataLocation(), true);
-      if (table instanceof BaseTable baseTable) {
-        TableMetadata metadata = baseTable.operations().current();
-        return LoadTableResponse.builder().withTableMetadata(metadata).build();
-      }
-      throw new IllegalStateException("Cannot wrap catalog that does not produce BaseTable");
+    if (table instanceof BaseTable baseTable) {
+      TableMetadata tableMetadata = baseTable.operations().current();
+      return buildLoadTableResponseWithDelegationCredentials(
+              identifier, tableMetadata, resolvedMode, actionsRequested, refreshCredentialsEndpoint)
+          .build();
     }
 
-    throw new BadRequestException(
-        "Register table overwrite is only supported for internal Polaris catalogs; unsupported catalog type: %s",
-        baseCatalog.getClass().getName());
+    throw new IllegalStateException(
+        "Cannot register table %s: unknown table format".formatted(identifier));
   }
 
   public boolean sendNotification(TableIdentifier identifier, NotificationRequest request) {
@@ -889,6 +891,57 @@ public abstract class IcebergCatalogHandler extends CatalogHandler implements Au
     return actionsRequested;
   }
 
+  private Set<PolarisStorageActions> authorizeRegisterTable(
+      TableIdentifier tableIdentifier,
+      EnumSet<AccessDelegationMode> delegationModes,
+      boolean overwrite) {
+
+    if (delegationModes.isEmpty()) {
+
+      if (overwrite) {
+        authorizeRegisterTableOverwriteOrThrow(
+            PolarisAuthorizableOperation.REGISTER_TABLE_OVERWRITE,
+            PolarisAuthorizableOperation.REGISTER_TABLE,
+            tableIdentifier);
+      } else {
+        authorizeCreateTableLikeUnderNamespaceOperationOrThrow(
+            PolarisAuthorizableOperation.REGISTER_TABLE, tableIdentifier);
+      }
+
+      return Set.of();
+
+    } else {
+
+      Set<PolarisStorageActions> actionsRequested =
+          EnumSet.of(PolarisStorageActions.READ, PolarisStorageActions.LIST);
+
+      try {
+        if (overwrite) {
+          authorizeRegisterTableOverwriteOrThrow(
+              PolarisAuthorizableOperation.REGISTER_TABLE_OVERWRITE_WITH_WRITE_DELEGATION,
+              PolarisAuthorizableOperation.REGISTER_TABLE_WITH_WRITE_DELEGATION,
+              tableIdentifier);
+        } else {
+          authorizeCreateTableLikeUnderNamespaceOperationOrThrow(
+              PolarisAuthorizableOperation.REGISTER_TABLE_WITH_WRITE_DELEGATION, tableIdentifier);
+        }
+        actionsRequested.add(PolarisStorageActions.WRITE);
+      } catch (ForbiddenException e) {
+        if (overwrite) {
+          authorizeRegisterTableOverwriteOrThrow(
+              PolarisAuthorizableOperation.REGISTER_TABLE_OVERWRITE_WITH_READ_DELEGATION,
+              PolarisAuthorizableOperation.REGISTER_TABLE_WITH_READ_DELEGATION,
+              tableIdentifier);
+        } else {
+          authorizeCreateTableLikeUnderNamespaceOperationOrThrow(
+              PolarisAuthorizableOperation.REGISTER_TABLE_WITH_READ_DELEGATION, tableIdentifier);
+        }
+      }
+
+      return actionsRequested;
+    }
+  }
+
   public Optional<LoadTableResponse> loadTable(
       TableIdentifier tableIdentifier,
       String snapshots,
diff --git a/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/AbstractIcebergCatalogHandlerAuthzTest.java b/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/AbstractIcebergCatalogHandlerAuthzTest.java
index 550e62f6a6..ee598f4f56 100644
--- a/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/AbstractIcebergCatalogHandlerAuthzTest.java
+++ b/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/AbstractIcebergCatalogHandlerAuthzTest.java
@@ -21,6 +21,7 @@ package org.apache.polaris.service.catalog.iceberg;
 import com.google.common.collect.ImmutableMap;
 import jakarta.inject.Inject;
 import java.time.Instant;
+import java.util.EnumSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Optional;
@@ -52,6 +53,7 @@ import org.apache.iceberg.rest.requests.CreateNamespaceRequest;
 import org.apache.iceberg.rest.requests.CreateTableRequest;
 import org.apache.iceberg.rest.requests.CreateViewRequest;
 import org.apache.iceberg.rest.requests.ImmutableCreateViewRequest;
+import org.apache.iceberg.rest.requests.ImmutableRegisterTableRequest;
 import org.apache.iceberg.rest.requests.RegisterTableRequest;
 import org.apache.iceberg.rest.requests.RenameTableRequest;
 import org.apache.iceberg.rest.requests.ReportMetricsRequest;
@@ -76,6 +78,7 @@ import org.apache.polaris.core.entity.PrincipalEntity;
 import org.apache.polaris.core.persistence.dao.entity.CreatePrincipalResult;
 import org.apache.polaris.core.persistence.resolver.PolarisResolutionManifest;
 import org.apache.polaris.service.admin.PolarisAuthzTestBase;
+import org.apache.polaris.service.catalog.AccessDelegationMode;
 import org.apache.polaris.service.context.catalog.PolarisLocalCatalogFactory;
 import org.apache.polaris.service.http.IfNoneMatch;
 import org.apache.polaris.service.types.NotificationRequest;
@@ -535,25 +538,25 @@ public abstract class AbstractIcebergCatalogHandlerAuthzTest extends PolarisAuth
 
     // To get a handy metadata file we can use one from another table.
     // to avoid overlapping directories, drop the original table and recreate it via registerTable
-    final String metadataLocation = newHandler().loadTable(TABLE_NS1_1, "all").metadataLocation();
+    String metadataLocation = newHandler().loadTable(TABLE_NS1_1, "all").metadataLocation();
     newHandler(Set.of(PRINCIPAL_ROLE2)).dropTableWithoutPurge(TABLE_NS1_1);
 
-    final RegisterTableRequest registerRequest =
-        new RegisterTableRequest() {
-          @Override
-          public String name() {
-            return TABLE_NS1_1.name();
-          }
-
-          @Override
-          public String metadataLocation() {
-            return metadataLocation;
-          }
-        };
+    RegisterTableRequest registerRequest =
+        ImmutableRegisterTableRequest.builder()
+            .name(TABLE_NS1_1.name())
+            .metadataLocation(metadataLocation)
+            .build();
 
     // Use PRINCIPAL_ROLE1 for privilege-testing, PRINCIPAL_ROLE2 for cleanup.
     return authzTestsBuilder("registerTable")
-        .action(() -> newHandler(Set.of(PRINCIPAL_ROLE1)).registerTable(NS1, registerRequest))
+        .action(
+            () ->
+                newHandler(Set.of(PRINCIPAL_ROLE1))
+                    .registerTable(
+                        NS1,
+                        registerRequest,
+                        EnumSet.noneOf(AccessDelegationMode.class),
+                        Optional.empty()))
         .cleanupAction(() -> newHandler(Set.of(PRINCIPAL_ROLE2)).dropTableWithoutPurge(TABLE_NS1_1))
         .shouldPassWith(PolarisPrivilege.TABLE_CREATE)
         .shouldPassWith(PolarisPrivilege.TABLE_FULL_METADATA)
@@ -609,7 +612,14 @@ public abstract class AbstractIcebergCatalogHandlerAuthzTest extends PolarisAuth
 
     // Registering a new table requires TABLE_CREATE (or broader privileges).
     return authzTestsBuilder("registerTable (insufficient)")
-        .action(() -> newHandler(Set.of(PRINCIPAL_ROLE1)).registerTable(NS2, registerRequest))
+        .action(
+            () ->
+                newHandler(Set.of(PRINCIPAL_ROLE1))
+                    .registerTable(
+                        NS2,
+                        registerRequest,
+                        EnumSet.noneOf(AccessDelegationMode.class),
+                        Optional.empty()))
         .cleanupAction(
             () -> {
               IcebergCatalogHandler cleanup = newHandler(Set.of(PRINCIPAL_ROLE2));
@@ -635,21 +645,7 @@ public abstract class AbstractIcebergCatalogHandlerAuthzTest extends PolarisAuth
   }
 
   @TestFactory
-  Stream<DynamicNode> testLoadTablePrivileges() {
-    return authzTestsBuilder("loadTable")
-        .action(() -> newHandler().loadTable(TABLE_NS1A_2, "all"))
-        .shouldPassWith(PolarisPrivilege.TABLE_READ_PROPERTIES)
-        .shouldPassWith(PolarisPrivilege.TABLE_WRITE_PROPERTIES)
-        .shouldPassWith(PolarisPrivilege.TABLE_READ_DATA)
-        .shouldPassWith(PolarisPrivilege.TABLE_WRITE_DATA)
-        .shouldPassWith(PolarisPrivilege.TABLE_FULL_METADATA)
-        .shouldPassWith(PolarisPrivilege.CATALOG_MANAGE_CONTENT)
-        .shouldPassWith(PolarisPrivilege.CATALOG_MANAGE_METADATA)
-        .createTests();
-  }
-
-  @TestFactory
-  Stream<DynamicNode> testRegisterTableOverwriteSufficientPrivileges() {
+  Stream<DynamicNode> testRegisterTableOverwritePrivileges() {
     // For overwrite, the caller needs TABLE_FULL_METADATA or higher privileges on the target
     // table. This is stricter than UPDATE_TABLE, which only requires TABLE_WRITE_PROPERTIES,
     // because overwriting involves both dropping the old table pointer and creating a new one.
@@ -670,15 +666,85 @@ public abstract class AbstractIcebergCatalogHandlerAuthzTest extends PolarisAuth
     Mockito.when(registerRequest.overwrite()).thenReturn(true);
 
     return authzTestsBuilder("registerTableOverwrite")
-        .action(() -> newHandler(Set.of(PRINCIPAL_ROLE1)).registerTable(NS1, registerRequest))
+        .action(
+            () ->
+                newHandler(Set.of(PRINCIPAL_ROLE1))
+                    .registerTable(
+                        NS1,
+                        registerRequest,
+                        EnumSet.noneOf(AccessDelegationMode.class),
+                        Optional.empty()))
+        .shouldPassWith(PolarisPrivilege.TABLE_FULL_METADATA)
+        .shouldPassWith(PolarisPrivilege.CATALOG_MANAGE_CONTENT)
+        .shouldPassWith(PolarisPrivilege.CATALOG_MANAGE_METADATA)
+        .shouldFailWith(PolarisPrivilege.TABLE_WRITE_PROPERTIES)
+        .shouldFailWith(PolarisPrivilege.TABLE_WRITE_DATA)
+        .shouldFailWith(PolarisPrivilege.NAMESPACE_FULL_METADATA)
+        .shouldFailWith(PolarisPrivilege.VIEW_FULL_METADATA)
+        .shouldFailWith(PolarisPrivilege.TABLE_CREATE)
+        .shouldFailWith(PolarisPrivilege.TABLE_DROP)
+        .shouldFailWith(PolarisPrivilege.TABLE_READ_PROPERTIES)
+        .shouldFailWith(PolarisPrivilege.TABLE_READ_DATA)
+        .shouldFailWith(PolarisPrivilege.TABLE_LIST)
+        .createTests();
+  }
+
+  @TestFactory
+  Stream<DynamicNode> testRegisterTableOverwriteNonExistentTable() {
+    // When overwrite is specified and the table does not exist, the authorization should
+    // fall back to a non-overwrite authorization level instead.
+    assertSuccess(
+        adminService.grantPrivilegeOnCatalogToRole(
+            CATALOG_NAME, CATALOG_ROLE2, PolarisPrivilege.TABLE_DROP));
+    assertSuccess(
+        adminService.grantPrivilegeOnCatalogToRole(
+            CATALOG_NAME, CATALOG_ROLE2, PolarisPrivilege.TABLE_READ_PROPERTIES));
+
+    String metadataLocation = newHandler().loadTable(TABLE_NS1_1, "all").metadataLocation();
+    newHandler(Set.of(PRINCIPAL_ROLE2)).dropTableWithoutPurge(TABLE_NS1_1);
+
+    RegisterTableRequest registerRequest =
+        ImmutableRegisterTableRequest.builder()
+            .name(TABLE_NS1_1.name())
+            .metadataLocation(metadataLocation)
+            .overwrite(true)
+            .build();
+
+    return authzTestsBuilder("registerTableOverwriteNonExistentTable")
+        .action(
+            () ->
+                newHandler(Set.of(PRINCIPAL_ROLE1))
+                    .registerTable(
+                        NS1,
+                        registerRequest,
+                        EnumSet.noneOf(AccessDelegationMode.class),
+                        Optional.empty()))
+        .cleanupAction(
+            () -> {
+              IcebergCatalogHandler cleanup = newHandler(Set.of(PRINCIPAL_ROLE2));
+              try {
+                cleanup.dropTableWithoutPurge(TABLE_NS1_1);
+              } catch (RuntimeException ignored) {
+                // Target table may not have been created; cleanup is best-effort.
+              }
+            })
         .shouldPassWith(PolarisPrivilege.TABLE_FULL_METADATA)
         .shouldPassWith(PolarisPrivilege.CATALOG_MANAGE_CONTENT)
         .shouldPassWith(PolarisPrivilege.CATALOG_MANAGE_METADATA)
+        .shouldPassWith(PolarisPrivilege.TABLE_CREATE) // Must pass since the table does not exist
+        .shouldFailWith(PolarisPrivilege.NAMESPACE_FULL_METADATA)
+        .shouldFailWith(PolarisPrivilege.VIEW_FULL_METADATA)
+        .shouldFailWith(PolarisPrivilege.TABLE_DROP)
+        .shouldFailWith(PolarisPrivilege.TABLE_READ_PROPERTIES)
+        .shouldFailWith(PolarisPrivilege.TABLE_WRITE_PROPERTIES)
+        .shouldFailWith(PolarisPrivilege.TABLE_READ_DATA)
+        .shouldFailWith(PolarisPrivilege.TABLE_WRITE_DATA)
+        .shouldFailWith(PolarisPrivilege.TABLE_LIST)
         .createTests();
   }
 
   @TestFactory
-  Stream<DynamicNode> testRegisterTableOverwriteTableLevelSufficientPrivileges() {
+  Stream<DynamicNode> testRegisterTableOverwriteTableLevel() {
     // REGISTER_TABLE_OVERWRITE authorizes against the table entity, so TABLE_FULL_METADATA
     // granted directly on the target table (not at catalog/namespace level) must suffice.
     assertSuccess(
@@ -692,7 +758,7 @@ public abstract class AbstractIcebergCatalogHandlerAuthzTest extends PolarisAuth
     Mockito.when(registerRequest.metadataLocation()).thenReturn(metadataLocation);
     Mockito.when(registerRequest.overwrite()).thenReturn(true);
 
-    return authzTestsBuilder("registerTableOverwrite (table-level grant)")
+    return authzTestsBuilder("registerTableOverwriteTableLevel")
         .grantAction(
             privilege ->
                 adminService.grantPrivilegeOnTableToRole(
@@ -701,52 +767,201 @@ public abstract class AbstractIcebergCatalogHandlerAuthzTest extends PolarisAuth
             privilege ->
                 adminService.revokePrivilegeOnTableFromRole(
                     CATALOG_NAME, CATALOG_ROLE1, TABLE_NS1_1, privilege))
-        .action(() -> newHandler(Set.of(PRINCIPAL_ROLE1)).registerTable(NS1, registerRequest))
+        .action(
+            () ->
+                newHandler(Set.of(PRINCIPAL_ROLE1))
+                    .registerTable(
+                        NS1,
+                        registerRequest,
+                        EnumSet.noneOf(AccessDelegationMode.class),
+                        Optional.empty()))
         .shouldPassWith(PolarisPrivilege.TABLE_FULL_METADATA)
         .shouldPassWith(PolarisPrivilege.CATALOG_MANAGE_CONTENT)
         .shouldPassWith(PolarisPrivilege.CATALOG_MANAGE_METADATA)
+        .shouldFailWith(PolarisPrivilege.TABLE_WRITE_PROPERTIES)
+        .shouldFailWith(PolarisPrivilege.TABLE_WRITE_DATA)
+        .shouldFailWith(PolarisPrivilege.NAMESPACE_FULL_METADATA)
+        .shouldFailWith(PolarisPrivilege.VIEW_FULL_METADATA)
+        .shouldFailWith(PolarisPrivilege.TABLE_CREATE)
+        .shouldFailWith(PolarisPrivilege.TABLE_DROP)
+        .shouldFailWith(PolarisPrivilege.TABLE_READ_PROPERTIES)
+        .shouldFailWith(PolarisPrivilege.TABLE_READ_DATA)
+        .shouldFailWith(PolarisPrivilege.TABLE_LIST)
+        .createTests();
+  }
+
+  @TestFactory
+  Stream<DynamicNode> testRegisterTableWithWriteDelegation() {
+    assertSuccess(
+        adminService.grantPrivilegeOnCatalogToRole(
+            CATALOG_NAME, CATALOG_ROLE2, PolarisPrivilege.TABLE_DROP));
+    assertSuccess(
+        adminService.grantPrivilegeOnCatalogToRole(
+            CATALOG_NAME, CATALOG_ROLE2, PolarisPrivilege.TABLE_READ_PROPERTIES));
+
+    String metadataLocation = newHandler().loadTable(TABLE_NS1_1, "all").metadataLocation();
+    newHandler(Set.of(PRINCIPAL_ROLE2)).dropTableWithoutPurge(TABLE_NS1_1);
+
+    RegisterTableRequest registerRequest =
+        ImmutableRegisterTableRequest.builder()
+            .name(TABLE_NS1_1.name())
+            .metadataLocation(metadataLocation)
+            .build();
+
+    return authzTestsBuilder("registerTableWithWriteDelegation")
+        .action(
+            () ->
+                newHandler(Set.of(PRINCIPAL_ROLE1))
+                    .registerTable(
+                        NS1,
+                        registerRequest,
+                        EnumSet.of(AccessDelegationMode.VENDED_CREDENTIALS),
+                        Optional.empty()))
+        .cleanupAction(() -> newHandler(Set.of(PRINCIPAL_ROLE2)).dropTableWithoutPurge(TABLE_NS1_1))
+        .shouldPassWith(PolarisPrivilege.TABLE_CREATE, PolarisPrivilege.TABLE_READ_DATA)
+        .shouldPassWith(PolarisPrivilege.TABLE_CREATE, PolarisPrivilege.TABLE_WRITE_DATA)
+        .shouldPassWith(PolarisPrivilege.TABLE_FULL_METADATA, PolarisPrivilege.TABLE_READ_DATA)
+        .shouldPassWith(PolarisPrivilege.TABLE_FULL_METADATA, PolarisPrivilege.TABLE_WRITE_DATA)
+        .shouldPassWith(PolarisPrivilege.CATALOG_MANAGE_METADATA, PolarisPrivilege.TABLE_READ_DATA)
+        .shouldPassWith(PolarisPrivilege.CATALOG_MANAGE_METADATA, PolarisPrivilege.TABLE_WRITE_DATA)
+        .shouldPassWith(PolarisPrivilege.CATALOG_MANAGE_CONTENT)
         .createTests();
   }
 
   @TestFactory
-  Stream<DynamicNode> testRegisterTableOverwriteInsufficientPermissions() {
-    /*
-     * Verifies that a variety of privileges are insufficient to authorize a registerTable operation
-     * with overwrite enabled. Grants only TABLE_READ_PROPERTIES to the cleanup role, mocks a
-     * RegisterTableRequest for overwriting an existing table, and asserts that privileges such as
-     * TABLE_WRITE_PROPERTIES (insufficient, only covers metadata updates), TABLE_CREATE, TABLE_DROP,
-     * NAMESPACE_FULL_METADATA, VIEW_FULL_METADATA, TABLE_READ_PROPERTIES, TABLE_READ_DATA, and
-     * TABLE_LIST do not permit the overwrite operation. Note that TABLE_CREATE and TABLE_DROP
-     * individually are insufficient - TABLE_FULL_METADATA (which combines all metadata operations)
-     * is the minimum required.
-     */
+  Stream<DynamicNode> testRegisterTableOverwriteWithWriteDelegation() {
+    assertSuccess(
+        adminService.grantPrivilegeOnCatalogToRole(
+            CATALOG_NAME, CATALOG_ROLE2, PolarisPrivilege.TABLE_DROP));
+    assertSuccess(
+        adminService.grantPrivilegeOnCatalogToRole(
+            CATALOG_NAME, CATALOG_ROLE2, PolarisPrivilege.TABLE_READ_PROPERTIES));
+
+    String metadataLocation = newHandler().loadTable(TABLE_NS1_1, "all").metadataLocation();
+
+    RegisterTableRequest registerRequest =
+        ImmutableRegisterTableRequest.builder()
+            .name(TABLE_NS1_1.name())
+            .metadataLocation(metadataLocation)
+            .overwrite(true)
+            .build();
 
+    return authzTestsBuilder("registerTableOverwriteWithWriteDelegation")
+        .action(
+            () ->
+                newHandler(Set.of(PRINCIPAL_ROLE1))
+                    .registerTable(
+                        NS1,
+                        registerRequest,
+                        EnumSet.of(AccessDelegationMode.VENDED_CREDENTIALS),
+                        Optional.empty()))
+        .shouldPassWith(PolarisPrivilege.TABLE_FULL_METADATA, PolarisPrivilege.TABLE_READ_DATA)
+        .shouldPassWith(PolarisPrivilege.TABLE_FULL_METADATA, PolarisPrivilege.TABLE_WRITE_DATA)
+        .shouldPassWith(PolarisPrivilege.CATALOG_MANAGE_METADATA, PolarisPrivilege.TABLE_READ_DATA)
+        .shouldPassWith(PolarisPrivilege.CATALOG_MANAGE_METADATA, PolarisPrivilege.TABLE_WRITE_DATA)
+        .shouldPassWith(PolarisPrivilege.CATALOG_MANAGE_CONTENT)
+        // should not pass with these since the table exists
+        .shouldFailWith(PolarisPrivilege.TABLE_CREATE, PolarisPrivilege.TABLE_READ_DATA)
+        .shouldFailWith(PolarisPrivilege.TABLE_CREATE, PolarisPrivilege.TABLE_WRITE_DATA)
+        .createTests();
+  }
+
+  @TestFactory
+  Stream<DynamicNode> testRegisterTableOverwriteWithWriteDelegationNonExistentTable() {
+    assertSuccess(
+        adminService.grantPrivilegeOnCatalogToRole(
+            CATALOG_NAME, CATALOG_ROLE2, PolarisPrivilege.TABLE_DROP));
     assertSuccess(
         adminService.grantPrivilegeOnCatalogToRole(
             CATALOG_NAME, CATALOG_ROLE2, PolarisPrivilege.TABLE_READ_PROPERTIES));
-    final String metadataLocation =
-        newHandler(Set.of(PRINCIPAL_ROLE2)).loadTable(TABLE_NS1_1, "all").metadataLocation();
 
-    // Mock RegisterTableRequest with overwrite=true
-    final RegisterTableRequest registerRequest = Mockito.mock(RegisterTableRequest.class);
-    Mockito.when(registerRequest.name()).thenReturn(TABLE_NS1_1.name());
-    Mockito.when(registerRequest.metadataLocation()).thenReturn(metadataLocation);
-    Mockito.when(registerRequest.overwrite()).thenReturn(true);
+    String metadataLocation = newHandler().loadTable(TABLE_NS1_1, "all").metadataLocation();
+    newHandler(Set.of(PRINCIPAL_ROLE2)).dropTableWithoutPurge(TABLE_NS1_1);
+
+    RegisterTableRequest registerRequest =
+        ImmutableRegisterTableRequest.builder()
+            .name(TABLE_NS1_1.name())
+            .metadataLocation(metadataLocation)
+            .overwrite(true)
+            .build();
+
+    return authzTestsBuilder("registerTableOverwriteWithWriteDelegationNonExistentTable")
+        .action(
+            () ->
+                newHandler(Set.of(PRINCIPAL_ROLE1))
+                    .registerTable(
+                        NS1,
+                        registerRequest,
+                        EnumSet.of(AccessDelegationMode.VENDED_CREDENTIALS),
+                        Optional.empty()))
+        .cleanupAction(() -> newHandler(Set.of(PRINCIPAL_ROLE2)).dropTableWithoutPurge(TABLE_NS1_1))
+        .shouldPassWith(PolarisPrivilege.TABLE_FULL_METADATA, PolarisPrivilege.TABLE_READ_DATA)
+        .shouldPassWith(PolarisPrivilege.TABLE_FULL_METADATA, PolarisPrivilege.TABLE_WRITE_DATA)
+        .shouldPassWith(PolarisPrivilege.CATALOG_MANAGE_METADATA, PolarisPrivilege.TABLE_READ_DATA)
+        .shouldPassWith(PolarisPrivilege.CATALOG_MANAGE_METADATA, PolarisPrivilege.TABLE_WRITE_DATA)
+        .shouldPassWith(PolarisPrivilege.CATALOG_MANAGE_CONTENT)
+        // should pass with these since the table does not exist
+        .shouldPassWith(PolarisPrivilege.TABLE_CREATE, PolarisPrivilege.TABLE_READ_DATA)
+        .shouldPassWith(PolarisPrivilege.TABLE_CREATE, PolarisPrivilege.TABLE_WRITE_DATA)
+        .createTests();
+  }
 
-    return authzTestsBuilder("registerTableOverwrite (insufficient)")
-        .action(() -> newHandler(Set.of(PRINCIPAL_ROLE1)).registerTable(NS1, registerRequest))
+  @TestFactory
+  Stream<DynamicNode> testRegisterTableOverwriteWithWriteDelegationTableLevel() {
+    assertSuccess(
+        adminService.grantPrivilegeOnCatalogToRole(
+            CATALOG_NAME, CATALOG_ROLE2, PolarisPrivilege.TABLE_DROP));
+    assertSuccess(
+        adminService.grantPrivilegeOnCatalogToRole(
+            CATALOG_NAME, CATALOG_ROLE2, PolarisPrivilege.TABLE_READ_PROPERTIES));
+
+    String metadataLocation = newHandler().loadTable(TABLE_NS1_1, "all").metadataLocation();
+
+    RegisterTableRequest registerRequest =
+        ImmutableRegisterTableRequest.builder()
+            .name(TABLE_NS1_1.name())
+            .metadataLocation(metadataLocation)
+            .overwrite(true)
+            .build();
+
+    return authzTestsBuilder("registerTableOverwriteWithWriteDelegationTableLevel")
+        .grantAction(
+            privilege ->
+                adminService.grantPrivilegeOnTableToRole(
+                    CATALOG_NAME, CATALOG_ROLE1, TABLE_NS1_1, privilege))
+        .revokeAction(
+            privilege ->
+                adminService.revokePrivilegeOnTableFromRole(
+                    CATALOG_NAME, CATALOG_ROLE1, TABLE_NS1_1, privilege))
+        .action(
+            () ->
+                newHandler(Set.of(PRINCIPAL_ROLE1))
+                    .registerTable(
+                        NS1,
+                        registerRequest,
+                        EnumSet.of(AccessDelegationMode.VENDED_CREDENTIALS),
+                        Optional.empty()))
+        .shouldPassWith(PolarisPrivilege.TABLE_FULL_METADATA, PolarisPrivilege.TABLE_READ_DATA)
+        .shouldPassWith(PolarisPrivilege.TABLE_FULL_METADATA, PolarisPrivilege.TABLE_WRITE_DATA)
+        .shouldPassWith(PolarisPrivilege.CATALOG_MANAGE_METADATA, PolarisPrivilege.TABLE_READ_DATA)
+        .shouldPassWith(PolarisPrivilege.CATALOG_MANAGE_METADATA, PolarisPrivilege.TABLE_WRITE_DATA)
+        .shouldPassWith(PolarisPrivilege.CATALOG_MANAGE_CONTENT)
+        .shouldFailWith(PolarisPrivilege.TABLE_CREATE, PolarisPrivilege.TABLE_READ_DATA)
+        .shouldFailWith(PolarisPrivilege.TABLE_CREATE, PolarisPrivilege.TABLE_WRITE_DATA)
+        .createTests();
+  }
+
+  @TestFactory
+  Stream<DynamicNode> testLoadTablePrivileges() {
+    return authzTestsBuilder("loadTable")
+        .action(() -> newHandler().loadTable(TABLE_NS1A_2, "all"))
+        .shouldPassWith(PolarisPrivilege.TABLE_READ_PROPERTIES)
+        .shouldPassWith(PolarisPrivilege.TABLE_WRITE_PROPERTIES)
+        .shouldPassWith(PolarisPrivilege.TABLE_READ_DATA)
+        .shouldPassWith(PolarisPrivilege.TABLE_WRITE_DATA)
         .shouldPassWith(PolarisPrivilege.TABLE_FULL_METADATA)
         .shouldPassWith(PolarisPrivilege.CATALOG_MANAGE_CONTENT)
         .shouldPassWith(PolarisPrivilege.CATALOG_MANAGE_METADATA)
-        .shouldFailWith(PolarisPrivilege.TABLE_WRITE_PROPERTIES)
-        .shouldFailWith(PolarisPrivilege.TABLE_WRITE_DATA)
-        .shouldFailWith(PolarisPrivilege.NAMESPACE_FULL_METADATA)
-        .shouldFailWith(PolarisPrivilege.VIEW_FULL_METADATA)
-        .shouldFailWith(PolarisPrivilege.TABLE_CREATE)
-        .shouldFailWith(PolarisPrivilege.TABLE_DROP)
-        .shouldFailWith(PolarisPrivilege.TABLE_READ_PROPERTIES)
-        .shouldFailWith(PolarisPrivilege.TABLE_READ_DATA)
-        .shouldFailWith(PolarisPrivilege.TABLE_LIST)
         .createTests();
   }
 
@@ -877,7 +1092,7 @@ public abstract class AbstractIcebergCatalogHandlerAuthzTest extends PolarisAuth
     // With fine-grained authorization disabled, TABLE_WRITE_PROPERTIES should work
     // even for operations that would require specific privileges when enabled
     return authzTestsBuilder("updateTable (coarse-grained fallback)")
-        .action(() -> newWrapperWithFineGrainedAuthzDisabled().updateTable(TABLE_NS1A_2, request))
+        .action(() -> newHandlerWithFineGrainedAuthzDisabled().updateTable(TABLE_NS1A_2, request))
         .shouldPassWith(PolarisPrivilege.TABLE_WRITE_PROPERTIES)
         .shouldPassWith(PolarisPrivilege.TABLE_WRITE_DATA)
         .shouldPassWith(PolarisPrivilege.TABLE_FULL_METADATA)
@@ -890,7 +1105,7 @@ public abstract class AbstractIcebergCatalogHandlerAuthzTest extends PolarisAuth
    * Creates a wrapper with fine-grained authorization explicitly disabled for testing the fallback
    * behavior to coarse-grained authorization.
    */
-  private IcebergCatalogHandler newWrapperWithFineGrainedAuthzDisabled() {
+  private IcebergCatalogHandler newHandlerWithFineGrainedAuthzDisabled() {
     PolarisPrincipal authenticatedPrincipal = PolarisPrincipal.of(principalEntity, Set.of());
 
     // Create a custom CallContext that returns a custom RealmConfig