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)))); + } + +}