/* * 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) 2016, 2017, Oracle and/or its affiliates. All rights reserved. */ package org.opensolaris.opengrok.authorization; import java.io.File; import java.io.IOException; 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; import org.opensolaris.opengrok.configuration.Configuration; import org.opensolaris.opengrok.configuration.Group; import org.opensolaris.opengrok.configuration.Nameable; import org.opensolaris.opengrok.configuration.Project; import org.opensolaris.opengrok.configuration.RuntimeEnvironment; import org.opensolaris.opengrok.logger.LoggerFactory; import org.opensolaris.opengrok.util.IOUtils; import org.opensolaris.opengrok.web.Statistics; /** * Placeholder for performing authorization checks. * * @author Krystof Tulinger */ public final class AuthorizationFramework { private static final Logger LOGGER = LoggerFactory.getLogger(AuthorizationFramework.class); private volatile static AuthorizationFramework instance = new AuthorizationFramework(); /** * 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; /** * Lock for safe reloads. */ private ReadWriteLock lock = new ReentrantReadWriteLock(); /** * Keeping track of the number of reloads in this framework. This can be * used by the plugins to invalidate the session and force reload the * authorization values. * * Starting at 0 and increases with every reload. * * The plugin should call RuntimeEnvironment.getPluginVersion() to get this * number. * * @see RuntimeEnvironment#getPluginVersion() */ private int pluginVersion = 0; /** * Plugin directory is set through RuntimeEnvironment. * * @return an instance of AuthorizationFramework * @see RuntimeEnvironment#getConfiguration * @see Configuration#setPluginDirectory */ public static AuthorizationFramework getInstance() { return instance; } /** * Get the plugin directory. */ 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); } /** * Checks if the request should have an access to project. See * {@link #checkAll} for more information about invocation order. * * @param request request object * @param project project object * @return true if yes * * @see #checkAll */ public boolean isAllowed(HttpServletRequest request, Project project) { return checkAll( request, "plugin_framework_project_cache", project, new AuthorizationEntity.PluginDecisionPredicate() { @Override 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; } }); } /** * Checks if the request should have an access to group. See * {@link #checkAll} for more information about invocation order. * * @param request request object * @param group group object * @return true if yes * * @see #checkAll */ public boolean isAllowed(HttpServletRequest request, Group group) { return checkAll( request, "plugin_framework_group_cache", group, new AuthorizationEntity.PluginDecisionPredicate() { @Override 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()); } }); } private AuthorizationFramework() { String path = RuntimeEnvironment.getInstance() .getPluginDirectory(); stack = RuntimeEnvironment.getInstance().getPluginStack(); setPluginDirectory(path); reload(); } /** * 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(); } /** * Get available plugins. * * This and couple of following methods are declared as synchronized because * <ol> * <li>plugins can be reloaded at anytime</li> * <li>requests are pretty asynchronous</li> * </ol> * * So this tries to ensure that there will be no * ConcurrentModificationException or other similar exceptions. * * @return the stack containing plugins/other stacks */ public AuthorizationStack getStack() { lock.readLock().lock(); try { return stack; } finally { lock.readLock().unlock(); } } /** * Set the internal stack to this new value. * * @param s new stack to be used */ public void setStack(AuthorizationStack s) { lock.writeLock().lock(); try { this.stack = s; } finally { lock.writeLock().unlock(); } } /** * Add an entity into the plugin stack. * * @param stack the stack * @param entity the authorization entity (stack or plugin) */ protected void addPlugin(AuthorizationStack stack, AuthorizationEntity entity) { if (stack != null) { stack.add(entity); } } /** * Add a plugin into the plugin stack. This has the same effect as invoking * addPlugin(stack, IAuthorizationPlugin, REQUIRED). * * @param stack the stack * @param plugin the authorization plugin */ public void addPlugin(AuthorizationStack stack, IAuthorizationPlugin plugin) { addPlugin(stack, plugin, AuthControlFlag.REQUIRED); } /** * Add a plugin into the plugin array. * * <h3>Configured plugin</h3> * For plugin which have an entry in configuration, the new plugin is put in * the place respecting the user-defined order of execution. * * <h3>New plugin</h3> * If there is no entry in configuration for this class, the plugin is * appended to the end of the plugin stack with flag <code>flag</code> * * <p> * <b>The plugin's load method is NOT invoked at this point</b></p> * * This has the same effect as invoking addPlugin(new * AuthorizationEntity(stack, flag, getClassName(plugin), plugin). * * @param stack the stack * @param plugin the authorization plugin * @param flag the flag for the new plugin */ public void addPlugin(AuthorizationStack stack, IAuthorizationPlugin plugin, AuthControlFlag flag) { if (stack != null) { LOGGER.log(Level.INFO, "Plugin class \"{0}\" was not found in configuration." + " 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)); } } /** * Remove and unload all plugins from the stack. * * @param stack the stack * @see AuthorizationEntity#unload() */ public void removeAll(AuthorizationStack stack) { unloadAllPlugins(stack); } /** * Load all plugins in the stack. If any plugin has not been loaded yet it * is marked as failed. * * @param stack the stack */ public void loadAllPlugins(AuthorizationStack stack) { if (stack != null) { stack.load(new TreeMap<>()); } } /** * Unload all plugins in the stack * * @param stack the stack */ public void unloadAllPlugins(AuthorizationStack stack) { if (stack != null) { stack.unload(); } } /** * 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.INFO, String.format("Class \"%s\" was not found: ", classname), ex); } catch (SecurityException ex) { LOGGER.log(Level.INFO, String.format("Class \"%s\" was found but it is placed in prohibited package: ", classname), ex); } catch (InstantiationException ex) { LOGGER.log(Level.INFO, String.format("Class \"%s\" could not be instantiated: ", classname), ex); } catch (IllegalAccessException ex) { LOGGER.log(Level.INFO, String.format("Class \"%s\" loader threw an exception: ", classname), ex); } catch (Throwable ex) { LOGGER.log(Level.INFO, String.format("Class \"%s\" loader threw an uknown 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. * * <p> * The classes implementing the {@link IAuthorizationPlugin} interface are * returned and initialized with a call to a non-parametric constructor. * </p> * * @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 */ private IAuthorizationPlugin loadClass(String classname) throws ClassNotFoundException, SecurityException, InstantiationException, IllegalAccessException { 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.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 */ protected List<Class> getInterfaces(Class c) { List<Class> interfaces = new LinkedList<>(); Class self = c; while (self != null && !interfaces.contains(IAuthorizationPlugin.class)) { interfaces.addAll(Arrays.asList(self.getInterfaces())); self = self.getSuperclass(); } return interfaces; } /** * Traverse list of files which possibly contain a java class and then * traverse a 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 classfiles list of files which possibly contain a java class * @param jarfiles list of jar files containing java classes * * @see #handleLoadClass(String) * @see #loadClass(String) */ private void loadClasses(AuthorizationStack stack, List<File> classfiles, List<File> jarfiles) { 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 && !stack.setPlugin(pf)) { // if there is not configured space -> append it to the stack addPlugin(stack, pf); } } for (File file : jarfiles) { try (JarFile jar = new JarFile(file)) { Enumeration<JarEntry> entries = jar.entries(); while (entries.hasMoreElements()) { JarEntry entry = entries.nextElement(); 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 && !stack.setPlugin(pf)) { // if there is not configured space -> append it to the stack addPlugin(stack, 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 classname = classname.substring(0, classname.lastIndexOf('.')); // strip .class return classname; } private String getClassName(JarEntry f) { String classname = f.getName().replace(File.separatorChar, '.'); // convert to package name return classname.substring(0, classname.lastIndexOf('.')); // strip .class } /** * Calling this function forces the framework to reload its stack. * * Plugins are taken from the pluginDirectory. * * Old instances of stack are removed and new list of stack is constructed. * Unload and load event is fired on each plugin. * * @see IAuthorizationPlugin#load(java.util.Map) * @see IAuthorizationPlugin#unload() * @see Configuration#getPluginDirectory() */ @SuppressWarnings("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; } 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 reaload the stack at runtime loader = (AuthorizationPluginClassLoader) AccessController.doPrivileged(new PrivilegedAction() { @Override public Object run() { return new AuthorizationPluginClassLoader(pluginDirectory); } }); // clone a new stack not interfering with the current stack AuthorizationStack newStack = RuntimeEnvironment.getInstance().getPluginStack().clone(); // increase the current plugin version tracked by the framework increasePluginVersion(); // load all other possible plugin classes loadClasses(newStack, IOUtils.listFilesRec(pluginDirectory, ".class"), IOUtils.listFiles(pluginDirectory, ".jar")); // fire load events loadAllPlugins(newStack); AuthorizationStack oldStack; /** * Replace the stack in a write lock to avoid inconsistent state between * the stack change and currently executing requests performing some * authorization on the same stack. * * @see #performCheck is controlled with a read lock */ lock.writeLock().lock(); try { oldStack = stack; stack = newStack; } finally { lock.writeLock().unlock(); } // clean the old stack removeAll(oldStack); oldStack = null; } /** * Returns the current plugin version in this framework. This can be used by * the plugin to invalidate the session and force reload the authorization * values. * * This number changes with every plugin reload. * * The plugin should call RuntimeEnvironment.getPluginVersion() to get this * number and act upon if it needs to renew the session. * * @return the current version number * @see RuntimeEnvironment#getPluginVersion() */ public int getPluginVersion() { return pluginVersion; } /** * Changes the plugin version to the next version. */ public void increasePluginVersion() { this.pluginVersion++; } /** * Sets the plugin version to an arbitrary number. * * @param pluginVersion the number */ public void setPluginVersion(int pluginVersion) { this.pluginVersion = pluginVersion; } /** * Checks if the request should have an access to a resource. This method is * thread safe with respect to the concurrent reload of plugins. * * <p> * Internally performed with a predicate. Using cache in request * attributes.</p> * * <h3>Order of plugin invocation</h3> * * <p> * The order of plugin invokation is given by the configuration * {@link RuntimeEnvironment#getPluginStack()} and appropriate actions are * taken when traversing the stack with set of keywords, such as:</p> * * <h4>required</h4> * Failure of such a plugin will ultimately lead to the authorization * framework returning failure but only after the remaining plugins have * been invoked. * * <h4>requisite</h4> * Like required, however, in the case that such a plugin returns a failure, * control is directly returned to the application. The return value is that * associated with the first required or requisite plugin to fail. * * <h4>sufficient</h4> * If such a plugin succeeds and no prior required plugin has failed the * authorization framework returns success to the application immediately * without calling any further plugins in the stack. A failure of a * sufficient plugin is ignored and processing of the plugin list continues * unaffected. * * <p> * Loaded plugins which do not occur in the configuration are appended to * the list with "required" keyword. As of the nature of the class discovery * this means that the order of invocation of these plugins is rather * random.</p> * * <p> * Plugins in the configuration which have not been loaded are skipped.</p> * * @param request request object * @param cache cache * @param name name * @param predicate predicate * @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; } Statistics stats = RuntimeEnvironment.getInstance().getStatistics(); Boolean val; Map<String, Boolean> m = (Map<String, Boolean>) request.getAttribute(cache); if (m == null) { m = new TreeMap<>(); } else if ((val = m.get(entity.getName())) != null) { // cache hit stats.addRequest(request, "authorization_cache_hits"); return val; } stats.addRequest(request, "authorization_cache_misses"); long time = System.currentTimeMillis(); boolean overallDecision = performCheck(entity, pluginPredicate, skippingPredicate); time = System.currentTimeMillis() - time; stats.addRequestTime(request, "authorization", time); stats.addRequestTime(request, String.format("authorization_%s", overallDecision ? "positive" : "negative"), time); stats.addRequestTime(request, String.format("authorization_%s_of_%s", overallDecision ? "positive" : "negative", entity.getName()), time); stats.addRequestTime(request, 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. * * @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 * @return true if entity is allowed; false otherwise */ private boolean performCheck(Nameable entity, AuthorizationEntity.PluginDecisionPredicate pluginPredicate, AuthorizationEntity.PluginSkippingPredicate skippingPredicate) { lock.readLock().lock(); try { return stack.isAllowed(entity, pluginPredicate, skippingPredicate); } finally { lock.readLock().unlock(); } } }