This is an automated email from the ASF dual-hosted git repository.
jongyoul pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/zeppelin.git
The following commit(s) were added to refs/heads/master by this push:
new 2593fc0839 [HOTFIX] Validate note access for interpreter bindings
2593fc0839 is described below
commit 2593fc0839cb006242b1a734d62de48c386d46e7
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]>
---
.../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 555d22ffd3..85a552e7f4 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
@@ -621,6 +621,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) {
@@ -638,9 +644,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>>() {
@@ -656,10 +668,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 ef6eb6d5ec..325cb9ee85 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;
@@ -969,6 +970,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;