github-actions[bot] commented on code in PR #65867:
URL: https://github.com/apache/doris/pull/65867#discussion_r3683702461


##########
fe/fe-connector/fe-connector-paimon/src/main/java/org/apache/doris/connector/paimon/PaimonScanPlanProvider.java:
##########
@@ -958,6 +964,245 @@ public Map<String, String> getScanNodeProperties(
         return props;
     }
 
+    /**
+     * Build the Paimon table object that is serialized to the BE.
+     *
+     * <p>Every table loaded from a metastore-backed Paimon catalog (HMS / 
DLF) carries a Paimon
+     * {@code HiveCatalogLoader} in its {@link CatalogEnvironment}. The BE 
only reads — via
+     * FE-resolved splits and the object store — and never needs the catalog, 
yet deserializing that
+     * loader forces the whole Hive metastore stack onto the BE classpath: 
{@code HiveConf}, the
+     * metastore API, and, when a system table resolves its latest snapshot, 
even the metastore
+     * client (DLF's {@code ProxyMetaStoreClient} and its REST stack). So we 
serialize a catalog-less
+     * table to the BE:
+     * <ul>
+     *   <li>data table: drop the catalog loader. A {@link FileStoreTable} is 
fully defined by
+     *       fileIO / location / schema / catalogEnvironment, and its dynamic 
options (time travel,
+     *       incremental) are merged into the schema by {@code copy(...)} — 
which
+     *       {@link #resolveScanTable} has already applied — so rebuilding from
+     *       fileIO / location / schema preserves everything except the 
catalog loader.</li>
+     *   <li>system table (e.g. {@code $snapshots}): rebuild it over a 
catalog-less data table so
+     *       {@code SnapshotManager#latestSnapshotId} lists the snapshot 
directory on the filesystem
+     *       instead of calling the metastore. The base table is the one the 
FE-side wrapper was
+     *       built over ({@link PaimonTableHandle#getSysBaseTable()}), and for 
the system tables that
+     *       pick their snapshot on the BE ({@link 
PaimonScanParams#resolvesSnapshotOnBackend}) what
+     *       the catalog would have done there is done here instead: see
+     *       {@link #authorizeDeferredScan} and {@link #pinCatalogSnapshot}. 
Every other system table
+     *       reads what the FE already planned, so it is handed over 
untouched. The relation-scoped
+     *       scan params {@link #resolveScanTable} applied to the original 
wrapper are re-applied to
+     *       the rebuilt one by {@link #reapplyScanParams}.</li>
+     * </ul>
+     */
+    // Package-private for direct unit testing (PaimonBackendBoundTableTest).
+    Table tableForBackend(PaimonTableHandle handle, Table scanTable) {
+        if (scanTable instanceof FileStoreTable) {
+            // resolveScanTable's copy(...) merged the relation's dynamic 
options into the schema,
+            // and the rebuild below goes through that schema, so this branch 
needs no re-application.
+            return dropCatalogLoader((FileStoreTable) scanTable);
+        }
+        if (!handle.isSystemTable()) {
+            return scanTable;
+        }
+        // The very same base table the FE-side wrapper was built over, so 
that the BE never sees a
+        // different schema generation than the one this query was planned 
with.
+        FileStoreTable dataTable = handle.getSysBaseTable();
+        if (dataTable == null) {
+            return scanTable;
+        }
+        String sysTableType = handle.getSysTableName();
+        boolean resolvesOnBackend = 
PaimonScanParams.resolvesSnapshotOnBackend(sysTableType);
+        if (PAIMON_FILES_SYSTEM_TABLE.equalsIgnoreCase(sysTableType)) {
+            authorizeDeferredScan(dataTable);
+        }
+        FileStoreTable baseForBackend = dropCatalogLoader(dataTable);
+        if (resolvesOnBackend) {
+            baseForBackend = pinCatalogSnapshot(baseForBackend, dataTable);
+        }
+        Table catalogLessSysTable = SystemTableLoader.load(sysTableType, 
baseForBackend);
+        if (catalogLessSysTable == null) {
+            return scanTable;
+        }
+        return reapplyScanParams(catalogLessSysTable, dataTable, 
resolvesOnBackend,
+                handle.getScanOptions());
+    }
+
+    /**
+     * Re-apply the relation-scoped scan params to a rebuilt system-table 
wrapper.
+     *
+     * <p>{@link #resolveScanTable} applies {@code @incr} / {@code @options} 
to the wrapper the
+     * handle carries, and every Paimon system table delegates {@code 
copy(...)} to the data table it
+     * wraps. The wrapper rebuilt above is a different object, so the same 
copy has to be redone on
+     * it, otherwise the BE would materialize its splits against the unpinned 
latest state. The
+     * branches mirror {@link #resolveScanTable}'s exactly — one chokepoint's 
worth of logic applied
+     * to two different objects.
+     *
+     * <p>This runs last on purpose: an explicit relation option outranks 
anything this class pins on
+     * the rebuilt table, and {@code copy(...)} lets the option win. An 
incremental relation outranks
+     * {@link #pinCatalogSnapshot} the same way but cannot inherit its bound, 
so it is bound to the
+     * catalog's snapshot separately by {@link 
PaimonIncrementalScanParams#bindRangeToCatalog}.
+     *
+     * <p>Known gap: the {@code @options} branch reaches tables outside
+     * {@link PaimonScanParams#resolvesSnapshotOnBackend}, {@code $ro} among 
them, and on a
+     * {@code scan.fallback-branch} table the {@code copy(...)} here is what 
triggers
+     * {@code rewriteFallbackOptions} - on the already catalog-less pair, so 
the fallback branch's
+     * {@code scan.snapshot-id} is derived from its snapshot directory rather 
than from the catalog
+     * pointer, the same gap {@link #pinCatalogSnapshot} documents. It lands 
harder here: unlike the
+     * pin this is {@code copy(...)}, not {@code copyWithoutTimeTravel(...)}, 
so it also time-travels
+     * the fallback branch's schema, and {@code $ro} is a data table rather 
than read-only metadata.
+     */
+    private Table reapplyScanParams(Table rebuiltSysTable, FileStoreTable 
dataTable,
+            boolean resolvesOnBackend, Map<String, String> scanOptions) {
+        if (scanOptions == null || scanOptions.isEmpty()) {
+            return rebuiltSysTable;
+        }
+        if (PaimonScanParams.isOptionsPin(scanOptions)) {
+            return PaimonScanParams.applyOptions(rebuiltSysTable, scanOptions);
+        }
+        Map<String, String> params = resolvesOnBackend
+                ? PaimonIncrementalScanParams.bindRangeToCatalog(scanOptions, 
dataTable)
+                : scanOptions;
+        return 
rebuiltSysTable.copy(PaimonIncrementalScanParams.applyResetsIfIncremental(params));
+    }
+
+    /**
+     * {@code $files} plans only partition-level splits on the FE and re-plans 
the base table on the
+     * BE through {@code DataTableScan#plan()} ({@code 
FilesTable.FilesRead#createReader}). That
+     * deferred plan normally authorizes itself through the catalog loader
+     * ({@code CatalogEnvironment#tableQueryAuth} -&gt; {@code 
Catalog#authTableQuery}); once the
+     * loader is dropped it silently allows everything. So authorize here, 
while the loader is still
+     * around. Paimon discards the predicates the call returns (row level 
access control is a TODO in
+     * {@code AbstractDataTableScan#authQuery}), so running it on the FE loses 
nothing.
+     *
+     * <p>Only {@code $files} may do this: {@code auth(null)} means "every 
column" to
+     * {@code Catalog#authTableQuery}, and the system tables that keep 
planning on the FE
+     * ({@code $ro}, {@code $row_tracking}, {@code $audit_log}, {@code 
$binlog}) already authorize
+     * themselves through {@code DataTableBatchScan} with the slot projection 
the query really reads.
+     * Authorizing those again for every column would reject a user allowed to 
read only some of the
+     * base columns. {@code $partitions} never reaches {@code plan()} on 
either side
+     * ({@code listPartitionEntries} does not authorize), so it has no 
authorization to transfer.
+     *
+     * <p>A {@code scan.fallback-branch} table has to be authorized branch by 
branch.
+     * {@code FallbackReadFileStoreTable#newScan} builds a {@code 
FallbackReadScan} over both
+     * branches' own scans, so each authorizes itself, and {@code 
FileStoreTableFactory#create} gives
+     * the fallback branch a {@link CatalogEnvironment} of its own carrying a 
branch-qualified
+     * {@code Identifier}. Since the pair delegates {@code 
catalogEnvironment()} to its main branch,
+     * authorizing the pair would check the main branch alone and never the 
fallback one - and once
+     * its loader is dropped that missing check turns into a permanent allow, 
letting a user denied on
+     * the fallback branch read the fallback rows of {@code $files}.
+     */
+    // Package-private for direct unit testing (PaimonBackendBoundTableTest).
+    static void authorizeDeferredScan(FileStoreTable dataTable) {
+        FileStoreTable undecorated = 
PaimonTableDecorators.unwrapToFallbackOrBase(dataTable);
+        if (undecorated instanceof FallbackReadFileStoreTable) {
+            FallbackReadFileStoreTable fallbackReadTable = 
(FallbackReadFileStoreTable) undecorated;
+            authorizeBranch(fallbackReadTable.wrapped());
+            authorizeBranch(fallbackReadTable.fallback());

Review Comment:
   [P1] Preserve conditional fallback authorization
   
   `FallbackReadScan.plan()` always plans the main scan, but invokes 
`fallbackScan.plan()` only when `listPartitions()` finds partitions not 
completed by main; that listing itself does not authorize. This unconditional 
call therefore rejects a `$files` query for a user denied on the fallback 
branch even when every selected partition is satisfied by main and Paimon would 
never invoke fallback's authorized `plan()` or return fallback files. This is 
the inverse of the earlier missing-auth case: please transfer the fallback 
authorization with the same branch-use condition (while retaining the check 
when fallback contributes), and cover a 
main-complete/main-allowed/fallback-denied case.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


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

Reply via email to