From 340c4dff37ef548e29899c40b2dbb02a180a56ee Mon Sep 17 00:00:00 2001 From: shyjsarah Date: Tue, 14 Apr 2026 10:34:02 +0800 Subject: [PATCH 1/3] [core] Add ViewNoPermissionException and handle ForbiddenException for view operations in RESTCatalog Align view permission handling with table in RESTCatalog by catching ForbiddenException and throwing ViewNoPermissionException for all view operations (get/create/drop/rename/alter/list). Co-Authored-By: Claude Opus 4.6 --- .../org/apache/paimon/catalog/Catalog.java | 27 +++++++++++++++++ .../org/apache/paimon/rest/RESTCatalog.java | 16 ++++++++++ .../paimon/rest/MockRESTCatalogTest.java | 5 ++++ .../apache/paimon/rest/RESTCatalogServer.java | 16 ++++++++++ .../apache/paimon/rest/RESTCatalogTest.java | 30 +++++++++++++++++++ 5 files changed, 94 insertions(+) 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 6d337f37169f..3716de7463aa 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 @@ -1433,6 +1433,33 @@ public String getTableId() { } } + /** Exception for trying to operate on the view that doesn't have permission. */ + class ViewNoPermissionException extends RuntimeException { + + private static final String MSG = "View %s has no permission. Cause by %s."; + + private final Identifier identifier; + + public ViewNoPermissionException(Identifier identifier, Throwable cause) { + super( + String.format( + MSG, + identifier.getFullName(), + cause != null && cause.getMessage() != null ? cause.getMessage() : ""), + cause); + this.identifier = identifier; + } + + @VisibleForTesting + public ViewNoPermissionException(Identifier identifier) { + this(identifier, null); + } + + public Identifier identifier() { + return identifier; + } + } + /** Exception for trying to alter a column that already exists. */ class ColumnAlreadyExistException extends Exception { 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 e81e6c8e6108..155d6a989324 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 @@ -899,6 +899,8 @@ public View getView(Identifier identifier) throws ViewNotExistException { return toView(identifier.getDatabaseName(), response); } catch (NoSuchResourceException e) { throw new ViewNotExistException(identifier); + } catch (ForbiddenException e) { + throw new ViewNoPermissionException(identifier, e); } } @@ -911,6 +913,8 @@ public void dropView(Identifier identifier, boolean ignoreIfNotExists) if (!ignoreIfNotExists) { throw new ViewNotExistException(identifier); } + } catch (ForbiddenException e) { + throw new ViewNoPermissionException(identifier, e); } } @@ -934,6 +938,8 @@ public void createView(Identifier identifier, View view, boolean ignoreIfExists) } } catch (BadRequestException e) { throw new IllegalArgumentException(e.getMessage()); + } catch (ForbiddenException e) { + throw new ViewNoPermissionException(identifier, e); } } @@ -945,6 +951,8 @@ public List listViews(String databaseName) throws DatabaseNotExistExcept : api.listViews(databaseName); } catch (NoSuchResourceException e) { throw new DatabaseNotExistException(databaseName); + } catch (ForbiddenException e) { + throw new DatabaseNoPermissionException(databaseName, e); } } @@ -959,6 +967,8 @@ public PagedList listViewsPaged( return api.listViewsPaged(databaseName, maxResults, pageToken, viewNamePattern); } catch (NoSuchResourceException e) { throw new DatabaseNotExistException(databaseName); + } catch (ForbiddenException e) { + throw new DatabaseNoPermissionException(databaseName, e); } } @@ -979,6 +989,8 @@ public PagedList listViewDetailsPaged( views.getNextPageToken()); } catch (NoSuchResourceException e) { throw new DatabaseNotExistException(db); + } catch (ForbiddenException e) { + throw new DatabaseNoPermissionException(db, e); } } @@ -1020,6 +1032,8 @@ public void renameView(Identifier fromView, Identifier toView, boolean ignoreIfN throw new ViewAlreadyExistException(toView); } catch (BadRequestException e) { throw new IllegalArgumentException(e.getMessage()); + } catch (ForbiddenException e) { + throw new ViewNoPermissionException(fromView, e); } } @@ -1040,6 +1054,8 @@ public void alterView( } } catch (BadRequestException e) { throw new IllegalArgumentException(e.getMessage()); + } catch (ForbiddenException e) { + throw new ViewNoPermissionException(identifier, e); } } diff --git a/paimon-core/src/test/java/org/apache/paimon/rest/MockRESTCatalogTest.java b/paimon-core/src/test/java/org/apache/paimon/rest/MockRESTCatalogTest.java index 70380c216398..f5045a4360a6 100644 --- a/paimon-core/src/test/java/org/apache/paimon/rest/MockRESTCatalogTest.java +++ b/paimon-core/src/test/java/org/apache/paimon/rest/MockRESTCatalogTest.java @@ -347,6 +347,11 @@ protected void revokeTablePermission(Identifier identifier) { restCatalogServer.addNoPermissionTable(identifier); } + @Override + protected void revokeViewPermission(Identifier identifier) { + restCatalogServer.addNoPermissionView(identifier); + } + @Override protected void authTableColumns(Identifier identifier, List columns) { restCatalogServer.addTableColumnAuth(identifier, columns); 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 41caed02637b..c80efe23504b 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 @@ -195,6 +195,7 @@ public class RESTCatalogServer { private final Map tableWithSnapshotId2SnapshotStore = new HashMap<>(); private final List noPermissionDatabases = new ArrayList<>(); private final List noPermissionTables = new ArrayList<>(); + private final List noPermissionViews = new ArrayList<>(); private final Map functionStore = new HashMap<>(); private final Map> columnAuthHandler = new HashMap<>(); private final Map> rowFilterAuthHandler = new HashMap<>(); @@ -276,6 +277,10 @@ public void addNoPermissionTable(Identifier identifier) { noPermissionTables.add(identifier.getFullName()); } + public void addNoPermissionView(Identifier identifier) { + noPermissionViews.add(identifier.getFullName()); + } + public void addTableColumnAuth(Identifier identifier, List select) { columnAuthHandler.put(identifier.getFullName(), select); } @@ -631,6 +636,14 @@ && isTableByIdRequest(request.getPath())) { e.getMessage(), 403); return mockResponse(response, 403); + } catch (Catalog.ViewNoPermissionException e) { + response = + new ErrorResponse( + ErrorResponse.RESOURCE_TYPE_VIEW, + e.identifier().getTableName(), + e.getMessage(), + 403); + return mockResponse(response, 403); } catch (Catalog.DatabaseAlreadyExistException e) { response = new ErrorResponse( @@ -2327,6 +2340,9 @@ private List listViews(Map parameters) { private MockResponse viewHandle(String method, Identifier identifier, String requestData) throws Exception { RESTResponse response; + if (noPermissionViews.contains(identifier.getFullName())) { + throw new Catalog.ViewNoPermissionException(identifier); + } if (viewStore.containsKey(identifier.getFullName())) { switch (method) { case "GET": diff --git a/paimon-core/src/test/java/org/apache/paimon/rest/RESTCatalogTest.java b/paimon-core/src/test/java/org/apache/paimon/rest/RESTCatalogTest.java index 78d8cd8e00a4..a07c6eaed281 100644 --- a/paimon-core/src/test/java/org/apache/paimon/rest/RESTCatalogTest.java +++ b/paimon-core/src/test/java/org/apache/paimon/rest/RESTCatalogTest.java @@ -278,6 +278,34 @@ void testDatabaseApiWhenNoPermission() { false)); } + @Test + void testApiWhenViewNoPermission() throws Exception { + Identifier identifier = Identifier.create("test_view_db", "no_permission_view"); + catalog.createDatabase(identifier.getDatabaseName(), false); + View view = createView(identifier); + catalog.createView(identifier, view, false); + revokeViewPermission(identifier); + assertThrows(Catalog.ViewNoPermissionException.class, () -> catalog.getView(identifier)); + assertThrows( + Catalog.ViewNoPermissionException.class, () -> catalog.dropView(identifier, false)); + assertThrows( + Catalog.ViewNoPermissionException.class, + () -> + catalog.renameView( + identifier, + Identifier.create("test_view_db", "no_permission_view2"), + false)); + assertThrows( + Catalog.ViewNoPermissionException.class, + () -> + catalog.alterView( + identifier, + ImmutableList.of( + ViewChange.addDialect( + "flink_1", "SELECT * FROM FLINK_TABLE_1")), + false)); + } + @Test void testApiWhenDatabaseNoExistAndNotIgnore() { String database = "test_no_exist_db"; @@ -3932,6 +3960,8 @@ protected abstract Catalog newRestCatalogWithDataToken(Map extra protected abstract void revokeTablePermission(Identifier identifier); + protected abstract void revokeViewPermission(Identifier identifier); + protected abstract void authTableColumns(Identifier identifier, List columns); protected abstract void revokeDatabasePermission(String database); From 2c6dc395135fdf225076118bc78e24cc1b95fae0 Mon Sep 17 00:00:00 2001 From: shyjsarah Date: Tue, 14 Apr 2026 16:01:25 +0800 Subject: [PATCH 2/3] [core] Add permission check for renameView in RESTCatalogServer test The renameViewHandle has a separate code path from viewHandle and was missing the noPermissionViews check. Co-Authored-By: Claude Opus 4.6 --- .../test/java/org/apache/paimon/rest/RESTCatalogServer.java | 3 +++ 1 file changed, 3 insertions(+) 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 c80efe23504b..e3297f85395e 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 @@ -2447,6 +2447,9 @@ private MockResponse renameViewHandle(String data) throws Exception { RenameTableRequest requestBody = RESTApi.fromJson(data, RenameTableRequest.class); Identifier fromView = requestBody.getSource(); Identifier toView = requestBody.getDestination(); + if (noPermissionViews.contains(fromView.getFullName())) { + throw new Catalog.ViewNoPermissionException(fromView); + } if (!viewStore.containsKey(fromView.getFullName())) { throw new Catalog.ViewNotExistException(fromView); } From 721ac32e83320f493d11a0d07dae86f6101fde87 Mon Sep 17 00:00:00 2001 From: shyjsarah Date: Wed, 15 Apr 2026 10:37:04 +0800 Subject: [PATCH 3/3] [core] Fix typo in ViewNoPermissionException message: Cause -> Caused Co-Authored-By: Claude Opus 4.6 --- .../src/main/java/org/apache/paimon/catalog/Catalog.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 3716de7463aa..ab66f937ea02 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 @@ -1436,7 +1436,7 @@ public String getTableId() { /** Exception for trying to operate on the view that doesn't have permission. */ class ViewNoPermissionException extends RuntimeException { - private static final String MSG = "View %s has no permission. Cause by %s."; + private static final String MSG = "View %s has no permission. Caused by %s."; private final Identifier identifier;