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;

Reply via email to