Add support to dynamically configure servlet Filter chains at runtime.
Recreates the Filter chain-of-responsiblity pattern to allow injection of chain
links on top of the CustomFilterAdapter javax.servlet.Filter. Thus, web.xml
creators can use org.opendaylight.aaa.filterchian.CustomFilterAdapater to
dynamically adjust links in the chain at runtime. This framework allows
pre/post-processing on HTTP/S requests from REST endpoints. Importantly,
since the Filter is added to the Servlet definition, the requests are viewed
after SSL decryption, allowing for true inspection. An example of how to
configure this for a REST endpoint is illustrated in this patch for the AAA
idmlight endpoints at aaa-idmlight/src/main/resources/web.xml.
A configuration admin managed service is introduced to track changes to the
"etc/org.opendaylight.aaa.filterchain.cfg" file. This file supports one
key/value combination, namely;
customFilterList=a.b.c.Filter1,c.d.e.Filter2,x.y.zFilterN
The value is a csv list of filters. Optionally, the user may specify a Filter
configuration file to introduce key/value init-params normally specified in
web.xml. An example is:
customFilterList=a.b.c.Filter1$etc/filter1.cfg,d.e.f.Filter2
If the desired filter is not included in the Imported aaa packages, it may be
dynamically imported to allow access:
karaf> bundle:dynamic-import <ID>
Where <ID> refers to the bundle ID of the bundle that houses the desired
Filter implementation.
This patch contains several unit tests, and boasts 88% line unit coverage. The
testing includes null chains, small chains, and quite large chains.
Change-Id: Ifa2994f4c10ae504763f704fa8dc19fd11093108
Signed-off-by: Ryan Goulding <ryandgoulding@gmail.com>
--- /dev/null
+<!-- Copyright (c) 2015 Brocade Communications Systems, Inc. and others.
+ All rights reserved. This program and the accompanying materials are made
+ available under the terms of the Eclipse Public License v1.0 which accompanies
+ this distribution, and is available at http://www.eclipse.org/legal/epl-v10.html -->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+ <parent>
+ <groupId>org.opendaylight.aaa</groupId>
+ <artifactId>aaa-parent</artifactId>
+ <version>0.4.0-SNAPSHOT</version>
+ <relativePath>../parent</relativePath>
+ </parent>
+
+ <artifactId>aaa-filterchain</artifactId>
+ <packaging>bundle</packaging>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.apache.felix</groupId>
+ <artifactId>org.apache.felix.dependencymanager</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-api</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>javax.servlet</groupId>
+ <artifactId>javax.servlet-api</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>com.google.guava</groupId>
+ <artifactId>guava</artifactId>
+ </dependency>
+
+ <!-- Testing Dependencies -->
+ <dependency>
+ <groupId>junit</groupId>
+ <artifactId>junit</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.mockito</groupId>
+ <artifactId>mockito-all</artifactId>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+ <build>
+ <pluginManagement>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.felix</groupId>
+ <artifactId>maven-bundle-plugin</artifactId>
+ <version>${bundle.plugin.version}</version>
+ <extensions>true</extensions>
+ <configuration>
+ <instructions>
+ <Bundle-Name>${project.groupId}.${project.artifactId}</Bundle-Name>
+ </instructions>
+ <manifestLocation>${project.basedir}/META-INF</manifestLocation>
+ </configuration>
+ </plugin>
+ </plugins>
+ </pluginManagement>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.felix</groupId>
+ <artifactId>maven-bundle-plugin</artifactId>
+ <extensions>true</extensions>
+ <configuration>
+ <instructions>
+ <Bundle-Activator>org.opendaylight.aaa.filterchain.Activator</Bundle-Activator>
+ <DynamicImport-Package>*</DynamicImport-Package>
+ </instructions>
+ </configuration>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-jar-plugin</artifactId>
+ </plugin>
+ <plugin>
+ <groupId>org.codehaus.mojo</groupId>
+ <artifactId>build-helper-maven-plugin</artifactId>
+ <executions>
+ <execution>
+ <id>attach-artifacts</id>
+ <phase>package</phase>
+ <goals>
+ <goal>attach-artifact</goal>
+ </goals>
+ <configuration>
+ <artifacts>
+ <artifact>
+ <file>${project.build.directory}/classes/filterchain.cfg</file>
+ <type>cfg</type>
+ <classifier>config</classifier>
+ </artifact>
+ </artifacts>
+ </configuration>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+</project>
--- /dev/null
+/*
+ * Copyright (c) 2016 Brocade Communications Systems, Inc. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+
+package org.opendaylight.aaa.filterchain;
+
+import org.apache.felix.dm.DependencyActivatorBase;
+import org.apache.felix.dm.DependencyManager;
+import org.opendaylight.aaa.filterchain.configuration.CustomFilterAdapterConfiguration;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.ServiceRegistration;
+import org.osgi.service.cm.ManagedService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Activator for <code>aaa-filterchain</code>, a bundle which provides the ability
+ * to inject custom <code>Filter</code>(s) in front of servlets.
+ *
+ * This class is also responsible for offering contextual <code>DEBUG</code>
+ * level clues concerning the activation of the <code>aaa-filterchain</code> bundle.
+ * To enable these debug messages, issue the following command in the karaf
+ * shell: <code>log:set debug org.opendaylight.aaa.filterchain.Activator</code>
+ *
+ * @author Ryan Goulding (ryandgoulding@gmail.com)
+ */
+public class Activator extends DependencyActivatorBase {
+
+ private static final Logger LOG = LoggerFactory.getLogger(Activator.class);
+
+ private ServiceRegistration<ManagedService> managedServiceServiceRegistration;
+
+ @Override
+ public void destroy(final BundleContext bc, final DependencyManager dm) throws Exception {
+ LOG.debug("Destroying the aaa-filterchain bundle");
+ managedServiceServiceRegistration.unregister();
+ }
+
+ @Override
+ public void init(final BundleContext bc, final DependencyManager dm) throws Exception {
+ LOG.debug("Initializing the aaa-filterchain bundle");
+ // Register the CustomFilterAdapterConfiguration ManagedService with the
+ // BundleContext so config values can be loaded from the config admin
+ managedServiceServiceRegistration = bc.registerService(ManagedService.class,
+ CustomFilterAdapterConfiguration.getInstance(),
+ CustomFilterAdapterConfiguration.getInstance().getDefaultProperties());
+ }
+
+}
--- /dev/null
+/*
+ * Copyright (c) 2016 Brocade Communications Systems, Inc. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+
+package org.opendaylight.aaa.filterchain.configuration;
+
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Dictionary;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Hashtable;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+import javax.servlet.Filter;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletContext;
+import javax.servlet.ServletException;
+
+import org.osgi.framework.Constants;
+import org.osgi.service.cm.ConfigurationException;
+import org.osgi.service.cm.ManagedService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Responsible for parsing configuration from the <code>CustomFilterAdapterConfiguration</code>
+ * configuration admin file (or service). This interface may be configured through the Karaf
+ * webconsole, or though adding a configuration file as follows. The configuration can be specified
+ * through performing the following:
+ *
+ * Edit <code>etc/org.opendaylight.aaa.filterchain.cfg</code>:
+ *
+ * Add Filters in the following form:
+ * <code>customFilterList = c.b.a.TestFilter1,f.d.e.TestFilter2,j.h.i.FilterN</code>
+ *
+ * If you wish to specify key/value init-params (normally done in web.xml), you can do so by adding
+ * them scoped to the filter on a new line. For example:
+ * <code>c.b.a.TestFilter1.propertyKey=propertyValue</code>
+ *
+ * Note: If you wish to use a Filter from an outside package that does not explicitly Export the
+ * bundle, you must first enable dynamic import for that bundle:
+ * <code>bundle:dynamic-import ID</code>
+ *
+ * This class follows the Singleton design pattern, and the instance is extracted through:
+ * <code>CustomFilterAdapterConfiguration.getInstance()</code>
+ *
+ * This class is a <code>CustomFilterAdapterConfiguration</code> Event Producer, and objects
+ * can subscribe for changes through:
+ * <code>CustomFilterAdapterConfiguration.registerCustomFilterAdapterConfigurationListener(...)</code>
+ *
+ * @author Ryan Goulding (ryandgoulding@gmail.com)
+ *
+ */
+public class CustomFilterAdapterConfiguration implements ManagedService {
+
+ /**
+ * Separates different filter definitions. For example:
+ * <code>customFilterList = c.b.a.TestFilter1,f.d.e.TestFilter2,j.h.i.FilterN</code>
+ */
+ static final String FILTER_DTO_SEPARATOR = ",";
+
+ private static final Logger LOG = LoggerFactory.getLogger(CustomFilterAdapterConfiguration.class);
+
+ /**
+ * PID key used by Apache Configuration Admin to store custom filter adapter configuration
+ * related information. Also represents to the config admin filename, which is
+ * <code>etc/org.opendaylight.aaa.filterchain.cfg</code>.
+ */
+ static final String CUSTOM_FILTER_ADAPTER_CONFIGURATION_PID = "org.opendaylight.aaa.filterchain";
+
+ /**
+ * <code>customFilterList</code> is the property advertised in the Karaf configuration admin
+ */
+ static final String CUSTOM_FILTER_LIST_KEY = "customFilterList";
+
+ /**
+ * Default the customFilterList to an empty string.
+ */
+ static final String DEFAULT_CUSTOM_FILTER_LIST_VALUE = "";
+
+ /**
+ * Stores the default configuration, which is a combination of the PID and key/value pairs.
+ */
+ private static final Hashtable<String, String> DEFAULT_CONFIGURATION = new Hashtable<>();
+
+ // For singleton
+ private static final CustomFilterAdapterConfiguration INSTANCE = new CustomFilterAdapterConfiguration();
+
+ // Initialize the defaults.
+ static {
+ DEFAULT_CONFIGURATION.put(Constants.SERVICE_PID, CUSTOM_FILTER_ADAPTER_CONFIGURATION_PID);
+ DEFAULT_CONFIGURATION.put(CUSTOM_FILTER_LIST_KEY, DEFAULT_CUSTOM_FILTER_LIST_VALUE);
+ }
+
+ /**
+ * List of listeners to notify upon config admin events.
+ */
+ private final List<CustomFilterAdapterListener> listeners = new CopyOnWriteArrayList<>();
+
+ /**
+ * Saves a local copy of the most recent configuration so when a listener
+ * is added, it can receive and initial update.
+ */
+ private volatile List<FilterDTO> filterDTOs = Collections.emptyList();
+
+ private CustomFilterAdapterConfiguration() {
+ // private for Singleton
+ }
+
+ /**
+ * Extract the Singleton <code>CustomFilterAdapterConfiguration</code>.
+ *
+ * @return The <code>CustomFilterAdapterConfiguration</code> instance
+ */
+ public static CustomFilterAdapterConfiguration getInstance() {
+ return INSTANCE;
+ }
+
+ public Dictionary<String, ?> getDefaultProperties() {
+ return DEFAULT_CONFIGURATION;
+ }
+
+ // Invoked in response to configuration admin changes
+ @Override
+ public void updated(final Dictionary<String, ?> properties)
+ throws ConfigurationException {
+
+ if (properties == null) {
+ updateListeners(DEFAULT_CONFIGURATION);
+ } else {
+ final Map<String, String> configuration = extractPropertiesMap(properties);
+ updateListeners(configuration);
+ }
+ }
+
+ /**
+ * Utility method to convert a properties <code>Dictionary</code> to a properties <code>Map</code>.
+ *
+ * @param propertiesDictionary Supplied by config admin.
+ * @return A properties map, which is a somewhat friendlier interface
+ */
+ private static Map<String, String> extractPropertiesMap(final Dictionary<String, ?> propertiesDictionary) {
+ final Map<String, String> propertiesMap = new HashMap<>();
+ final Enumeration<String> keys = propertiesDictionary.keys();
+ while (keys.hasMoreElements()) {
+ final String key = keys.nextElement();
+ try {
+ final String value = (String) propertiesDictionary.get(key);
+ propertiesMap.put(key, value);
+ } catch (final ClassCastException e) {
+ // should never happen; Just put here to be explicit.
+ LOG.error("skipping CustomFilterAdapterConfiguration config for key \"{}\"; can't parse value",
+ key, e);
+ }
+ }
+ // add in the PID
+ propertiesMap.put(Constants.SERVICE_PID, CUSTOM_FILTER_ADAPTER_CONFIGURATION_PID);
+ return propertiesMap;
+ }
+
+ /**
+ * Notify all listeners of a change event.
+ */
+ private void updateListeners(final Map<String, String> configuration) {
+ final List<FilterDTO> customFilterList = getCustomFilterList(configuration);
+ this.filterDTOs = customFilterList;
+ for (CustomFilterAdapterListener listener : listeners) {
+ updateListener(listener, customFilterList);
+ }
+ }
+
+ /**
+ * Update a particular listener with the new injected <code>FilterDTO</code> list.
+ *
+ * @param listener The <code>CustomFilterAdapter</code> instance
+ * @param customFilterList The newly injected <code>FilterDTO</code> list
+ */
+ private void updateListener(final CustomFilterAdapterListener listener, final List<FilterDTO> customFilterList) {
+ final ServletContext listenerServletContext = extractServletContext(listener);
+ final List<Filter> filterList = convertCustomFilterList(customFilterList, listenerServletContext);
+ listener.updateInjectedFilters(filterList);
+ }
+
+ /**
+ * Utility method to extract a <code>ServletContext</code> from a listener's
+ * <code>FilterConfig</code>.
+ *
+ * @param listener An object which listens for filter chain configuration changes.
+ * @return An extracted <code>ServletContext</code>, or null if either the
+ * <code>FilterConfig</code> of <code>ServletContext</code> is null
+ */
+ private static ServletContext extractServletContext(final CustomFilterAdapterListener listener) {
+ final FilterConfig listenerFilterConfig = listener.getFilterConfig();
+ final ServletContext listenerServletContext =
+ (listenerFilterConfig != null ? listenerFilterConfig.getServletContext() : null);
+ return listenerServletContext;
+ }
+
+ /**
+ * Converts a List of class names (possibly Filters) and attempts to spawn corresponding
+ * <code>javax.servlet.Filter</code> instances.
+ *
+ * @param customFilterList a list of class names, ideally Filters
+ * @return a list of derived Filter(s)
+ */
+ private List<Filter> convertCustomFilterList(final List<FilterDTO> customFilterList,
+ final ServletContext servletContext) {
+
+ final ImmutableList.Builder<Filter> injectedFiltersBuilder = ImmutableList.builder();
+ for (FilterDTO customFilter : customFilterList) {
+ final Filter injectedFilter = injectAndInitializeCustomFilter(customFilter, servletContext);
+ if (injectedFilter != null) {
+ injectedFiltersBuilder.add(injectedFilter);
+ }
+ }
+ return injectedFiltersBuilder.build();
+ }
+
+ /**
+ * Utility method used to inject and initialize a Filter. If initialization fails, it is
+ * logged but the Filter is still added to the chain.
+ *
+ * @param customFilter DTO containing Filter and properties path, if one exists.
+ * @param servletContext Scoped to the listener
+ * @return A filter, or null if one cannot be instantiated.
+ */
+ private static Filter injectAndInitializeCustomFilter(final FilterDTO customFilter, final ServletContext servletContext) {
+ LOG.info("Attempting to load Class.forName({})", customFilter);
+ try {
+ final Filter filter = newInstanceOf(customFilter.getClassName());
+ initializeInjectedFilter(customFilter, filter, servletContext);
+ return filter;
+ } catch (final ClassNotFoundException e) {
+ LOG.error("skipping {} as it couldn't be found", customFilter, e);
+ } catch (final ClassCastException e) {
+ LOG.error("skipping {} as it could not be cast as javax.servlet.Filter", customFilter, e);
+ } catch (final InstantiationException | IllegalAccessException e) {
+ LOG.error("skipping {} as it cannot be instantiated", customFilter, e);
+ }
+ return null;
+ }
+
+ /**
+ * Utility to inject a custom filter into the classpath.
+ *
+ * @param customFilterClassName fully qualified name of desired Filter
+ * @return The Filter instance
+ * @throws ClassNotFoundException The class couldn't be found, possibly since
+ * dynamic imports weren't enabled on the target bundle.
+ * @throws InstantiationException The class couldn't be created
+ * @throws IllegalAccessException Security manager ruled the class wasn't allowed
+ * to be created.
+ */
+ private static Filter newInstanceOf(final String customFilterClassName)
+ throws ClassNotFoundException, InstantiationException, IllegalAccessException {
+
+ final Class<?> clazz = Class.forName(customFilterClassName);
+ @SuppressWarnings("unchecked")
+ final Class<Filter> filterClazz = (Class<Filter>) clazz;
+ final Filter filter = filterClazz.newInstance();
+ return filter;
+ }
+
+ /**
+ * Attempt to initialize with a generated <code>FilterConfig</code>. Gracefully continue if
+ * initialization fails, but log the encountered Exception.
+ *
+ * @param filterDTO The filter config file location is contained in the filterDTO object
+ * @param filter The already created filter, which we need to initialize.
+ * @param servletContext Scoped to the listener.
+ */
+ private static void initializeInjectedFilter(final FilterDTO filterDTO, final Filter filter, final ServletContext servletContext) {
+ try {
+ final Map<String, String> initParams = filterDTO.getInitParams();
+ final FilterConfig filterConfig = InjectedFilterConfig.createInjectedFilterConfig(filter, servletContext, initParams);
+ filter.init(filterConfig);
+ } catch (final ServletException e) {
+ LOG.error("Although {} was injected into the filter chain, {}.init() failed; continuing anyway",
+ filterDTO.getClassName(), filterDTO.getClassName(), e);
+ }
+ }
+
+ /**
+ * Allows creation of <code>FilterConfig</code> from a key/value properties file.
+ */
+ private static class InjectedFilterConfig implements FilterConfig {
+
+ private final String filterName;
+ private final ServletContext servletContext;
+ private final Map<String, String> filterConfig;
+
+ // private for Factory Method pattern
+ private InjectedFilterConfig(final Filter filter, final ServletContext servletContext,
+ final Map<String, String> filterConfig) {
+
+ this.filterName = filter.getClass().getSimpleName();
+ this.servletContext = servletContext;
+ this.filterConfig = filterConfig;
+ }
+
+ public static InjectedFilterConfig createInjectedFilterConfig(final Filter filter, final ServletContext servletContext,
+ final Map<String, String> filterConfig) {
+ return new InjectedFilterConfig(filter, servletContext, filterConfig);
+ }
+
+ // The following is implemented for conformance with the FilterConfig
+ // interface. It is never called.
+ @Override
+ public String getFilterName() {
+ return filterName;
+ }
+
+ // The following method is implemented for conformance with the FilterConfig
+ // interface. It is never called.
+ @Override
+ public String getInitParameter(final String paramName) {
+ return (filterConfig != null ? filterConfig.get(paramName) : null);
+ }
+
+ // The following method is implemented for conformance with the FilterConfig
+ // interface. It is never called.
+ @Override
+ public Enumeration<String> getInitParameterNames() {
+ return new Enumeration<String>() {
+ final Iterator<String> keySet = filterConfig.keySet().iterator();
+
+ @Override
+ public boolean hasMoreElements() {
+ return (keySet != null ? keySet.hasNext() : false);
+ }
+
+ @Override
+ public String nextElement() {
+ return (keySet != null ? keySet.next() : null);
+ }
+ };
+ }
+
+ // The following method is implemented for conformance with the FilterConfig
+ // interface. It is never called.
+ @Override
+ public ServletContext getServletContext() {
+ return servletContext;
+ }
+ }
+
+ /**
+ * Essentially a tuple of (filterClassName, propertiesFileName). Allows quicker passing
+ * and return of Filter information.
+ */
+ private static class FilterDTO {
+
+ private final String clazzName;
+ private final Map<String, String> initParams;
+
+ // private for factory method pattern
+ private FilterDTO(final String clazzName, final Map<String, String> initParams) {
+ this.clazzName = clazzName;
+ this.initParams = initParams;
+ }
+
+ public static FilterDTO createFilterDTO(final String clazzName, final Map<String, String> initParams) {
+ return new FilterDTO(clazzName, initParams);
+ }
+
+ String getClassName() {
+ return this.clazzName;
+ }
+
+ /**
+ * Attempts to extract a map of key/value pairs from a given file.
+ *
+ * @return
+ */
+ Map<String, String> getInitParams() {
+ return initParams;
+ }
+ }
+
+ /**
+ * Extracts the custom filter list as provided by Karaf Configuration Admin.
+ *
+ * @return A <code>non-null</code> <code>List</code> of the custom filter fully qualified class names.
+ */
+ private List<FilterDTO> getCustomFilterList(final Map<String, String> configuration) {
+ final String customFilterListValue = configuration.get(CUSTOM_FILTER_LIST_KEY);
+ final ImmutableList.Builder<FilterDTO> customFilterListBuilder =
+ ImmutableList.builder();
+ if (customFilterListValue != null) {
+ // Creates the list from comma separate values; whitespace is removed first
+ final List<String> filterClazzNames = Arrays.asList(
+ customFilterListValue.replaceAll("\\s","").split(FILTER_DTO_SEPARATOR));
+ for (String filterClazzName : filterClazzNames) {
+ if (!Strings.isNullOrEmpty(filterClazzName)) {
+ final Map<String, String> applicableConfigs = extractPropertiesForFilter(
+ filterClazzName, configuration);
+ final FilterDTO filterDTO = FilterDTO.createFilterDTO(
+ filterClazzName, applicableConfigs);
+ customFilterListBuilder.add(filterDTO);
+ }
+ }
+ }
+ return customFilterListBuilder.build();
+ }
+
+ /**
+ * Extract a subset of properties that apply to a particular Filter.
+ *
+ * @param clazzName prefix used to specify key value pair (i.e., a.b.c.Filter.property)
+ * @param fullConfiguration The entire configuration dictionary, which is traversed for applicable properties.
+ * @return A Map of applicable properties for a filter.
+ */
+ private static Map<String, String> extractPropertiesForFilter(
+ final String clazzName, final Map<String, String> fullConfiguration) {
+
+ final Map<String, String> extractedConfig = new HashMap<>();
+ final Set<String> fullConfigurationKeySet = fullConfiguration.keySet();
+ for (String key : fullConfigurationKeySet) {
+ final int lastDotSeparator = key.lastIndexOf(".");
+ if (lastDotSeparator >= 0) {
+ final String comparisonClazzNameSubstring = key.substring(0, lastDotSeparator);
+ if (comparisonClazzNameSubstring.equals(clazzName)) {
+ final String value = fullConfiguration.get(key);
+ final String filterInitParamKey = key.substring(lastDotSeparator + 1);
+ extractedConfig.put(filterInitParamKey, value);
+ }
+ } else {
+ if (!key.equals(CUSTOM_FILTER_LIST_KEY)) {
+ LOG.error("couldn't parse property \"{}\"; skipping", key);
+ }
+ }
+ }
+ return extractedConfig;
+ }
+
+ /**
+ * Register for config changes.
+ *
+ * @param listener A listener implementing <code>CustomFilterAdapterListener</code>
+ */
+ public void registerCustomFilterAdapterConfigurationListener(final CustomFilterAdapterListener listener) {
+ this.listeners.add(listener);
+ this.updateListener(listener, this.filterDTOs);
+ }
+}
--- /dev/null
+/*
+ * Copyright (c) 2016 Brocade Communications Systems, Inc. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+
+package org.opendaylight.aaa.filterchain.configuration;
+
+import java.util.EventListener;
+import java.util.List;
+
+import javax.servlet.Filter;
+import javax.servlet.FilterConfig;
+
+/**
+ * React to changes to custom Filter list in config admin.
+ *
+ * @author Ryan Goulding (ryandgoulding@gmail.com)
+ */
+public interface CustomFilterAdapterListener extends EventListener {
+
+ /**
+ * React to configuration admin changes.
+ *
+ * @param injectedFilters the updated list of filters
+ */
+ public void updateInjectedFilters(final List<Filter> injectedFilters);
+
+ /**
+ * Extract the associated Filter Configuration.
+ *
+ * @return filter configuration information
+ */
+ public FilterConfig getFilterConfig();
+}
--- /dev/null
+/*
+ * Copyright (c) 2016 Brocade Communications Systems, Inc. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+
+package org.opendaylight.aaa.filterchain.filters;
+
+import java.io.IOException;
+import java.util.Iterator;
+import java.util.List;
+
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+
+/**
+ * Recreates the <code>javax.servlet.Filter</code> chain of responsibility
+ * Pattern to allow for programmatic injection of Filters. Essentially, the
+ * links of the injected chain are traversed, and if the Requests makes it
+ * through all of the injected Filters, then the original, existing chain
+ * is maintained.
+ *
+ * This revision of code assumes that the url-pattern for injected filters
+ * is exactly the same as the one specified for <code>CustomFilterAdapter</code>.
+ *
+ * @author Ryan Goulding (ryandgoulding@gmail.com)
+ *
+ */
+public final class AAAFilterChain implements FilterChain {
+
+ // Must be stored locally to be used within javax.servlet.FilterChain.doFilter(
+ // ServletRequest, ServletResponse) due to rigid API contract.
+ private volatile Iterator<Filter> injectedFilterChainIterator;
+ private volatile FilterChain existingFilterChain;
+
+ private AAAFilterChain() {
+ }
+
+ public static AAAFilterChain createAAAFilterChain() {
+ return new AAAFilterChain();
+ }
+
+ @Override
+ public void doFilter(final ServletRequest request, final ServletResponse response)
+ throws IOException, ServletException {
+
+ // Recursive call using the next available link in the chain iterator If a "next" link does
+ // not exist and we have traversed the chain thus far, that means the Request has successfully
+ // traversed the injected filter chain links and we can invoke filtering on the existing chain.
+ if (injectedFilterChainIterator.hasNext()) {
+ injectedFilterChainIterator.next().doFilter(request, response, this);
+ } else {
+ existingFilterChain.doFilter(request, response);
+ }
+ }
+
+ /**
+ * A wrapper method used to inject a new Filter chain. Essentially, this just adds links to the existing chain.
+ *
+ * @param request Wrapped parameter passed directly to <code>doFilter(ServletRequest, ServletResponse)</code>
+ * @param response Wrapped parameter passed directly to <code>doFilter(ServletRequest, ServletResponse)</code>
+ * @param existingFilterChain The chain provided from Jersey as defined in the Servlet's <code>web.xml</code>
+ * @param injectedFilterChain The programmatically injected chain, which may be empty
+ * @throws IOException Wrapped exception handling from <code>doFilter(ServletRequest, ServletResponse)</code>
+ * @throws ServletException Wrapped exception handling from <code>doFilter(ServletRequest, ServletResponse)</code>
+ */
+ public void doFilter(final ServletRequest request, final ServletResponse response,
+ final FilterChain existingFilterChain, final List<Filter> injectedFilterChain)
+ throws IOException, ServletException {
+
+ this.existingFilterChain = existingFilterChain;
+ this.injectedFilterChainIterator = injectedFilterChain.iterator();
+ doFilter(request, response);
+ }
+}
--- /dev/null
+/*
+ * Copyright (c) 2016 Brocade Communications Systems, Inc. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+
+package org.opendaylight.aaa.filterchain.filters;
+
+import com.google.common.collect.ImmutableList;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+
+import org.opendaylight.aaa.filterchain.configuration.CustomFilterAdapterConfiguration;
+import org.opendaylight.aaa.filterchain.configuration.CustomFilterAdapterListener;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Recreates the Chain of Responsibility pattern for
+ * <code>javax.servlet.Filter</code>(s). Jersey 1.17 does not include the
+ * ability to programmatically add Filter(s), as Filter chains are defined at
+ * compile time within the <code>web.xml</code> file. This Adapter dynamically
+ * adds the capability to dynamically insert links into the filter chain.
+ *
+ * This Adapter is enabled by placing the <code>CustomFilterAdapter</code> in
+ * the Servlet's <code>web.xml</code> definition (ideally directly after the
+ * <code>AAAFilter</code> Filter, as ordering is honored directly).
+ *
+ * <code>CustomFilterAdapter.doFilter(...)</code> calls
+ * <code>AAAFilterChain.doFilter(...)</code>, which honors the injected filter
+ * chain links, and then continues the original filter chain.
+ *
+ * This code was designed specifically to work with the common, generic
+ * <code>javax.servlet.Filter</code> interface; thus, certain choices, such as
+ * creating a new <code>AAAFilterChain</code> per request, were necessary to
+ * preserve the existing API contracts (i.e., the injected chain is stored as a
+ * local variable in <code>AAAFilterChain</code> so it may be used in existing
+ * methods (could not be passed as a parameter), and if a new chain was not
+ * spawned each time, there is a risk that the existingChain changes in the
+ * middle of requests, causing inconsistent behavior.
+ *
+ * @author Ryan Goulding (ryandgoulding@gmail.com)
+ *
+ */
+public class CustomFilterAdapter implements Filter, CustomFilterAdapterListener {
+
+ private static final Logger LOG = LoggerFactory.getLogger(CustomFilterAdapter.class);
+
+ private FilterConfig filterConfig;
+
+ /**
+ * Stores the injected filter chain.
+ * TODO can this be an ArrayList?
+ */
+ private volatile List<Filter> injectedFilterChain = Collections.emptyList();
+
+ @Override
+ public void destroy() {
+ LOG.info("Destroying CustomFilterAdapter");
+ }
+
+ @Override
+ public void doFilter(final ServletRequest request, final ServletResponse response,
+ final FilterChain chain) throws IOException, ServletException {
+
+ // chain is the existing chain of responsibility, and filterChain
+ // contains the new links to inject into the existing chain. Since
+ // Jersey spawns <code>chain</code> for each request, a new chain
+ List<Filter> localFilterChain = injectedFilterChain;
+ if (!localFilterChain.isEmpty()) {
+ AAAFilterChain.createAAAFilterChain().doFilter(request, response, chain, localFilterChain);
+ } else {
+ chain.doFilter(request, response);
+ }
+ }
+
+ @Override
+ public void init(final FilterConfig filterConfig) throws ServletException {
+ LOG.info("Initializing CustomFilterAdapter");
+ // register as a listener for config admin changes
+ CustomFilterAdapterConfiguration.getInstance()
+ .registerCustomFilterAdapterConfigurationListener(this);
+ this.filterConfig = filterConfig;
+ }
+
+ /**
+ * Updates the injected filter chain.
+ *
+ * @param filterChain The injected chain
+ */
+ private void setInjectedFilterChain(final List<Filter> filterChain) {
+ this.injectedFilterChain = ImmutableList.copyOf(filterChain);
+ final String commaSeperatedFilterChain = this.injectedFilterChain.stream()
+ .map(i -> i.getClass().getSimpleName()).collect(Collectors.joining(","));
+ LOG.info("Injecting a new filter chain with {} Filters: {}", filterChain.size(), commaSeperatedFilterChain);
+ }
+
+ @Override
+ public void updateInjectedFilters(final List<Filter> injectedFilters) {
+ this.setInjectedFilterChain(injectedFilters);
+ }
+
+ @Override
+ public FilterConfig getFilterConfig() {
+ return this.filterConfig;
+ }
+}
--- /dev/null
+/*
+ * Copyright (c) 2016 Brocade Communications Systems, Inc. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+
+package org.opendaylight.aaa.filterchain.filters;
+
+import java.io.IOException;
+
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Used to help guide configuration of a deployment. If an operator is confused
+ * whether a Filter is reached/traversed, he or she can use this class to produce
+ * some basic karaf log output.
+ *
+ * This functionality is particularly useful when developing new Filter(s); for
+ * example, you can sandwich the newly created filter in between filter1 and filter2
+ * to ensure that the chain is traversed correctly and in order.
+ *
+ * @author Ryan Goulding (ryandgoulding@gmail.com)
+ *
+ */
+public class TestFilter1 implements Filter {
+
+ private static final Logger LOG = LoggerFactory.getLogger(TestFilter1.class);
+ @Override
+ public void destroy() {
+ }
+
+ @Override
+ public void doFilter(final ServletRequest request, final ServletResponse response,
+ final FilterChain filterChain) throws IOException, ServletException {
+ LOG.error("INGRESS FILTER1");
+ filterChain.doFilter(request, response);
+ LOG.error("EGRESS FILTER1");
+ }
+
+ @Override
+ public void init(FilterConfig filterConfig) throws ServletException {
+ }
+
+}
--- /dev/null
+/*
+ * Copyright (c) 2016 Brocade Communications Systems, Inc. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+
+package org.opendaylight.aaa.filterchain.filters;
+
+import java.io.IOException;
+
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Used to help guide configuration of a deployment. If an operator is confused
+ * whether a Filter is reached/traversed, he or she can use this class to produce
+ * some basic karaf log output.
+ *
+ * This functionality is particularly useful when developing new Filter(s); for
+ * example, you can sandwich the newly created filter in between filter1 and filter2
+ * to ensure that the chain is traversed correctly and in order.
+ *
+ * @author Ryan Goulding (ryandgoulding@gmail.com)
+ *
+ */
+public class TestFilter2 implements Filter {
+
+ private static final Logger LOG = LoggerFactory.getLogger(TestFilter2.class);
+ @Override
+ public void destroy() {
+ }
+
+ @Override
+ public void doFilter(final ServletRequest request, final ServletResponse response,
+ final FilterChain filterChain) throws IOException, ServletException {
+ LOG.error("INGRESS FILTER2");
+ filterChain.doFilter(request, response);
+ LOG.error("EGRESS FILTER2");
+ }
+
+ @Override
+ public void init(final FilterConfig filterConfig) throws ServletException {
+ }
+
+}
--- /dev/null
+customFilterList=
--- /dev/null
+/*
+ * Copyright (c) 2016 Brocade Communications Systems, Inc. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+
+package org.opendaylight.aaa.filterchain.configuration;
+
+import static org.junit.Assert.*;
+
+import java.util.Dictionary;
+import java.util.Enumeration;
+import java.util.Hashtable;
+import java.util.List;
+
+import javax.servlet.Filter;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletContext;
+
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.opendaylight.aaa.filterchain.filters.ExtractFilterConfigFilter;
+import org.osgi.framework.Constants;
+
+/**
+ * @author Ryan Goulding (ryandgoulding@gmail.com)
+ */
+public class CustomFilterAdapterConfigurationTest {
+
+ private static final CustomFilterAdapterConfiguration customFilterAdapterConfiguration =
+ CustomFilterAdapterConfiguration.getInstance();
+ private static final List<Filter>[] updatedInjectedFilters = (List<Filter>[]) new List[1];
+ private static final CustomFilterAdapterListener customFilterAdapterListener =
+ new CustomFilterAdapterListener() {
+ @Override
+ public void updateInjectedFilters(final List<Filter> injectedFilters) {
+ updatedInjectedFilters[0] = injectedFilters;
+ }
+
+ @Override
+ public FilterConfig getFilterConfig() {
+ return null;
+ }
+ };
+
+ @BeforeClass
+ public static void setup() {
+ customFilterAdapterConfiguration.registerCustomFilterAdapterConfigurationListener(
+ customFilterAdapterListener);
+ }
+
+ @Test
+ public void testGetInstance() throws Exception {
+ final CustomFilterAdapterConfiguration customFilterAdapterConfiguration =
+ CustomFilterAdapterConfiguration.getInstance();
+ assertNotNull(customFilterAdapterConfiguration);
+ assertTrue(customFilterAdapterConfiguration instanceof CustomFilterAdapterConfiguration);
+ }
+
+ @Test
+ public void testGetDefaultProperties() throws Exception {
+ // just pull out the defaults and make sure they are in line with what is expected
+ final CustomFilterAdapterConfiguration customFilterAdapterConfiguration =
+ CustomFilterAdapterConfiguration.getInstance();
+ final Dictionary<String, ?> defaultProperties = customFilterAdapterConfiguration.getDefaultProperties();
+ assertEquals(2, defaultProperties.size());
+ assertEquals(CustomFilterAdapterConfiguration.CUSTOM_FILTER_ADAPTER_CONFIGURATION_PID,
+ defaultProperties.get(Constants.SERVICE_PID));
+ assertEquals(CustomFilterAdapterConfiguration.DEFAULT_CUSTOM_FILTER_LIST_VALUE,
+ defaultProperties.get(CustomFilterAdapterConfiguration.CUSTOM_FILTER_LIST_KEY));
+ }
+
+ // also tests "getCustomFilterList()" and
+ // "registerCustomFilterAdapaterConfigurationListener".
+ @Test
+ public void testUpdatedUnresolvableClass() throws Exception {
+ // test a class that won't resolve
+ final Dictionary<String, String> oneUnresolvableFilterDictionary =
+ new Hashtable<>();
+ final String oneUnresolvableFilter = "org.opendaylight.aaa.filterchain.filters.TestFilter1,"
+ + "org.opendaylight.aaa.filterchain.filters.FilterDoesntExist";
+ oneUnresolvableFilterDictionary.put(CustomFilterAdapterConfiguration.CUSTOM_FILTER_LIST_KEY,
+ oneUnresolvableFilter);
+ customFilterAdapterConfiguration.updated(oneUnresolvableFilterDictionary);
+ assertEquals(1, updatedInjectedFilters[0].size());
+ }
+
+ @Test
+ public void testUpdated() throws Exception {
+ // test valid input
+ final Dictionary<String, String> updatedDictionary = new Hashtable<>();
+ final String customFilterListValue = "org.opendaylight.aaa.filterchain.filters.TestFilter1,"
+ + "org.opendaylight.aaa.filterchain.filters.TestFilter2";
+ updatedDictionary.put(CustomFilterAdapterConfiguration.CUSTOM_FILTER_LIST_KEY,
+ customFilterListValue);
+ customFilterAdapterConfiguration.updated(updatedDictionary);
+ assertEquals(2, updatedInjectedFilters[0].size());
+ }
+
+ @Test
+ public void testUpdatedWithNull() throws Exception {
+ // test null for updated
+ customFilterAdapterConfiguration.updated(null);
+ assertEquals(0, updatedInjectedFilters[0].size());
+ }
+
+ @Test
+ public void testUpdatedWithNonInstantiableFilter() throws Exception {
+ // test with a class that cannot be instantiated
+ final String cannotInstantiateClassName =
+ "org.opendaylight.aaa.filterchain.filters.CannotInstantiate";
+ final Dictionary<String, String> cannotInstantiateDictionary = new Hashtable<>();
+ cannotInstantiateDictionary.put(CustomFilterAdapterConfiguration.CUSTOM_FILTER_LIST_KEY,
+ cannotInstantiateClassName);
+ customFilterAdapterConfiguration.updated(cannotInstantiateDictionary);
+ assertEquals(0, updatedInjectedFilters[0].size());
+ }
+
+ @Test
+ public void testUpdatedWithNonFilterClass() throws Exception {
+ // test with a class that cannot be cast to a javax.servlet.Filter
+ final String notAFilterClassName =
+ "org.opendaylight.aaa.filterchain.filters.NotAFilter";
+ final Dictionary<String, String> notAFilterDictionary = new Hashtable<>();
+ notAFilterDictionary.put(CustomFilterAdapterConfiguration.CUSTOM_FILTER_LIST_KEY,
+ notAFilterClassName);
+ customFilterAdapterConfiguration.updated(notAFilterDictionary);
+ assertEquals(0, updatedInjectedFilters[0].size());
+ }
+
+ @Test
+ public void testUpdatedWithCfgFile() throws Exception {
+ final String filterList =
+ "org.opendaylight.aaa.filterchain.filters.ExtractFilterConfigFilter,"
+ + "org.opendaylight.aaa.filterchain.filters.TestFilter2";
+ final Dictionary<String, String> filterWithCfgFileDictionary =
+ new Hashtable<>();
+ filterWithCfgFileDictionary.put(CustomFilterAdapterConfiguration.CUSTOM_FILTER_LIST_KEY,
+ filterList);
+ filterWithCfgFileDictionary.put("org.opendaylight.aaa.filterchain.filters.ExtractFilterConfigFilter.key", "value");
+ filterWithCfgFileDictionary.put("badkey", "badkeyvalue");
+ customFilterAdapterConfiguration.updated(filterWithCfgFileDictionary);
+ assertEquals(2, updatedInjectedFilters[0].size());
+
+ final ExtractFilterConfigFilter extractableFilter =
+ (ExtractFilterConfigFilter) updatedInjectedFilters[0].get(0);
+ final FilterConfig filterConfig = extractableFilter.getFilterConfig();
+ final String value = filterConfig.getInitParameter("key");
+ assertNotNull(value);
+ assertEquals("value", value);
+ }
+
+ @Test
+ public void testListenerWithNonNullServletConfig() throws Exception {
+ // just ensures a non-null ServletConfig is accepted.
+ customFilterAdapterConfiguration.registerCustomFilterAdapterConfigurationListener(
+ new CustomFilterAdapterListener() {
+
+ @Override
+ public void updateInjectedFilters(List<Filter> injectedFilters) {
+ }
+
+ @Override
+ public FilterConfig getFilterConfig() {
+ return new FilterConfig() {
+
+ @Override
+ public String getFilterName() {
+ return null;
+ }
+
+ @Override
+ public ServletContext getServletContext() {
+ return null;
+ }
+
+ @Override
+ public String getInitParameter(String s) {
+ return null;
+ }
+
+ @Override
+ public Enumeration<String> getInitParameterNames() {
+ return null;
+ }
+ };
+ }
+ }
+ );
+ customFilterAdapterConfiguration.updated(null);
+ assertEquals(0, updatedInjectedFilters[0].size());
+ }
+
+ @Test
+ public void testUpdatedFilterInitThrowsException() throws Exception {
+ final Dictionary<String, String> initThrowsException = new Hashtable<>();
+ initThrowsException.put(CustomFilterAdapterConfiguration.CUSTOM_FILTER_LIST_KEY,
+ "org.opendaylight.aaa.filterchain.filters.FilterInitThrowsException");
+ customFilterAdapterConfiguration.updated(initThrowsException);
+ assertEquals(1, updatedInjectedFilters[0].size());
+ }
+}
\ No newline at end of file
--- /dev/null
+/*
+ * Copyright (c) 2016 Brocade Communications Systems, Inc. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+
+package org.opendaylight.aaa.filterchain.filters;
+
+import static org.junit.Assert.*;
+import static org.mockito.Mockito.*;
+
+import java.io.IOException;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Vector;
+
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+/**
+ * Tests chain functionality with null, 0, 1, 2, 100 and 101 links.
+ *
+ * @author Ryan Goulding (ryandgoulding@gmail.com)
+ */
+public class AAAFilterChainTest {
+
+ final ServletRequest servletRequest = mock(ServletRequest.class);
+ final ServletResponse servletResponse = mock(ServletResponse.class);
+ final static FilterChain filterChain = mock(FilterChain.class);
+
+ @BeforeClass
+ public static void setup() throws IOException, ServletException {
+ // existingChain.doFilter() should always just return (so the egress path can be traversed)
+ doAnswer(invocationOnMock -> null).when(filterChain).doFilter(any(), any());
+ }
+
+ @Test
+ public void testCreateAAAFilterChain() throws Exception {
+ final AAAFilterChain aaaFilterChain = AAAFilterChain.createAAAFilterChain();
+ assertNotNull(aaaFilterChain);
+ assertTrue(aaaFilterChain instanceof AAAFilterChain);
+ }
+
+ @Test
+ public void testDoFilterNoFilters() throws IOException, ServletException {
+ final ServletRequest servletRequest = mock(ServletRequest.class);
+ final ServletResponse servletResponse = mock(ServletResponse.class);
+ final FilterChain filterChain = mock(FilterChain.class);
+ doAnswer(invocationOnMock -> null).when(filterChain).doFilter(any(), any());
+ final List<Filter> injectedFilterChain = new Vector<>();
+ AAAFilterChain.createAAAFilterChain().doFilter(servletRequest, servletResponse,
+ filterChain, injectedFilterChain);
+ }
+
+ @Test
+ public void testDoFilter() throws IOException, ServletException {
+ testChain(1);
+ testChain(2);
+ testChain(100);
+ testChain(101);
+ }
+
+ /**
+ * Test chain traversal of a certain size.
+ *
+ * @param numLinks The number of links to traverse in the chain.
+ * @throws IOException
+ * @throws ServletException
+ */
+ public void testChain(final int numLinks) throws IOException, ServletException {
+ final FilterChainMockUtils.TestFilterDTO testFilterDTO = FilterChainMockUtils.createFilterChain(numLinks);
+ final List<String> callStack = testFilterDTO.getCallStack();
+ final List<Filter> injectedFilterChain = testFilterDTO.getFilters();
+ AAAFilterChain.createAAAFilterChain().doFilter(servletRequest, servletResponse,
+ filterChain, injectedFilterChain);
+ final List<String> expectedCallStack = FilterChainMockUtils.formExpectedCallStack(numLinks);
+ final Iterator<String> expectedIterator = expectedCallStack.iterator();
+ for (String actual : callStack) {
+ assertEquals(expectedIterator.next(), actual);
+ }
+ callStack.clear();
+ injectedFilterChain.clear();
+ expectedCallStack.clear();
+ }
+
+}
\ No newline at end of file
--- /dev/null
+/*
+ * Copyright (c) 2016 Brocade Communications Systems, Inc. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+
+package org.opendaylight.aaa.filterchain.filters;
+
+import java.io.IOException;
+
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+
+/**
+ * Test Filter that is non-instantiable. Used in JUnit tests.
+ *
+ * @author Ryan Goulding (ryandgoulding@gmail.com)
+ */
+public class CannotInstantiate implements Filter {
+ private CannotInstantiate() {
+ }
+
+ @Override
+ public void init(FilterConfig filterConfig) throws ServletException {
+ }
+
+ @Override
+ public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
+ }
+
+ @Override
+ public void destroy() {
+ }
+}
--- /dev/null
+/*
+ * Copyright (c) 2016 Brocade Communications Systems, Inc. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+
+package org.opendaylight.aaa.filterchain.filters;
+
+import static org.junit.Assert.*;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+
+import java.io.IOException;
+import java.util.Enumeration;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Vector;
+
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletContext;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+
+import org.junit.Test;
+
+/**
+ * @author Ryan Goulding (ryandgoulding@gmail.com)
+ */
+public class CustomFilterAdapterTest {
+
+ final ServletRequest servletRequest = mock(ServletRequest.class);
+ final ServletResponse servletResponse = mock(ServletResponse.class);
+ final FilterChain filterChain = mock(FilterChain.class);
+
+ @Test
+ public void testDoFilter() throws Exception {
+ final CustomFilterAdapter customFilterAdapter = new CustomFilterAdapter();
+ final FilterChainMockUtils.TestFilterDTO testFilterDTO = FilterChainMockUtils.createFilterChain(3);
+ customFilterAdapter.updateInjectedFilters(testFilterDTO.getFilters());
+ final boolean[] existingFilterChainEncountered = {false};
+ doAnswer(invocationOnMock -> {
+ existingFilterChainEncountered[0] = true;
+ return null;
+ }).when(filterChain).doFilter(any(), any());
+ customFilterAdapter.doFilter(servletRequest, servletResponse,
+ filterChain);
+ assertTrue(existingFilterChainEncountered[0]);
+ customFilterAdapter.destroy();
+ }
+
+ // also tests CustomFilterAdapter.getFilterConfig()
+ @Test
+ public void testInit() throws Exception {
+ final CustomFilterAdapter customFilterAdapter = new CustomFilterAdapter();
+ // test tolerance of null filter config
+ customFilterAdapter.init(null);
+ assertNull(customFilterAdapter.getFilterConfig());
+
+ // Test that added filter config essentially has no effect, since
+ // CustomFilterAdapter doesn't accept any configuration init-params.
+ final FilterConfig filterConfig = new FilterConfig() {
+
+ @Override
+ public String getFilterName() {
+ return null;
+ }
+
+ @Override
+ public ServletContext getServletContext() {
+ return null;
+ }
+
+ @Override
+ public String getInitParameter(String s) {
+ return null;
+ }
+
+ @Override
+ public Enumeration<String> getInitParameterNames() {
+ return null;
+ }
+ };
+ customFilterAdapter.init(filterConfig);
+ assertEquals(filterConfig, customFilterAdapter.getFilterConfig());
+ customFilterAdapter.destroy();
+ }
+
+ @Test
+ public void testUpdateInjectedFilters() throws IOException, ServletException {
+ testUpdateInjectedFilters(0);
+ testUpdateInjectedFilters(1);
+ testUpdateInjectedFilters(2);
+ testUpdateInjectedFilters(100);
+ }
+
+ private void testUpdateInjectedFilters(final int size) throws IOException, ServletException {
+ final CustomFilterAdapter customFilterAdapter = new CustomFilterAdapter();
+ final FilterChainMockUtils.TestFilterDTO testFilterDTO = FilterChainMockUtils.createFilterChain(size);
+ customFilterAdapter.updateInjectedFilters(testFilterDTO.getFilters());
+ customFilterAdapter.doFilter(servletRequest, servletResponse, filterChain);
+ final List<String> callStack = testFilterDTO.getCallStack();
+ final List<String> expectedCallStack = FilterChainMockUtils.formExpectedCallStack(size);
+ final Iterator<String> expectedIterator = expectedCallStack.iterator();
+ for (String actual : callStack) {
+ assertEquals(expectedIterator.next(), actual);
+ }
+ customFilterAdapter.destroy();
+ }
+
+ @Test
+ public void testRealFilters() throws Exception {
+ final CustomFilterAdapter customFilterAdapter = new CustomFilterAdapter();
+ final List<Filter> injectedFilters = new Vector<>();
+ injectedFilters.add(new TestFilter1());
+ injectedFilters.add(new TestFilter2());
+ for (Filter filter : injectedFilters) {
+ filter.init(null);
+ }
+ customFilterAdapter.updateInjectedFilters(injectedFilters);
+ customFilterAdapter.doFilter(servletRequest, servletResponse, filterChain);
+ final boolean[] existingFilterChainEncountered = {false};
+ doAnswer(invocationOnMock -> {
+ existingFilterChainEncountered[0] = true;
+ return null;
+ }).when(filterChain).doFilter(any(), any());
+ customFilterAdapter.doFilter(servletRequest, servletResponse,
+ filterChain);
+ assertTrue(existingFilterChainEncountered[0]);
+ for (Filter filter : injectedFilters) {
+ filter.destroy();
+ }
+ }
+}
\ No newline at end of file
--- /dev/null
+/*
+ * Copyright (c) 2016 Brocade Communications Systems, Inc. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+
+package org.opendaylight.aaa.filterchain.filters;
+
+import java.io.IOException;
+
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+
+/**
+ * Implementation of <code>Filter</code> that allows extraction of filterConfig for
+ * testing purposes.
+ *
+ * @author Ryan Goulding (ryandgoulding@gmail.com)
+ */
+public class ExtractFilterConfigFilter implements Filter {
+
+ private FilterConfig filterConfig;
+
+ @Override
+ public void init(final FilterConfig filterConfig) throws ServletException {
+ this.filterConfig = filterConfig;
+ }
+
+ @Override
+ public void doFilter(final ServletRequest servletRequest, final ServletResponse servletResponse, final FilterChain filterChain) throws IOException, ServletException {
+
+ }
+
+ @Override
+ public void destroy() {
+
+ }
+
+ public FilterConfig getFilterConfig() {
+ return filterConfig;
+ }
+}
--- /dev/null
+/*
+ * Copyright (c) 2016 Brocade Communications Systems, Inc. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+
+package org.opendaylight.aaa.filterchain.filters;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Vector;
+
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+
+/**
+ * Some Mock utilities used in many different JUnit Test classes.
+ *
+ * @author Ryan Goulding (ryandgoulding@gmail.com)
+ */
+public class FilterChainMockUtils {
+ /**
+ * Creates a mock <code>Filter</code> which adds ingress output to callStack, calls doFilter, then
+ * adds egress output to the callStack.
+ *
+ * @param name The name of the filter
+ * @param callStack Just a list of String events
+ * @return The mocked up filter
+ * @throws IOException
+ * @throws ServletException
+ */
+ private static Filter createMockFilter(final String name, final List<String> callStack) throws IOException, ServletException {
+ final Filter testFilter = mock(Filter.class);
+ doAnswer(invocationOnMock -> {
+ callStack.add(name + " ingress");
+ final Object[] args = invocationOnMock.getArguments();
+ final ServletRequest servletRequest = (ServletRequest) args[0];
+ final ServletResponse servletResponse = (ServletResponse) args[1];
+ final FilterChain filterChain = (FilterChain) args[2];
+ filterChain.doFilter(servletRequest, servletResponse);
+ callStack.add(name + " egress");
+ return null;
+ }).when(testFilter).doFilter(any(), any(), any());
+ return testFilter;
+ }
+
+ /**
+ * Forms the expected call stack, which is really just an event listing.
+ *
+ * @param size the number of filters used to create the call stack
+ * @return A list of String events
+ */
+ static List<String> formExpectedCallStack(final int size) {
+ final List<String> expected = new Vector<>();
+ for (int i = 0; i < size; i++) {
+ expected.add(String.format("filter%d ingress", i));
+ }
+ for (int i = size - 1; i >= 0; i--) {
+ expected.add(String.format("filter%d egress", i));
+ }
+ return expected;
+ }
+
+ /**
+ * Creates a filter chain test case.
+ *
+ * @param size The number of links for the test case
+ * @return the test case
+ * @throws IOException
+ * @throws ServletException
+ */
+ static TestFilterDTO createFilterChain(final int size) throws IOException, ServletException {
+ final List<Filter> filters = new Vector<>();
+ final List<String> callBack = new Vector<>();
+ for (int i = 0; i < size; i++) {
+ final Filter filter = createMockFilter(String.format("filter%d",i), callBack);
+ filters.add(filter);
+ }
+
+ return new TestFilterDTO(filters, callBack);
+ }
+
+ /**
+ * A holder class for the tuple <code>(filters,callStack)</code>
+ */
+ static final class TestFilterDTO {
+ private List<Filter> filters;
+ private List<String> callStack;
+
+ public TestFilterDTO(final List<Filter> filters, final List<String> callStack) {
+ this.filters = filters;
+ this.callStack = callStack;
+ }
+
+ public List<Filter> getFilters() {
+ return filters;
+ }
+ public List<String> getCallStack() {
+ return callStack;
+ }
+ }
+}
--- /dev/null
+/*
+ * Copyright (c) 2016 Brocade Communications Systems, Inc. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+
+package org.opendaylight.aaa.filterchain.filters;
+
+import java.io.IOException;
+
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+
+/**
+ * init() throws a ServletException. This is used in JUnit tests.
+ *
+ * @author Ryan Goulding (ryandgoulding@gmail.com)
+ */
+public class FilterInitThrowsException implements Filter {
+
+ @Override
+ public void init(FilterConfig filterConfig) throws ServletException {
+ throw new ServletException();
+ }
+
+ @Override
+ public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
+ }
+
+ @Override
+ public void destroy() {
+ }
+}
--- /dev/null
+/*
+ * Copyright (c) 2016 Brocade Communications Systems, Inc. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+
+package org.opendaylight.aaa.filterchain.filters;
+
+import java.util.Vector;
+
+/**
+ * A class that is not a Filter. Used in JUnit tests.
+ *
+ * @author Ryan Goulding (ryandgoulding@gmail.com)
+ */
+public class NotAFilter extends Vector<String> {
+}
<groupId>org.opendaylight.aaa</groupId>
<artifactId>aaa-authn-api</artifactId>
</dependency>
+ <dependency>
+ <groupId>org.opendaylight.aaa</groupId>
+ <artifactId>aaa-shiro</artifactId>
+ </dependency>
<dependency>
<groupId>org.opendaylight.aaa</groupId>
<artifactId>aaa-authn</artifactId>
</dependency>
+ <dependency>
+ <groupId>org.opendaylight.aaa</groupId>
+ <artifactId>aaa-filterchain</artifactId>
+ </dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<extensions>true</extensions>
<configuration>
<instructions>
- <Import-Package>org.opendaylight.aaa.shiro.realm,org.apache.shiro.web.env,org.apache.shiro.authc,org.opendaylight.aaa.shiro.web.env,org.opendaylight.aaa.shiro.filters,javax.servlet.http,javax.ws.rs,javax.ws.rs.core,javax.xml.bind.annotation,org.apache.felix.dm,org.opendaylight.aaa,org.opendaylight.aaa.api.*,org.osgi.framework,org.slf4j,org.eclipse.jetty.servlets,com.sun.jersey.spi.container.servlet,com.google.*,org.opendaylight.*,org.osgi.util.tracker</Import-Package>
+ <Import-Package>org.opendaylight.aaa.shiro.realm,org.apache.shiro.web.env,org.apache.shiro.authc,org.opendaylight.aaa.shiro.web.env,org.opendaylight.aaa.shiro.filters,org.opendaylight.aaa.filterchain.filters,javax.servlet.http,javax.ws.rs,javax.ws.rs.core,javax.xml.bind.annotation,org.apache.felix.dm,org.opendaylight.aaa,org.opendaylight.aaa.api.*,org.osgi.framework,org.slf4j,org.eclipse.jetty.servlets,com.sun.jersey.spi.container.servlet,com.google.*,org.opendaylight.*,org.osgi.util.tracker</Import-Package>
<Web-ContextPath>/auth</Web-ContextPath>
<!--<Web-Connectors>adminConn</Web-Connectors> -->
<!--Bundle-Activator>org.opendaylight.aaa.idm.Activator</Bundle-Activator-->
<servlet-name>IdmLight</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>
+ <filter>
+ <filter-name>DynamicFilterChain</filter-name>
+ <filter-class>org.opendaylight.aaa.filterchain.filters.CustomFilterAdapter</filter-class>
+ </filter>
+
+ <filter-mapping>
+ <filter-name>DynamicFilterChain</filter-name>
+ <url-pattern>/*</url-pattern>
+ </filter-mapping>
<context-param>
<param-name>shiroEnvironmentClass</param-name>
<configuration>
<instructions>
<Bundle-Activator>org.opendaylight.aaa.shiro.Activator</Bundle-Activator>
+ <DynamicImport-Package>*</DynamicImport-Package>
</instructions>
</configuration>
</plugin>
@Override
public void destroy(BundleContext bc, DependencyManager dm) throws Exception {
- final String DEBUG_MESSAGE = "Destroying the aaa-shiro bundle";
- LOG.debug(DEBUG_MESSAGE);
+ LOG.debug("Destroying the aaa-shiro bundle");
}
@Override
public void init(BundleContext bc, DependencyManager dm) throws Exception {
- final String DEBUG_MESSAGE = "Initializing the aaa-shiro bundle";
- LOG.debug(DEBUG_MESSAGE);
+ LOG.debug("Initializing the aaa-shiro bundle");
}
}
<artifactId>aaa-h2-store</artifactId>
<version>${project.version}</version>
</dependency>
+ <dependency>
+ <groupId>${project.groupId}</groupId>
+ <artifactId>aaa-filterchain</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>${project.groupId}</groupId>
+ <artifactId>aaa-filterchain</artifactId>
+ <version>${project.version}</version>
+ <type>cfg</type>
+ <classifier>config</classifier>
+ </dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>aaa-h2-store</artifactId>
<scope>import</scope>
<type>pom</type>
</dependency>
+ <dependency>
+ <groupId>org.opendaylight.aaa</groupId>
+ <artifactId>aaa-filterchain</artifactId>
+ <version>0.4.0-SNAPSHOT</version>
+ </dependency>
+ <dependency>
+ <groupId>org.opendaylight.aaa</groupId>
+ <artifactId>aaa-filterchain</artifactId>
+ <version>0.4.0-SNAPSHOT</version>
+ <type>cfg</type>
+ <classifier>config</classifier>
+ </dependency>
<dependency>
<groupId>org.opendaylight.yangtools</groupId>
<artifactId>yangtools-artifacts</artifactId>
<bundle>mvn:org.opendaylight.aaa/aaa-authn-sts/{{VERSION}}</bundle>
<bundle>mvn:org.opendaylight.aaa/aaa-authn-store/{{VERSION}}</bundle>
<bundle>mvn:org.opendaylight.aaa/aaa-authn-basic/{{VERSION}}</bundle>
+ <bundle>mvn:org.opendaylight.aaa/aaa-filterchain/{{VERSION}}</bundle>
<bundle>mvn:com.google.guava/guava/{{VERSION}}</bundle>
<!--H2 Store -->
<configfile finalname="/etc/org.opendaylight.aaa.authn.cfg">mvn:org.opendaylight.aaa/aaa-authn/{{VERSION}}/cfg/config</configfile>
<configfile finalname="/etc/org.opendaylight.aaa.tokens.cfg">mvn:org.opendaylight.aaa/aaa-authn-store/{{VERSION}}/cfg/config</configfile>
<configfile finalname="/etc/org.opendaylight.aaa.federation.cfg">mvn:org.opendaylight.aaa/aaa-authn-federation/{{VERSION}}/cfg/config</configfile>
+ <configfile finalname="/etc/org.opendaylight.aaa.filterchain.cfg">mvn:org.opendaylight.aaa/aaa-filterchain/{{VERSION}}/cfg/config</configfile>
</feature>
<feature name='odl-aaa-authn' description='OpenDaylight :: AAA :: Authentication - NO CLUSTER'
<bundle>mvn:org.opendaylight.aaa/aaa-authn-sts/{{VERSION}}</bundle>
<bundle>mvn:org.opendaylight.aaa/aaa-authn-store/{{VERSION}}</bundle>
<bundle>mvn:org.opendaylight.aaa/aaa-authn-basic/{{VERSION}}</bundle>
+ <bundle>mvn:org.opendaylight.aaa/aaa-filterchain/{{VERSION}}</bundle>
<bundle>mvn:com.google.guava/guava/{{VERSION}}</bundle>
<!--H2 Store -->
<configfile finalname="/etc/org.opendaylight.aaa.authn.cfg">mvn:org.opendaylight.aaa/aaa-authn/{{VERSION}}/cfg/config</configfile>
<configfile finalname="/etc/org.opendaylight.aaa.tokens.cfg">mvn:org.opendaylight.aaa/aaa-authn-store/{{VERSION}}/cfg/config</configfile>
<configfile finalname="/etc/org.opendaylight.aaa.federation.cfg">mvn:org.opendaylight.aaa/aaa-authn-federation/{{VERSION}}/cfg/config</configfile>
+ <configfile finalname="/etc/org.opendaylight.aaa.filterchain.cfg">mvn:org.opendaylight.aaa/aaa-filterchain/{{VERSION}}/cfg/config</configfile>
</feature>
<feature name='odl-aaa-authn-mdsal-cluster' description='OpenDaylight :: AAA :: Authentication :: MD-SAL'
<bundle>mvn:org.opendaylight.aaa/aaa-authn-mdsal-api/{{VERSION}}</bundle>
<bundle>mvn:org.opendaylight.aaa/aaa-authn-mdsal-store-impl/{{VERSION}}</bundle>
<bundle>mvn:org.opendaylight.aaa/aaa-authn-basic/{{VERSION}}</bundle>
+ <bundle>mvn:org.opendaylight.aaa/aaa-filterchain/{{VERSION}}</bundle>
<bundle>mvn:com.google.guava/guava/{{VERSION}}</bundle>
<!-- IDMLight -->
<configfile finalname="etc/opendaylight/karaf/08-authn-config.xml">mvn:org.opendaylight.aaa/aaa-authn-mdsal-config/{{VERSION}}/xml/config</configfile>
<configfile finalname="/etc/org.opendaylight.aaa.authn.cfg">mvn:org.opendaylight.aaa/aaa-authn/{{VERSION}}/cfg/config</configfile>
<configfile finalname="/etc/org.opendaylight.aaa.federation.cfg">mvn:org.opendaylight.aaa/aaa-authn-federation/{{VERSION}}/cfg/config</configfile>
-
+ <configfile finalname="/etc/org.opendaylight.aaa.filterchain.cfg">mvn:org.opendaylight.aaa/aaa-filterchain/{{VERSION}}/cfg/config</configfile>
</feature>
<feature name='odl-aaa-keystone-plugin' description='OpenDaylight :: AAA :: Keystone Plugin - NO CLUSTER'
<module>aaa-h2-store</module>
<module>aaa-cert</module>
<module>aaa-cli</module>
+ <module>aaa-filterchain</module>
</modules>
<scm>