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

JingsongLi pushed a commit to branch rest-add-table-via-view-endpoint
in repository https://gitbox.apache.org/repos/asf/paimon.git

commit db862ccaeeb6b659c9a285d833c76ce13439868b
Author: JingsongLi <[email protected]>
AuthorDate: Thu Jun 4 11:57:04 2026 +0800

    [rest] Add view penetration endpoint for table access via view
    
    Add POST 
/v1/{prefix}/databases/{db}/tables/{table}/via/{via_db}/{via_object}
    endpoint that enables view penetration: if the caller has permission on a 
view,
    they can access the underlying table referenced by that view.
    
    This API can only be called by trusted engines. The server must authenticate
    whether the caller is a trusted engine.
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
---
 .../main/java/org/apache/paimon/rest/RESTApi.java  | 26 +++++++++++++++++
 .../java/org/apache/paimon/rest/ResourcePaths.java | 14 +++++++++
 .../java/org/apache/paimon/catalog/Catalog.java    | 16 +++++++++++
 .../org/apache/paimon/catalog/DelegateCatalog.java |  5 ++++
 .../java/org/apache/paimon/rest/RESTCatalog.java   | 33 ++++++++++++++++++++++
 .../org/apache/paimon/rest/RESTCatalogServer.java  | 32 +++++++++++++++++++++
 6 files changed, 126 insertions(+)

diff --git a/paimon-api/src/main/java/org/apache/paimon/rest/RESTApi.java 
b/paimon-api/src/main/java/org/apache/paimon/rest/RESTApi.java
index 263c4e2c06..26be5bcb40 100644
--- a/paimon-api/src/main/java/org/apache/paimon/rest/RESTApi.java
+++ b/paimon-api/src/main/java/org/apache/paimon/rest/RESTApi.java
@@ -519,6 +519,32 @@ public class RESTApi {
         return client.get(resourcePaths.table(tableId), 
GetTableResponse.class, restAuthFunction);
     }
 
+    /**
+     * Get table via a view (view penetration). If the caller has permission 
on the view identified
+     * by {@code via}, they can access the underlying table referenced by the 
view.
+     *
+     * <p>This API can only be called by trusted engines. The server must 
authenticate whether the
+     * caller is a trusted engine.
+     *
+     * @param table database name and table name of the target table.
+     * @param via database name and object name of the view through which 
access is granted.
+     * @return {@link GetTableResponse}
+     * @throws NoSuchResourceException Exception thrown on HTTP 404 means the 
table or view not
+     *     exists
+     * @throws ForbiddenException Exception thrown on HTTP 403 means don't 
have the permission
+     */
+    public GetTableResponse getTableVia(Identifier table, Identifier via) {
+        return client.post(
+                resourcePaths.tableVia(
+                        table.getDatabaseName(),
+                        table.getObjectName(),
+                        via.getDatabaseName(),
+                        via.getObjectName()),
+                new ForwardBranchRequest(),
+                GetTableResponse.class,
+                restAuthFunction);
+    }
+
     /**
      * Load latest snapshot for table.
      *
diff --git a/paimon-api/src/main/java/org/apache/paimon/rest/ResourcePaths.java 
b/paimon-api/src/main/java/org/apache/paimon/rest/ResourcePaths.java
index 28f79d0409..6bb62d5c73 100644
--- a/paimon-api/src/main/java/org/apache/paimon/rest/ResourcePaths.java
+++ b/paimon-api/src/main/java/org/apache/paimon/rest/ResourcePaths.java
@@ -43,6 +43,7 @@ public class ResourcePaths {
     protected static final String FUNCTIONS = "functions";
     protected static final String FUNCTION_DETAILS = "function-details";
     protected static final String ID = "id";
+    protected static final String VIA = "via";
 
     private static final Joiner SLASH = Joiner.on("/").skipNulls();
 
@@ -94,6 +95,19 @@ public class ResourcePaths {
                 encodeString(objectName));
     }
 
+    public String tableVia(String databaseName, String objectName, String 
viaDb, String viaObject) {
+        return SLASH.join(
+                V1,
+                prefix,
+                DATABASES,
+                encodeString(databaseName),
+                TABLES,
+                encodeString(objectName),
+                VIA,
+                encodeString(viaDb),
+                encodeString(viaObject));
+    }
+
     public String renameTable() {
         return SLASH.join(V1, prefix, TABLES, "rename");
     }
diff --git a/paimon-core/src/main/java/org/apache/paimon/catalog/Catalog.java 
b/paimon-core/src/main/java/org/apache/paimon/catalog/Catalog.java
index 57fa040a2a..1fd3a24959 100644
--- a/paimon-core/src/main/java/org/apache/paimon/catalog/Catalog.java
+++ b/paimon-core/src/main/java/org/apache/paimon/catalog/Catalog.java
@@ -157,6 +157,22 @@ public interface Catalog extends AutoCloseable {
      */
     Table getTable(Identifier identifier) throws TableNotExistException;
 
+    /**
+     * Return a {@link Table} identified by the given {@link Identifier}, 
accessed via a view (view
+     * penetration). If the caller has permission on the view, they can access 
the underlying table.
+     *
+     * <p>This API can only be called by trusted engines. The server must 
authenticate whether the
+     * caller is a trusted engine.
+     *
+     * @param table Path of the target table
+     * @param via Path of the view through which access is granted
+     * @return The requested table
+     * @throws TableNotExistException if the target does not exist
+     */
+    default Table getTable(Identifier table, Identifier via) throws 
TableNotExistException {
+        return getTable(table);
+    }
+
     /**
      * Return a {@link Table} identified by the given tableId.
      *
diff --git 
a/paimon-core/src/main/java/org/apache/paimon/catalog/DelegateCatalog.java 
b/paimon-core/src/main/java/org/apache/paimon/catalog/DelegateCatalog.java
index 0f18f7d045..5e034bdff2 100644
--- a/paimon-core/src/main/java/org/apache/paimon/catalog/DelegateCatalog.java
+++ b/paimon-core/src/main/java/org/apache/paimon/catalog/DelegateCatalog.java
@@ -375,6 +375,11 @@ public abstract class DelegateCatalog implements Catalog {
         return wrapped.getTable(identifier);
     }
 
+    @Override
+    public Table getTable(Identifier table, Identifier via) throws 
TableNotExistException {
+        return wrapped.getTable(table, via);
+    }
+
     @Override
     public View getView(Identifier identifier) throws ViewNotExistException {
         return wrapped.getView(identifier);
diff --git a/paimon-core/src/main/java/org/apache/paimon/rest/RESTCatalog.java 
b/paimon-core/src/main/java/org/apache/paimon/rest/RESTCatalog.java
index 9d76cfdf4f..3054a12185 100644
--- a/paimon-core/src/main/java/org/apache/paimon/rest/RESTCatalog.java
+++ b/paimon-core/src/main/java/org/apache/paimon/rest/RESTCatalog.java
@@ -319,6 +319,20 @@ public class RESTCatalog implements Catalog {
                 true);
     }
 
+    @Override
+    public Table getTable(Identifier table, Identifier via) throws 
TableNotExistException {
+        return CatalogUtils.loadTable(
+                this,
+                table,
+                path -> fileIOForData(path, table),
+                this::fileIOFromOptions,
+                i -> loadTableMetadataVia(i, via),
+                null,
+                null,
+                context,
+                true);
+    }
+
     @Override
     public Optional<TableSnapshot> loadSnapshot(Identifier identifier)
             throws TableNotExistException {
@@ -480,6 +494,25 @@ public class RESTCatalog implements Catalog {
         return toTableMetadata(identifier.getDatabaseName(), response);
     }
 
+    /**
+     * Load table metadata via a view identifier (view penetration).
+     *
+     * <p>This API can only be called by trusted engines. The server must 
authenticate whether the
+     * caller is a trusted engine.
+     */
+    public TableMetadata loadTableMetadataVia(Identifier table, Identifier via)
+            throws TableNotExistException {
+        GetTableResponse response;
+        try {
+            response = api.getTableVia(table, via);
+        } catch (NoSuchResourceException e) {
+            throw new TableNotExistException(table);
+        } catch (ForbiddenException e) {
+            throw new TableNoPermissionException(table, e);
+        }
+        return toTableMetadata(table.getDatabaseName(), response);
+    }
+
     private TableMetadata toTableMetadata(String db, GetTableResponse 
response) {
         TableSchema schema = TableSchema.create(response.getSchemaId(), 
response.getSchema());
         Map<String, String> options = new HashMap<>(schema.options());
diff --git 
a/paimon-core/src/test/java/org/apache/paimon/rest/RESTCatalogServer.java 
b/paimon-core/src/test/java/org/apache/paimon/rest/RESTCatalogServer.java
index af5d94e3f6..2aca3a3de7 100644
--- a/paimon-core/src/test/java/org/apache/paimon/rest/RESTCatalogServer.java
+++ b/paimon-core/src/test/java/org/apache/paimon/rest/RESTCatalogServer.java
@@ -413,6 +413,10 @@ public class RESTCatalogServer {
                                 resources.length == 5
                                         && 
ResourcePaths.TABLES.equals(resources[1])
                                         && 
ResourcePaths.SNAPSHOTS.equals(resources[3]);
+                        boolean isTableVia =
+                                resources.length == 5
+                                        && 
ResourcePaths.TABLES.equals(resources[1])
+                                        && 
ResourcePaths.VIA.equals(resources[3]);
                         boolean isTableAuth =
                                 resources.length == 4
                                         && 
ResourcePaths.TABLES.equals(resources[1])
@@ -529,6 +533,8 @@ public class RESTCatalogServer {
                             return resetConsumer(identifier, 
restAuthParameter.data());
                         } else if (isLoadSnapshot) {
                             return loadSnapshot(identifier, resources[4]);
+                        } else if (isTableVia) {
+                            return tableViaHandle(identifier);
                         } else if (isTableAuth) {
                             return authTable(identifier, 
restAuthParameter.data());
                         } else if (isCommitSnapshot) {
@@ -1701,6 +1707,32 @@ public class RESTCatalogServer {
                 404);
     }
 
+    // This API can only be called by trusted engines. The server must 
authenticate
+    // whether the caller is a trusted engine.
+    private MockResponse tableViaHandle(Identifier identifier) throws 
Exception {
+        if (noPermissionTables.contains(identifier.getFullName())) {
+            throw new Catalog.TableNoPermissionException(identifier);
+        }
+        TableMetadata tableMetadata = 
tableMetadataStore.get(identifier.getFullName());
+        Schema schema = tableMetadata.schema().toSchema();
+        String path = schema.options().remove(PATH.key());
+        RESTResponse response =
+                new GetTableResponse(
+                        tableMetadata.uuid(),
+                        identifier.getDatabaseName(),
+                        identifier.getObjectName(),
+                        path,
+                        tableMetadata.isExternal(),
+                        tableMetadata.schema().id(),
+                        schema,
+                        "owner",
+                        1L,
+                        "created",
+                        1L,
+                        "updated");
+        return mockResponse(response, 200);
+    }
+
     private MockResponse tableHandle(String method, String data, Identifier 
identifier)
             throws Exception {
         RESTResponse response;

Reply via email to