This is an automated email from the ASF dual-hosted git repository. jongyoul pushed a commit to branch branch-0.12 in repository https://gitbox.apache.org/repos/asf/zeppelin.git
commit ab742842c5930a35fdedcaa319be174c88481d4d Author: Jongyoul Lee <[email protected]> AuthorDate: Thu Aug 20 16:59:28 2026 +0900 [HOTFIX] Validate note access for interpreter bindings ### What is this PR for? Ensure that interpreter binding operations follow the permissions of the associated note. - Reading interpreter bindings requires reader permission. - Updating interpreter bindings requires writer permission. - Requests without the required permission stop before returning binding data or applying changes. - Add regression tests for permitted and rejected read/write requests. ### What type of PR is it? Hot Fix ### Todos * [x] Add note permission checks for interpreter binding operations * [x] Add regression tests * [x] Run `NotebookServerTest` ### What is the Jira issue? N/A ### How should this be tested? ```bash mkdir -p spark/interpreter/target ./mvnw -pl spark/interpreter resources:resources<at>copy-interpreter-setting ./mvnw -pl zeppelin-server -Dtest=NotebookServerTest test ``` Result: 25 tests run, 0 failures, 0 errors. ### Screenshots (if appropriate) N/A ### Questions: * Does the license files need to update? No. * Is there breaking changes for older versions? No API compatibility changes. Requests without the required note permission are now rejected as intended. * Does this needs documentation? No. Closes #5431 from jongyoul/codex/security-interpreter-bindings-authz. Signed-off-by: Jongyoul Lee <[email protected]> (cherry picked from commit 2593fc0839cb006242b1a734d62de48c386d46e7) Signed-off-by: Jongyoul Lee <[email protected]> --- .../org/apache/zeppelin/socket/NotebookServer.java | 22 +++++-- .../apache/zeppelin/socket/NotebookServerTest.java | 76 ++++++++++++++++++++++ 2 files changed, 94 insertions(+), 4 deletions(-) diff --git a/zeppelin-server/src/main/java/org/apache/zeppelin/socket/NotebookServer.java b/zeppelin-server/src/main/java/org/apache/zeppelin/socket/NotebookServer.java index c4c428f080..718ee05b25 100644 --- a/zeppelin-server/src/main/java/org/apache/zeppelin/socket/NotebookServer.java +++ b/zeppelin-server/src/main/java/org/apache/zeppelin/socket/NotebookServer.java @@ -618,6 +618,12 @@ public class NotebookServer implements AngularObjectRegistryListener, getNotebook().processNote(noteId, note -> { if (note != null) { + if (!authorizationService.isReader(noteId, context.getUserAndRoles())) { + permissionError(conn, "get interpreter bindings from", + context.getAutheInfo().getUser(), context.getUserAndRoles(), + authorizationService.getReaders(noteId)); + return null; + } List<InterpreterSetting> bindedSettings = note.getBindedInterpreterSettings(new ArrayList<>(context.getUserAndRoles())); for (InterpreterSetting setting : bindedSettings) { @@ -635,9 +641,15 @@ public class NotebookServer implements AngularObjectRegistryListener, List<InterpreterSettingsList> settingList = new ArrayList<>(); String noteId = (String) fromMessage.data.get("noteId"); // use write lock, because defaultInterpreterGroup is overwritten - getNotebook().processNote(noteId, + boolean permitted = getNotebook().processNote(noteId, note -> { if (note != null) { + if (!authorizationService.isWriter(noteId, context.getUserAndRoles())) { + permissionError(conn, "save interpreter bindings for", + context.getAutheInfo().getUser(), context.getUserAndRoles(), + authorizationService.getWriters(noteId)); + return false; + } List<String> settingIdList = gson.fromJson(String.valueOf(fromMessage.data.get("selectedSettingIds")), new TypeToken<ArrayList<String>>() { @@ -653,10 +665,12 @@ public class NotebookServer implements AngularObjectRegistryListener, setting.getInterpreterInfos(), true)); } } - return null; + return true; }); - conn.send(serializeMessage( - new Message(OP.INTERPRETER_BINDINGS).put("interpreterBindings", settingList))); + if (permitted) { + conn.send(serializeMessage( + new Message(OP.INTERPRETER_BINDINGS).put("interpreterBindings", settingList))); + } } public void broadcastNote(Note note) { diff --git a/zeppelin-server/src/test/java/org/apache/zeppelin/socket/NotebookServerTest.java b/zeppelin-server/src/test/java/org/apache/zeppelin/socket/NotebookServerTest.java index d982d46a33..5fbb08bcc9 100644 --- a/zeppelin-server/src/test/java/org/apache/zeppelin/socket/NotebookServerTest.java +++ b/zeppelin-server/src/test/java/org/apache/zeppelin/socket/NotebookServerTest.java @@ -77,6 +77,7 @@ import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; import org.mockito.Mockito; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -923,6 +924,81 @@ class NotebookServerTest extends AbstractTestRestApi { } } + @Test + void getInterpreterBindingsRequiresReaderPermission() throws IOException { + AuthenticationInfo owner = new AuthenticationInfo("binding-owner"); + String noteId = notebook.createNote("private-binding-read", owner); + try { + setNotePermissions(noteId, "binding-owner", "binding-owner"); + NotebookSocket socket = createWebSocket(); + Message message = new Message(OP.GET_INTERPRETER_BINDINGS).put("noteId", noteId); + + notebookServer.getInterpreterBindings(socket, serviceContext("binding-attacker"), message); + + ArgumentCaptor<String> response = ArgumentCaptor.forClass(String.class); + verify(socket).send(response.capture()); + assertEquals(OP.AUTH_INFO, notebookServer.deserializeMessage(response.getValue()).op); + + reset(socket); + setNotePermissions(noteId, "binding-owner", "binding-reader"); + notebookServer.getInterpreterBindings(socket, serviceContext("binding-reader"), message); + + verify(socket).send(response.capture()); + assertEquals(OP.INTERPRETER_BINDINGS, + notebookServer.deserializeMessage(response.getValue()).op); + } finally { + notebook.removeNote(noteId, owner); + } + } + + @Test + void saveInterpreterBindingsRequiresWriterPermission() throws IOException { + AuthenticationInfo owner = new AuthenticationInfo("binding-owner"); + String noteId = notebook.createNote("private-binding-write", owner); + try { + setNotePermissions(noteId, "binding-owner", "binding-reader"); + String initialGroup = notebook.processNote(noteId, Note::getDefaultInterpreterGroup); + String replacementGroup = initialGroup.equals("md") ? "spark" : "md"; + Message message = new Message(OP.SAVE_INTERPRETER_BINDINGS) + .put("noteId", noteId) + .put("selectedSettingIds", Arrays.asList(replacementGroup)); + NotebookSocket socket = createWebSocket(); + + notebookServer.saveInterpreterBindings(socket, serviceContext("binding-reader"), message); + + assertEquals(initialGroup, + notebook.processNote(noteId, Note::getDefaultInterpreterGroup)); + ArgumentCaptor<String> response = ArgumentCaptor.forClass(String.class); + verify(socket).send(response.capture()); + assertEquals(OP.AUTH_INFO, notebookServer.deserializeMessage(response.getValue()).op); + + reset(socket); + authorizationService.setWriters(noteId, + new HashSet<>(Arrays.asList("binding-owner", "binding-writer"))); + notebookServer.saveInterpreterBindings(socket, serviceContext("binding-writer"), message); + + assertEquals(replacementGroup, + notebook.processNote(noteId, Note::getDefaultInterpreterGroup)); + verify(socket).send(response.capture()); + assertEquals(OP.INTERPRETER_BINDINGS, + notebookServer.deserializeMessage(response.getValue()).op); + } finally { + notebook.removeNote(noteId, owner); + } + } + + private void setNotePermissions(String noteId, String owner, String reader) throws IOException { + authorizationService.setOwners(noteId, new HashSet<>(Arrays.asList(owner))); + authorizationService.setReaders(noteId, new HashSet<>(Arrays.asList(owner, reader))); + authorizationService.setRunners(noteId, new HashSet<>(Arrays.asList(owner))); + authorizationService.setWriters(noteId, new HashSet<>(Arrays.asList(owner))); + } + + private ServiceContext serviceContext(String user) { + return new ServiceContext(new AuthenticationInfo(user), + new HashSet<>(Arrays.asList(user))); + } + @Test void testNoteRevision() throws IOException { String noteId = null;
