diff --git a/conf/zeppelin-site.xml.template b/conf/zeppelin-site.xml.template index b12a072c148..e665a9b3c10 100755 --- a/conf/zeppelin-site.xml.template +++ b/conf/zeppelin-site.xml.template @@ -410,6 +410,12 @@ Anonymous user allowed by default + + zeppelin.username.force.lowercase + false + Force convert username case to lower case, useful for Active Directory/LDAP. Default is not to change case + + zeppelin.notebook.default.owner.username 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 2960cd03faa..061e230be14 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 @@ -517,6 +517,10 @@ public boolean isAnonymousAllowed() { return getBoolean(ConfVars.ZEPPELIN_ANONYMOUS_ALLOWED); } + public boolean isUsernameForceLowerCase() { + return getBoolean(ConfVars.ZEPPELIN_USERNAME_FORCE_LOWERCASE); + } + public boolean isNotebookPublic() { return getBoolean(ConfVars.ZEPPELIN_NOTEBOOK_PUBLIC); } @@ -767,6 +771,7 @@ public enum ConfVars { // i.e. http://localhost:8080 ZEPPELIN_ALLOWED_ORIGINS("zeppelin.server.allowed.origins", "*"), ZEPPELIN_ANONYMOUS_ALLOWED("zeppelin.anonymous.allowed", true), + ZEPPELIN_USERNAME_FORCE_LOWERCASE("zeppelin.username.force.lowercase", false), ZEPPELIN_CREDENTIALS_PERSIST("zeppelin.credentials.persist", true), ZEPPELIN_CREDENTIALS_ENCRYPT_KEY("zeppelin.credentials.encryptKey", null), ZEPPELIN_WEBSOCKET_MAX_TEXT_MESSAGE_SIZE("zeppelin.websocket.max.text.message.size", "1024000"), diff --git a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/InterpreterOption.java b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/InterpreterOption.java index e8a92255fc9..0c01d97ecf7 100644 --- a/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/InterpreterOption.java +++ b/zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/InterpreterOption.java @@ -19,6 +19,7 @@ import java.util.ArrayList; import java.util.List; +import org.apache.zeppelin.conf.ZeppelinConfiguration; /** * @@ -27,6 +28,7 @@ public class InterpreterOption { public static final transient String SHARED = "shared"; public static final transient String SCOPED = "scoped"; public static final transient String ISOLATED = "isolated"; + private static ZeppelinConfiguration conf = ZeppelinConfiguration.create(); // always set it as true, keep this field just for backward compatibility boolean remote = true; @@ -66,6 +68,13 @@ public void setUserPermission(boolean setPermission) { } public List getOwners() { + if (null != owners && conf.isUsernameForceLowerCase()) { + List lowerCaseUsers = new ArrayList(); + for (String owner : owners) { + lowerCaseUsers.add(owner.toLowerCase()); + } + return lowerCaseUsers; + } return owners; } diff --git a/zeppelin-server/src/main/java/org/apache/zeppelin/utils/SecurityUtils.java b/zeppelin-server/src/main/java/org/apache/zeppelin/utils/SecurityUtils.java index f9f5f228ff0..f5329f31683 100644 --- a/zeppelin-server/src/main/java/org/apache/zeppelin/utils/SecurityUtils.java +++ b/zeppelin-server/src/main/java/org/apache/zeppelin/utils/SecurityUtils.java @@ -44,6 +44,7 @@ import org.apache.zeppelin.conf.ZeppelinConfiguration; import org.apache.zeppelin.realm.ActiveDirectoryGroupRealm; import org.apache.zeppelin.realm.LdapRealm; +import org.apache.zeppelin.server.ZeppelinServer; /** * Tools for securing Zeppelin. @@ -91,6 +92,11 @@ public static String getPrincipal() { String principal; if (subject.isAuthenticated()) { principal = extractPrincipal(subject); + if (ZeppelinServer.notebook.getConf().isUsernameForceLowerCase()) { + log.debug("Converting principal name " + principal + + " to lower case:" + principal.toLowerCase()); + principal = principal.toLowerCase(); + } } else { principal = ANONYMOUS; } diff --git a/zeppelin-server/src/test/java/org/apache/zeppelin/security/SecurityUtilsTest.java b/zeppelin-server/src/test/java/org/apache/zeppelin/security/SecurityUtilsTest.java index ac6f1688002..0c535b968eb 100644 --- a/zeppelin-server/src/test/java/org/apache/zeppelin/security/SecurityUtilsTest.java +++ b/zeppelin-server/src/test/java/org/apache/zeppelin/security/SecurityUtilsTest.java @@ -21,23 +21,26 @@ import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.when; +import java.io.IOException; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.net.InetAddress; +import java.net.URISyntaxException; +import java.net.UnknownHostException; import org.apache.commons.configuration.ConfigurationException; +import org.apache.zeppelin.conf.ZeppelinConfiguration; +import org.apache.zeppelin.notebook.Notebook; +import org.apache.zeppelin.server.ZeppelinServer; +import org.apache.zeppelin.utils.SecurityUtils; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; +import org.mockito.Mockito; import org.powermock.api.mockito.PowerMockito; import org.powermock.core.classloader.annotations.PrepareForTest; import org.powermock.modules.junit4.PowerMockRunner; - -import java.net.InetAddress; -import java.net.URISyntaxException; -import java.net.UnknownHostException; - import sun.security.acl.PrincipalImpl; -import org.apache.zeppelin.conf.ZeppelinConfiguration; -import org.apache.zeppelin.utils.SecurityUtils; - @RunWith(PowerMockRunner.class) @PrepareForTest(org.apache.shiro.SecurityUtils.class) public class SecurityUtilsTest { @@ -113,12 +116,49 @@ public void notAURIOrigin() @Test public void canGetPrincipalName() { String expectedName = "java.security.Principal.getName()"; + setupPrincipalName(expectedName); + assertEquals(expectedName, SecurityUtils.getPrincipal()); + } + + @Test + public void testUsernameForceLowerCase() throws IOException, InterruptedException { + String expectedName = "java.security.Principal.getName()"; + System.setProperty(ZeppelinConfiguration.ConfVars.ZEPPELIN_USERNAME_FORCE_LOWERCASE + .getVarName(), String.valueOf(true)); + setupPrincipalName(expectedName); + assertEquals(expectedName.toLowerCase(), SecurityUtils.getPrincipal()); + + } + + private void setupPrincipalName(String expectedName) { SecurityUtils.setIsEnabled(true); PowerMockito.mockStatic(org.apache.shiro.SecurityUtils.class); when(org.apache.shiro.SecurityUtils.getSubject()).thenReturn(subject); when(subject.isAuthenticated()).thenReturn(true); when(subject.getPrincipal()).thenReturn(new PrincipalImpl(expectedName)); - assertEquals(expectedName, SecurityUtils.getPrincipal()); + Notebook notebook = Mockito.mock(Notebook.class); + try { + setFinalStatic(ZeppelinServer.class.getDeclaredField("notebook"), notebook); + when(ZeppelinServer.notebook.getConf()) + .thenReturn(new ZeppelinConfiguration(this.getClass().getResource("/zeppelin-site.xml"))); + } catch (NoSuchFieldException e) { + e.printStackTrace(); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } catch (ConfigurationException e) { + e.printStackTrace(); + } + } + + private void setFinalStatic(Field field, Object newValue) + throws NoSuchFieldException, IllegalAccessException { + field.setAccessible(true); + Field modifiersField = Field.class.getDeclaredField("modifiers"); + modifiersField.setAccessible(true); + modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL); + field.set(null, newValue); } + + } diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/NotebookAuthorization.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/NotebookAuthorization.java index f73b49e3a09..137af651aa9 100644 --- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/NotebookAuthorization.java +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/NotebookAuthorization.java @@ -205,6 +205,21 @@ public void setWriters(String noteId, Set entities) { saveToFile(); } + /* + * If case conversion is enforced, then change entity names to lower case + */ + private Set checkCaseAndConvert(Set entities) { + if (conf.isUsernameForceLowerCase()) { + Set set2 = new HashSet(); + for (String name : entities) { + set2.add(name.toLowerCase()); + } + return set2; + } else { + return entities; + } + } + public Set getOwners(String noteId) { Map> noteAuthInfo = authInfo.get(noteId); Set entities = null; @@ -214,6 +229,8 @@ public Set getOwners(String noteId) { entities = noteAuthInfo.get("owners"); if (entities == null) { entities = new HashSet<>(); + } else { + entities = checkCaseAndConvert(entities); } } return entities; @@ -228,6 +245,8 @@ public Set getReaders(String noteId) { entities = noteAuthInfo.get("readers"); if (entities == null) { entities = new HashSet<>(); + } else { + entities = checkCaseAndConvert(entities); } } return entities; @@ -242,6 +261,8 @@ public Set getRunners(String noteId) { entities = noteAuthInfo.get("runners"); if (entities == null) { entities = new HashSet<>(); + } else { + entities = checkCaseAndConvert(entities); } } return entities; @@ -256,6 +277,8 @@ public Set getWriters(String noteId) { entities = noteAuthInfo.get("writers"); if (entities == null) { entities = new HashSet<>(); + } else { + entities = checkCaseAndConvert(entities); } } return entities;