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

morningman pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git


The following commit(s) were added to refs/heads/master by this push:
     new e5406443893 [fix](iceberg) Keep the Kerberos identity on Iceberg 
procedure worker threads (#68902)
e5406443893 is described below

commit e54064438936dd408a3577cc6a975f04a6e69f05
Author: Mingyu Chen (Rayner) <[email protected]>
AuthorDate: Mon Oct 12 14:27:53 2026 +0800

    [fix](iceberg) Keep the Kerberos identity on Iceberg procedure worker 
threads (#68902)
    
    ### What problem does this PR solve?
    
    Issue Number: None
    
    Related PR: #67695
    
    Problem Summary:
    
    **In short.** This ports the tests of branch-4.1 #67695, which made
    Iceberg maintenance actions run inside the catalog's authentication
    scope, and fixes what the ported Kerberos case found on master: on a
    Kerberos-secured catalog, `expire_snapshots` committed its new metadata
    and then failed while Iceberg read the expired snapshots' manifest lists
    on its worker threads, which do not carry the catalog's Kerberos
    identity. `cherrypick_snapshot`, `publish_changes` and
    `rewrite_data_files` planning open files on those threads too.
    
    **Background.** On branch-4.x an action loaded the table under the
    catalog's authentication context and then read and committed metadata
    after that context had ended, so on a Kerberized HDFS catalog the commit
    could run under the FE process identity. #67695 captures the action's
    metadata operations and the catalog's authenticator as one generation,
    runs table acquisition and the commit in that scope, and retries once
    when the catalog is reset before the mutation starts.
    
    On master the actions live in `fe-connector-iceberg`:
    `ConnectorExecuteAction` calls `IcebergProcedureOps.execute`, which runs
    the table load, the procedure body and its commit inside one
    `ConnectorContext.executeAuthenticated`. Each call takes its catalog ops
    and its authenticator from the same connector instance, and the catalog
    generation it starts with is leased until the call ends
    (`IcebergCatalogResourceTracker`), so a table from a newer generation
    can never meet an older authenticator, and the generation fence and
    retry of #67695 have nothing to port.
    
    That `doAs` covers only the calling thread, though. Iceberg reads and
    writes metadata files on its shared worker pool, and those threads run
    without the plugin's Kerberos login. For scans and writes the connector
    already wraps the table's `FileIO` so that every file is opened inside
    the catalog's `doAs` (`IcebergScanPlanProvider.wrapTableForScan`,
    `IcebergConnectorTransaction.openTransaction`); a file opened that way
    keeps its authenticated connection on any thread. Procedures were handed
    the raw table.
    
    **The problem, and what it cost.**
    
    1. On a Kerberos-secured HDFS catalog, a procedure fails as soon as
    Iceberg opens a metadata file on a worker thread. The first CI run of
    this PR hit it: `test_iceberg_hadoop_catalog_kerberos` failed at
    `expire_snapshots` with `Failed to expire snapshots: Failed to open
    input stream for file: hdfs://.../metadata/snap-....avro`.
    `RemoveSnapshots` had already committed the new metadata, so the
    snapshots were gone while their files stayed on HDFS, the statement
    reported an error, and the FE did not refresh the table.
    
    | Procedure on a Kerberos HDFS catalog | Opened on a worker thread |
    Before | After |
    |---|---|---|---|
    | `expire_snapshots`, when something expires | the expired snapshots'
    manifest lists and manifests, after the commit | commits, then fails |
    succeeds |
    | `cherrypick_snapshot` / `publish_changes`, when main has moved on |
    the manifest of the picked files | fails | succeeds |
    | `rewrite_data_files` on a table with more than one manifest | the
    manifests, while planning | fails | succeeds |
    
    2. Two of #67695's tests have no counterpart on master:
    
    | branch-4.x test | master |
    |---|---|
    |
    `ExecuteActionCommandTest.executesActionWithinCatalogAuthenticationScope`
    | `IcebergProcedureOpsTest` only asserts that one auth scope is entered.
    Loading the table inside `executeAuthenticated` and running the body
    after it returns keeps every existing case green. |
    | `test_iceberg_hadoop_catalog_kerberos`: `rollback_to_snapshot` and
    `expire_snapshots` on the Kerberized HDFS catalog | The suite creates,
    inserts and counts, but runs no action. |
    
    **How this PR fixes it.**
    
    - `IcebergProcedureOps.wrapTableForProcedure`: the procedure body and
    the rewrite planner get the table with its `FileIO` wrapped like the
    scan and write paths. Non-Kerberos catalogs keep the loaded table
    unchanged.
    - `IcebergAuthenticatedFileIO.withPrefixOperations`: unlike the scan and
    write wrapper, the procedure's `FileIO` keeps the delegate's prefix
    listing, inside the same `doAs`, because `remove_orphan_files` rejects a
    `FileIO` without it. Bulk deletes stay hidden, so deletes go file by
    file through the authenticated `deleteFile`; `HadoopFileIO`'s own bulk
    delete would run on its private pool, outside any `doAs`. One visible
    effect: `expire_snapshots`' `max_concurrent_deletes` used to be ignored
    with a warning on these catalogs, because the raw `HadoopFileIO`
    supports bulk deletes, and now takes effect.
    - Tests:
    - `IcebergProcedureOpsKerberosIoTest` (new) runs procedures on a local
    Hadoop table through an authenticator that marks only the thread inside
    its `doAs`, the way a Kerberos `Subject` is invisible to pool threads,
    and a `FileIO` that records every call made outside it:
    `expire_snapshots` after the rollback sequence of the Kerberos suite,
    `cherrypick_snapshot` after main has moved on, `remove_orphan_files`,
    and `rewrite_data_files` planning on a two-manifest table. A
    non-Kerberos catalog keeps the loaded table.
    - `IcebergAuthenticatedFileIOTest`: prefix listing and deletion run
    inside `doAs`, bulk deletes are not exposed, and a delegate without
    prefix listing gets none.
    - From #67695:
    `IcebergProcedureOpsTest.commitsTheProcedureInsideTheAuthScope` reads
    the table's current snapshot just before `executeAuthenticated` leaves
    its scope and asserts the rollback has already been committed (the test
    `RecordingConnectorContext` gains a hook that runs at that point);
    `test_iceberg_hadoop_catalog_kerberos` gains the two steps of #67695
    unchanged: a second insert, `rollback_to_snapshot` to the first snapshot
    (two rows back to one), a third insert, and
    `expire_snapshots("retain_last" = "1")` (three snapshots down to one).
    
    
    
`ExecuteActionCommandTest.retriesRollbackOnCatalogAuthenticationGenerationChange`
    is not ported: master has no generation retry to test.
    
    Not changed: `ConnectorExecuteAction` refreshes the table only when the
    procedure returns, so a failure after Iceberg's commit still skips the
    refresh. branch-4.x's `ExecuteActionCommand` behaves the same way
    (`logRefreshTable` runs only after the action returns); with this fix
    the Kerberos case no longer fails after the commit.
    
    **Results.** Each change is pinned by a test that fails without it:
    
    | Change undone | Result |
    |---|---|
    | `execute` hands the action the loaded table | the `expire_snapshots`
    case records six opens on `iceberg-worker-pool` threads outside the
    `doAs` (three `snap-*.avro` manifest lists, three manifests), the CI
    failure; the `cherrypick_snapshot` case records the new manifest's
    `newOutputFile` |
    | the rewrite planner gets the loaded table | the planning case records
    two manifest opens on worker threads |
    | the plain `IcebergAuthenticatedFileIO` instead of
    `withPrefixOperations` | `remove_orphan_files` fails with "requires
    FileIO prefix listing support" |
    | the procedure body runs after `executeAuthenticated` returns |
    `commitsTheProcedureInsideTheAuthScope` fails |
    
    The fe-connector-iceberg tests (1507) pass. The External pipeline runs
    `test_iceberg_hadoop_catalog_kerberos`.
    
    Co-authored-by: Gabriel <[email protected]>
---
 .../iceberg/IcebergAuthenticatedFileIO.java        |  48 ++-
 .../connector/iceberg/IcebergProcedureOps.java     |  44 ++-
 .../connector/iceberg/IcebergScanPlanning.java     |  14 +-
 .../iceberg/IcebergAuthenticatedFileIOTest.java    |  60 +++-
 .../iceberg/IcebergProcedureOpsKerberosIoTest.java | 338 +++++++++++++++++++++
 .../connector/iceberg/IcebergProcedureOpsTest.java |  22 ++
 .../iceberg/IcebergRestScanPlanningTest.java       |  42 +++
 .../iceberg/RecordingConnectorContext.java         |   9 +-
 .../test_iceberg_hadoop_catalog_kerberos.groovy    |  71 ++++-
 9 files changed, 634 insertions(+), 14 deletions(-)

diff --git 
a/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergAuthenticatedFileIO.java
 
b/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergAuthenticatedFileIO.java
index bfefb188fb7..c612f448a7c 100644
--- 
a/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergAuthenticatedFileIO.java
+++ 
b/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergAuthenticatedFileIO.java
@@ -20,8 +20,10 @@ package org.apache.doris.connector.iceberg;
 import org.apache.doris.kerberos.HadoopAuthenticator;
 
 import org.apache.iceberg.io.FileIO;
+import org.apache.iceberg.io.FileInfo;
 import org.apache.iceberg.io.InputFile;
 import org.apache.iceberg.io.OutputFile;
+import org.apache.iceberg.io.SupportsPrefixOperations;
 
 import java.io.IOException;
 import java.io.UncheckedIOException;
@@ -54,9 +56,10 @@ import java.util.Objects;
  * <p>The bundled {@code HadoopFileIO} additionally implements {@code 
DelegateFileIO} (bulk / prefix ops), used only
  * by maintenance actions (orphan-file cleanup), never by the append/rewrite 
commit path exercised here; a caller
  * that probes for those interfaces simply falls back to per-file operations, 
which route through the wrapped
- * primitives above. Kept a plain {@link FileIO} deliberately to avoid 
coupling to that optional surface.
+ * primitives above. Kept a plain {@link FileIO} deliberately to avoid 
coupling to that optional surface; the
+ * procedure path, which does need prefix listing, uses {@link 
#withPrefixOperations}.
  */
-final class IcebergAuthenticatedFileIO implements FileIO {
+class IcebergAuthenticatedFileIO implements FileIO {
 
     private final FileIO delegate;
     private final HadoopAuthenticator authenticator;
@@ -66,6 +69,20 @@ final class IcebergAuthenticatedFileIO implements FileIO {
         this.authenticator = Objects.requireNonNull(authenticator, 
"authenticator");
     }
 
+    /**
+     * Wraps {@code delegate} for an {@code ALTER TABLE EXECUTE} procedure. 
When the delegate lists and deletes by
+     * prefix ({@code HadoopFileIO} does), so does the result, inside the same 
{@code doAs}:
+     * {@code remove_orphan_files} rejects a FileIO without prefix listing. 
Bulk deletes stay hidden, so callers
+     * delete file by file through {@link #deleteFile}, which authenticates on 
any thread; {@code HadoopFileIO}'s own
+     * bulk delete would run on its private pool, outside any {@code doAs}.
+     */
+    static IcebergAuthenticatedFileIO withPrefixOperations(FileIO delegate, 
HadoopAuthenticator authenticator) {
+        if (delegate instanceof SupportsPrefixOperations) {
+            return new PrefixOperations((SupportsPrefixOperations) delegate, 
authenticator);
+        }
+        return new IcebergAuthenticatedFileIO(delegate, authenticator);
+    }
+
     @Override
     public InputFile newInputFile(String path) {
         return doAs(() -> delegate.newInputFile(path));
@@ -104,11 +121,36 @@ final class IcebergAuthenticatedFileIO implements FileIO {
         delegate.close();
     }
 
-    private <T> T doAs(PrivilegedExceptionAction<T> action) {
+    <T> T doAs(PrivilegedExceptionAction<T> action) {
         try {
             return authenticator.doAs(action);
         } catch (IOException e) {
             throw new UncheckedIOException(e);
         }
     }
+
+    private static final class PrefixOperations extends 
IcebergAuthenticatedFileIO
+            implements SupportsPrefixOperations {
+
+        private final SupportsPrefixOperations prefixDelegate;
+
+        private PrefixOperations(SupportsPrefixOperations delegate, 
HadoopAuthenticator authenticator) {
+            super(delegate, authenticator);
+            this.prefixDelegate = delegate;
+        }
+
+        @Override
+        public Iterable<FileInfo> listPrefix(String prefix) {
+            // Like newInputFile: HadoopFileIO resolves the FileSystem here 
and the returned iterable lists through it.
+            return doAs(() -> prefixDelegate.listPrefix(prefix));
+        }
+
+        @Override
+        public void deletePrefix(String prefix) {
+            doAs(() -> {
+                prefixDelegate.deletePrefix(prefix);
+                return null;
+            });
+        }
+    }
 }
diff --git 
a/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergProcedureOps.java
 
b/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergProcedureOps.java
index e0a19bd2e5e..1b77d762060 100644
--- 
a/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergProcedureOps.java
+++ 
b/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergProcedureOps.java
@@ -32,6 +32,11 @@ import 
org.apache.doris.connector.spi.procedure.ConnectorRewriteGroup;
 import org.apache.doris.connector.spi.procedure.ConnectorRewriteStatistics;
 import org.apache.doris.connector.spi.procedure.ProcedureExecutionMode;
 import org.apache.doris.connector.spi.pushdown.ConnectorPredicate;
+import org.apache.doris.kerberos.HadoopAuthenticator;
+
+import org.apache.iceberg.BaseTable;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.TableOperations;
 
 import java.time.ZoneId;
 import java.util.Arrays;
@@ -203,7 +208,7 @@ public class IcebergProcedureOps implements 
ConnectorProcedureOps {
             try {
                 result = context.executeAuthenticated(() ->
                         ops.withTable(handle.getDbName(), 
handle.getTableName(),
-                                table -> action.execute(table, session)));
+                                table -> 
action.execute(wrapTableForProcedure(table), session)));
             } catch (DorisConnectorException e) {
                 throw e;
             } catch (Exception e) {
@@ -237,8 +242,11 @@ public class IcebergProcedureOps implements 
ConnectorProcedureOps {
             groups = ops.withTable(handle.getDbName(), handle.getTableName(), 
planner::planAndOrganizeTasks);
         } else {
             try {
-                groups = context.executeAuthenticated(() -> ops.withTable(
-                        handle.getDbName(), handle.getTableName(), 
planner::planAndOrganizeTasks));
+                // A server-planned table (RESTTable) keeps its own newScan(), 
which asks the REST server for the
+                // file scan tasks; rebuilding it as a plain BaseTable would 
plan from local manifests instead.
+                groups = context.executeAuthenticated(() -> 
ops.withTable(handle.getDbName(), handle.getTableName(),
+                        table -> 
planner.planAndOrganizeTasks(IcebergScanPlanning.requiresServerSideScanPlanning(table)
+                                ? table : wrapTableForProcedure(table))));
             } catch (DorisConnectorException e) {
                 throw e;
             } catch (Exception e) {
@@ -249,6 +257,36 @@ public class IcebergProcedureOps implements 
ConnectorProcedureOps {
         return 
groups.stream().map(IcebergProcedureOps::toConnectorRewriteGroup).collect(Collectors.toList());
     }
 
+    /**
+     * Routes the procedure table's {@code io()} through the plugin-side 
Kerberos {@code doAs} — the procedure-path
+     * mirror of {@code IcebergScanPlanProvider.wrapTableForScan} and
+     * {@code IcebergConnectorTransaction.openTransaction}. The {@code 
executeAuthenticated} scope around the body
+     * covers only the calling thread, while iceberg reads and writes metadata 
files on its shared worker pool:
+     * {@code expire_snapshots} reads the expired snapshots' manifest lists 
there after committing (CI:
+     * test_iceberg_hadoop_catalog_kerberos failed "Failed to open input 
stream for file: .../snap-*.avro"),
+     * {@code cherrypick_snapshot} writes the picked files' manifest there, 
and rewrite planning reads the manifests
+     * of a multi-manifest table there. The wrapped FileIO keeps prefix 
listing for {@code remove_orphan_files}
+     * ({@link IcebergAuthenticatedFileIO#withPrefixOperations}). Rewrite 
planning skips the wrap for a server-planned
+     * table, whose scan tasks come from the REST server ({@link 
#planInAuthScopeUntracked}).
+     *
+     * <p>Non-Kerberos catalogs ({@code getPluginAuthenticator() == null}) and 
offline tests (plain context /
+     * non-{@link BaseTable} fakes) get the table unchanged. Package-private 
for unit testing.
+     */
+    Table wrapTableForProcedure(Table table) {
+        if (!(context instanceof TcclPinningConnectorContext) || !(table 
instanceof BaseTable)) {
+            return table;
+        }
+        HadoopAuthenticator auth = ((TcclPinningConnectorContext) 
context).getPluginAuthenticator();
+        if (auth == null) {
+            return table;
+        }
+        BaseTable baseTable = (BaseTable) table;
+        TableOperations rawOps = baseTable.operations();
+        return new BaseTable(new IcebergAuthenticatedTableOperations(rawOps,
+                IcebergAuthenticatedFileIO.withPrefixOperations(rawOps.io(), 
auth)),
+                table.name(), baseTable.reporter());
+    }
+
     private <T> T withCatalogLease(Supplier<T> operation) {
         if (resourceTracker == null) {
             return operation.get();
diff --git 
a/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergScanPlanning.java
 
b/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergScanPlanning.java
index bf48244ff84..dfef09e818b 100644
--- 
a/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergScanPlanning.java
+++ 
b/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergScanPlanning.java
@@ -26,11 +26,17 @@ import org.apache.iceberg.Table;
 final class IcebergScanPlanning {
     private IcebergScanPlanning() {}
 
+    /** Whether the SDK table plans its scans on the catalog server ({@code 
RESTTable}), not from local manifests. */
+    static boolean requiresServerSideScanPlanning(Table table) {
+        // RESTTable overrides this to false; plain BaseTable inherits true.
+        return table instanceof SupportsDistributedScanPlanning
+                && !((SupportsDistributedScanPlanning) 
table).allowDistributedPlanning();
+    }
+
     static void rejectServerSideScanPlanning(Table table, String tableName) {
-        if (table instanceof SupportsDistributedScanPlanning
-                && !((SupportsDistributedScanPlanning) 
table).allowDistributedPlanning()) {
-            // RESTTable overrides this to false; plain BaseTable inherits 
true. Doris cannot consume
-            // server tasks/residuals or scan-scoped credentials yet, so 
reject before local manifest I/O.
+        if (requiresServerSideScanPlanning(table)) {
+            // Doris cannot consume server tasks/residuals or scan-scoped 
credentials yet, so reject before
+            // local manifest I/O.
             throw new DorisConnectorException("Iceberg server-side scan 
planning is not supported for table "
                     + tableName + "; configure the REST catalog to use 
client-side scan planning");
         }
diff --git 
a/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergAuthenticatedFileIOTest.java
 
b/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergAuthenticatedFileIOTest.java
index 352b477f53c..bbabe19075b 100644
--- 
a/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergAuthenticatedFileIOTest.java
+++ 
b/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergAuthenticatedFileIOTest.java
@@ -20,9 +20,13 @@ package org.apache.doris.connector.iceberg;
 import org.apache.doris.kerberos.HadoopAuthenticator;
 
 import org.apache.hadoop.security.UserGroupInformation;
+import org.apache.iceberg.io.DelegateFileIO;
 import org.apache.iceberg.io.FileIO;
+import org.apache.iceberg.io.FileInfo;
 import org.apache.iceberg.io.InputFile;
 import org.apache.iceberg.io.OutputFile;
+import org.apache.iceberg.io.SupportsBulkOperations;
+import org.apache.iceberg.io.SupportsPrefixOperations;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
 
@@ -65,6 +69,35 @@ public class IcebergAuthenticatedFileIOTest {
         Assertions.assertFalse(auth.inDoAs, "doAs must have exited after each 
call");
     }
 
+    @Test
+    public void withPrefixOperationsListsAndDeletesByPrefixInsideDoAs() {
+        RecordingAuthenticator auth = new RecordingAuthenticator();
+        PrefixRecordingFileIO delegate = new PrefixRecordingFileIO(auth);
+
+        IcebergAuthenticatedFileIO io = 
IcebergAuthenticatedFileIO.withPrefixOperations(delegate, auth);
+
+        // remove_orphan_files rejects a FileIO without prefix listing.
+        Assertions.assertTrue(io instanceof SupportsPrefixOperations);
+        // HadoopFileIO's bulk delete runs on its own pool, outside any doAs, 
so callers must delete one by one.
+        Assertions.assertFalse(io instanceof SupportsBulkOperations, "bulk 
deletes must stay hidden");
+        ((SupportsPrefixOperations) io).listPrefix("hdfs://nn/t/");
+        ((SupportsPrefixOperations) io).deletePrefix("hdfs://nn/t/tmp/");
+        io.newInputFile("hdfs://nn/t/in");
+        Assertions.assertEquals(3, auth.doAsCount);
+        Assertions.assertEquals(Arrays.asList("listPrefix", "deletePrefix", 
"newInputFile"), delegate.reachedInDoAs);
+    }
+
+    @Test
+    public void withPrefixOperationsAddsNoPrefixListingToAPlainDelegate() {
+        RecordingAuthenticator auth = new RecordingAuthenticator();
+
+        IcebergAuthenticatedFileIO io =
+                IcebergAuthenticatedFileIO.withPrefixOperations(new 
RecordingFileIO(auth), auth);
+
+        Assertions.assertFalse(io instanceof SupportsPrefixOperations,
+                "the wrapper must not claim prefix listing its delegate cannot 
do");
+    }
+
     @Test
     public void nonAuthMethodsForwardWithoutDoAs() {
         RecordingAuthenticator auth = new RecordingAuthenticator();
@@ -105,7 +138,7 @@ public class IcebergAuthenticatedFileIOTest {
     }
 
     /** FileIO double: records the name of each factory call together with 
whether it ran inside a doAs. */
-    private static final class RecordingFileIO implements FileIO {
+    private static class RecordingFileIO implements FileIO {
         private final RecordingAuthenticator auth;
         final List<String> reachedInDoAs = new ArrayList<>();
         boolean closed;
@@ -114,7 +147,7 @@ public class IcebergAuthenticatedFileIOTest {
             this.auth = auth;
         }
 
-        private void record(String op) {
+        void record(String op) {
             // Only record when observed inside the authenticator's doAs — the 
property under test.
             if (auth.inDoAs) {
                 reachedInDoAs.add(op);
@@ -150,4 +183,27 @@ public class IcebergAuthenticatedFileIOTest {
             closed = true;
         }
     }
+
+    /** Prefix-capable double shaped like {@code HadoopFileIO}, which also 
offers bulk deletes. */
+    private static final class PrefixRecordingFileIO extends RecordingFileIO 
implements DelegateFileIO {
+        PrefixRecordingFileIO(RecordingAuthenticator auth) {
+            super(auth);
+        }
+
+        @Override
+        public Iterable<FileInfo> listPrefix(String prefix) {
+            record("listPrefix");
+            return Collections.emptyList();
+        }
+
+        @Override
+        public void deletePrefix(String prefix) {
+            record("deletePrefix");
+        }
+
+        @Override
+        public void deleteFiles(Iterable<String> pathsToDelete) {
+            record("deleteFiles");
+        }
+    }
 }
diff --git 
a/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergProcedureOpsKerberosIoTest.java
 
b/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergProcedureOpsKerberosIoTest.java
new file mode 100644
index 00000000000..d4d78c10e1a
--- /dev/null
+++ 
b/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergProcedureOpsKerberosIoTest.java
@@ -0,0 +1,338 @@
+// 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.doris.connector.iceberg;
+
+import org.apache.doris.connector.spi.procedure.ConnectorProcedureResult;
+import org.apache.doris.connector.spi.procedure.ConnectorRewriteGroup;
+import org.apache.doris.kerberos.HadoopAuthenticator;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.security.UserGroupInformation;
+import org.apache.iceberg.BaseTable;
+import org.apache.iceberg.DataFile;
+import org.apache.iceberg.DataFiles;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.Snapshot;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.hadoop.HadoopFileIO;
+import org.apache.iceberg.hadoop.HadoopTableOperations;
+import org.apache.iceberg.hadoop.HadoopTables;
+import org.apache.iceberg.io.FileInfo;
+import org.apache.iceberg.io.InputFile;
+import org.apache.iceberg.io.OutputFile;
+import org.apache.iceberg.io.SupportsPrefixOperations;
+import org.apache.iceberg.types.Types;
+import org.apache.iceberg.util.LockManagers;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.attribute.FileTime;
+import java.security.PrivilegedExceptionAction;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * Guards the Kerberos I/O seam of {@code ALTER TABLE ... EXECUTE}: every 
metadata file a procedure opens, lists or
+ * deletes must be reached inside the plugin-side {@code doAs}, including the 
files iceberg opens on its shared
+ * worker pool.
+ *
+ * <p>WHY it matters: {@link IcebergProcedureOps} runs the procedure inside 
{@code executeAuthenticated}, whose
+ * {@code doAs} covers only the calling thread. {@code expire_snapshots} 
commits the new metadata and then reads the
+ * expired snapshots' manifest lists on the worker pool; through the raw table 
FileIO those reads reach secured HDFS
+ * without Kerberos credentials (CI: test_iceberg_hadoop_catalog_kerberos 
failed "Failed to open input stream for
+ * file: .../snap-*.avro" on expire_snapshots after rollback_to_snapshot). The 
authenticator here marks only the
+ * thread that entered {@code doAs}, as a Kerberos {@code Subject} is 
invisible to pool threads, and the table's
+ * FileIO records every call made outside that mark.
+ *
+ * <p>No Mockito: real {@code HadoopTables} tables in a temp directory and 
recording doubles, mirroring
+ * {@link IcebergScanPlanProviderKerberosScanIoTest}.
+ */
+public class IcebergProcedureOpsKerberosIoTest {
+
+    private static final Schema SCHEMA = new 
Schema(Types.NestedField.required(1, "id", Types.IntegerType.get()));
+    private static final IcebergTableHandle HANDLE = new 
IcebergTableHandle("db1", "t1");
+
+    @Test
+    public void 
expireSnapshotsReadsExpiredManifestListsInsidePluginDoAs(@TempDir Path temp) 
throws Exception {
+        // The CI sequence: the rollback leaves the second snapshot outside 
main's ancestry, so the expiry walks the
+        // reachable files and reads every expired manifest list on the worker 
pool.
+        Table table = createTable(temp);
+        appendFile(table, temp.resolve("s1.parquet"));
+        long firstSnapshot = table.currentSnapshot().snapshotId();
+        appendFile(table, temp.resolve("s2.parquet"));
+        table.manageSnapshots().rollbackTo(firstSnapshot).commit();
+        appendFile(table, temp.resolve("s3.parquet"));
+        ThreadMarkingAuthenticator auth = new ThreadMarkingAuthenticator();
+        DoAsCheckingFileIO io = new DoAsCheckingFileIO(auth);
+
+        kerberosProcedureOps(withFileIO(table, io), auth).execute(null, HANDLE,
+                "expire_snapshots", ImmutableMap.of("retain_last", "1"), null, 
Collections.emptyList());
+
+        table.refresh();
+        Assertions.assertEquals(1, 
ImmutableList.copyOf(table.snapshots()).size());
+        // Only the second snapshot's data file is left unreferenced, and the 
cleanup deleted it.
+        Assertions.assertFalse(Files.exists(temp.resolve("s2.parquet")));
+        Assertions.assertTrue(Files.exists(temp.resolve("s1.parquet")));
+        Assertions.assertTrue(Files.exists(temp.resolve("s3.parquet")));
+        Assertions.assertTrue(io.workerThreadCalls.get() > 0,
+                "the cleanup must have opened files on iceberg's worker pool, 
or this test proves nothing");
+        // MUTATION: handing the action the loaded table instead of 
wrapTableForProcedure(table) -> the worker-pool
+        // manifest-list reads run outside the doAs -> red (the CI failure).
+        Assertions.assertEquals(Collections.emptyList(), io.callsOutsideDoAs);
+    }
+
+    @Test
+    public void cherrypickSnapshotWritesItsManifestInsidePluginDoAs(@TempDir 
Path temp) throws Exception {
+        // Main moves on after the append is staged, so the cherry-pick cannot 
fast-forward: iceberg writes the
+        // picked files into a new manifest on its worker pool.
+        Table table = createTable(temp);
+        appendFile(table, temp.resolve("a.parquet"));
+        long mainSnapshot = table.currentSnapshot().snapshotId();
+        
table.newAppend().appendFile(dataFile(createOldFile(temp.resolve("staged.parquet")))).stageOnly().commit();
+        table.refresh();
+        long stagedSnapshot = 
ImmutableList.copyOf(table.snapshots()).stream().mapToLong(Snapshot::snapshotId)
+                .filter(id -> id != mainSnapshot).findFirst().getAsLong();
+        appendFile(table, temp.resolve("b.parquet"));
+        ThreadMarkingAuthenticator auth = new ThreadMarkingAuthenticator();
+        DoAsCheckingFileIO io = new DoAsCheckingFileIO(auth);
+
+        kerberosProcedureOps(withFileIO(table, io), auth).execute(null, 
HANDLE, "cherrypick_snapshot",
+                ImmutableMap.of("snapshot_id", 
String.valueOf(stagedSnapshot)), null, Collections.emptyList());
+
+        table.refresh();
+        Assertions.assertEquals("3", 
table.currentSnapshot().summary().get("total-data-files"));
+        Assertions.assertTrue(io.workerThreadCalls.get() > 0,
+                "the cherry-pick must have written its manifest on iceberg's 
worker pool, or this test proves nothing");
+        // MUTATION: handing the action the loaded table instead of 
wrapTableForProcedure(table) -> the worker-pool
+        // manifest write runs outside the doAs -> red.
+        Assertions.assertEquals(Collections.emptyList(), io.callsOutsideDoAs);
+    }
+
+    @Test
+    public void removeOrphanFilesListsAndDeletesInsidePluginDoAs(@TempDir Path 
temp) throws Exception {
+        Table table = createTable(temp);
+        appendFile(table, temp.resolve("table/data/live.parquet"));
+        Path orphan = createOldFile(temp.resolve("table/data/orphan.parquet"));
+        ThreadMarkingAuthenticator auth = new ThreadMarkingAuthenticator();
+        DoAsCheckingFileIO io = new DoAsCheckingFileIO(auth);
+
+        // MUTATION: wrapping with a FileIO that drops the delegate's prefix 
listing -> "remove_orphan_files
+        // requires FileIO prefix listing support" -> red.
+        ConnectorProcedureResult result = 
kerberosProcedureOps(withFileIO(table, io), auth).execute(null, HANDLE,
+                "remove_orphan_files", ImmutableMap.of(
+                        "older_than", 
String.valueOf(System.currentTimeMillis() - Duration.ofHours(24).toMillis()),
+                        "dry_run", "false"),
+                null, Collections.emptyList());
+
+        Assertions.assertEquals(ImmutableList.of("1", "1"), 
result.getRows().get(0));
+        Assertions.assertFalse(Files.exists(orphan));
+        
Assertions.assertTrue(Files.exists(temp.resolve("table/data/live.parquet")));
+        Assertions.assertTrue(io.listPrefixCalls.get() > 0, "the orphan scan 
must list through the table FileIO");
+        Assertions.assertEquals(Collections.emptyList(), io.callsOutsideDoAs);
+    }
+
+    @Test
+    public void rewritePlanningReadsManifestsInsidePluginDoAs(@TempDir Path 
temp) throws Exception {
+        // Two fast appends leave two data manifests, so iceberg plans the 
scan on its worker pool.
+        Table table = createTable(temp);
+        appendFile(table, temp.resolve("a.parquet"));
+        appendFile(table, temp.resolve("b.parquet"));
+        ThreadMarkingAuthenticator auth = new ThreadMarkingAuthenticator();
+        DoAsCheckingFileIO io = new DoAsCheckingFileIO(auth);
+
+        List<ConnectorRewriteGroup> groups = 
kerberosProcedureOps(withFileIO(table, io), auth).planRewrite(null, HANDLE,
+                "rewrite_data_files", ImmutableMap.of("rewrite-all", "true"), 
null, Collections.emptyList());
+
+        Assertions.assertEquals(1, groups.size());
+        Assertions.assertEquals(2, groups.get(0).getDataFileCount());
+        Assertions.assertTrue(io.workerThreadCalls.get() > 0,
+                "planning must have read manifests on iceberg's worker pool, 
or this test proves nothing");
+        // MUTATION: planning the loaded table instead of 
wrapTableForProcedure(table) -> the worker-pool manifest
+        // reads run outside the doAs -> red.
+        Assertions.assertEquals(Collections.emptyList(), io.callsOutsideDoAs);
+    }
+
+    @Test
+    public void nonKerberosProcedureKeepsTheLoadedTable(@TempDir Path temp) {
+        Table table = createTable(temp);
+        // A null plugin authenticator is a non-Kerberos catalog; a plain 
context is an offline test or fe-core fake.
+        IcebergProcedureOps nonKerberos = new 
IcebergProcedureOps(Collections.emptyMap(), opsReturning(table),
+                new TcclPinningConnectorContext(new 
RecordingConnectorContext(), getClass().getClassLoader(),
+                        () -> null));
+        IcebergProcedureOps plainContext = new 
IcebergProcedureOps(Collections.emptyMap(), opsReturning(table),
+                new RecordingConnectorContext());
+
+        Assertions.assertSame(table, nonKerberos.wrapTableForProcedure(table));
+        Assertions.assertSame(table, 
plainContext.wrapTableForProcedure(table));
+    }
+
+    private static Table createTable(Path temp) {
+        return new HadoopTables(new Configuration()).create(SCHEMA, 
PartitionSpec.unpartitioned(),
+                Collections.emptyMap(), 
temp.resolve("table").toUri().toString());
+    }
+
+    private static void appendFile(Table table, Path path) throws IOException {
+        
table.newFastAppend().appendFile(dataFile(createOldFile(path))).commit();
+    }
+
+    private static DataFile dataFile(Path path) {
+        return DataFiles.builder(PartitionSpec.unpartitioned())
+                .withPath(path.toUri().toString())
+                .withFileSizeInBytes(1)
+                .withRecordCount(1)
+                .build();
+    }
+
+    private static Path createOldFile(Path path) throws IOException {
+        Files.createDirectories(path.getParent());
+        Files.write(path, new byte[] {1});
+        Files.setLastModifiedTime(path, FileTime.fromMillis(1));
+        return path;
+    }
+
+    /** The same table, reopened over {@code io} so that every metadata file 
goes through it. */
+    private static Table withFileIO(Table table, DoAsCheckingFileIO io) {
+        HadoopTableOperations ops = new HadoopTableOperations(new 
org.apache.hadoop.fs.Path(table.location()), io,
+                new Configuration(), LockManagers.defaultLockManager()) {
+        };
+        return new BaseTable(ops, table.name());
+    }
+
+    private static IcebergProcedureOps kerberosProcedureOps(Table table, 
ThreadMarkingAuthenticator auth) {
+        TcclPinningConnectorContext context = new TcclPinningConnectorContext(
+                new RecordingConnectorContext(), 
IcebergProcedureOpsKerberosIoTest.class.getClassLoader(), () -> auth);
+        return new IcebergProcedureOps(Collections.emptyMap(), 
opsReturning(table), context);
+    }
+
+    private static RecordingIcebergCatalogOps opsReturning(Table table) {
+        RecordingIcebergCatalogOps ops = new RecordingIcebergCatalogOps();
+        ops.table = table;
+        return ops;
+    }
+
+    /**
+     * Runs the action inline and marks only the current thread as inside the 
doAs, as a Kerberos {@code Subject}
+     * entered on one thread is invisible to iceberg's pool threads.
+     */
+    private static final class ThreadMarkingAuthenticator implements 
HadoopAuthenticator {
+        private final ThreadLocal<Boolean> inDoAs = ThreadLocal.withInitial(() 
-> false);
+
+        boolean inDoAs() {
+            return inDoAs.get();
+        }
+
+        @Override
+        public UserGroupInformation getUGI() {
+            throw new UnsupportedOperationException("doAs is overridden; no 
real UGI in this test");
+        }
+
+        @Override
+        public <T> T doAs(PrivilegedExceptionAction<T> action) throws 
IOException {
+            boolean outer = inDoAs.get();
+            inDoAs.set(true);
+            try {
+                return action.run();
+            } catch (IOException | RuntimeException e) {
+                throw e;
+            } catch (Exception e) {
+                throw new IOException(e);
+            } finally {
+                inDoAs.set(outer);
+            }
+        }
+    }
+
+    /**
+     * {@code HadoopFileIO} that records each call made outside the 
authenticator's doAs and counts the calls made
+     * off the test thread.
+     */
+    private static final class DoAsCheckingFileIO implements 
SupportsPrefixOperations {
+        private final HadoopFileIO delegate = new HadoopFileIO(new 
Configuration());
+        private final ThreadMarkingAuthenticator auth;
+        private final Thread testThread = Thread.currentThread();
+        final List<String> callsOutsideDoAs = Collections.synchronizedList(new 
ArrayList<>());
+        final AtomicInteger workerThreadCalls = new AtomicInteger();
+        final AtomicInteger listPrefixCalls = new AtomicInteger();
+
+        DoAsCheckingFileIO(ThreadMarkingAuthenticator auth) {
+            this.auth = auth;
+        }
+
+        private void check(String op, String path) {
+            if (Thread.currentThread() != testThread) {
+                workerThreadCalls.incrementAndGet();
+            }
+            if (!auth.inDoAs()) {
+                callsOutsideDoAs.add(op + " " + path + " on " + 
Thread.currentThread().getName());
+            }
+        }
+
+        @Override
+        public InputFile newInputFile(String path) {
+            check("newInputFile", path);
+            return delegate.newInputFile(path);
+        }
+
+        @Override
+        public InputFile newInputFile(String path, long length) {
+            check("newInputFile", path);
+            return delegate.newInputFile(path, length);
+        }
+
+        @Override
+        public OutputFile newOutputFile(String path) {
+            check("newOutputFile", path);
+            return delegate.newOutputFile(path);
+        }
+
+        @Override
+        public void deleteFile(String path) {
+            check("deleteFile", path);
+            delegate.deleteFile(path);
+        }
+
+        @Override
+        public Iterable<FileInfo> listPrefix(String prefix) {
+            check("listPrefix", prefix);
+            listPrefixCalls.incrementAndGet();
+            return delegate.listPrefix(prefix);
+        }
+
+        @Override
+        public void deletePrefix(String prefix) {
+            check("deletePrefix", prefix);
+            delegate.deletePrefix(prefix);
+        }
+
+        @Override
+        public Map<String, String> properties() {
+            return delegate.properties();
+        }
+    }
+}
diff --git 
a/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergProcedureOpsTest.java
 
b/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergProcedureOpsTest.java
index fd7e4035738..4370ccbb367 100644
--- 
a/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergProcedureOpsTest.java
+++ 
b/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergProcedureOpsTest.java
@@ -45,6 +45,7 @@ import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
+import java.util.concurrent.atomic.AtomicLong;
 import java.util.stream.Collectors;
 
 /**
@@ -298,6 +299,27 @@ public class IcebergProcedureOpsTest {
         Assertions.assertEquals(1, ops.tableCleanupCount, "procedure execution 
releases its operation-owned table");
     }
 
+    @Test
+    public void commitsTheProcedureInsideTheAuthScope() {
+        // On a Kerberized catalog the metadata commit must run under the 
catalog's identity, so the procedure has
+        // committed before the auth scope ends, not after the scope hands the 
loaded table back.
+        // MUTATION: loading the table inside executeAuthenticated but running 
the body outside it -> red.
+        InMemoryCatalog catalog = catalogWithTwoSnapshots();
+        TableIdentifier id = TableIdentifier.of("db1", "t");
+        long snap1 = catalog.loadTable(id).history().get(0).snapshotId();
+        RecordingIcebergCatalogOps ops = new RecordingIcebergCatalogOps();
+        ops.table = catalog.loadTable(id);
+        RecordingConnectorContext ctx = new RecordingConnectorContext();
+        AtomicLong snapshotAtScopeExit = new AtomicLong();
+        ctx.beforeScopeExit = () -> 
snapshotAtScopeExit.set(catalog.loadTable(id).currentSnapshot().snapshotId());
+        IcebergProcedureOps procOps = new 
IcebergProcedureOps(Collections.emptyMap(), ops, ctx);
+
+        procOps.execute(SESSION, new IcebergTableHandle("db1", "t"), 
"rollback_to_snapshot",
+                ImmutableMap.of("snapshot_id", String.valueOf(snap1)), null, 
Collections.emptyList());
+
+        Assertions.assertEquals(snap1, snapshotAtScopeExit.get(), "the 
rollback is committed inside the auth scope");
+    }
+
     @Test
     public void threadsSessionToTimestampBody() {
         InMemoryCatalog catalog = catalogWithTwoSnapshots();
diff --git 
a/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergRestScanPlanningTest.java
 
b/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergRestScanPlanningTest.java
index bb48030e317..fe0de66d869 100644
--- 
a/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergRestScanPlanningTest.java
+++ 
b/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergRestScanPlanningTest.java
@@ -24,8 +24,10 @@ import org.apache.doris.connector.spi.ConnectorSession;
 import org.apache.doris.connector.spi.ConnectorStatementScope;
 import org.apache.doris.connector.spi.DorisConnectorException;
 import org.apache.doris.connector.spi.scan.ConnectorScanRequest;
+import org.apache.doris.kerberos.HadoopAuthenticator;
 
 import com.sun.net.httpserver.HttpServer;
+import org.apache.hadoop.security.UserGroupInformation;
 import org.apache.iceberg.BaseTable;
 import org.apache.iceberg.DataFiles;
 import org.apache.iceberg.PartitionSpec;
@@ -52,6 +54,7 @@ import org.junit.jupiter.api.function.Executable;
 import java.io.IOException;
 import java.net.InetSocketAddress;
 import java.nio.charset.StandardCharsets;
+import java.security.PrivilegedExceptionAction;
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.atomic.AtomicInteger;
@@ -202,6 +205,26 @@ public class IcebergRestScanPlanningTest {
         }
     }
 
+    @Test
+    public void kerberosRewritePlanningAsksTheServerForTasks() throws 
Exception {
+        try (RestFixture fixture = new RestFixture(true, true, true)) {
+            Table table = fixture.load();
+            RecordingIcebergCatalogOps ops = new RecordingIcebergCatalogOps();
+            ops.table = table;
+            IcebergProcedureOps procedureOps = new 
IcebergProcedureOps(Map.of(), ops, new TcclPinningConnectorContext(
+                    new RecordingConnectorContext(), 
getClass().getClassLoader(), InlineAuthenticator::new));
+
+            // The fixture answers every plan request with HTTP 500, so 
planning fails; the test is where it went.
+            Assertions.assertThrows(DorisConnectorException.class, () -> 
procedureOps.planRewrite(null,
+                    new IcebergTableHandle("db", "t"), "rewrite_data_files", 
Map.of("rewrite-all", "true"), null,
+                    List.of()));
+            // MUTATION: planning wrapTableForProcedure(table) -> the plain 
BaseTable plans from local manifests ->
+            // the recording FileIO fails the read and no plan request reaches 
the server -> red.
+            Assertions.assertEquals(1, fixture.planRequests.get());
+            Assertions.assertEquals(0, fixture.io(table).reads.get());
+        }
+    }
+
     private static void reject(Executable action) {
         DorisConnectorException failure = 
Assertions.assertThrows(DorisConnectorException.class, action);
         Assertions.assertEquals("Iceberg server-side scan planning is not 
supported for table db.t; "
@@ -280,6 +303,25 @@ public class IcebergRestScanPlanningTest {
         }
     }
 
+    /** A Kerberos-catalog stand-in: runs the action inline, without a real 
UGI. */
+    private static final class InlineAuthenticator implements 
HadoopAuthenticator {
+        @Override
+        public UserGroupInformation getUGI() {
+            throw new UnsupportedOperationException("doAs is overridden; no 
real UGI in this test");
+        }
+
+        @Override
+        public <T> T doAs(PrivilegedExceptionAction<T> action) throws 
IOException {
+            try {
+                return action.run();
+            } catch (IOException | RuntimeException e) {
+                throw e;
+            } catch (Exception e) {
+                throw new IOException(e);
+            }
+        }
+    }
+
     public static class RecordingFileIO implements FileIO {
         private final AtomicInteger reads = new AtomicInteger();
 
diff --git 
a/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/RecordingConnectorContext.java
 
b/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/RecordingConnectorContext.java
index 238637b7f35..a2d2240af72 100644
--- 
a/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/RecordingConnectorContext.java
+++ 
b/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/RecordingConnectorContext.java
@@ -56,6 +56,9 @@ final class RecordingConnectorContext implements 
ConnectorContext, ConnectorStor
 
     int authCount;
     boolean failAuth;
+    /** Runs when a task has finished but {@link #executeAuthenticated} has 
not left its scope yet, so a test can
+     * see what the task did inside the scope. */
+    Runnable beforeScopeExit;
 
     /** Storage properties the fake returns from {@link 
#getStorageProperties()} — the typed fe-filesystem
      * seam both the scan and (design S3) the write path derive their 
BE-canonical static creds from via
@@ -168,7 +171,11 @@ final class RecordingConnectorContext implements 
ConnectorContext, ConnectorStor
             // Deliberately do NOT call task -> the wrapped seam call must not 
run.
             throw new RuntimeException("auth failed");
         }
-        return task.call();
+        T result = task.call();
+        if (beforeScopeExit != null) {
+            beforeScopeExit.run();
+        }
+        return result;
     }
 
     /** Locations the connector asked the engine to clean (B1 managed-location 
cleanup). */
diff --git 
a/regression-test/suites/external_table_p0/kerberos/test_iceberg_hadoop_catalog_kerberos.groovy
 
b/regression-test/suites/external_table_p0/kerberos/test_iceberg_hadoop_catalog_kerberos.groovy
index 9844499284d..77a0926a6ac 100644
--- 
a/regression-test/suites/external_table_p0/kerberos/test_iceberg_hadoop_catalog_kerberos.groovy
+++ 
b/regression-test/suites/external_table_p0/kerberos/test_iceberg_hadoop_catalog_kerberos.groovy
@@ -97,6 +97,75 @@ suite("test_iceberg_hadoop_catalog_kerberos", "p0,external") 
{
           'partition_val2'
         );
     """
+    def snapshotsAfterFirstInsert = sql """
+        select snapshot_id from ${test_tbl_name}\$snapshots order by 
committed_at
+    """
+    assert snapshotsAfterFirstInsert.size() == 1
+    String firstSnapshotId = String.valueOf(snapshotsAfterFirstInsert[0][0])
+
+    sql """
+        insert into ${test_tbl_name} values (
+          '2024-05-27 12:34:56',
+          false,
+          456,
+          4567890123456,
+          45.67,
+          89.012,
+          45678.9012,
+          'another example',
+          '2024-05-27',
+          '2024-05-27 14:00:00',
+          'partition_val1',
+          'partition_val2'
+        );
+    """
+    def dataBeforeRollback = sql """select count(1) from ${test_tbl_name}"""
+    assert dataBeforeRollback.get(0).get(0) == 2
+
+    // Rollback must commit metadata with the same Kerberos identity used to 
acquire the table.
+    sql """
+        alter table ${catalog_name}.${database_name}.${test_tbl_name}
+        execute rollback_to_snapshot("snapshot_id" = "${firstSnapshotId}")
+    """
+    def dataAfterRollback = sql """select count(1) from ${test_tbl_name}"""
+    assert dataAfterRollback.get(0).get(0) == 1
+
+    sql """
+        insert into ${test_tbl_name} values (
+          '2024-05-27 12:34:56',
+          false,
+          456,
+          4567890123456,
+          45.67,
+          89.012,
+          45678.9012,
+          'another example',
+          '2024-05-27',
+          '2024-05-27 14:00:00',
+          'partition_val1',
+          'partition_val2'
+        );
+    """
+
+    def snapshotsBeforeExpire = sql """
+        select snapshot_id from ${test_tbl_name}\$snapshots order by 
committed_at
+    """
+    assert snapshotsBeforeExpire.size() == 3
+
+    // The action must reuse the catalog's Kerberos identity while committing 
new metadata.
+    sql """
+        alter table ${catalog_name}.${database_name}.${test_tbl_name}
+        execute expire_snapshots("retain_last" = "1")
+    """
+
+    def snapshotsAfterExpire = sql """
+        select snapshot_id from ${test_tbl_name}\$snapshots order by 
committed_at
+    """
+    assert snapshotsAfterExpire.size() == 1
+    // COUNT(1) can be answered from the manifests alone; read the rows so 
both retained data files are opened.
+    def rowsAfterExpire = sql """select col2, col7 from ${test_tbl_name} order 
by col2"""
+    assert rowsAfterExpire == [[123, 'example text'], [456, 'another example']]
+
     def dataResult = sql """select count(1) from ${test_tbl_name} """
-    assert dataResult.get(0).get(0) == 1
+    assert dataResult.get(0).get(0) == 2
 }


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to