diff --git a/docs/setup/operation/configuration.md b/docs/setup/operation/configuration.md
index 9588cd25a5b..119fd9febdc 100644
--- a/docs/setup/operation/configuration.md
+++ b/docs/setup/operation/configuration.md
@@ -508,6 +508,36 @@ Sources descending by priority:
true |
Value to enable/disable version control support in Notes. |
+
+ ZEPPELIN_OWNER_ROLE |
+ zeppelin.notebook.default.owner.username |
+ |
+ Username of the Zeppelin Note Administrator |
+
+
+ ZEPPELIN_OWNER_ROLES |
+ zeppelin.notebook.default.owners |
+ |
+ Comma-separated list of global note owners, which are de facto note administrators. |
+
+
+ ZEPPELIN_WRITER_ROLES |
+ zeppelin.notebook.default.writers |
+ |
+ Comma-separated list of global note writers. |
+
+
+ ZEPPELIN_RUNNER_ROLES |
+ zeppelin.notebook.default.runners |
+ |
+ Comma-separated list of global note runners. |
+
+
+ ZEPPELIN_READER_ROLES |
+ zeppelin.notebook.default.readers |
+ |
+ Comma-separated list of global note readers. |
+
diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/conf/ZeppelinConfiguration.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/conf/ZeppelinConfiguration.java
index 3b9ebee0bad..1419188d6c8 100644
--- a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/conf/ZeppelinConfiguration.java
+++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/conf/ZeppelinConfiguration.java
@@ -1086,6 +1086,10 @@ public enum ConfVars {
ZEPPELIN_INTERPRETER_SCHEDULER_POOL_SIZE("zeppelin.scheduler.threadpool.size", 100),
ZEPPELIN_OWNER_ROLE("zeppelin.notebook.default.owner.username", ""),
+ ZEPPELIN_OWNER_ROLES("zeppelin.notebook.default.owners", ""),
+ ZEPPELIN_WRITER_ROLES("zeppelin.notebook.default.writers", ""),
+ ZEPPELIN_READER_ROLES("zeppelin.notebook.default.readers", ""),
+ ZEPPELIN_RUNNER_ROLES("zeppelin.notebook.default.runners", ""),
ZEPPELIN_RUN_MODE("zeppelin.run.mode", "auto"), // auto | local | k8s | Docker
diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/AuthorizationService.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/AuthorizationService.java
index 70e7aac1592..58aa97137fc 100644
--- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/AuthorizationService.java
+++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/AuthorizationService.java
@@ -26,6 +26,8 @@
import jakarta.inject.Inject;
import java.io.IOException;
+import java.util.Collections;
+import java.util.EnumSet;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
@@ -43,6 +45,12 @@ public class AuthorizationService {
private final ZeppelinConfiguration zConf;
private final ConfigStorage configStorage;
+ private static final Set VALID_ROLES_CONF_VARS = EnumSet.of(
+ ZeppelinConfiguration.ConfVars.ZEPPELIN_OWNER_ROLES,
+ ZeppelinConfiguration.ConfVars.ZEPPELIN_WRITER_ROLES,
+ ZeppelinConfiguration.ConfVars.ZEPPELIN_READER_ROLES,
+ ZeppelinConfiguration.ConfVars.ZEPPELIN_RUNNER_ROLES);
+
// contains roles for each user (username --> roles)
private Map> userRoles = new ConcurrentHashMap<>();
@@ -106,8 +114,9 @@ public void removeNoteAuth(String noteId) {
private Set normalizeUsers(Set users) {
Set returnUser = new HashSet<>();
for (String user : users) {
- if (!user.trim().isEmpty()) {
- returnUser.add(user.trim());
+ String trimmedUser = user.trim();
+ if (!trimmedUser.isEmpty()) {
+ returnUser.add(trimmedUser);
}
}
return returnUser;
@@ -235,28 +244,67 @@ public Set getRoles(String user) {
}
public boolean isOwner(String noteId, Set entities) {
- return isMember(entities, getOwners(noteId)) || isAdmin(entities);
+ return isMember(entities, constructRoles(getOwners(noteId), getDefaultOwners())) ||
+ isAdmin(entities);
}
public boolean isWriter(String noteId, Set entities) {
- return isMember(entities, getWriters(noteId)) ||
- isMember(entities, getOwners(noteId)) ||
- isAdmin(entities);
+ return isMember(entities, constructRoles(getWriters(noteId), getDefaultWriters())) ||
+ isMember(entities, constructRoles(getOwners(noteId), getDefaultOwners())) ||
+ isAdmin(entities);
}
public boolean isReader(String noteId, Set entities) {
- return isMember(entities, getReaders(noteId)) ||
- isMember(entities, getOwners(noteId)) ||
- isMember(entities, getWriters(noteId)) ||
- isMember(entities, getRunners(noteId)) ||
- isAdmin(entities);
+ return isMember(entities, constructRoles(getReaders(noteId), getDefaultReaders())) ||
+ isMember(entities, constructRoles(getOwners(noteId), getDefaultOwners())) ||
+ isMember(entities, constructRoles(getWriters(noteId), getDefaultWriters())) ||
+ isMember(entities, constructRoles(getRunners(noteId), getDefaultRunners())) ||
+ isAdmin(entities);
}
public boolean isRunner(String noteId, Set entities) {
- return isMember(entities, getRunners(noteId)) ||
- isMember(entities, getWriters(noteId)) ||
- isMember(entities, getOwners(noteId)) ||
- isAdmin(entities);
+ return isMember(entities, constructRoles(getRunners(noteId), getDefaultRunners())) ||
+ isMember(entities, constructRoles(getWriters(noteId), getDefaultWriters())) ||
+ isMember(entities, constructRoles(getOwners(noteId), getDefaultOwners())) ||
+ isAdmin(entities);
+ }
+
+ private Set constructRoles(Set noteRoles, Set globalRoles) {
+ Set roles = new HashSet<>(noteRoles);
+ // If the note has no role, the note right is for everyone, so we are not allowed to add the default roles
+ if (!roles.isEmpty()) {
+ roles.addAll(globalRoles);
+ }
+ return roles;
+ }
+
+ private Set getDefaultOwners() {
+ return getDefaultRoles(ZeppelinConfiguration.ConfVars.ZEPPELIN_OWNER_ROLES);
+ }
+
+ private Set getDefaultWriters() {
+ return getDefaultRoles(ZeppelinConfiguration.ConfVars.ZEPPELIN_WRITER_ROLES);
+ }
+
+ private Set getDefaultReaders() {
+ return getDefaultRoles(ZeppelinConfiguration.ConfVars.ZEPPELIN_READER_ROLES);
+ }
+
+ private Set getDefaultRunners() {
+ return getDefaultRoles(ZeppelinConfiguration.ConfVars.ZEPPELIN_RUNNER_ROLES);
+ }
+
+ private Set getDefaultRoles(ZeppelinConfiguration.ConfVars confvar) {
+ if (!VALID_ROLES_CONF_VARS.contains(confvar)) {
+ LOGGER.warn("getDefaultRoles is used with {}, which is not valid", confvar);
+ return Collections.emptySet();
+ }
+ Set defaultRoles = new HashSet<>();
+ String defaultRolesConf = zConf.getString(confvar);
+ if (StringUtils.isNotBlank(defaultRolesConf)) {
+ Collections.addAll(defaultRoles, StringUtils.split(defaultRolesConf, ','));
+ }
+ return normalizeUsers(defaultRoles);
}
private boolean isAdmin(Set entities) {
diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/storage/InMemoryConfigStorage.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/storage/InMemoryConfigStorage.java
new file mode 100644
index 00000000000..332d41e0d8f
--- /dev/null
+++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/storage/InMemoryConfigStorage.java
@@ -0,0 +1,70 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.zeppelin.storage;
+
+import java.io.IOException;
+
+import org.apache.zeppelin.conf.ZeppelinConfiguration;
+import org.apache.zeppelin.interpreter.InterpreterInfoSaving;
+import org.apache.zeppelin.notebook.NotebookAuthorizationInfoSaving;
+
+/**
+ * Storing config in memory
+ * This class is primarily for test cases
+ */
+public class InMemoryConfigStorage extends ConfigStorage {
+
+ private InterpreterInfoSaving settingInfos;
+ private NotebookAuthorizationInfoSaving authorizationInfoSaving;
+ private String credentials;
+
+ public InMemoryConfigStorage(ZeppelinConfiguration zConf) {
+ super(zConf);
+ }
+
+ @Override
+ public void save(InterpreterInfoSaving settingInfos) throws IOException {
+ this.settingInfos = settingInfos;
+ }
+
+ @Override
+ public InterpreterInfoSaving loadInterpreterSettings() throws IOException {
+ return settingInfos;
+ }
+
+ @Override
+ public void save(NotebookAuthorizationInfoSaving authorizationInfoSaving) throws IOException {
+ this.authorizationInfoSaving = authorizationInfoSaving;
+ }
+
+ @Override
+ public NotebookAuthorizationInfoSaving loadNotebookAuthorization() throws IOException {
+ return authorizationInfoSaving;
+ }
+
+ @Override
+ public String loadCredentials() throws IOException {
+ return credentials;
+ }
+
+ @Override
+ public void saveCredentials(String credentials) throws IOException {
+ this.credentials = credentials;
+ }
+
+}
\ No newline at end of file
diff --git a/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/AuthorizationServiceTest.java b/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/AuthorizationServiceTest.java
new file mode 100644
index 00000000000..6606628c562
--- /dev/null
+++ b/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/AuthorizationServiceTest.java
@@ -0,0 +1,206 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.zeppelin.notebook;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+
+import org.apache.zeppelin.conf.ZeppelinConfiguration;
+import org.apache.zeppelin.notebook.repo.InMemoryNotebookRepo;
+import org.apache.zeppelin.notebook.repo.NotebookRepo;
+import org.apache.zeppelin.storage.ConfigStorage;
+import org.apache.zeppelin.storage.InMemoryConfigStorage;
+import org.apache.zeppelin.user.AuthenticationInfo;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+class AuthorizationServiceTest {
+ private ZeppelinConfiguration zConf;
+ private AuthorizationService authorizationService;
+ private static final String BLANK_ROLE = " ";
+ private static final String EMPTY_ROLE = "";
+ private static final String TEST_USER_1 = "TestUser1";
+ private static final String TEST_USER_2 = "TestUser2";
+
+ @BeforeEach
+ private void setup() throws IOException {
+ zConf = mock(ZeppelinConfiguration.class);
+ when(zConf.isNotebookPublic()).thenReturn(false);
+ NotebookRepo notebookRepo = new InMemoryNotebookRepo();
+ NoteManager noteManager = new NoteManager(notebookRepo, zConf);
+ ConfigStorage storage = new InMemoryConfigStorage(zConf);
+ authorizationService = new AuthorizationService(noteManager, zConf, storage);
+ }
+
+ @Test
+ void testDefaultOwners() throws IOException {
+ Note testNote = new Note();
+ authorizationService.createNoteAuth(testNote.getId(), new AuthenticationInfo(TEST_USER_1));
+
+ // Comma separated with trim
+ when(zConf.getString(ZeppelinConfiguration.ConfVars.ZEPPELIN_OWNER_ROLES)).thenReturn("TestGroup, TestGroup2");
+ for (String role : Arrays.asList("TestGroup", "TestGroup2")) {
+ assertTrue(authorizationService.isOwner(testNote.getId(), new HashSet<>(Arrays.asList(role))));
+ assertTrue(authorizationService.isRunner(testNote.getId(), new HashSet<>(Arrays.asList(role))));
+ assertTrue(authorizationService.isWriter(testNote.getId(), new HashSet<>(Arrays.asList(role))));
+ assertTrue(authorizationService.isReader(testNote.getId(), new HashSet<>(Arrays.asList(role))));
+ }
+
+ // Empty - Blank
+ when(zConf.getString(ZeppelinConfiguration.ConfVars.ZEPPELIN_OWNER_ROLES)).thenReturn(BLANK_ROLE);
+ assertFalse(authorizationService.isOwner(testNote.getId(), new HashSet<>(Arrays.asList(BLANK_ROLE))));
+ // Empty - Empty
+ when(zConf.getString(ZeppelinConfiguration.ConfVars.ZEPPELIN_OWNER_ROLES)).thenReturn(EMPTY_ROLE);
+ assertFalse(authorizationService.isOwner(testNote.getId(), new HashSet<>(Arrays.asList(EMPTY_ROLE))));
+ // Empty - null
+ when(zConf.getString(ZeppelinConfiguration.ConfVars.ZEPPELIN_OWNER_ROLES)).thenReturn(null);
+ assertTrue(authorizationService.isOwner(testNote.getId(), new HashSet<>(Arrays.asList(TEST_USER_1))));
+
+ }
+
+ @Test
+ void testDefaultRunners() throws IOException {
+ Note testNote = new Note();
+ authorizationService.createNoteAuth(testNote.getId(), new AuthenticationInfo(TEST_USER_1));
+
+ // Comma separated with trim
+ when(zConf.getString(ZeppelinConfiguration.ConfVars.ZEPPELIN_RUNNER_ROLES)).thenReturn("TestGroup, TestGroup2");
+ for (String role : Arrays.asList("TestGroup", "TestGroup2")) {
+ assertTrue(authorizationService.isRunner(testNote.getId(), new HashSet<>(Arrays.asList(role))));
+ assertTrue(authorizationService.isReader(testNote.getId(), new HashSet<>(Arrays.asList(role))));
+ assertFalse(authorizationService.isOwner(testNote.getId(), new HashSet<>(Arrays.asList(role))));
+ assertFalse(authorizationService.isWriter(testNote.getId(), new HashSet<>(Arrays.asList(role))));
+ }
+
+ // Empty - Blank
+ when(zConf.getString(ZeppelinConfiguration.ConfVars.ZEPPELIN_RUNNER_ROLES)).thenReturn(BLANK_ROLE);
+ assertFalse(authorizationService.isRunner(testNote.getId(), new HashSet<>(Arrays.asList(BLANK_ROLE))));
+ // Empty - Empty
+ when(zConf.getString(ZeppelinConfiguration.ConfVars.ZEPPELIN_RUNNER_ROLES)).thenReturn(EMPTY_ROLE);
+ assertFalse(authorizationService.isRunner(testNote.getId(), new HashSet<>(Arrays.asList(EMPTY_ROLE))));
+ // Empty - null
+ when(zConf.getString(ZeppelinConfiguration.ConfVars.ZEPPELIN_RUNNER_ROLES)).thenReturn(null);
+ assertTrue(authorizationService.isRunner(testNote.getId(), new HashSet<>(Arrays.asList(TEST_USER_1))));
+ }
+
+ @Test
+ void testDefaultWriters() throws IOException {
+ Note testNote = new Note();
+ authorizationService.createNoteAuth(testNote.getId(), new AuthenticationInfo(TEST_USER_1));
+
+ // Comma separated with trim
+ when(zConf.getString(ZeppelinConfiguration.ConfVars.ZEPPELIN_WRITER_ROLES)).thenReturn("TestGroup, TestGroup2");
+ for (String role : Arrays.asList("TestGroup", "TestGroup2")) {
+ assertTrue(authorizationService.isWriter(testNote.getId(), new HashSet<>(Arrays.asList(role))));
+ assertTrue(authorizationService.isRunner(testNote.getId(), new HashSet<>(Arrays.asList(role))));
+ assertTrue(authorizationService.isReader(testNote.getId(), new HashSet<>(Arrays.asList(role))));
+ assertFalse(authorizationService.isOwner(testNote.getId(), new HashSet<>(Arrays.asList(role))));
+ }
+
+ // Empty - Blank
+ when(zConf.getString(ZeppelinConfiguration.ConfVars.ZEPPELIN_WRITER_ROLES)).thenReturn(BLANK_ROLE);
+ assertFalse(authorizationService.isWriter(testNote.getId(), new HashSet<>(Arrays.asList(BLANK_ROLE))));
+ // Empty - Empty
+ when(zConf.getString(ZeppelinConfiguration.ConfVars.ZEPPELIN_WRITER_ROLES)).thenReturn(EMPTY_ROLE);
+ assertFalse(authorizationService.isWriter(testNote.getId(), new HashSet<>(Arrays.asList(EMPTY_ROLE))));
+ // Empty - null
+ when(zConf.getString(ZeppelinConfiguration.ConfVars.ZEPPELIN_WRITER_ROLES)).thenReturn(null);
+ assertTrue(authorizationService.isWriter(testNote.getId(), new HashSet<>(Arrays.asList(TEST_USER_1))));
+ }
+
+ @Test
+ void testDefaultReaders() throws IOException {
+ Note testNote = new Note();
+ authorizationService.createNoteAuth(testNote.getId(), new AuthenticationInfo(TEST_USER_1));
+
+ // Comma separated with trim
+ when(zConf.getString(ZeppelinConfiguration.ConfVars.ZEPPELIN_READER_ROLES)).thenReturn("TestGroup, TestGroup2");
+ for (String role : Arrays.asList("TestGroup", "TestGroup2")) {
+ assertTrue(authorizationService.isReader(testNote.getId(), new HashSet<>(Arrays.asList(role))));
+ assertFalse(authorizationService.isOwner(testNote.getId(), new HashSet<>(Arrays.asList(role))));
+ assertFalse(authorizationService.isRunner(testNote.getId(), new HashSet<>(Arrays.asList(role))));
+ assertFalse(authorizationService.isWriter(testNote.getId(), new HashSet<>(Arrays.asList(role))));
+ }
+
+ // Empty - Blank
+ when(zConf.getString(ZeppelinConfiguration.ConfVars.ZEPPELIN_READER_ROLES)).thenReturn(BLANK_ROLE);
+ assertFalse(authorizationService.isReader(testNote.getId(), new HashSet<>(Arrays.asList(BLANK_ROLE))));
+ // Empty - Empty
+ when(zConf.getString(ZeppelinConfiguration.ConfVars.ZEPPELIN_READER_ROLES)).thenReturn(EMPTY_ROLE);
+ assertFalse(authorizationService.isReader(testNote.getId(), new HashSet<>(Arrays.asList(EMPTY_ROLE))));
+ // Empty - null
+ when(zConf.getString(ZeppelinConfiguration.ConfVars.ZEPPELIN_READER_ROLES)).thenReturn(null);
+ assertTrue(authorizationService.isReader(testNote.getId(), new HashSet<>(Arrays.asList(TEST_USER_1))));
+ }
+
+ @Test
+ void testWorldReadable() throws IOException {
+ Note testNote = new Note();
+ authorizationService.createNoteAuth(testNote.getId(), new AuthenticationInfo(TEST_USER_1));
+ authorizationService.setReaders(testNote.getId(), Collections.emptySet());
+
+ assertTrue(authorizationService.isReader(testNote.getId(), new HashSet<>(Arrays.asList(TEST_USER_2))));
+ assertFalse(authorizationService.isRunner(testNote.getId(), new HashSet<>(Arrays.asList(TEST_USER_2))));
+ assertFalse(authorizationService.isWriter(testNote.getId(), new HashSet<>(Arrays.asList(TEST_USER_2))));
+ assertFalse(authorizationService.isOwner(testNote.getId(), new HashSet<>(Arrays.asList(TEST_USER_2))));
+ }
+
+ @Test
+ void testWorldRunnable() throws IOException {
+ Note testNote = new Note();
+ authorizationService.createNoteAuth(testNote.getId(), new AuthenticationInfo(TEST_USER_1));
+ authorizationService.setRunners(testNote.getId(), Collections.emptySet());
+
+ assertTrue(authorizationService.isReader(testNote.getId(), new HashSet<>(Arrays.asList(TEST_USER_2))));
+ assertTrue(authorizationService.isRunner(testNote.getId(), new HashSet<>(Arrays.asList(TEST_USER_2))));
+ assertFalse(authorizationService.isWriter(testNote.getId(), new HashSet<>(Arrays.asList(TEST_USER_2))));
+ assertFalse(authorizationService.isOwner(testNote.getId(), new HashSet<>(Arrays.asList(TEST_USER_2))));
+ }
+
+ @Test
+ void testWorldWritable() throws IOException {
+ Note testNote = new Note();
+ authorizationService.createNoteAuth(testNote.getId(), new AuthenticationInfo(TEST_USER_1));
+ authorizationService.setWriters(testNote.getId(), Collections.emptySet());
+
+ assertTrue(authorizationService.isReader(testNote.getId(), new HashSet<>(Arrays.asList(TEST_USER_2))));
+ assertTrue(authorizationService.isRunner(testNote.getId(), new HashSet<>(Arrays.asList(TEST_USER_2))));
+ assertTrue(authorizationService.isWriter(testNote.getId(), new HashSet<>(Arrays.asList(TEST_USER_2))));
+ assertFalse(authorizationService.isOwner(testNote.getId(), new HashSet<>(Arrays.asList(TEST_USER_2))));
+ }
+
+ @Test
+ void testWorldOwnership() throws IOException {
+ Note testNote = new Note();
+ authorizationService.createNoteAuth(testNote.getId(), new AuthenticationInfo(TEST_USER_1));
+ authorizationService.setOwners(testNote.getId(), Collections.emptySet());
+
+ assertTrue(authorizationService.isReader(testNote.getId(), new HashSet<>(Arrays.asList(TEST_USER_2))));
+ assertTrue(authorizationService.isRunner(testNote.getId(), new HashSet<>(Arrays.asList(TEST_USER_2))));
+ assertTrue(authorizationService.isWriter(testNote.getId(), new HashSet<>(Arrays.asList(TEST_USER_2))));
+ assertTrue(authorizationService.isOwner(testNote.getId(), new HashSet<>(Arrays.asList(TEST_USER_2))));
+ }
+
+}