diff --git a/opengrok-indexer/src/main/java/org/opengrok/indexer/authorization/AuthorizationFramework.java b/opengrok-indexer/src/main/java/org/opengrok/indexer/authorization/AuthorizationFramework.java index 0a9d3211d77..6826d64e803 100644 --- a/opengrok-indexer/src/main/java/org/opengrok/indexer/authorization/AuthorizationFramework.java +++ b/opengrok-indexer/src/main/java/org/opengrok/indexer/authorization/AuthorizationFramework.java @@ -22,22 +22,10 @@ */ package org.opengrok.indexer.authorization; -import java.io.File; -import java.io.IOException; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Modifier; -import java.security.AccessController; -import java.security.PrivilegedAction; -import java.util.Arrays; -import java.util.Enumeration; -import java.util.LinkedList; -import java.util.List; import java.util.Map; import java.util.TreeMap; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; -import java.util.jar.JarEntry; -import java.util.jar.JarFile; import java.util.logging.Level; import java.util.logging.Logger; import javax.servlet.http.HttpServletRequest; @@ -47,8 +35,8 @@ import org.opengrok.indexer.configuration.Nameable; import org.opengrok.indexer.configuration.Project; import org.opengrok.indexer.configuration.RuntimeEnvironment; +import org.opengrok.indexer.framework.PluginFramework; import org.opengrok.indexer.logger.LoggerFactory; -import org.opengrok.indexer.util.IOUtils; import org.opengrok.indexer.web.Statistics; /** @@ -56,30 +44,26 @@ * * @author Krystof Tulinger */ -public final class AuthorizationFramework { +public final class AuthorizationFramework extends PluginFramework { private static final Logger LOGGER = LoggerFactory.getLogger(AuthorizationFramework.class); - /** - * Plugin directory. - */ - private File pluginDirectory; - - /** - * Customized class loader for plugin classes. - */ - private AuthorizationPluginClassLoader loader; - /** * Stack of available plugins/stacks in the order of the execution. */ AuthorizationStack stack; + /** * New stack. This is set by {@code setStack()} and used for delayed * reconfiguration in {@code reload()}. */ AuthorizationStack newStack; + /** + * Stack of plugins intended for loading. + */ + AuthorizationStack loadingStack; + /** * Lock for safe reloads. */ @@ -89,20 +73,14 @@ public final class AuthorizationFramework { * Keeping track of the number of reloads in this framework. This is * used to invalidate the session and force reload the authorization values * stored in HTTP session. - * + *

* Starting at 0 and increases with every reload. */ private long pluginVersion = 0; - /** - * Whether to load plugins from class files and jar files. - */ - private boolean loadClasses = true; - private boolean loadJars = true; - // HTTP session attribute that holds plugin version private static final String SESSION_VERSION = "opengrok-authorization-session-version"; - + /** * Create a new instance of authorization framework with no plugin * directory and the default plugin stack. @@ -110,7 +88,7 @@ public final class AuthorizationFramework { public AuthorizationFramework() { this(null); } - + /** * Create a new instance of authorization framework with the plugin * directory and the default plugin stack. @@ -125,70 +103,12 @@ public AuthorizationFramework(String path) { * Create a new instance of authorization framework with the plugin * directory and the plugin stack. * - * @param path the plugin directory path + * @param path the plugin directory path * @param stack the top level stack configuration */ public AuthorizationFramework(String path, AuthorizationStack stack) { + super(IAuthorizationPlugin.class, path); this.stack = stack; - setPluginDirectory(path); - } - - /** - * Get the plugin directory. - * @return plugin directory file - */ - public synchronized File getPluginDirectory() { - return pluginDirectory; - } - - /** - * Set the plugin directory. - * - * @param pluginDirectory the directory - */ - public synchronized void setPluginDirectory(File pluginDirectory) { - this.pluginDirectory = pluginDirectory; - } - - /** - * Set the plugin directory. - * - * @param directory the directory path - */ - public void setPluginDirectory(String directory) { - setPluginDirectory(directory != null ? new File(directory) : null); - } - - /** - * Make {@code reload()} search for plugins in class files. - * @param flag true or false - */ - public void setLoadClasses(boolean flag) { - loadClasses = flag; - } - - /** - * Whether to search for plugins in class files. - * @return true if enabled, false otherwise - */ - public boolean isLoadClassesEnabled() { - return loadClasses; - } - - /** - * Make {@code reload()} search for plugins in jar files. - * @param flag true or false - */ - public void setLoadJars(boolean flag) { - loadJars = flag; - } - - /** - * Whether to search for plugins in class files. - * @return true if enabled, false otherwise - */ - public boolean isLoadJarsEnabled() { - return loadJars; } /** @@ -198,7 +118,6 @@ public boolean isLoadJarsEnabled() { * @param request request object * @param project project object * @return true if yes - * * @see #checkAll */ public boolean isAllowed(HttpServletRequest request, Project project) { @@ -211,22 +130,22 @@ public boolean isAllowed(HttpServletRequest request, Project project) { public boolean decision(IAuthorizationPlugin plugin) { return plugin.isAllowed(request, project); } - }, new AuthorizationEntity.PluginSkippingPredicate() { - @Override - public boolean shouldSkip(AuthorizationEntity authEntity) { - // shouldn't skip if there is no setup - if (authEntity.forProjects().isEmpty() && authEntity.forGroups().isEmpty()) { - return false; - } - - // shouldn't skip if the project is contained in the setup - if (authEntity.forProjects().contains(project.getName())) { - return false; - } - - return true; - } - }); + }, new AuthorizationEntity.PluginSkippingPredicate() { + @Override + public boolean shouldSkip(AuthorizationEntity authEntity) { + // shouldn't skip if there is no setup + if (authEntity.forProjects().isEmpty() && authEntity.forGroups().isEmpty()) { + return false; + } + + // shouldn't skip if the project is contained in the setup + if (authEntity.forProjects().contains(project.getName())) { + return false; + } + + return true; + } + }); } /** @@ -234,9 +153,8 @@ public boolean shouldSkip(AuthorizationEntity authEntity) { * {@link #checkAll} for more information about invocation order. * * @param request request object - * @param group group object + * @param group group object * @return true if yes - * * @see #checkAll */ public boolean isAllowed(HttpServletRequest request, Group group) { @@ -249,43 +167,29 @@ public boolean isAllowed(HttpServletRequest request, Group group) { public boolean decision(IAuthorizationPlugin plugin) { return plugin.isAllowed(request, group); } - }, new AuthorizationEntity.PluginSkippingPredicate() { - @Override - public boolean shouldSkip(AuthorizationEntity authEntity) { - // shouldn't skip if there is no setup - if (authEntity.forProjects().isEmpty() && authEntity.forGroups().isEmpty()) { - return false; - } - - // shouldn't skip if the group is contained in the setup - return !authEntity.forGroups().contains(group.getName()); - } - }); - } + }, new AuthorizationEntity.PluginSkippingPredicate() { + @Override + public boolean shouldSkip(AuthorizationEntity authEntity) { + // shouldn't skip if there is no setup + if (authEntity.forProjects().isEmpty() && authEntity.forGroups().isEmpty()) { + return false; + } - /** - * Return the java canonical name for the plugin class. If the canonical - * name does not exist it returns the usual java name. - * - * @param plugin the plugin - * @return the class name - */ - protected String getClassName(IAuthorizationPlugin plugin) { - if (plugin.getClass().getCanonicalName() != null) { - return plugin.getClass().getCanonicalName(); - } - return plugin.getClass().getName(); + // shouldn't skip if the group is contained in the setup + return !authEntity.forGroups().contains(group.getName()); + } + }); } /** * Get available plugins. - * + *

* This method and couple of following methods use locking because *

    *
  1. plugins can be reloaded at anytime
  2. *
  3. requests are pretty asynchronous
  4. *
- * + *

* So this tries to ensure that there will be no * {@code ConcurrentModificationException} or other similar exceptions. * @@ -313,7 +217,7 @@ public void setStack(AuthorizationStack s) { /** * Add an entity into the plugin stack. * - * @param stack the stack + * @param stack the stack * @param entity the authorization entity (stack or plugin) */ protected void addPlugin(AuthorizationStack stack, AuthorizationEntity entity) { @@ -326,7 +230,7 @@ protected void addPlugin(AuthorizationStack stack, AuthorizationEntity entity) { * Add a plug-in into the plug-in stack. This has the same effect as invoking * addPlugin(stack, IAuthorizationPlugin, REQUIRED). * - * @param stack the stack + * @param stack the stack * @param plugin the authorization plug-in */ public void addPlugin(AuthorizationStack stack, IAuthorizationPlugin plugin) { @@ -343,21 +247,21 @@ public void addPlugin(AuthorizationStack stack, IAuthorizationPlugin plugin) { *

New plugin

* If there is no entry in configuration for this class, the plugin is * appended to the end of the plugin stack with flag flag - * - *

The plug-in's load method is NOT invoked at this point

* + *

The plug-in's load method is NOT invoked at this point

+ *

* This has the same effect as invoking * {@code addPlugin(new AuthorizationEntity(stack, flag, * getClassName(plugin), plugin)}. * - * @param stack the stack + * @param stack the stack * @param plugin the authorization plug-in - * @param flag the flag for the new plug-in + * @param flag the flag for the new plug-in */ public void addPlugin(AuthorizationStack stack, IAuthorizationPlugin plugin, AuthControlFlag flag) { if (stack != null) { LOGGER.log(Level.WARNING, "Plugin class \"{0}\" was not found in configuration." - + " Appending the plugin at the end of the list with flag \"{1}\"", + + " Appending the plugin at the end of the list with flag \"{1}\"", new Object[]{getClassName(plugin), flag}); addPlugin(stack, new AuthorizationPlugin(flag, getClassName(plugin), plugin)); } @@ -372,7 +276,7 @@ public void removeAll() { unloadAllPlugins(stack); stack.getStack().clear(); } - + private void removeAll(AuthorizationStack stack) { unloadAllPlugins(stack); stack.getStack().clear(); @@ -401,182 +305,28 @@ public void unloadAllPlugins(AuthorizationStack stack) { } } - /** - * Wrapper around the class loading. Report all exceptions into the log. - * - * @param classname full name of the class - * @return the class implementing the {@link IAuthorizationPlugin} interface - * or null if there is no such class - * - * @see #loadClass(String) - */ - public IAuthorizationPlugin handleLoadClass(String classname) { - try { - return loadClass(classname); - } catch (ClassNotFoundException ex) { - LOGGER.log(Level.WARNING, String.format("Class \"%s\" was not found", classname), ex); - } catch (SecurityException ex) { - LOGGER.log(Level.WARNING, String.format("Class \"%s\" was found but it is placed in prohibited package: ", classname), ex); - } catch (InstantiationException ex) { - LOGGER.log(Level.WARNING, String.format("Class \"%s\" could not be instantiated: ", classname), ex); - } catch (IllegalAccessException ex) { - LOGGER.log(Level.WARNING, String.format("Class \"%s\" loader threw an exception: ", classname), ex); - } catch (Throwable ex) { - LOGGER.log(Level.WARNING, String.format("Class \"%s\" loader threw an unknown error: ", classname), ex); - } - return null; - } - - /** - * Load a class into JVM with custom class loader. Call a non-parametric - * constructor to create a new instance of that class. - * - *

- * The classes implementing the {@link IAuthorizationPlugin} interface are - * returned and initialized with a call to a non-parametric constructor. - *

- * - * @param classname the full name of the class to load - * @return the class implementing the {@link IAuthorizationPlugin} interface - * or null if there is no such class - * - * @throws ClassNotFoundException when the class can not be found - * @throws SecurityException when it is prohibited to load such class - * @throws InstantiationException when it is impossible to create a new - * instance of that class - * @throws IllegalAccessException when the constructor of the class is not - * accessible - * @throws NoSuchMethodException when the class does not have no-argument constructor - * @throws InvocationTargetException if the underlying constructor of the class throws an exception - */ - @SuppressWarnings("rawtypes") - private IAuthorizationPlugin loadClass(String classname) throws ClassNotFoundException, - SecurityException, - InstantiationException, - IllegalAccessException, - NoSuchMethodException, - InvocationTargetException { - - Class c = loader.loadClass(classname); - - // check for implemented interfaces - for (Class intf1 : getInterfaces(c)) { - if (intf1.getCanonicalName().equals(IAuthorizationPlugin.class.getCanonicalName()) - && !Modifier.isAbstract(c.getModifiers())) { - // call to non-parametric constructor - return (IAuthorizationPlugin) c.getDeclaredConstructor().newInstance(); - } - } - LOGGER.log(Level.FINEST, "Plugin class \"{0}\" does not implement IAuthorizationPlugin interface.", classname); - return null; - } - - /** - * Get all available interfaces of a class c. - * - * @param c class - * @return array of interfaces of the class c - */ - @SuppressWarnings("rawtypes") - protected List getInterfaces(Class c) { - List interfaces = new LinkedList<>(); - Class self = c; - while (self != null && !interfaces.contains(IAuthorizationPlugin.class)) { - interfaces.addAll(Arrays.asList(self.getInterfaces())); - self = self.getSuperclass(); + @Override + protected void classLoaded(IAuthorizationPlugin plugin) { + if (!loadingStack.setPlugin(plugin)) { + LOGGER.log(Level.INFO, "plugin {0} is not configured in the stack", plugin.getClass().getCanonicalName()); } - return interfaces; } /** - * Traverse list of files which possibly contain a java class - * to load all classes which are contained within them into the given stack. - * Each class is loaded with {@link #handleLoadClass(String)} which - * delegates the loading to the custom class loader - * {@link #loadClass(String)}. - * - * @param stack the stack where to add the loaded classes - * @param classfiles list of files which possibly contain a java class + * Prepare the loading stack for new plugins. * - * @see #handleLoadClass(String) - * @see #loadClass(String) - */ - private void loadClassFiles(AuthorizationStack stack, List classfiles) { - IAuthorizationPlugin pf; - - for (File file : classfiles) { - String classname = getClassName(file); - if (classname.isEmpty()) { - continue; - } - // Load the class in memory and try to find a configured space for this class. - if ((pf = handleLoadClass(classname)) != null) { - if (!stack.setPlugin(pf)) { - LOGGER.log(Level.INFO, "plugin {0} is not configured in the stack", - classname); - } - } - } - } - - /** - * Traverse list of jar files to load all classes which are contained within - * them into the given stack. - * Each class is loaded with {@link #handleLoadClass(String)} which - * delegates the loading to the custom class loader - * {@link #loadClass(String)}. - * - * @param stack the stack where to add the loaded classes - * @param jarfiles list of jar files containing java classes - * - * @see #handleLoadClass(String) - * @see #loadClass(String) + * @see #classLoaded(IAuthorizationPlugin) */ - private void loadJarFiles(AuthorizationStack stack, List jarfiles) { - IAuthorizationPlugin pf; - - - for (File file : jarfiles) { - try (JarFile jar = new JarFile(file)) { - Enumeration entries = jar.entries(); - while (entries.hasMoreElements()) { - JarEntry entry = entries.nextElement(); - if (!entry.getName().endsWith(".class")) { - continue; - } - String classname = getClassName(entry); - if (!entry.getName().endsWith(".class") || classname.isEmpty()) { - continue; - } - // Load the class in memory and try to find a configured space for this class. - if ((pf = handleLoadClass(classname)) != null) { - if (!stack.setPlugin(pf)) { - LOGGER.log(Level.INFO, "plugin {0} is not configured in the stack", - classname); - } - } - } - } catch (IOException ex) { - LOGGER.log(Level.WARNING, "Could not manipulate with file because of: ", ex); - } + @Override + protected void beforeReload() { + if (this.newStack == null) { + // Clone a new stack not interfering with the current stack. + loadingStack = getStack().clone(); + } else { + loadingStack = this.newStack.clone(); } } - private String getClassName(File f) { - String classname = f.getAbsolutePath().substring(pluginDirectory.getAbsolutePath().length() + 1, f.getAbsolutePath().length()); - classname = classname.replace(File.separatorChar, '.'); // convert to package name - // no need to check for the index from lastIndexOf because we're in a branch - // where we expect the .class suffix - classname = classname.substring(0, classname.lastIndexOf('.')); // strip .class - return classname; - } - - private String getClassName(JarEntry f) { - // java jar always uses / as separator - String classname = f.getName().replace('/', '.'); // convert to package name - return classname.substring(0, classname.lastIndexOf('.')); // strip .class - } - /** * Calling this function forces the framework to reload its stack. * @@ -584,7 +334,7 @@ private String getClassName(JarEntry f) { * Plugins are taken from the pluginDirectory.

* *

- * Old instances of stack are removed and new list of stack is constructed. + * Old instances in stack are removed and new list of stack is constructed. * Unload and load event is fired on each plugin.

* *

@@ -596,46 +346,15 @@ private String getClassName(JarEntry f) { * @see Configuration#getPluginDirectory() */ @SuppressWarnings({"rawtypes", "unchecked"}) - public void reload() { - if (pluginDirectory == null || !pluginDirectory.isDirectory() || !pluginDirectory.canRead()) { - LOGGER.log(Level.WARNING, "Plugin directory not found or not readable: {0}. " - + "All requests allowed.", pluginDirectory); - return; - } + @Override + protected void afterReload() { if (stack == null) { LOGGER.log(Level.WARNING, "Plugin stack not found in configuration: null. All requests allowed."); return; } - LOGGER.log(Level.INFO, "Plugins are being reloaded from {0}", pluginDirectory.getAbsolutePath()); - - // trashing out the old instance of the loaded enables us - // to reload the stack at runtime - loader = (AuthorizationPluginClassLoader) AccessController.doPrivileged(new PrivilegedAction() { - @Override - public Object run() { - return new AuthorizationPluginClassLoader(pluginDirectory); - } - }); - - AuthorizationStack newLocalStack; - if (this.newStack == null) { - // Clone a new stack not interfering with the current stack. - newLocalStack = getStack().clone(); - } else { - newLocalStack = this.newStack.clone(); - } - - // Load all other possible plugin classes. - if (isLoadClassesEnabled()) { - loadClassFiles(newLocalStack, IOUtils.listFilesRec(pluginDirectory, ".class")); - } - if (isLoadJarsEnabled()) { - loadJarFiles(newLocalStack, IOUtils.listFiles(pluginDirectory, ".jar")); - } - // fire load events - loadAllPlugins(newLocalStack); + loadAllPlugins(loadingStack); AuthorizationStack oldStack; /** @@ -648,8 +367,8 @@ public Object run() { lock.writeLock().lock(); try { oldStack = stack; - stack = newLocalStack; - + stack = loadingStack; + // increase the current plugin version tracked by the framework increasePluginVersion(); } finally { @@ -662,13 +381,14 @@ public Object run() { // clean the old stack removeAll(oldStack); oldStack = null; + loadingStack = null; } /** * Returns the current plugin version in this framework. - * + *

* This number changes with every {@code reload()}. - * + *

* Assumes the {@code lock} is held for reading. * * @return the current version number @@ -679,7 +399,7 @@ private long getPluginVersion() { /** * Changes the plugin version to the next version. - * + *

* Assumes that {@code lock} is held for writing. */ private void increasePluginVersion() { @@ -688,7 +408,7 @@ private void increasePluginVersion() { /** * Is this session marked as invalid? - * + *

* Assumes the {@code lock} is held for reading. * * @param session the request session @@ -698,7 +418,7 @@ private boolean isSessionInvalid(HttpSession session) { if (session.getAttribute(SESSION_VERSION) == null) { return true; } - + long version = (long) session.getAttribute(SESSION_VERSION); return version != getPluginVersion(); @@ -745,20 +465,19 @@ private boolean isSessionInvalid(HttpSession session) { *

* Plugins in the configuration which have not been loaded are skipped.

* - * @param request request object - * @param cache cache - * @param entity entity with name - * @param pluginPredicate predicate to determine the plugin's decision for the request + * @param request request object + * @param cache cache + * @param entity entity with name + * @param pluginPredicate predicate to determine the plugin's decision for the request * @param skippingPredicate predicate to determine if the plugin should be skipped for this request * @return true if yes - * * @see RuntimeEnvironment#getPluginStack() */ @SuppressWarnings("unchecked") private boolean checkAll(HttpServletRequest request, String cache, Nameable entity, AuthorizationEntity.PluginDecisionPredicate pluginPredicate, AuthorizationEntity.PluginSkippingPredicate skippingPredicate) { - + if (stack == null) { return true; } @@ -786,7 +505,7 @@ private boolean checkAll(HttpServletRequest request, String cache, Nameable enti long time = 0; boolean overallDecision = false; - + lock.readLock().lock(); try { // Make sure there is a HTTP session that corresponds to current plugin version. @@ -818,29 +537,29 @@ private boolean checkAll(HttpServletRequest request, String cache, Nameable enti String.format("authorization_of_%s", entity.getName()), time); } - + m.put(entity.getName(), overallDecision); request.setAttribute(cache, m); - + return overallDecision; } /** * Perform the actual check for the entity. - * + *

* Assumes that {@code lock} is held in read mode. - * - * @param entity either a project or a group - * @param pluginPredicate a predicate that decides if the authorization is - * successful for the given plugin + * + * @param entity either a project or a group + * @param pluginPredicate a predicate that decides if the authorization is + * successful for the given plugin * @param skippingPredicate predicate that decides if given authorization - * entity should be omitted from the authorization process + * entity should be omitted from the authorization process * @return true if entity is allowed; false otherwise */ private boolean performCheck(Nameable entity, AuthorizationEntity.PluginDecisionPredicate pluginPredicate, AuthorizationEntity.PluginSkippingPredicate skippingPredicate) { - - return stack.isAllowed(entity, pluginPredicate, skippingPredicate); + + return stack.isAllowed(entity, pluginPredicate, skippingPredicate); } } diff --git a/opengrok-indexer/src/main/java/org/opengrok/indexer/authorization/AuthorizationPluginClassLoader.java b/opengrok-indexer/src/main/java/org/opengrok/indexer/framework/PluginClassLoader.java similarity index 87% rename from opengrok-indexer/src/main/java/org/opengrok/indexer/authorization/AuthorizationPluginClassLoader.java rename to opengrok-indexer/src/main/java/org/opengrok/indexer/framework/PluginClassLoader.java index e3e4a279624..172294c59ed 100644 --- a/opengrok-indexer/src/main/java/org/opengrok/indexer/authorization/AuthorizationPluginClassLoader.java +++ b/opengrok-indexer/src/main/java/org/opengrok/indexer/framework/PluginClassLoader.java @@ -17,10 +17,10 @@ * CDDL HEADER END */ - /* +/* * Copyright (c) 2016, 2019, Oracle and/or its affiliates. All rights reserved. */ -package org.opengrok.indexer.authorization; +package org.opengrok.indexer.framework; import java.io.File; import java.io.FileInputStream; @@ -37,39 +37,39 @@ import org.opengrok.indexer.logger.LoggerFactory; /** - * Class loader for authorization plugins. + * Class loader for plugins from .class and .jar files. * * @author Krystof Tulinger */ -public class AuthorizationPluginClassLoader extends ClassLoader { +public class PluginClassLoader extends ClassLoader { @SuppressWarnings("rawtypes") private final Map cache = new HashMap<>(); - private static final Logger LOGGER = LoggerFactory.getLogger(AuthorizationPluginClassLoader.class); + private static final Logger LOGGER = LoggerFactory.getLogger(PluginClassLoader.class); private static final String[] CLASS_WHITELIST = new String[]{ - "org.opengrok.indexer.configuration.Group", - "org.opengrok.indexer.configuration.Project", - "org.opengrok.indexer.configuration.RuntimeEnvironment", - "org.opengrok.indexer.authorization.IAuthorizationPlugin", - "org.opengrok.indexer.authorization.plugins.*", - "org.opengrok.indexer.util.*", - "org.opengrok.indexer.logger.*" + "org.opengrok.indexer.configuration.Group", + "org.opengrok.indexer.configuration.Project", + "org.opengrok.indexer.configuration.RuntimeEnvironment", + "org.opengrok.indexer.authorization.IAuthorizationPlugin", + "org.opengrok.indexer.authorization.plugins.*", + "org.opengrok.indexer.util.*", + "org.opengrok.indexer.logger.*" }; private static final String[] PACKAGE_BLACKLIST = new String[]{ - "java", - "javax", - "org.w3c", - "org.xml", - "org.omg", - "sun" + "java", + "javax", + "org.w3c", + "org.xml", + "org.omg", + "sun" }; private final File directory; - public AuthorizationPluginClassLoader(File directory) { - super(AuthorizationPluginClassLoader.class.getClassLoader()); + public PluginClassLoader(File directory) { + super(PluginClassLoader.class.getClassLoader()); this.directory = directory; } @@ -99,8 +99,8 @@ public boolean accept(File dir, String name) { Class c = defineClass(classname, bytes, 0, bytes.length); LOGGER.log(Level.FINE, "Class \"{0}\" found in file \"{1}\"", new Object[]{ - classname, - f.getAbsolutePath() + classname, + f.getAbsolutePath() }); return c; } @@ -125,8 +125,8 @@ private Class loadClassFromFile(String classname) throws ClassNotFoundException Class c = defineClass(classname, bytes, 0, bytes.length); LOGGER.log(Level.FINEST, "Class \"{0}\" found in file \"{1}\"", new Object[]{ - classname, - f.getAbsolutePath() + classname, + f.getAbsolutePath() }); return c; } @@ -178,7 +178,7 @@ private void checkPackage(String name) throws SecurityException { /** * Loads the class with given name. - * + *

* Order of lookup: *

    *
  1. already loaded classes
  2. @@ -186,14 +186,14 @@ private void checkPackage(String name) throws SecurityException { *
  3. loading from .class files
  4. *
  5. loading from .jar files
  6. *
- * + *

* Package blacklist: {@link #PACKAGE_BLACKLIST}.
* Classes whitelist: {@link #CLASS_WHITELIST}. * * @param name class name * @return loaded class or null * @throws ClassNotFoundException if class is not found - * @throws SecurityException if the loader cannot access the class + * @throws SecurityException if the loader cannot access the class */ @Override public Class loadClass(String name) throws ClassNotFoundException, SecurityException { @@ -202,7 +202,7 @@ public Class loadClass(String name) throws ClassNotFoundException, SecurityEx /** * Loads the class with given name. - * + *

* Order of lookup: *

    *
  1. already loaded classes
  2. @@ -210,15 +210,15 @@ public Class loadClass(String name) throws ClassNotFoundException, SecurityEx *
  3. loading from .class files
  4. *
  5. loading from .jar files
  6. *
- * + *

* Package blacklist: {@link #PACKAGE_BLACKLIST}.
* Classes whitelist: {@link #CLASS_WHITELIST}. * - * @param name class name + * @param name class name * @param resolveIt if the class should be resolved * @return loaded class or null * @throws ClassNotFoundException if class is not found - * @throws SecurityException if the loader cannot access the class + * @throws SecurityException if the loader cannot access the class */ @Override @SuppressWarnings("rawtypes") diff --git a/opengrok-indexer/src/main/java/org/opengrok/indexer/framework/PluginFramework.java b/opengrok-indexer/src/main/java/org/opengrok/indexer/framework/PluginFramework.java new file mode 100644 index 00000000000..e6586a54447 --- /dev/null +++ b/opengrok-indexer/src/main/java/org/opengrok/indexer/framework/PluginFramework.java @@ -0,0 +1,381 @@ +/* + * CDDL HEADER START + * + * The contents of this file are subject to the terms of the + * Common Development and Distribution License (the "License"). + * You may not use this file except in compliance with the License. + * + * See LICENSE.txt included in this distribution for the specific + * language governing permissions and limitations under the License. + * + * When distributing Covered Code, include this CDDL HEADER in each + * file and include the License file at LICENSE.txt. + * If applicable, add the following below this CDDL HEADER, with the + * fields enclosed by brackets "[]" replaced with your own identifying + * information: Portions Copyright [yyyy] [name of copyright owner] + * + * CDDL HEADER END + */ + +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + */ +package org.opengrok.indexer.framework; + +import java.io.File; +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Modifier; +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.util.Arrays; +import java.util.Enumeration; +import java.util.LinkedList; +import java.util.List; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.opengrok.indexer.authorization.IAuthorizationPlugin; +import org.opengrok.indexer.logger.LoggerFactory; +import org.opengrok.indexer.util.IOUtils; + +/** + * Plugin framework for plugins of type {@code PluginType}. + * + * @author Krystof Tulinger + */ +public abstract class PluginFramework { + + private static final Logger LOGGER = LoggerFactory.getLogger(PluginFramework.class); + + /** + * Class of the plugin type, necessary for instantiating and searching. + */ + private final Class classType; + + /** + * Plugin directory. + */ + private File pluginDirectory; + + /** + * Customized class loader for plugin classes. + */ + private PluginClassLoader loader; + + /** + * Whether to load plugins from class files and jar files. + */ + private boolean loadClasses = true; + private boolean loadJars = true; + + /** + * Create a new instance of plugin framework for a plugin directory. + * + * @param classType the class of the plugin type + * @param path the plugin directory path + */ + public PluginFramework(Class classType, String path) { + this.classType = classType; + setPluginDirectory(path); + } + + /** + * Get the plugin directory. + * + * @return plugin directory file + */ + public synchronized File getPluginDirectory() { + return pluginDirectory; + } + + /** + * Set the plugin directory. + * + * @param pluginDirectory the directory + */ + public synchronized void setPluginDirectory(File pluginDirectory) { + this.pluginDirectory = pluginDirectory; + } + + /** + * Set the plugin directory. + * + * @param directory the directory path + */ + public void setPluginDirectory(String directory) { + setPluginDirectory(directory != null ? new File(directory) : null); + } + + /** + * Make {@code reload()} search for plugins in class files. + * + * @param flag true or false + */ + public void setLoadClasses(boolean flag) { + loadClasses = flag; + } + + /** + * Whether to search for plugins in class files. + * + * @return true if enabled, false otherwise + */ + public boolean isLoadClassesEnabled() { + return loadClasses; + } + + /** + * Make {@code reload()} search for plugins in jar files. + * + * @param flag true or false + */ + public void setLoadJars(boolean flag) { + loadJars = flag; + } + + /** + * Whether to search for plugins in class files. + * + * @return true if enabled, false otherwise + */ + public boolean isLoadJarsEnabled() { + return loadJars; + } + + /** + * Return the java canonical name for the plugin class. If the canonical + * name does not exist it returns the usual java name. + * + * @param plugin the plugin + * @return the class name + */ + protected String getClassName(IAuthorizationPlugin plugin) { + if (plugin.getClass().getCanonicalName() != null) { + return plugin.getClass().getCanonicalName(); + } + return plugin.getClass().getName(); + } + + /** + * Wrapper around the class loading. Report all exceptions into the log. + * + * @param classname full name of the class + * @return the class implementing the {@link IAuthorizationPlugin} interface + * or null if there is no such class + * @see #loadClass(String) + */ + public PluginType handleLoadClass(String classname) { + try { + return loadClass(classname); + } catch (ClassNotFoundException ex) { + LOGGER.log(Level.WARNING, String.format("Class \"%s\" was not found", classname), ex); + } catch (SecurityException ex) { + LOGGER.log(Level.WARNING, String.format("Class \"%s\" was found but it is placed in prohibited package: ", classname), ex); + } catch (InstantiationException ex) { + LOGGER.log(Level.WARNING, String.format("Class \"%s\" could not be instantiated: ", classname), ex); + } catch (IllegalAccessException ex) { + LOGGER.log(Level.WARNING, String.format("Class \"%s\" loader threw an exception: ", classname), ex); + } catch (Throwable ex) { + LOGGER.log(Level.WARNING, String.format("Class \"%s\" loader threw an unknown error: ", classname), ex); + } + return null; + } + + /** + * Load a class into JVM with custom class loader. Call a non-parametric + * constructor to create a new instance of that class. + * + *

+ * The classes implementing/extending the {@code PluginType} type are + * returned and initialized with a call to a non-parametric constructor. + *

+ * + * @param classname the full name of the class to load + * @return the class implementing/extending the {@code PluginType} class + * or null if there is no such class + * @throws ClassNotFoundException when the class can not be found + * @throws SecurityException when it is prohibited to load such class + * @throws InstantiationException when it is impossible to create a new + * instance of that class + * @throws IllegalAccessException when the constructor of the class is not + * accessible + * @throws NoSuchMethodException when the class does not have no-argument constructor + * @throws InvocationTargetException if the underlying constructor of the class throws an exception + */ + @SuppressWarnings({"rawtypes", "unchecked"}) + private PluginType loadClass(String classname) throws ClassNotFoundException, + SecurityException, + InstantiationException, + IllegalAccessException, + NoSuchMethodException, + InvocationTargetException { + + Class c = loader.loadClass(classname); + + // check for implemented interfaces or extended superclasses + for (Class intf1 : getSuperclassesAndInterfaces(c)) { + if (intf1.getCanonicalName().equals(classType.getCanonicalName()) + && !Modifier.isAbstract(c.getModifiers())) { + // call to non-parametric constructor + return (PluginType) c.getDeclaredConstructor().newInstance(); + } + } + LOGGER.log(Level.FINEST, "Plugin class \"{0}\" does not implement IAuthorizationPlugin interface.", classname); + return null; + } + + /** + * Get all available interfaces or superclasses of a class clazz. + * + * @param clazz class + * @return list of interfaces or superclasses of the class clazz + */ + @SuppressWarnings("rawtypes") + protected List getSuperclassesAndInterfaces(Class clazz) { + List types = new LinkedList<>(); + Class self = clazz; + while (self != null && self != classType && !types.contains(classType)) { + types.add(self); + types.addAll(Arrays.asList(self.getInterfaces())); + self = self.getSuperclass(); + } + return types; + } + + /** + * Traverse list of files which possibly contain a java class + * to load all classes. + * Each class is loaded with {@link #handleLoadClass(String)} which + * delegates the loading to the custom class loader + * {@link #loadClass(String)}. + * + * @param classfiles list of files which possibly contain a java class + * @see #handleLoadClass(String) + * @see #loadClass(String) + */ + private void loadClassFiles(List classfiles) { + PluginType plugin; + + for (File file : classfiles) { + String classname = getClassName(file); + if (classname.isEmpty()) { + continue; + } + // Load the class in memory and try to find a configured space for this class. + if ((plugin = handleLoadClass(classname)) != null) { + classLoaded(plugin); + } + } + } + + /** + * Traverse list of jar files to load all classes. + *

+ * Each class is loaded with {@link #handleLoadClass(String)} which + * delegates the loading to the custom class loader + * {@link #loadClass(String)}. + * + * @param jarfiles list of jar files containing java classes + * @see #handleLoadClass(String) + * @see #loadClass(String) + */ + private void loadJarFiles(List jarfiles) { + PluginType pf; + + for (File file : jarfiles) { + try (JarFile jar = new JarFile(file)) { + Enumeration entries = jar.entries(); + while (entries.hasMoreElements()) { + JarEntry entry = entries.nextElement(); + if (!entry.getName().endsWith(".class")) { + continue; + } + String classname = getClassName(entry); + if (!entry.getName().endsWith(".class") || classname.isEmpty()) { + continue; + } + // Load the class in memory and try to find a configured space for this class. + if ((pf = handleLoadClass(classname)) != null) { + classLoaded(pf); + } + } + } catch (IOException ex) { + LOGGER.log(Level.WARNING, "Could not manipulate with file because of: ", ex); + } + } + } + + private String getClassName(File f) { + String classname = f.getAbsolutePath().substring(pluginDirectory.getAbsolutePath().length() + 1, f.getAbsolutePath().length()); + classname = classname.replace(File.separatorChar, '.'); // convert to package name + // no need to check for the index from lastIndexOf because we're in a branch + // where we expect the .class suffix + classname = classname.substring(0, classname.lastIndexOf('.')); // strip .class + return classname; + } + + private String getClassName(JarEntry f) { + // java jar always uses / as separator + String classname = f.getName().replace('/', '.'); // convert to package name + return classname.substring(0, classname.lastIndexOf('.')); // strip .class + } + + /** + * Allow the implementing class to interact with the loaded class when the class + * was loaded with the custom class loader. + * + * @param plugin the loaded plugin + */ + protected abstract void classLoaded(PluginType plugin); + + + /** + * Perform custom operations before the plugins are loaded. + */ + protected abstract void beforeReload(); + + /** + * Perform custom operations when the framework has reloaded all available plugins. + *

+ * When this is invoked, all plugins has been loaded into the memory and for each available plugin + * the {@link #classLoaded(Object)} was invoked. + */ + protected abstract void afterReload(); + + /** + * Calling this function forces the framework to reload the plugins. + * + *

+ * Plugins are taken from the pluginDirectory.

+ */ + @SuppressWarnings({"rawtypes", "unchecked"}) + public final void reload() { + if (pluginDirectory == null || !pluginDirectory.isDirectory() || !pluginDirectory.canRead()) { + LOGGER.log(Level.WARNING, "Plugin directory not found or not readable: {0}. " + + "All requests allowed.", pluginDirectory); + return; + } + + LOGGER.log(Level.INFO, "Plugins are being reloaded from {0}", pluginDirectory.getAbsolutePath()); + + // trashing out the old instance of the loaded enables us + // to reload the stack at runtime + loader = (PluginClassLoader) AccessController.doPrivileged((PrivilegedAction) () -> new PluginClassLoader(pluginDirectory)); + + // notify the implementing class that the reload is about to begin + beforeReload(); + + // load all other possible plugin classes. + if (isLoadClassesEnabled()) { + loadClassFiles(IOUtils.listFilesRec(pluginDirectory, ".class")); + } + if (isLoadJarsEnabled()) { + loadJarFiles(IOUtils.listFiles(pluginDirectory, ".jar")); + } + + // notify the implementing class that the reload has ended + afterReload(); + } +} diff --git a/opengrok-indexer/src/test/java/org/opengrok/indexer/authorization/AuthorizationPluginClassLoaderTest.java b/opengrok-indexer/src/test/java/org/opengrok/indexer/authorization/PluginClassLoaderTest.java similarity index 89% rename from opengrok-indexer/src/test/java/org/opengrok/indexer/authorization/AuthorizationPluginClassLoaderTest.java rename to opengrok-indexer/src/test/java/org/opengrok/indexer/authorization/PluginClassLoaderTest.java index 144719b22ea..925d5eede12 100644 --- a/opengrok-indexer/src/test/java/org/opengrok/indexer/authorization/AuthorizationPluginClassLoaderTest.java +++ b/opengrok-indexer/src/test/java/org/opengrok/indexer/authorization/PluginClassLoaderTest.java @@ -25,26 +25,25 @@ import java.io.File; import java.net.URISyntaxException; import java.nio.file.Paths; -import java.util.logging.Level; import org.junit.Assert; import org.junit.Test; import org.opengrok.indexer.configuration.Group; import org.opengrok.indexer.configuration.Project; -import org.opengrok.indexer.logger.LoggerUtil; +import org.opengrok.indexer.framework.PluginClassLoader; import org.opengrok.indexer.web.DummyHttpServletRequest; -public class AuthorizationPluginClassLoaderTest { +public class PluginClassLoaderTest { private final File pluginDirectory; - public AuthorizationPluginClassLoaderTest() throws URISyntaxException { + public PluginClassLoaderTest() throws URISyntaxException { pluginDirectory = Paths.get(getClass().getResource("/authorization/plugins/testplugins.jar").toURI()).toFile().getParentFile(); Assert.assertTrue(pluginDirectory.isDirectory()); } @Test public void testProhibitedPackages() { - AuthorizationPluginClassLoader instance = new AuthorizationPluginClassLoader(null); + PluginClassLoader instance = new PluginClassLoader(null); try { instance.loadClass("java.lang.plugin.MyPlugin"); @@ -97,7 +96,7 @@ public void testProhibitedPackages() { @Test public void testProhibitedNames() { - AuthorizationPluginClassLoader instance = new AuthorizationPluginClassLoader(null); + PluginClassLoader instance = new PluginClassLoader(null); try { instance.loadClass("org.opengrok.indexer.configuration.Group"); @@ -139,8 +138,8 @@ public void testProhibitedNames() { @Test @SuppressWarnings("rawtypes") public void testNonExistingPlugin() { - AuthorizationPluginClassLoader instance - = new AuthorizationPluginClassLoader(pluginDirectory); + PluginClassLoader instance + = new PluginClassLoader(pluginDirectory); loadClass(instance, "org.sample.plugin.NoPlugin", true); } @@ -148,8 +147,8 @@ public void testNonExistingPlugin() { @Test @SuppressWarnings("rawtypes") public void testFalsePlugin() { - AuthorizationPluginClassLoader instance - = new AuthorizationPluginClassLoader(pluginDirectory); + PluginClassLoader instance + = new PluginClassLoader(pluginDirectory); Class clazz = loadClass(instance, "opengrok.auth.plugin.FalsePlugin"); @@ -169,8 +168,8 @@ public void testFalsePlugin() { @Test @SuppressWarnings("rawtypes") public void testTruePlugin() { - AuthorizationPluginClassLoader instance - = new AuthorizationPluginClassLoader(pluginDirectory); + PluginClassLoader instance + = new PluginClassLoader(pluginDirectory); Class clazz = loadClass(instance, "opengrok.auth.plugin.TruePlugin"); @@ -203,12 +202,12 @@ private IAuthorizationPlugin getNewInstance(Class c) { } @SuppressWarnings("rawtypes") - private Class loadClass(AuthorizationPluginClassLoader loader, String name) { + private Class loadClass(PluginClassLoader loader, String name) { return loadClass(loader, name, false); } @SuppressWarnings("rawtypes") - private Class loadClass(AuthorizationPluginClassLoader loader, String name, boolean shouldFail) { + private Class loadClass(PluginClassLoader loader, String name, boolean shouldFail) { Class clazz = null; try { clazz = loader.loadClass(name);