envs) {
super(commandLine, envs);
}
diff --git a/zeppelin-server/src/main/java/org/apache/zeppelin/interpreter/remote/ProcessLaunchObserver.java b/zeppelin-server/src/main/java/org/apache/zeppelin/interpreter/remote/ProcessLaunchObserver.java
new file mode 100644
index 00000000000..a2c92a80a0a
--- /dev/null
+++ b/zeppelin-server/src/main/java/org/apache/zeppelin/interpreter/remote/ProcessLaunchObserver.java
@@ -0,0 +1,34 @@
+/*
+ * 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.interpreter.remote;
+
+/**
+ * Observes the output produced while an interpreter process is launched.
+ *
+ * Implementations can use the output to discover an external application and monitor its
+ * lifecycle. This interface deliberately exposes no cluster-manager-specific types so that
+ * implementations and their dependencies can live in optional plugins.
+ */
+@FunctionalInterface
+public interface ProcessLaunchObserver {
+
+ ProcessLaunchObserver NO_OP = (launchOutput, interpreterProcess) -> { };
+
+ void onProcessLaunch(
+ String launchOutput, RemoteInterpreterManagedProcess interpreterProcess);
+}
diff --git a/zeppelin-server/src/main/java/org/apache/zeppelin/notebook/repo/NotebookRepoSync.java b/zeppelin-server/src/main/java/org/apache/zeppelin/notebook/repo/NotebookRepoSync.java
index eb5b1e37e58..ccce415d7b2 100644
--- a/zeppelin-server/src/main/java/org/apache/zeppelin/notebook/repo/NotebookRepoSync.java
+++ b/zeppelin-server/src/main/java/org/apache/zeppelin/notebook/repo/NotebookRepoSync.java
@@ -82,7 +82,7 @@ public void init(ZeppelinConfiguration zConf, NoteParser noteParser) throws IOEx
for (int i = 0; i < Math.min(storageClassNames.length, getMaxRepoNum()); i++) {
NotebookRepo notebookRepo =
pluginManager.loadNotebookRepo(storageClassNames[i].trim());
- notebookRepo.init(zConf, noteParser);
+ initNotebookRepo(notebookRepo, zConf, noteParser);
repos.add(notebookRepo);
}
@@ -90,7 +90,7 @@ public void init(ZeppelinConfiguration zConf, NoteParser noteParser) throws IOEx
if (getRepoCount() == 0) {
LOGGER.info("No storage could be initialized, using default {} storage", DEFAULT_STORAGE);
NotebookRepo defaultNotebookRepo = pluginManager.loadNotebookRepo(DEFAULT_STORAGE);
- defaultNotebookRepo.init(zConf, noteParser);
+ initNotebookRepo(defaultNotebookRepo, zConf, noteParser);
repos.add(defaultNotebookRepo);
}
// sync for anonymous mode on start
@@ -103,6 +103,19 @@ public void init(ZeppelinConfiguration zConf, NoteParser noteParser) throws IOEx
}
}
+ private void initNotebookRepo(
+ NotebookRepo notebookRepo, ZeppelinConfiguration zConf, NoteParser noteParser)
+ throws IOException {
+ Thread thread = Thread.currentThread();
+ ClassLoader previousClassLoader = thread.getContextClassLoader();
+ try {
+ thread.setContextClassLoader(notebookRepo.getClass().getClassLoader());
+ notebookRepo.init(zConf, noteParser);
+ } finally {
+ thread.setContextClassLoader(previousClassLoader);
+ }
+ }
+
public List getNotebookRepos(AuthenticationInfo subject) {
List reposSetting = new ArrayList<>();
NotebookRepoWithSettings repoWithSettings;
diff --git a/zeppelin-server/src/main/java/org/apache/zeppelin/plugin/PluginManager.java b/zeppelin-server/src/main/java/org/apache/zeppelin/plugin/PluginManager.java
index b229d0f0f57..07c22011c8d 100644
--- a/zeppelin-server/src/main/java/org/apache/zeppelin/plugin/PluginManager.java
+++ b/zeppelin-server/src/main/java/org/apache/zeppelin/plugin/PluginManager.java
@@ -22,6 +22,7 @@
import org.apache.zeppelin.interpreter.launcher.SparkInterpreterLauncher;
import org.apache.zeppelin.interpreter.launcher.StandardInterpreterLauncher;
import org.apache.zeppelin.interpreter.recovery.RecoveryStorage;
+import org.apache.zeppelin.interpreter.remote.ProcessLaunchObserver;
import org.apache.zeppelin.notebook.repo.GitNotebookRepo;
import org.apache.zeppelin.notebook.repo.NotebookRepo;
import org.apache.zeppelin.notebook.repo.VFSNotebookRepo;
@@ -35,9 +36,13 @@
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Collections;
import java.util.HashMap;
+import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
+import java.util.ServiceLoader;
+import java.util.Set;
import jakarta.inject.Inject;
@@ -51,7 +56,8 @@ public class PluginManager {
private final String pluginsDir;
private final ZeppelinConfiguration zConf;
- private Map cachedLaunchers = new HashMap<>();
+ private final Map cachedLaunchers = new HashMap<>();
+ private final Map pluginClassLoaders = new HashMap<>();
private List builtinLauncherClassNames = Arrays.asList(
StandardInterpreterLauncher.class.getName(),
@@ -85,10 +91,13 @@ public NotebookRepo loadNotebookRepo(String notebookRepoClassName) throws IOExce
}
NotebookRepo notebookRepo = null;
try {
- notebookRepo = (NotebookRepo) (Class.forName(notebookRepoClassName, true, pluginClassLoader)).newInstance();
- } catch (InstantiationException | IllegalAccessException | ClassNotFoundException e) {
+ notebookRepo = withContextClassLoader(pluginClassLoader, () ->
+ (NotebookRepo) Class.forName(notebookRepoClassName, true, pluginClassLoader)
+ .getDeclaredConstructor()
+ .newInstance());
+ } catch (ReflectiveOperationException e) {
throw new IOException("Fail to instantiate notebookrepo " + notebookRepoClassName +
- " from plugin classpath:" + pluginsDir, e);
+ " from plugin classpath:" + pluginsDir, e);
}
return notebookRepo;
@@ -112,10 +121,12 @@ public synchronized InterpreterLauncher loadInterpreterLauncher(String launcherP
if (builtinLauncherClassNames.contains(launcherClassName) ||
Boolean.parseBoolean(System.getProperty("zeppelin.isTest", "false"))) {
try {
- return (InterpreterLauncher)
+ InterpreterLauncher launcher = (InterpreterLauncher)
(Class.forName(launcherClassName))
.getConstructor(ZeppelinConfiguration.class, RecoveryStorage.class)
.newInstance(zConf, recoveryStorage);
+ configureProcessLaunchObservers(launcher);
+ return launcher;
} catch (InstantiationException | IllegalAccessException | ClassNotFoundException
| NoSuchMethodException | InvocationTargetException e) {
throw new IOException("Fail to instantiate InterpreterLauncher from classpath directly:"
@@ -126,37 +137,184 @@ public synchronized InterpreterLauncher loadInterpreterLauncher(String launcherP
URLClassLoader pluginClassLoader = getPluginClassLoader(pluginsDir, "Launcher", launcherPlugin);
InterpreterLauncher launcher = null;
try {
- launcher = (InterpreterLauncher) (Class.forName(launcherClassName, true, pluginClassLoader))
- .getConstructor(ZeppelinConfiguration.class, RecoveryStorage.class)
- .newInstance(zConf, recoveryStorage);
- } catch (InstantiationException | IllegalAccessException | ClassNotFoundException
- | NoSuchMethodException | InvocationTargetException e) {
+ launcher = withContextClassLoader(pluginClassLoader, () ->
+ (InterpreterLauncher) Class.forName(launcherClassName, true, pluginClassLoader)
+ .getConstructor(ZeppelinConfiguration.class, RecoveryStorage.class)
+ .newInstance(zConf, recoveryStorage));
+ } catch (ReflectiveOperationException e) {
throw new IOException("Fail to instantiate Launcher " + launcherPlugin +
" from plugin pluginDir: " + pluginsDir, e);
}
+ configureProcessLaunchObservers(launcher);
cachedLaunchers.put(launcherPlugin, launcher);
return launcher;
}
+ private void configureProcessLaunchObservers(InterpreterLauncher launcher) throws IOException {
+ if (launcher instanceof StandardInterpreterLauncher) {
+ ((StandardInterpreterLauncher) launcher)
+ .setProcessLaunchObservers(loadServiceProviders(ProcessLaunchObserver.class));
+ }
+ }
+
private URLClassLoader getPluginClassLoader(String pluginsDir,
String pluginType,
String pluginName) throws IOException {
File pluginFolder = new File(pluginsDir + "/" + pluginType + "/" + pluginName);
+ return getPluginClassLoader(pluginFolder);
+ }
+
+ private synchronized URLClassLoader getPluginClassLoader(File pluginFolder) throws IOException {
if (!pluginFolder.exists() || pluginFolder.isFile()) {
LOGGER.warn("PluginFolder {} doesn't exist or is not a directory", pluginFolder.getAbsolutePath());
return null;
}
+ String pluginFolderPath = pluginFolder.getCanonicalPath();
+ if (pluginClassLoaders.containsKey(pluginFolderPath)) {
+ return pluginClassLoaders.get(pluginFolderPath);
+ }
List urls = new ArrayList<>();
- for (File file : pluginFolder.listFiles()) {
- LOGGER.debug("Add file {} to classpath of plugin: {}", file.getAbsolutePath(), pluginName);
- urls.add(file.toURI().toURL());
+ File[] pluginFiles = pluginFolder.listFiles();
+ if (pluginFiles != null) {
+ for (File file : pluginFiles) {
+ LOGGER.debug("Add file {} to classpath of plugin: {}",
+ file.getAbsolutePath(), pluginFolder.getName());
+ urls.add(file.toURI().toURL());
+ }
}
if (urls.isEmpty()) {
- LOGGER.warn("Can not load plugin {}, because the plugin folder {} is empty.", pluginName , pluginFolder);
+ LOGGER.warn("Can not load plugin, because the plugin folder {} is empty.", pluginFolder);
return null;
}
- return new URLClassLoader(urls.toArray(new URL[0]));
+ URLClassLoader classLoader = new URLClassLoader(
+ urls.toArray(new URL[0]), PluginManager.class.getClassLoader());
+ pluginClassLoaders.put(pluginFolderPath, classLoader);
+ return classLoader;
+ }
+
+ /**
+ * Load an extension class from any plugin directory.
+ *
+ * This is used by configurable extension points such as ConfigStorage and RecoveryStorage,
+ * whose implementation class name is stored in zeppelin-site.xml. The implementation and all
+ * of its third-party dependencies stay in the plugin classloader.
+ */
+ public Class> loadPluginClass(String className) throws IOException {
+ try {
+ return Class.forName(className, true, PluginManager.class.getClassLoader());
+ } catch (ClassNotFoundException e) {
+ File pluginFolder = findPluginFolder(className);
+ if (pluginFolder == null) {
+ throw new IOException("Unable to find plugin class: " + className, e);
+ }
+ try {
+ URLClassLoader classLoader = getPluginClassLoader(pluginFolder);
+ return withContextClassLoader(
+ classLoader, () -> Class.forName(className, true, classLoader));
+ } catch (ClassNotFoundException pluginError) {
+ throw new IOException("Unable to load plugin class: " + className, pluginError);
+ } catch (ReflectiveOperationException pluginError) {
+ throw new IOException("Unable to initialize plugin class: " + className, pluginError);
+ }
+ }
+ }
+
+ public T createPluginInstance(String className,
+ Class>[] parameterTypes,
+ Object[] parameters) throws IOException {
+ try {
+ Class> pluginClass = loadPluginClass(className);
+ @SuppressWarnings("unchecked") T instance = withContextClassLoader(
+ pluginClass.getClassLoader(),
+ () -> (T) pluginClass.getConstructor(parameterTypes).newInstance(parameters));
+ return instance;
+ } catch (ReflectiveOperationException e) {
+ throw new IOException("Unable to instantiate plugin class: " + className, e);
+ }
+ }
+
+ /** Load service providers without adding their dependency jars to the server classpath. */
+ public List loadServiceProviders(Class serviceType) throws IOException {
+ List providers = new ArrayList<>();
+ Set providerClassNames = new LinkedHashSet<>();
+ for (File pluginFolder : getPluginFolders()) {
+ URLClassLoader classLoader = getPluginClassLoader(pluginFolder);
+ if (classLoader == null) {
+ continue;
+ }
+ Thread thread = Thread.currentThread();
+ ClassLoader previousClassLoader = thread.getContextClassLoader();
+ try {
+ thread.setContextClassLoader(classLoader);
+ for (T provider : ServiceLoader.load(serviceType, classLoader)) {
+ if (provider.getClass().getClassLoader() == classLoader &&
+ providerClassNames.add(provider.getClass().getName())) {
+ providers.add(provider);
+ }
+ }
+ } finally {
+ thread.setContextClassLoader(previousClassLoader);
+ }
+ }
+ return providers;
+ }
+
+ /** Return the isolated classpath containing a configured plugin class. */
+ public List getPluginClasspath(String className) throws IOException {
+ File pluginFolder = findPluginFolder(className);
+ if (pluginFolder == null) {
+ return Collections.emptyList();
+ }
+ File[] files = pluginFolder.listFiles();
+ if (files == null) {
+ return Collections.emptyList();
+ }
+ return Arrays.asList(files);
+ }
+
+ private File findPluginFolder(String className) throws IOException {
+ String classResource = className.replace('.', '/') + ".class";
+ for (File pluginFolder : getPluginFolders()) {
+ URLClassLoader classLoader = getPluginClassLoader(pluginFolder);
+ if (classLoader != null && classLoader.findResource(classResource) != null) {
+ return pluginFolder;
+ }
+ }
+ return null;
+ }
+
+ private List getPluginFolders() {
+ File root = new File(pluginsDir);
+ File[] pluginTypes = root.listFiles(File::isDirectory);
+ if (pluginTypes == null) {
+ return Collections.emptyList();
+ }
+ List pluginFolders = new ArrayList<>();
+ for (File pluginType : pluginTypes) {
+ File[] folders = pluginType.listFiles(File::isDirectory);
+ if (folders != null) {
+ pluginFolders.addAll(Arrays.asList(folders));
+ }
+ }
+ return pluginFolders;
+ }
+
+ @FunctionalInterface
+ private interface ReflectiveAction {
+ T run() throws ReflectiveOperationException;
+ }
+
+ private static T withContextClassLoader(
+ ClassLoader classLoader, ReflectiveAction action) throws ReflectiveOperationException {
+ Thread thread = Thread.currentThread();
+ ClassLoader previousClassLoader = thread.getContextClassLoader();
+ try {
+ thread.setContextClassLoader(classLoader);
+ return action.run();
+ } finally {
+ thread.setContextClassLoader(previousClassLoader);
+ }
}
}
diff --git a/zeppelin-server/src/main/java/org/apache/zeppelin/realm/ExternalLoginRealm.java b/zeppelin-server/src/main/java/org/apache/zeppelin/realm/ExternalLoginRealm.java
new file mode 100644
index 00000000000..350e1a5c24e
--- /dev/null
+++ b/zeppelin-server/src/main/java/org/apache/zeppelin/realm/ExternalLoginRealm.java
@@ -0,0 +1,49 @@
+/*
+ * 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.realm;
+
+import java.util.Map;
+
+import jakarta.ws.rs.core.Cookie;
+import org.apache.shiro.authc.AuthenticationException;
+import org.apache.shiro.authc.AuthenticationToken;
+
+/**
+ * Contract used by the server login endpoint to interact with optional SSO realms without
+ * depending on their implementation classes.
+ */
+public interface ExternalLoginRealm {
+
+ AuthenticationToken getLoginAuthenticationToken(Map cookies)
+ throws AuthenticationException;
+
+ String getLoginPrincipal(AuthenticationToken token) throws AuthenticationException;
+
+ boolean shouldRedirectOnMissingToken();
+
+ int getLoginPriority();
+
+ String getProviderUrl();
+
+ String getRedirectParam();
+
+ String getLogin();
+
+ String getLogout();
+
+ Boolean getLogoutAPI();
+}
diff --git a/zeppelin-server/src/main/java/org/apache/zeppelin/realm/GroupResolver.java b/zeppelin-server/src/main/java/org/apache/zeppelin/realm/GroupResolver.java
new file mode 100644
index 00000000000..db70f23abc3
--- /dev/null
+++ b/zeppelin-server/src/main/java/org/apache/zeppelin/realm/GroupResolver.java
@@ -0,0 +1,26 @@
+/*
+ * 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.realm;
+
+import java.io.IOException;
+import java.util.Set;
+
+/** Resolves the external groups associated with a user. */
+public interface GroupResolver {
+
+ Set resolve(String principal) throws IOException;
+}
diff --git a/zeppelin-server/src/main/java/org/apache/zeppelin/realm/LdapRealm.java b/zeppelin-server/src/main/java/org/apache/zeppelin/realm/LdapRealm.java
index be8a0f0c68d..c01c72d93f1 100644
--- a/zeppelin-server/src/main/java/org/apache/zeppelin/realm/LdapRealm.java
+++ b/zeppelin-server/src/main/java/org/apache/zeppelin/realm/LdapRealm.java
@@ -44,9 +44,6 @@
import javax.naming.ldap.LdapContext;
import javax.naming.ldap.LdapName;
import javax.naming.ldap.PagedResultsControl;
-import org.apache.hadoop.conf.Configuration;
-import org.apache.hadoop.security.alias.CredentialProvider;
-import org.apache.hadoop.security.alias.CredentialProviderFactory;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.ShiroException;
import org.apache.shiro.authc.AuthenticationInfo;
@@ -184,6 +181,8 @@ public class LdapRealm extends DefaultLdapRealm {
private String hadoopSecurityCredentialPath;
private static final String KEYSTORE_PASS = "ldapRealm.systemPassword";
+ private static final String HADOOP_SECRET_RESOLVER =
+ "org.apache.zeppelin.realm.hadoop.HadoopCredentialProviderSecretResolver";
private boolean authorizationEnabled;
@@ -228,15 +227,13 @@ static String getSystemPassword(String hadoopSecurityCredentialPath,
String keystorePass) {
String password = "";
try {
- Configuration configuration = new Configuration();
- configuration.set(CredentialProviderFactory.CREDENTIAL_PROVIDER_PATH,
- hadoopSecurityCredentialPath);
- CredentialProvider provider = CredentialProviderFactory.getProviders(configuration).get(0);
- CredentialProvider.CredentialEntry credEntry = provider.getCredentialEntry(keystorePass);
- if (credEntry != null) {
- password = new String(credEntry.getCredential());
+ SecretResolver resolver =
+ SecurityProviderLoader.load(HADOOP_SECRET_RESOLVER, SecretResolver.class);
+ char[] credential = resolver.resolve(hadoopSecurityCredentialPath, keystorePass);
+ if (credential != null) {
+ password = new String(credential);
}
- } catch (IOException e) {
+ } catch (IOException | ReflectiveOperationException e) {
throw new ShiroException("Error from getting credential entry from keystore", e);
}
if (org.apache.commons.lang3.StringUtils.isEmpty(password)) {
diff --git a/zeppelin-server/src/main/java/org/apache/zeppelin/realm/SecretResolver.java b/zeppelin-server/src/main/java/org/apache/zeppelin/realm/SecretResolver.java
new file mode 100644
index 00000000000..7f1c34edf64
--- /dev/null
+++ b/zeppelin-server/src/main/java/org/apache/zeppelin/realm/SecretResolver.java
@@ -0,0 +1,25 @@
+/*
+ * 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.realm;
+
+import java.io.IOException;
+
+/** Resolves a named secret from an external provider. */
+public interface SecretResolver {
+
+ char[] resolve(String providerPath, String alias) throws IOException;
+}
diff --git a/zeppelin-server/src/main/java/org/apache/zeppelin/realm/SecurityProviderLoader.java b/zeppelin-server/src/main/java/org/apache/zeppelin/realm/SecurityProviderLoader.java
new file mode 100644
index 00000000000..24b37d8c13c
--- /dev/null
+++ b/zeppelin-server/src/main/java/org/apache/zeppelin/realm/SecurityProviderLoader.java
@@ -0,0 +1,34 @@
+/*
+ * 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.realm;
+
+/** Loads optional security providers from the class loader that created the Shiro environment. */
+public final class SecurityProviderLoader {
+
+ private SecurityProviderLoader() {
+ }
+
+ public static T load(String className, Class providerType)
+ throws ReflectiveOperationException {
+ ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
+ if (classLoader == null) {
+ classLoader = SecurityProviderLoader.class.getClassLoader();
+ }
+ Class> providerClass = Class.forName(className, true, classLoader);
+ return providerType.cast(providerClass.getDeclaredConstructor().newInstance());
+ }
+}
diff --git a/zeppelin-server/src/main/java/org/apache/zeppelin/realm/ZeppelinRoleProvider.java b/zeppelin-server/src/main/java/org/apache/zeppelin/realm/ZeppelinRoleProvider.java
new file mode 100644
index 00000000000..c7c76701385
--- /dev/null
+++ b/zeppelin-server/src/main/java/org/apache/zeppelin/realm/ZeppelinRoleProvider.java
@@ -0,0 +1,25 @@
+/*
+ * 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.realm;
+
+import java.util.Set;
+
+/** Supplies the Zeppelin roles associated with an authenticated principal. */
+public interface ZeppelinRoleProvider {
+
+ Set mapGroupPrincipals(String principal);
+}
diff --git a/zeppelin-server/src/main/java/org/apache/zeppelin/realm/jwt/KnoxJwtRealm.java b/zeppelin-server/src/main/java/org/apache/zeppelin/realm/jwt/KnoxJwtRealm.java
index 0a1d8decc9c..98419406dd5 100644
--- a/zeppelin-server/src/main/java/org/apache/zeppelin/realm/jwt/KnoxJwtRealm.java
+++ b/zeppelin-server/src/main/java/org/apache/zeppelin/realm/jwt/KnoxJwtRealm.java
@@ -16,48 +16,55 @@
*/
package org.apache.zeppelin.realm.jwt;
-import java.nio.charset.Charset;
-import java.nio.charset.StandardCharsets;
-import java.util.Date;
-import org.apache.commons.io.FileUtils;
-import org.apache.hadoop.conf.Configuration;
-import org.apache.hadoop.security.Groups;
-import org.apache.shiro.authc.AuthenticationInfo;
-import org.apache.shiro.authc.AuthenticationToken;
-import org.apache.shiro.authc.SimpleAccount;
-import org.apache.shiro.authz.AuthorizationInfo;
-import org.apache.shiro.authz.SimpleAuthorizationInfo;
-import org.apache.shiro.realm.AuthorizingRealm;
-import org.apache.shiro.subject.PrincipalCollection;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
import java.security.PublicKey;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.security.interfaces.RSAPublicKey;
import java.text.ParseException;
-import java.util.HashSet;
-import java.util.List;
+import java.util.Collections;
+import java.util.Date;
+import java.util.Map;
import java.util.Set;
import jakarta.servlet.ServletException;
+import jakarta.ws.rs.core.Cookie;
import com.nimbusds.jose.JWSObject;
import com.nimbusds.jose.JWSVerifier;
import com.nimbusds.jose.crypto.RSASSAVerifier;
import com.nimbusds.jwt.SignedJWT;
+import org.apache.commons.io.FileUtils;
+import org.apache.shiro.ShiroException;
+import org.apache.shiro.authc.AuthenticationException;
+import org.apache.shiro.authc.AuthenticationInfo;
+import org.apache.shiro.authc.AuthenticationToken;
+import org.apache.shiro.authc.SimpleAccount;
+import org.apache.shiro.authz.AuthorizationInfo;
+import org.apache.shiro.authz.SimpleAuthorizationInfo;
+import org.apache.shiro.realm.AuthorizingRealm;
+import org.apache.shiro.subject.PrincipalCollection;
+import org.apache.zeppelin.realm.ExternalLoginRealm;
+import org.apache.zeppelin.realm.GroupResolver;
+import org.apache.zeppelin.realm.SecurityProviderLoader;
+import org.apache.zeppelin.realm.ZeppelinRoleProvider;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
/**
* Created for org.apache.zeppelin.server.
*/
-public class KnoxJwtRealm extends AuthorizingRealm {
+public class KnoxJwtRealm extends AuthorizingRealm
+ implements ExternalLoginRealm, ZeppelinRoleProvider {
private static final Logger LOGGER = LoggerFactory.getLogger(KnoxJwtRealm.class);
+ private static final String HADOOP_GROUP_RESOLVER =
+ "org.apache.zeppelin.realm.hadoop.HadoopGroupResolver";
private String providerUrl;
private String redirectParam;
@@ -66,21 +73,19 @@ public class KnoxJwtRealm extends AuthorizingRealm {
private String login;
private String logout;
private Boolean logoutAPI;
+ private String groupResolverClass = HADOOP_GROUP_RESOLVER;
- /**
- * Hadoop Groups implementation.
- */
- private Groups hadoopGroups;
+ private GroupResolver groupResolver = principal -> Collections.emptySet();
@Override
protected void onInit() {
super.onInit();
try {
- Configuration hadoopConfig = new Configuration();
- hadoopGroups = new Groups(hadoopConfig);
+ groupResolver = SecurityProviderLoader.load(groupResolverClass, GroupResolver.class);
} catch (final Exception e) {
- LOGGER.error("Exception in onInit", e);
+ throw new ShiroException(
+ "Unable to load the Knox group resolver: " + groupResolverClass, e);
}
}
@@ -215,22 +220,17 @@ protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principal
}
/**
- * Query the Hadoop implementation of {@link Groups} to retrieve groups for provided user.
+ * Query the configured resolver to retrieve groups for the provided user.
*/
public Set mapGroupPrincipals(final String mappedPrincipalName) {
- /* return the groups as seen by Hadoop */
- Set groups;
try {
- final List groupList = hadoopGroups
- .getGroups(mappedPrincipalName);
+ Set groups = groupResolver.resolve(mappedPrincipalName);
if (LOGGER.isDebugEnabled()) {
LOGGER.debug(String.format("group found %s, %s",
- mappedPrincipalName, groupList.toString()));
+ mappedPrincipalName, groups.toString()));
}
-
- groups = new HashSet<>(groupList);
-
+ return groups;
} catch (final IOException e) {
if (e.toString().contains("No groups found for user")) {
/* no groups found move on */
@@ -240,9 +240,49 @@ public Set mapGroupPrincipals(final String mappedPrincipalName) {
/* Log the error and return empty group */
LOGGER.info(String.format("errorGettingUserGroups for %s", mappedPrincipalName));
}
- groups = new HashSet<>();
+ return Collections.emptySet();
+ }
+ }
+
+ void setGroupResolver(GroupResolver groupResolver) {
+ this.groupResolver = groupResolver;
+ }
+
+ @Override
+ public AuthenticationToken getLoginAuthenticationToken(
+ Map cookies) {
+ Cookie cookie = cookies.get(cookieName);
+ if (cookie == null || cookie.getValue() == null) {
+ return null;
+ }
+ return new JWTAuthenticationToken(null, cookie.getValue());
+ }
+
+ @Override
+ public String getLoginPrincipal(AuthenticationToken token) throws AuthenticationException {
+ try {
+ return getName((JWTAuthenticationToken) token);
+ } catch (ParseException e) {
+ throw new AuthenticationException("Unable to parse the Knox JWT", e);
}
- return groups;
+ }
+
+ @Override
+ public boolean shouldRedirectOnMissingToken() {
+ return true;
+ }
+
+ @Override
+ public int getLoginPriority() {
+ return 100;
+ }
+
+ public String getGroupResolverClass() {
+ return groupResolverClass;
+ }
+
+ public void setGroupResolverClass(String groupResolverClass) {
+ this.groupResolverClass = groupResolverClass;
}
public String getProviderUrl() {
diff --git a/zeppelin-server/src/main/java/org/apache/zeppelin/rest/LoginRestApi.java b/zeppelin-server/src/main/java/org/apache/zeppelin/rest/LoginRestApi.java
index d8b8c93b93e..d92a9177c8e 100644
--- a/zeppelin-server/src/main/java/org/apache/zeppelin/rest/LoginRestApi.java
+++ b/zeppelin-server/src/main/java/org/apache/zeppelin/rest/LoginRestApi.java
@@ -16,7 +16,6 @@
*/
package org.apache.zeppelin.rest;
-import java.text.ParseException;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
@@ -29,7 +28,6 @@
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.Context;
-import jakarta.ws.rs.core.Cookie;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.Response.Status;
@@ -43,10 +41,7 @@
import org.apache.zeppelin.annotation.ZeppelinApi;
import org.apache.zeppelin.conf.ZeppelinConfiguration;
import org.apache.zeppelin.notebook.AuthorizationService;
-import org.apache.zeppelin.realm.jwt.JWTAuthenticationToken;
-import org.apache.zeppelin.realm.jwt.KnoxJwtRealm;
-import org.apache.zeppelin.realm.kerberos.KerberosRealm;
-import org.apache.zeppelin.realm.kerberos.KerberosToken;
+import org.apache.zeppelin.realm.ExternalLoginRealm;
import org.apache.zeppelin.server.JsonResponse;
import org.apache.zeppelin.service.AuthenticationService;
import org.apache.zeppelin.ticket.TicketContainer;
@@ -78,108 +73,57 @@ public LoginRestApi(ZeppelinConfiguration zConf,
@ZeppelinApi
public Response getLogin(@Context HttpHeaders headers) {
JsonResponse