diff options
author | DUVAL Thomas <thomas.duval@orange.com> | 2016-06-16 14:50:31 +0200 |
---|---|---|
committer | DUVAL Thomas <thomas.duval@orange.com> | 2016-06-16 14:50:31 +0200 |
commit | adf7e6616c2a8d6f60207059288423f693509928 (patch) | |
tree | b79848d3b61f28e975f4730de541532c5089c6ed /odl-aaa-moon/aaa/aaa-shiro | |
parent | 506a1fc1252268fa31ba89882ea55b7665579965 (diff) |
Add new version of aaa
Change-Id: I94d72011e6019e66c98f46d11436a5cb33ff295d
Diffstat (limited to 'odl-aaa-moon/aaa/aaa-shiro')
33 files changed, 3530 insertions, 0 deletions
diff --git a/odl-aaa-moon/aaa/aaa-shiro/pom.xml b/odl-aaa-moon/aaa/aaa-shiro/pom.xml new file mode 100644 index 00000000..ea551532 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-shiro/pom.xml @@ -0,0 +1,169 @@ +<!-- 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.3.2-Beryllium-SR2</version> + <relativePath>../parent</relativePath> + </parent> + + <artifactId>aaa-shiro</artifactId> + <packaging>bundle</packaging> + + <dependencies> + <!-- jersey client for moon authN --> + <dependency> + <groupId>com.sun.jersey</groupId> + <artifactId>jersey-client</artifactId> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>org.json</groupId> + <artifactId>json</artifactId> + <version>20140107</version> + </dependency> + <!-- OAuth2 dependencies for moon --> + <dependency> + <groupId>org.apache.oltu.oauth2</groupId> + <artifactId>org.apache.oltu.oauth2.authzserver</artifactId> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>org.apache.oltu.oauth2</groupId> + <artifactId>org.apache.oltu.oauth2.common</artifactId> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>org.apache.oltu.oauth2</groupId> + <artifactId>org.apache.oltu.oauth2.resourceserver</artifactId> + <scope>provided</scope> + </dependency> + <!-- end --> + <dependency> + <groupId>org.apache.felix</groupId> + <artifactId>org.apache.felix.dependencymanager</artifactId> + </dependency> + <dependency> + <groupId>org.opendaylight.aaa</groupId> + <artifactId>aaa-authn-sts</artifactId> + </dependency> + <dependency> + <groupId>org.opendaylight.aaa</groupId> + <artifactId>aaa-authn-basic</artifactId> + </dependency> + <dependency> + <groupId>org.apache.shiro</groupId> + <artifactId>shiro-core</artifactId> + </dependency> + <dependency> + <groupId>org.apache.shiro</groupId> + <artifactId>shiro-web</artifactId> + </dependency> + <dependency> + <groupId>org.slf4j</groupId> + <artifactId>slf4j-api</artifactId> + </dependency> + <dependency> + <groupId>commons-beanutils</groupId> + <artifactId>commons-beanutils</artifactId> + <version>1.8.3</version> + </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> + <dependency> + <groupId>ch.qos.logback</groupId> + <artifactId>logback-core</artifactId> + <version>1.1.6</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>ch.qos.logback</groupId> + <artifactId>logback-classic</artifactId> + <version>1.1.6</version> + <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> + <Import-Package> + * + </Import-Package> + <Web-ContextPath>/moon</Web-ContextPath> + <Bundle-Activator>org.opendaylight.aaa.shiro.Activator</Bundle-Activator> + </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/shiro.ini</file> + <type>cfg</type> + <classifier>configuration</classifier> + </artifact> + </artifacts> + </configuration> + </execution> + </executions> + </plugin> + </plugins> + </build> +</project> diff --git a/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/Activator.java b/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/Activator.java new file mode 100644 index 00000000..2f1c98f7 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/Activator.java @@ -0,0 +1,45 @@ +/* + * 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 + */ + +package org.opendaylight.aaa.shiro; + +import org.apache.felix.dm.DependencyActivatorBase; +import org.apache.felix.dm.DependencyManager; +import org.osgi.framework.BundleContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This scaffolding allows the use of AAA Filters without AuthN or AuthZ + * enabled. This is done to support workflows such as those included in the + * <code>odl-restconf-noauth</code> feature. + * + * This class is also responsible for offering contextual <code>DEBUG</code> + * level clues concerning the activation of the <code>aaa-shiro</code> bundle. + * To enable these debug messages, issue the following command in the karaf + * shell: <code>log:set debug org.opendaylight.aaa.shiro.Activator</code> + * + * @author Ryan Goulding (ryandgoulding@gmail.com) + */ +public class Activator extends DependencyActivatorBase { + + private static final Logger LOG = LoggerFactory.getLogger(Activator.class); + + @Override + public void destroy(BundleContext bc, DependencyManager dm) throws Exception { + final String DEBUG_MESSAGE = "Destroying the aaa-shiro bundle"; + LOG.debug(DEBUG_MESSAGE); + } + + @Override + public void init(BundleContext bc, DependencyManager dm) throws Exception { + final String DEBUG_MESSAGE = "Initializing the aaa-shiro bundle"; + LOG.debug(DEBUG_MESSAGE); + } + +} diff --git a/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/ServiceProxy.java b/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/ServiceProxy.java new file mode 100644 index 00000000..e4485d73 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/ServiceProxy.java @@ -0,0 +1,94 @@ +/* + * 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.shiro; + +import org.opendaylight.aaa.shiro.filters.AAAFilter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Responsible for enabling and disabling the AAA service. By default, the + * service is disabled; the AAAFilter will not require AuthN or AuthZ. The + * service is enabled through calling + * <code>ServiceProxy.getInstance().setEnabled(true)</code>. AuthN and AuthZ are + * disabled by default in order to support workflows such as the feature + * <code>odl-restconf-noauth</code>. + * + * The AAA service is enabled through installing the <code>odl-aaa-shiro</code> + * feature. The <code>org.opendaylight.aaa.shiroact.Activator()</code> + * constructor calls enables AAA through the ServiceProxy, which in turn enables + * the AAAFilter. + * + * ServiceProxy is a singleton; access to the ServiceProxy is granted through + * the <code>getInstance()</code> function. + * + * @author Ryan Goulding (ryandgoulding@gmail.com) + * @see <a + * href="https://github.com/opendaylight/netconf/blob/master/opendaylight/restconf/sal-rest-connector/src/main/resources/WEB-INF/web.xml">resconf + * web,xml</a> + * @see <code>org.opendaylight.aaa.shiro.Activator</code> + * @see <code>org.opendaylight.aaa.shiro.filters.AAAFilter</code> + */ +public class ServiceProxy { + private static final Logger LOG = LoggerFactory.getLogger(ServiceProxy.class); + + /** + * AuthN and AuthZ are disabled by default to support workflows included in + * features such as <code>odl-restconf-noauth</code> + */ + public static final boolean DEFAULT_AA_ENABLE_STATUS = false; + + private static ServiceProxy instance = new ServiceProxy(); + private volatile boolean enabled = false; + private AAAFilter filter; + + /** + * private for singleton pattern + */ + private ServiceProxy() { + final String INFO_MESSAGE = "Creating the ServiceProxy"; + LOG.info(INFO_MESSAGE); + } + + /** + * @return ServiceProxy, a feature level singleton + */ + public static ServiceProxy getInstance() { + return instance; + } + + /** + * Enables/disables the feature, cascading the state information to the + * AAAFilter. + * + * @param enabled A flag indicating whether to enable the Service. + */ + public synchronized void setEnabled(final boolean enabled) { + this.enabled = enabled; + final String SERVICE_ENABLED_INFO_MESSAGE = "Setting ServiceProxy enabled to " + enabled; + LOG.info(SERVICE_ENABLED_INFO_MESSAGE); + // check for null because of non-determinism in bundle load + if (filter != null) { + filter.setEnabled(enabled); + } + } + + /** + * Extract whether the service is enabled. + * + * @param filter + * register an optional Filter for callback if enable state + * changes + * @return Whether the service is enabled + */ + public synchronized boolean getEnabled(final AAAFilter filter) { + this.filter = filter; + return enabled; + } +} diff --git a/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/accounting/Accounter.java b/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/accounting/Accounter.java new file mode 100644 index 00000000..e768ea59 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/accounting/Accounter.java @@ -0,0 +1,38 @@ +/* + * 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 + */ +package org.opendaylight.aaa.shiro.accounting; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Accounter is a common place to output AAA messages. Use this class through + * invoking <code>Logger.output("message")</code>. + * + * @author Ryan Goulding (ryandgoulding@gmail.com) + */ +public class Accounter { + + private static final Logger LOG = LoggerFactory.getLogger(Accounter.class); + + /* + * Essentially makes Accounter a singleton, avoiding the verbosity of + * <code>Accounter.getInstance().output("message")</code>. + */ + private Accounter() { + } + + /** + * Account for a particular <code>message</code> + * + * @param message A message for the aggregated AAA log. + */ + public static void output(final String message) { + LOG.debug(message); + } +} diff --git a/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/authorization/DefaultRBACRules.java b/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/authorization/DefaultRBACRules.java new file mode 100644 index 00000000..9e84c988 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/authorization/DefaultRBACRules.java @@ -0,0 +1,78 @@ +/* + * 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 + */ +package org.opendaylight.aaa.shiro.authorization; + +import com.google.common.collect.Sets; +import java.util.Collection; +import java.util.HashSet; + +/** + * A singleton container of default authorization rules that are installed as + * part of Shiro initialization. This class defines an immutable set of rules + * that are needed to provide system-wide security. These include protecting + * certain MD-SAL leaf nodes that contain AAA data from random access. This is + * not a place to define your custom rule set; additional RBAC rules are + * configured through the shiro initialization file: + * <code>$KARAF_HOME/shiro.ini</code> + * + * An important distinction to consider is that Shiro URL rules work to protect + * the system at the Web layer, and <code>AuthzDomDataBroker</code> works to + * protect the system down further at the DOM layer. + * + * @author Ryan Goulding (ryandgoulding@gmail.com) + * + */ +public class DefaultRBACRules { + + private static DefaultRBACRules instance; + + /** + * a collection of the default security rules + */ + private Collection<RBACRule> rbacRules = new HashSet<RBACRule>(); + + /** + * protects the AAA MD-SAL store by preventing access to the leaf nodes to + * non-admin users. + */ + private static final RBACRule PROTECT_AAA_MDSAL = RBACRule.createAuthorizationRule( + "*/authorization/*", Sets.newHashSet("admin")); + + /* + * private for singleton pattern + */ + private DefaultRBACRules() { + // rbacRules.add(PROTECT_AAA_MDSAL); + } + + /** + * + * @return the container instance for the default RBAC Rules + */ + public static final DefaultRBACRules getInstance() { + if (null == instance) { + instance = new DefaultRBACRules(); + } + return instance; + } + + /** + * + * @return a copy of the default rules, so any modifications to the returned + * reference do not affect the <code>DefaultRBACRules</code>. + */ + public final Collection<RBACRule> getRBACRules() { + // Returns a copy of the rbacRules set such that the original set keeps + // its contract of remaining immutable. Calls to rbacRules.add() are + // encapsulated solely in <code>DefaultRBACRules</code>. + // + // Since this method is only called at shiro initialiation time, + // memory consumption of creating a new set is a non-issue. + return Sets.newHashSet(rbacRules); + } +} diff --git a/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/authorization/RBACRule.java b/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/authorization/RBACRule.java new file mode 100644 index 00000000..0da95eb4 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/authorization/RBACRule.java @@ -0,0 +1,170 @@ +/* + * 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 + */ +package org.opendaylight.aaa.shiro.authorization; + +import com.google.common.base.Preconditions; +import com.google.common.collect.Sets; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A container for RBAC Rules. An RBAC Rule is composed of a url pattern which + * may contain asterisk characters (*), and a collection of roles. These are + * represented in shiro.ini in the following format: + * <code>urlPattern=roles[atLeastOneCommaSeperatedRole]</code> + * + * RBACRules are immutable; that is, you cannot change the url pattern or the + * roles after creation. This is done for security purposes. RBACRules are + * created through utilizing a static factory method: + * <code>RBACRule.createRBACRule()</code> + * + * @author Ryan Goulding (ryandgoulding@gmail.com) + * + */ +public class RBACRule { + + private static final Logger LOG = LoggerFactory.getLogger(RBACRule.class); + + /** + * a url pattern that can optional contain asterisk characters (*) + */ + private String urlPattern; + + /** + * a collection of role names, such as "admin" and "user" + */ + private Collection<String> roles = new HashSet<String>(); + + /** + * Creates an RBAC Rule. Made private for static factory method. + * + * @param urlPattern + * Cannot be null or the empty string. + * @param roles + * Must contain at least one role. + * @throws NullPointerException + * if <code>urlPattern</code> or <code>roles</code> is null + * @throws IllegalArgumentException + * if <code>urlPattern</code> is an empty string or + * <code>roles</code> is an empty collection. + */ + private RBACRule(final String urlPattern, final Collection<String> roles) + throws NullPointerException, IllegalArgumentException { + + this.setUrlPattern(urlPattern); + this.setRoles(roles); + } + + /** + * The static factory method used to create RBACRules. + * + * @param urlPattern + * Cannot be null or the empty string. + * @param roles + * Cannot be null or an emtpy collection. + * @return An immutable RBACRule + */ + public static RBACRule createAuthorizationRule(final String urlPattern, + final Collection<String> roles) { + + RBACRule authorizationRule = null; + try { + authorizationRule = new RBACRule(urlPattern, roles); + } catch (Exception e) { + LOG.error("Cannot instantiate the AuthorizationRule", e); + } + return authorizationRule; + } + + /** + * + * @return the urlPattern for the RBACRule + */ + public String getUrlPattern() { + return urlPattern; + } + + /* + * helper to ensure the url pattern is not the empty string + */ + private static void checkUrlPatternLength(final String urlPattern) + throws IllegalArgumentException { + + final String EXCEPTION_MESSAGE = "Empty String is not allowed for urlPattern"; + if (urlPattern.isEmpty()) { + throw new IllegalArgumentException(EXCEPTION_MESSAGE); + } + } + + private void setUrlPattern(final String urlPattern) throws NullPointerException, + IllegalArgumentException { + + Preconditions.checkNotNull(urlPattern); + checkUrlPatternLength(urlPattern); + this.urlPattern = urlPattern; + } + + /** + * + * @return a copy of the rule, so any modifications to the returned + * reference do not affect the immutable <code>RBACRule</code>. + */ + public Collection<String> getRoles() { + // Returns a copy of the roles collection such that the original set + // keeps + // its contract of remaining immutable. + // + // Since this method is only called at shiro initialiation time, + // memory consumption of creating a new set is a non-issue. + return Sets.newHashSet(roles); + } + + /* + * check to ensure the roles collection is not empty + */ + private static void checkRolesCollectionSize(final Collection<String> roles) + throws IllegalArgumentException { + + final String EXCEPTION_MESSAGE = "roles must contain at least 1 role"; + if (roles.isEmpty()) { + throw new IllegalArgumentException(EXCEPTION_MESSAGE); + } + } + + private void setRoles(final Collection<String> roles) throws NullPointerException, + IllegalArgumentException { + + Preconditions.checkNotNull(roles); + checkRolesCollectionSize(roles); + this.roles = roles; + } + + /** + * Generates a string representation of the <code>RBACRule</code> roles in + * shiro form. + * + * @return roles string representation in the form + * <code>roles[roleOne,roleTwo]</code> + */ + public String getRolesInShiroFormat() { + final String ROLES_STRING = "roles"; + return ROLES_STRING + Arrays.toString(roles.toArray()); + } + + /** + * Generates the string representation of the <code>RBACRule</code> in shiro + * form. For example: <code>urlPattern=roles[admin,user]</code> + */ + @Override + public String toString() { + return String.format("%s=%s", urlPattern, getRolesInShiroFormat()); + } +} diff --git a/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/filters/AAAFilter.java b/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/filters/AAAFilter.java new file mode 100644 index 00000000..47dd9549 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/filters/AAAFilter.java @@ -0,0 +1,72 @@ +/* + * 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 + */ + +package org.opendaylight.aaa.shiro.filters; + +import org.apache.shiro.web.servlet.ShiroFilter; +import org.opendaylight.aaa.shiro.ServiceProxy; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The RESTCONF AAA JAX-RS 1.X Web Filter. This class is also responsible for + * delivering debug information; to enable these debug statements, please issue + * the following in the karaf shell: + * + * <code>log:set debug org.opendaylight.aaa.shiro.filters.AAAFilter</code> + * + * @author Ryan Goulding (ryandgoulding@gmail.com) + * @see <code>javax.servlet.Filter</code> + * @see <code>org.apache.shiro.web.servlet.ShiroFilter</code> + */ +public class AAAFilter extends ShiroFilter { + + private static final Logger LOG = LoggerFactory.getLogger(AAAFilter.class); + + public AAAFilter() { + super(); + final String DEBUG_MESSAGE = "Creating the AAAFilter"; + LOG.debug(DEBUG_MESSAGE); + } + + /* + * (non-Javadoc) + * + * Adds context clues that aid in debugging. Also initializes the enable + * status to correspond with + * <code>ServiceProxy.getInstance.getEnabled()</code>. + * + * @see org.apache.shiro.web.servlet.ShiroFilter#init() + */ + @Override + public void init() throws Exception { + super.init(); + final String DEBUG_MESSAGE = "Initializing the AAAFilter"; + LOG.debug(DEBUG_MESSAGE); + // sets the filter to the startup value. Because of non-determinism in + // bundle loading, this passes an instance of itself along so that if + // the + // enable status changes, then AAAFilter enable status is changed. + setEnabled(ServiceProxy.getInstance().getEnabled(this)); + } + + /* + * (non-Javadoc) + * + * Adds context clues to aid in debugging whether the filter is enabled. + * + * @see + * org.apache.shiro.web.servlet.OncePerRequestFilter#setEnabled(boolean) + */ + @Override + public void setEnabled(boolean enabled) { + super.setEnabled(enabled); + final String DEBUG_MESSAGE = "Setting AAAFilter enabled to " + enabled; + LOG.debug(DEBUG_MESSAGE); + } +} diff --git a/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/filters/AAAShiroFilter.java b/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/filters/AAAShiroFilter.java new file mode 100644 index 00000000..530acfac --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/filters/AAAShiroFilter.java @@ -0,0 +1,51 @@ +/* + * 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.shiro.filters; + +import org.apache.shiro.web.servlet.ShiroFilter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The default AAA JAX-RS 1.X Web Filter. Unlike AAAFilter, which is aimed towards + * supporting RESTCONF and its existing API mechanisms, AAAShiroFilter is a generic + * <code>ShiroFilter</code> for use with any other ODL Servlets. The main difference + * is that <code>AAAFilter</code> was designed to support the existing noauth + * mechanism, while this filter cannot be disabled. + * + * This class is also responsible for delivering debug information; to enable these + * debug statements, please issue the following in the karaf shell: + * + * <code>log:set debug org.opendaylight.aaa.shiro.filters.AAAShiroFilter</code> + * + * @author Ryan Goulding (ryandgoulding@gmail.com) + * @see <code>javax.servlet.Filter</code> + * @see <code>org.apache.shiro.web.servlet.ShiroFilter</code> + */ +public class AAAShiroFilter extends ShiroFilter { + + private static final Logger LOG = LoggerFactory.getLogger(AAAShiroFilter.class); + + public AAAShiroFilter() { + LOG.debug("Creating the AAAShiroFilter"); + } + + /* + * (non-Javadoc) + * + * Adds context clues that aid in debugging. + * + * @see org.apache.shiro.web.servlet.ShiroFilter#init() + */ + @Override + public void init() throws Exception { + super.init(); + LOG.debug("Initializing the AAAShiroFilter"); + } +} diff --git a/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/filters/AuthenticationListener.java b/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/filters/AuthenticationListener.java new file mode 100644 index 00000000..080ab114 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/filters/AuthenticationListener.java @@ -0,0 +1,52 @@ +/* + * 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.shiro.filters; + +import org.apache.shiro.authc.AuthenticationException; +import org.apache.shiro.authc.AuthenticationInfo; +import org.apache.shiro.authc.AuthenticationToken; +import org.apache.shiro.subject.PrincipalCollection; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Follows the event-listener pattern; the <code>Authenticator</code> notifies this class about + * authentication attempts. <code>AuthenticationListener</code> logs successful and unsuccessful + * authentication attempts appropriately. Log messages are emitted at the <code>DEBUG</code> log + * level. To enable the messages out of the box, use the following command from karaf: + * <code>log:set DEBUG org.opendaylight.aaa.shiro.authc.AuthenicationListener</code> + * + * @author Ryan Goulding (ryandgoulding@gmail.com) + */ +public class AuthenticationListener implements org.apache.shiro.authc.AuthenticationListener { + + private static final Logger LOG = LoggerFactory.getLogger(AuthenticationListener.class); + + @Override + public void onSuccess(final AuthenticationToken authenticationToken, final AuthenticationInfo authenticationInfo) { + if (LOG.isDebugEnabled()) { + final String successMessage = AuthenticationTokenUtils.generateSuccessfulAuthenticationMessage(authenticationToken); + LOG.debug(successMessage); + } + } + + @Override + public void onFailure(final AuthenticationToken authenticationToken, final AuthenticationException e) { + if (LOG.isDebugEnabled()) { + final String failureMessage = AuthenticationTokenUtils.generateUnsuccessfulAuthenticationMessage(authenticationToken); + LOG.debug(failureMessage); + } + } + + @Override + public void onLogout(final PrincipalCollection principalCollection) { + // Do nothing; AAA is aimed at RESTCONF, which stateless by definition. + // Including this output would very quickly pollute the log. + } +} diff --git a/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/filters/AuthenticationTokenUtils.java b/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/filters/AuthenticationTokenUtils.java new file mode 100644 index 00000000..a5f0c10d --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/filters/AuthenticationTokenUtils.java @@ -0,0 +1,129 @@ +/* + * 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.shiro.filters; + +import com.google.common.base.Preconditions; +import org.apache.shiro.authc.AuthenticationToken; +import org.apache.shiro.authc.UsernamePasswordToken; + +/** + * Utility methods for forming audit trail output based on an <code>AuthenticationToken</code>. + * + * @author Ryan Goulding (ryandgoulding@gmail.com) + */ +public class AuthenticationTokenUtils { + + /** + * default value used in messaging when the "user" field is unparsable from the HTTP REST request + */ + static final String DEFAULT_USERNAME = "an unknown user"; + + /** + * default value used in messaging when the "user" field is not present in the HTTP REST request, implying + * a different implementation of <code>AuthenticationToken</code> such as <code>CasToken</code>. + */ + static final String DEFAULT_TOKEN = "an un-parsable token type"; + + /** + * default value used in messaging when the "host" field cannot be determined. + */ + static final String DEFAULT_HOSTNAME = "an unknown host"; + + private AuthenticationTokenUtils() { + // private to prevent instantiation + } + + /** + * Determines whether the supplied <code>Token</code> is a <code>UsernamePasswordToken</code>. + * + * @param token A generic <code>Token</code>, which might be a <code>UsernamePasswordToken</code> + * @return Whether the supplied <code>Token</code> is a <code>UsernamePasswordToken</code> + */ + public static boolean isUsernamePasswordToken(final AuthenticationToken token) { + return token instanceof UsernamePasswordToken; + } + + /** + * Extracts the username if possible. If the supplied token is a <code>UsernamePasswordToken</code> + * and the username field is not set, <code>DEFAULT_USERNAME</code> is returned. If the supplied + * token is not a <code>UsernamePasswordToken</code> (i.e., a <code>CasToken</code> or other + * implementation of <code>AuthenticationToken</code>), then <code>DEFAULT_TOKEN</code> is + * returned. + * + * @param token An <code>AuthenticationToken</code>, possibly a <code>UsernamePasswordToken</code> + * @return the username, <code>DEFAULT_USERNAME</code> or <code>DEFAULT_TOKEN</code> depending on input + */ + public static String extractUsername(final AuthenticationToken token) { + if (isUsernamePasswordToken(token)) { + final UsernamePasswordToken upt = (UsernamePasswordToken) token; + return extractField(upt.getUsername(), DEFAULT_USERNAME); + } + return DEFAULT_TOKEN; + } + + /** + * Extracts the hostname if possible. If the supplied token is a <code>UsernamePasswordToken</code> + * and the hostname field is not set, <code>DEFAULT_HOSTNAME</code> is returned. If the supplied + * token is not a <code>UsernamePasswordToken</code> (i.e., a <code>CasToken</code> or other + * implementation of <code>AuthenticationToken</code>), then <code>DEFAULT_HOSTNAME</code> is + * returned. + * + * @param token An <code>AuthenticationToken</code>, possibly a <code>UsernamePasswordToken</code> + * @return the hostname, or <code>DEFAULT_USERNAME</code> depending on input + */ + public static String extractHostname(final AuthenticationToken token) { + if (isUsernamePasswordToken(token)) { + final UsernamePasswordToken upt = (UsernamePasswordToken) token; + return extractField(upt.getHost(), DEFAULT_HOSTNAME); + } + return DEFAULT_HOSTNAME; + } + + /** + * Utility method to generate a generic message indicating Authentication was unsuccessful. + * + * @param token An <code>AuthenticationToken</code>, possibly a <code>UsernamePasswordToken</code> + * @return A message indicating authentication was unsuccessful + */ + public static String generateUnsuccessfulAuthenticationMessage(final AuthenticationToken token) { + final String username = extractUsername(token); + final String remoteHostname = extractHostname(token); + return String.format("Unsuccessful authentication attempt by %s from %s", username, remoteHostname); + } + + /** + * Utility method to generate a generic message indicating Authentication was successful. + * + * @param token An <code>AuthenticationToken</code>, possibly a <code>UsernamePasswordToken</code> + * @return A message indicating authentication was successful + */ + public static String generateSuccessfulAuthenticationMessage(final AuthenticationToken token) { + final String username = extractUsername(token); + final String remoteHostname = extractHostname(token); + return String.format("Successful authentication attempt by %s from %s", username, remoteHostname); + } + + /** + * Utility method that returns <code>field</code>, or <code>defaultValue</code> if <code>field</code> is null. + * + * @param field A generic string, which is possibly null. + * @param defaultValue A non-null value returned if <code>field</code> is null + * @return <code>field</code> or <code>defaultValue</code> if field is null + * @throws IllegalArgumentException If <code>defaultValue</code> is null + */ + private static String extractField(final String field, final String defaultValue) + throws IllegalArgumentException { + + Preconditions.checkNotNull(defaultValue, "defaultValue can't be null"); + if (field != null) { + return field; + } + return defaultValue; + } +} diff --git a/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/filters/MoonOAuthFilter.java b/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/filters/MoonOAuthFilter.java new file mode 100644 index 00000000..241b7c28 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/filters/MoonOAuthFilter.java @@ -0,0 +1,186 @@ +/* + * 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 + */ + +package org.opendaylight.aaa.shiro.filters; + +import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST; +import static javax.servlet.http.HttpServletResponse.SC_CREATED; +import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR; +import static javax.servlet.http.HttpServletResponse.SC_UNAUTHORIZED; + +import java.io.IOException; +import java.io.PrintWriter; + +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.oltu.oauth2.as.response.OAuthASResponse; +import org.apache.oltu.oauth2.common.exception.OAuthProblemException; +import org.apache.oltu.oauth2.common.exception.OAuthSystemException; +import org.apache.oltu.oauth2.common.message.OAuthResponse; +import org.apache.oltu.oauth2.common.message.types.TokenType; +import org.apache.shiro.SecurityUtils; +import org.apache.shiro.authc.AuthenticationException; +import org.apache.shiro.authc.AuthenticationToken; +import org.apache.shiro.authc.UsernamePasswordToken; +import org.apache.shiro.subject.Subject; +import org.apache.shiro.web.filter.authc.AuthenticatingFilter; +import org.opendaylight.aaa.AuthenticationBuilder; +import org.opendaylight.aaa.ClaimBuilder; +import org.opendaylight.aaa.api.Authentication; +import org.opendaylight.aaa.api.Claim; +import org.opendaylight.aaa.shiro.moon.MoonPrincipal; +import org.opendaylight.aaa.sts.OAuthRequest; +import org.opendaylight.aaa.sts.ServiceLocator; + +/** + * MoonOAuthFilter filters oauth1 requests form token based authentication + * @author Alioune BA alioune.ba@orange.com + * + */ +public class MoonOAuthFilter extends AuthenticatingFilter{ + + private static final String DOMAIN_SCOPE_REQUIRED = "Domain scope required"; + private static final String NOT_IMPLEMENTED = "not_implemented"; + private static final String UNAUTHORIZED = "unauthorized"; + private static final String UNAUTHORIZED_CREDENTIALS = "Unauthorized: Login/Password incorrect"; + + static final String TOKEN_GRANT_ENDPOINT = "/token"; + static final String TOKEN_REVOKE_ENDPOINT = "/revoke"; + static final String TOKEN_VALIDATE_ENDPOINT = "/validate"; + + @Override + protected UsernamePasswordToken createToken(ServletRequest request, ServletResponse response) throws Exception { + // TODO Auto-generated method stub + HttpServletRequest httpRequest = (HttpServletRequest) request; + OAuthRequest oauthRequest = new OAuthRequest(httpRequest); + return new UsernamePasswordToken(oauthRequest.getUsername(),oauthRequest.getPassword()); + } + + @Override + protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception { + // TODO Auto-generated method stub + Subject currentUser = SecurityUtils.getSubject(); + return executeLogin(request, response); + } + + protected boolean onLoginSuccess(AuthenticationToken token, Subject subject, + ServletRequest request, ServletResponse response) throws Exception { + HttpServletResponse httpResponse= (HttpServletResponse) response; + MoonPrincipal principal = (MoonPrincipal) subject.getPrincipals().getPrimaryPrincipal(); + Claim claim = principal.principalToClaim(); + oauthAccessTokenResponse(httpResponse,claim,"",principal.getToken()); + return true; + } + + protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, + ServletRequest request, ServletResponse response) { + HttpServletResponse resp = (HttpServletResponse) response; + error(resp, SC_BAD_REQUEST, UNAUTHORIZED_CREDENTIALS); + return false; + } + + protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception { + + HttpServletRequest req= (HttpServletRequest) request; + HttpServletResponse resp = (HttpServletResponse) response; + try { + if (req.getServletPath().equals(TOKEN_GRANT_ENDPOINT)) { + UsernamePasswordToken token = createToken(request, response); + if (token == null) { + String msg = "A valid non-null AuthenticationToken " + + "must be created in order to execute a login attempt."; + throw new IllegalStateException(msg); + } + try { + Subject subject = getSubject(request, response); + subject.login(token); + return onLoginSuccess(token, subject, request, response); + } catch (AuthenticationException e) { + return onLoginFailure(token, e, request, response); + } + } else if (req.getServletPath().equals(TOKEN_REVOKE_ENDPOINT)) { + //TODO: deleteAccessToken(req, resp); + } else if (req.getServletPath().equals(TOKEN_VALIDATE_ENDPOINT)) { + //TODO: validateToken(req, resp); + } + } catch (AuthenticationException e) { + error(resp, SC_UNAUTHORIZED, e.getMessage()); + } catch (OAuthProblemException oe) { + error(resp, oe); + } catch (Exception e) { + error(resp, e); + } + return false; + } + + private void oauthAccessTokenResponse(HttpServletResponse resp, Claim claim, String clientId, String token) + throws OAuthSystemException, IOException { + if (claim == null) { + throw new AuthenticationException(UNAUTHORIZED); + } + + // Cache this token... + Authentication auth = new AuthenticationBuilder(new ClaimBuilder(claim).setClientId( + clientId).build()).setExpiration(tokenExpiration()).build(); + ServiceLocator.getInstance().getTokenStore().put(token, auth); + + OAuthResponse r = OAuthASResponse.tokenResponse(SC_CREATED).setAccessToken(token) + .setTokenType(TokenType.BEARER.toString()) + .setExpiresIn(Long.toString(auth.expiration())) + .buildJSONMessage(); + write(resp, r); + } + + private void write(HttpServletResponse resp, OAuthResponse r) throws IOException { + resp.setStatus(r.getResponseStatus()); + PrintWriter pw = resp.getWriter(); + pw.print(r.getBody()); + pw.flush(); + pw.close(); + } + + private long tokenExpiration() { + return ServiceLocator.getInstance().getTokenStore().tokenExpiration(); + } + + // Emit an error OAuthResponse with the given HTTP code + private void error(HttpServletResponse resp, int httpCode, String error) { + try { + OAuthResponse r = OAuthResponse.errorResponse(httpCode).setError(error) + .buildJSONMessage(); + write(resp, r); + } catch (Exception e1) { + // Nothing to do here + } + } + + private void error(HttpServletResponse resp, OAuthProblemException e) { + try { + OAuthResponse r = OAuthResponse.errorResponse(SC_BAD_REQUEST).error(e) + .buildJSONMessage(); + write(resp, r); + } catch (Exception e1) { + // Nothing to do here + } + } + + private void error(HttpServletResponse resp, Exception e) { + try { + OAuthResponse r = OAuthResponse.errorResponse(SC_INTERNAL_SERVER_ERROR) + .setError(e.getClass().getName()) + .setErrorDescription(e.getMessage()).buildJSONMessage(); + write(resp, r); + } catch (Exception e1) { + // Nothing to do here + } + } + +} diff --git a/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/filters/ODLHttpAuthenticationFilter.java b/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/filters/ODLHttpAuthenticationFilter.java new file mode 100644 index 00000000..90b0101e --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/filters/ODLHttpAuthenticationFilter.java @@ -0,0 +1,78 @@ +/* + * 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 + */ + +package org.opendaylight.aaa.shiro.filters; + +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; + +import org.apache.shiro.codec.Base64; +import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter; +import org.apache.shiro.web.util.WebUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Extends <code>BasicHttpAuthenticationFilter</code> to include ability to + * authenticate OAuth2 tokens, which is needed for backwards compatibility with + * <code>TokenAuthFilter</code>. + * + * This behavior is enabled by default for backwards compatibility. To disable + * OAuth2 functionality, just comment out the following line from the + * <code>etc/shiro.ini</code> file: + * <code>authcBasic = org.opendaylight.aaa.shiro.filters.ODLHttpAuthenticationFilter</code> + * then restart the karaf container. + * + * @author Ryan Goulding (ryandgoulding@gmail.com) + * + */ +public class ODLHttpAuthenticationFilter extends BasicHttpAuthenticationFilter { + + private static final Logger LOG = LoggerFactory.getLogger(ODLHttpAuthenticationFilter.class); + + // defined in lower-case for more efficient string comparison + protected static final String BEARER_SCHEME = "bearer"; + + protected static final String OPTIONS_HEADER = "OPTIONS"; + + public ODLHttpAuthenticationFilter() { + super(); + LOG.info("Creating the ODLHttpAuthenticationFilter"); + } + + @Override + protected String[] getPrincipalsAndCredentials(String scheme, String encoded) { + final String decoded = Base64.decodeToString(encoded); + // attempt to decode username/password; otherwise decode as token + if (decoded.contains(":")) { + return decoded.split(":"); + } + return new String[] { encoded }; + } + + @Override + protected boolean isLoginAttempt(String authzHeader) { + final String authzScheme = getAuthzScheme().toLowerCase(); + final String authzHeaderLowerCase = authzHeader.toLowerCase(); + return authzHeaderLowerCase.startsWith(authzScheme) + || authzHeaderLowerCase.startsWith(BEARER_SCHEME); + } + + @Override + protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, + Object mappedValue) { + final HttpServletRequest httpRequest = WebUtils.toHttp(request); + final String httpMethod = httpRequest.getMethod(); + if (OPTIONS_HEADER.equalsIgnoreCase(httpMethod)) { + return true; + } else { + return super.isAccessAllowed(httpRequest, response, mappedValue); + } + } +} diff --git a/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/moon/MoonPrincipal.java b/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/moon/MoonPrincipal.java new file mode 100644 index 00000000..9dd2fd4f --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/moon/MoonPrincipal.java @@ -0,0 +1,160 @@ +/* + * 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 + */ +package org.opendaylight.aaa.shiro.moon; + +import com.google.common.collect.ImmutableSet; + +import java.io.Serializable; +import java.util.Set; + +import org.opendaylight.aaa.api.Claim; + +/** + * MoonPrincipal contains all user's information returned by moon on successful authentication + * @author Alioune BA alioune.ba@orange.com + * + */ +public class MoonPrincipal { + + private final String username; + private final String domain; + private final String userId; + private final Set<String> roles; + private final String token; + + + public MoonPrincipal(String username, String domain, String userId, Set<String> roles, String token) { + this.username = username; + this.domain = domain; + this.userId = userId; + this.roles = roles; + this.token = token; + } + + public MoonPrincipal createODLPrincipal(String username, String domain, + String userId, Set<String> roles, String token) { + + return new MoonPrincipal(username, domain, userId, roles,token); + } + + public Claim principalToClaim (){ + return new MoonClaim("", this.getUserId(), this.getUsername(), this.getDomain(), this.getRoles()); + } + + public String getUsername() { + return this.username; + } + + public String getDomain() { + return this.domain; + } + + public String getUserId() { + return this.userId; + } + + public Set<String> getRoles() { + return this.roles; + } + + public String getToken(){ + return this.token; + } + + public class MoonClaim implements Claim, Serializable { + private static final long serialVersionUID = -8115027645190209125L; + private int hashCode = 0; + private String clientId; + private String userId; + private String user; + private String domain; + private ImmutableSet<String> roles; + + public MoonClaim(String clientId, String userId, String user, String domain, Set<String> roles) { + this.clientId = clientId; + this.userId = userId; + this.user = user; + this.domain = domain; + this.roles = ImmutableSet.<String> builder().addAll(roles).build(); + + if (userId.isEmpty() || user.isEmpty() || roles.isEmpty() || roles.contains("")) { + throw new IllegalStateException("The Claim is missing one or more of the required fields."); + } + } + + @Override + public String clientId() { + return clientId; + } + + @Override + public String userId() { + return userId; + } + + @Override + public String user() { + return user; + } + + @Override + public String domain() { + return domain; + } + + @Override + public Set<String> roles() { + return roles; + } + public String getClientId() { + return clientId; + } + + public void setClientId(String clientId) { + this.clientId = clientId; + } + + public String getUserId() { + return userId; + } + + public void setUserId(String userId) { + this.userId = userId; + } + + public String getUser() { + return user; + } + + public void setUser(String user) { + this.user = user; + } + + public String getDomain() { + return domain; + } + + public void setDomain(String domain) { + this.domain = domain; + } + + public ImmutableSet<String> getRoles() { + return roles; + } + + public void setRoles(ImmutableSet<String> roles) { + this.roles = roles; + } + + @Override + public String toString() { + return "clientId:" + clientId + "," + "userId:" + userId + "," + "userName:" + user + + "," + "domain:" + domain + "," + "roles:" + roles ; + } + } +}
\ No newline at end of file diff --git a/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/moon/MoonTokenEndpoint.java b/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/moon/MoonTokenEndpoint.java new file mode 100644 index 00000000..a954a606 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/moon/MoonTokenEndpoint.java @@ -0,0 +1,30 @@ +/* + * 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 + */ + +package org.opendaylight.aaa.shiro.moon; + + +import java.io.IOException; + +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class MoonTokenEndpoint extends HttpServlet{ + + private static final long serialVersionUID = 4980356362831585417L; + private static final Logger LOG = LoggerFactory.getLogger(MoonTokenEndpoint.class); + + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException { + LOG.debug("MoonTokenEndpoint Servlet doPost"); + } + +}
\ No newline at end of file diff --git a/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/realm/MoonRealm.java b/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/realm/MoonRealm.java new file mode 100644 index 00000000..9ebbb4d7 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/realm/MoonRealm.java @@ -0,0 +1,99 @@ +/* + * 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 + */ +package org.opendaylight.aaa.shiro.realm; + +import com.sun.jersey.api.client.Client; +import com.sun.jersey.api.client.ClientResponse; +import com.sun.jersey.api.client.WebResource; +import com.sun.jersey.api.client.config.ClientConfig; +import com.sun.jersey.api.client.config.DefaultClientConfig; + +import java.util.LinkedHashSet; +import java.util.Set; + +import org.apache.shiro.authc.AuthenticationException; +import org.apache.shiro.authc.AuthenticationInfo; +import org.apache.shiro.authc.AuthenticationToken; +import org.apache.shiro.authc.SimpleAuthenticationInfo; +import org.apache.shiro.authc.UsernamePasswordToken; +import org.apache.shiro.authz.AuthorizationInfo; +import org.apache.shiro.realm.AuthorizingRealm; +import org.apache.shiro.subject.PrincipalCollection; +import org.json.JSONException; +import org.json.JSONObject; +import org.json.JSONTokener; +import org.opendaylight.aaa.shiro.moon.MoonPrincipal; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +/** + * MoonRealm is a Shiro Realm that authenticates users from OPNFV/moon platform + * @author Alioune BA alioune.ba@orange.com + * + */ +public class MoonRealm extends AuthorizingRealm{ + + private static final Logger LOG = LoggerFactory.getLogger(MoonRealm.class); + @Override + protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection arg0) { + // TODO Auto-generated method stub + return null; + } + + @Override + protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { + // TODO Auto-generated method stub + String username = ""; + String password = ""; + String domain = "sdn"; + username = (String) authenticationToken.getPrincipal(); + final UsernamePasswordToken upt = (UsernamePasswordToken) authenticationToken; + password = new String(upt.getPassword()); + final MoonPrincipal moonPrincipal = moonAuthenticate(username,password,domain); + if (moonPrincipal!=null){ + return new SimpleAuthenticationInfo(moonPrincipal, password.toCharArray(),getName()); + }else{ + return null; + } + } + + public MoonPrincipal moonAuthenticate(String username, String password, String domain){ + + String output = ""; + ClientConfig config = new DefaultClientConfig(); + Client client = Client.create(config); + JSONTokener tokener; + JSONObject object =null; + Set<String> UserRoles = new LinkedHashSet<>(); + + String server = System.getenv("MOON_SERVER_ADDR"); + String port = System.getenv("MOON_SERVER_PORT"); + String URL = "http://" +server+ ":" +port+ "/moon/auth/tokens"; + LOG.debug("Moon server is at: {} ", server); + WebResource webResource = client.resource(URL); + String input = "{\"username\": \""+ username + "\"," + "\"password\":" + "\"" + password + "\"," + "\"project\":" + "\"" + domain + "\"" + "}";; + ClientResponse response = webResource.type("application/json").post(ClientResponse.class, input); + output = response.getEntity(String.class); + tokener = new JSONTokener(output); + object = new JSONObject(tokener); + try { + if (object.getString("token")!=null){ + String token = object.getString("token"); + String userID = username+"@"+domain; + for (int i=0; i< object.getJSONArray("roles").length(); i++){ + UserRoles.add((String) object.getJSONArray("roles").get(i)); + } + MoonPrincipal principal = new MoonPrincipal(username,domain,userID,UserRoles,token); + return principal; + } + }catch (JSONException e){ + throw new IllegalStateException("Authentication Error : "+ object.getJSONObject("error").getString("title")); + } + return null; + } + +} diff --git a/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/realm/ODLJndiLdapRealm.java b/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/realm/ODLJndiLdapRealm.java new file mode 100644 index 00000000..7d0bafd7 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/realm/ODLJndiLdapRealm.java @@ -0,0 +1,315 @@ +/* + * Copyright (c) 2015, 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.shiro.realm; + +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.Set; + +import javax.naming.NamingEnumeration; +import javax.naming.NamingException; +import javax.naming.directory.Attribute; +import javax.naming.directory.Attributes; +import javax.naming.directory.SearchControls; +import javax.naming.directory.SearchResult; +import javax.naming.ldap.LdapContext; + +import org.apache.shiro.authc.AuthenticationException; +import org.apache.shiro.authc.AuthenticationInfo; +import org.apache.shiro.authc.AuthenticationToken; +import org.apache.shiro.authz.AuthorizationInfo; +import org.apache.shiro.authz.SimpleAuthorizationInfo; +import org.apache.shiro.realm.ldap.JndiLdapRealm; +import org.apache.shiro.realm.ldap.LdapContextFactory; +import org.apache.shiro.realm.ldap.LdapUtils; +import org.apache.shiro.subject.PrincipalCollection; +import org.apache.shiro.util.Nameable; +import org.opendaylight.aaa.shiro.accounting.Accounter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * An extended implementation of + * <code>org.apache.shiro.realm.ldap.JndiLdapRealm</code> which includes + * additional Authorization capabilities. To enable this Realm, add the + * following to <code>shiro.ini</code>: + * + *<code>#ldapRealm = org.opendaylight.aaa.shiro.realm.ODLJndiLdapRealmAuthNOnly + *#ldapRealm.userDnTemplate = uid={0},ou=People,dc=DOMAIN,dc=TLD + *#ldapRealm.contextFactory.url = ldap://URL:389 + *#ldapRealm.searchBase = dc=DOMAIN,dc=TLD + *#ldapRealm.ldapAttributeForComparison = objectClass + *# The CSV list of enabled realms. In order to enable a realm, add it to the + *# list below: + * securityManager.realms = $tokenAuthRealm, $ldapRealm</code> + * + * The values above are specific to the deployed LDAP domain. If the defaults + * are not sufficient, alternatives can be derived through enabling + * <code>TRACE</code> level logging. To enable <code>TRACE</code> level + * logging, issue the following command in the karaf shell: + * <code>log:set TRACE org.opendaylight.aaa.shiro.realm.ODLJndiLdapRealm</code> + * + * @author Ryan Goulding (ryandgoulding@gmail.com) + * @see <code>org.apache.shiro.realm.ldap.JndiLdapRealm</code> + * @see <a + * href="https://shiro.apache.org/static/1.2.3/apidocs/org/apache/shiro/realm/ldap/JndiLdapRealm.html">Shiro + * documentation</a> + */ +public class ODLJndiLdapRealm extends JndiLdapRealm implements Nameable { + + private static final Logger LOG = LoggerFactory.getLogger(ODLJndiLdapRealm.class); + + /** + * When an LDAP Authorization lookup is made for a user account, a list of + * attributes are returned. The attributes are used to determine LDAP + * grouping, which is equivalent to ODL role(s). The default value is + * set to "objectClass", which is common attribute for LDAP systems. + * The actual value may be configured through setting + * <code>ldapAttributeForComparison</code>. + */ + private static final String DEFAULT_LDAP_ATTRIBUTE_FOR_COMPARISON = "objectClass"; + + /** + * The LDAP nomenclature for user ID, which is used in the authorization query process. + */ + private static final String UID = "uid"; + + /** + * The searchBase for the ldap query, which indicates the LDAP realms to + * search. By default, this is set to the + * <code>super.getUserDnSuffix()</code>. + */ + private String searchBase = super.getUserDnSuffix(); + + /** + * When an LDAP Authorization lookup is made for a user account, a list of + * attributes is returned. The attributes are used to determine LDAP + * grouping, which is equivalent to ODL role(s). The default is set to + * <code>DEFAULT_LDAP_ATTRIBUTE_FOR_COMPARISON</code>. + */ + private String ldapAttributeForComparison = DEFAULT_LDAP_ATTRIBUTE_FOR_COMPARISON; + + /* + * Adds debugging information surrounding creation of ODLJndiLdapRealm + */ + public ODLJndiLdapRealm() { + super(); + final String DEBUG_MESSAGE = "Creating ODLJndiLdapRealm"; + LOG.debug(DEBUG_MESSAGE); + } + + /* + * (non-Javadoc) Overridden to expose important audit trail information for + * accounting. + * + * @see + * org.apache.shiro.realm.ldap.JndiLdapRealm#doGetAuthenticationInfo(org + * .apache.shiro.authc.AuthenticationToken) + */ + @Override + protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) + throws AuthenticationException { + + // Delegates all AuthN lookup responsibility to the super class + try { + final String username = getUsername(token); + logIncomingConnection(username); + return super.doGetAuthenticationInfo(token); + } catch (ClassCastException e) { + LOG.info("Couldn't service the LDAP connection", e); + } + return null; + } + + /** + * Logs an incoming LDAP connection + * + * @param username + * the requesting user + */ + protected void logIncomingConnection(final String username) { + LOG.info("AAA LDAP connection from {}", username); + Accounter.output("AAA LDAP connection from " + username); + } + + /** + * Extracts the username from <code>token</code> + * + * @param token Encoded token which could contain a username + * @return The extracted username + * @throws ClassCastException + * The incoming token is not username/password (i.e., X.509 + * certificate) + */ + public static String getUsername(AuthenticationToken token) throws ClassCastException { + if (null == token) { + return null; + } + return (String) token.getPrincipal(); + } + + @Override + protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { + + AuthorizationInfo ai = null; + try { + ai = this.queryForAuthorizationInfo(principals, getContextFactory()); + } catch (NamingException e) { + LOG.error("Unable to query for AuthZ info", e); + } + return ai; + } + + /** + * extracts a username from <code>principals</code> + * + * @param principals A single principal extracted for the username + * @return The username if possible + * @throws ClassCastException + * the PrincipalCollection contains an element that is not in + * username/password form (i.e., X.509 certificate) + */ + protected String getUsername(final PrincipalCollection principals) throws ClassCastException { + + if (null == principals) { + return null; + } + return (String) getAvailablePrincipal(principals); + } + + /* + * (non-Javadoc) + * + * This method is only called if doGetAuthenticationInfo(...) completes successfully AND + * the requested endpoint has an RBAC restriction. To add an RBAC restriction, edit the + * etc/shiro.ini file and add a url to the url section. E.g., + * + * <code>/** = authcBasic, roles[person]</code> + * + * @see org.apache.shiro.realm.ldap.JndiLdapRealm#queryForAuthorizationInfo(org.apache.shiro.subject.PrincipalCollection, org.apache.shiro.realm.ldap.LdapContextFactory) + */ + @Override + protected AuthorizationInfo queryForAuthorizationInfo(PrincipalCollection principals, + LdapContextFactory ldapContextFactory) throws NamingException { + + AuthorizationInfo authorizationInfo = null; + try { + final String username = getUsername(principals); + final LdapContext ldapContext = ldapContextFactory.getSystemLdapContext(); + final Set<String> roleNames; + + try { + roleNames = getRoleNamesForUser(username, ldapContext); + authorizationInfo = buildAuthorizationInfo(roleNames); + } finally { + LdapUtils.closeContext(ldapContext); + } + } catch (ClassCastException e) { + LOG.error("Unable to extract a valid user", e); + } + return authorizationInfo; + } + + public static AuthorizationInfo buildAuthorizationInfo(final Set<String> roleNames) { + if (null == roleNames) { + return null; + } + return new SimpleAuthorizationInfo(roleNames); + } + + /** + * extracts the Set of roles associated with a user based on the username + * and ldap context (server). + * + * @param username The username for the request + * @param ldapContext The specific system context provided by <code>shiro.ini</code> + * @return A set of roles + * @throws NamingException If the ldap search fails + */ + protected Set<String> getRoleNamesForUser(final String username, final LdapContext ldapContext) + throws NamingException { + + // Stores the role names, which are equivalent to the set of group names extracted + // from the LDAP query. + final Set<String> roleNames = new LinkedHashSet<String>(); + + final SearchControls searchControls = createSearchControls(); + + LOG.debug("Asking the configured LDAP about which groups uid=\"{}\" belongs to using " + + "searchBase=\"{}\" ldapAttributeForComparison=\"{}\"", + username, searchBase, ldapAttributeForComparison); + final NamingEnumeration<SearchResult> answer = ldapContext.search(searchBase, + String.format("%s=%s", UID, username), searchControls); + + // Cursor based traversal over the LDAP query result + while (answer.hasMoreElements()) { + final SearchResult searchResult = answer.next(); + final Attributes attrs = searchResult.getAttributes(); + if (attrs != null) { + // Extract the attributes from the LDAP search. + // attrs.getAttr(String) was not chosen, since all attributes should be exposed + // in trace logging should the operator wish to use an alternate attribute. + final NamingEnumeration<? extends Attribute> ae = attrs.getAll(); + while (ae.hasMore()) { + final Attribute attr = ae.next(); + LOG.trace("LDAP returned \"{}\" attribute for \"{}\"", attr.getID(), username); + if (attr.getID().equals(ldapAttributeForComparison)) { + // Stresses the point that LDAP groups are EQUIVALENT to ODL role names + // TODO make this configurable via a Strategy pattern so more interesting mappings can be made + final Collection<String> groupNamesExtractedFromLdap = LdapUtils.getAllAttributeValues(attr); + final Collection<String> roleNamesFromLdapGroups = groupNamesExtractedFromLdap; + if (LOG.isTraceEnabled()) { + for (String roleName : roleNamesFromLdapGroups) { + LOG.trace("Mapped the \"{}\" LDAP group to ODL role for \"{}\"", roleName, username); + } + } + roleNames.addAll(roleNamesFromLdapGroups); + } + } + } + } + return roleNames; + } + + /** + * A utility method to help create the search controls for the LDAP lookup + * + * @return A generic set of search controls for LDAP scoped to subtree + */ + protected static SearchControls createSearchControls() { + SearchControls searchControls = new SearchControls(); + searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE); + return searchControls; + } + + @Override + public String getUserDnSuffix() { + return super.getUserDnSuffix(); + } + + /** + * Injected from <code>shiro.ini</code> configuration. + * + * @param searchBase The desired value for searchBase + */ + public void setSearchBase(final String searchBase) { + // public for injection reasons + this.searchBase = searchBase; + } + + /** + * Injected from <code>shiro.ini</code> configuration. + * + * @param ldapAttributeForComparison The attribute from which groups are extracted + */ + public void setLdapAttributeForComparison(final String ldapAttributeForComparison) { + // public for injection reasons + this.ldapAttributeForComparison = ldapAttributeForComparison; + } +} diff --git a/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/realm/ODLJndiLdapRealmAuthNOnly.java b/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/realm/ODLJndiLdapRealmAuthNOnly.java new file mode 100644 index 00000000..978266c5 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/realm/ODLJndiLdapRealmAuthNOnly.java @@ -0,0 +1,102 @@ +/* + * 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.shiro.realm; + +import org.apache.shiro.authc.AuthenticationException; +import org.apache.shiro.authc.AuthenticationInfo; +import org.apache.shiro.authc.AuthenticationToken; +import org.apache.shiro.realm.ldap.JndiLdapRealm; +import org.opendaylight.aaa.shiro.accounting.Accounter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Wrapper class for <code>org.apache.shiro.realm.ldap.JndiLdapRealm</code>. + * This implementation disables Authorization so any LDAP user is able to access + * server resources. This is particularly useful for quickly prototyping ODL + * without worrying about resolving LDAP attributes (groups) to OpenDaylight + * roles. + * + * The motivation for subclassing Shiro's implementation is two-fold: 1) Enhance + * the default logging of Shiro. This allows us to more easily log incoming + * connections, providing some security auditing. 2) Provide a common package in + * the classpath for ODL supported Realm implementations (i.e., + * <code>org.opendaylight.aaa.shiro.realm</code>), which consolidates the number + * of <code>Import-Package</code> statements consumers need to enumerate. For + * example, the netconf project only needs to import + * <code>org.opendaylight.aaa.shiro.realm</code>, and does not need to worry + * about importing Shiro packages. + * + * @author Ryan Goulding (ryandgoulding@gmail.com) + * + */ +public class ODLJndiLdapRealmAuthNOnly extends JndiLdapRealm { + + private static final Logger LOG = LoggerFactory.getLogger(ODLJndiLdapRealmAuthNOnly.class); + + private static final String LDAP_CONNECTION_MESSAGE = "AAA LDAP connection from "; + + /* + * Adds debugging information surrounding creation of ODLJndiLdapRealm + */ + public ODLJndiLdapRealmAuthNOnly() { + super(); + LOG.debug("Creating ODLJndiLdapRealmAuthNOnly"); + } + + /* + * (non-Javadoc) Overridden to expose important audit trail information for + * accounting. + * + * @see + * org.apache.shiro.realm.ldap.JndiLdapRealm#doGetAuthenticationInfo(org + * .apache.shiro.authc.AuthenticationToken) + */ + @Override + protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) + throws AuthenticationException { + + try { + final String username = getUsername(token); + logIncomingConnection(username); + return super.doGetAuthenticationInfo(token); + } catch (ClassCastException e) { + LOG.info("Couldn't service the LDAP connection", e); + } + return null; + } + + /** + * Logs an incoming LDAP connection + * + * @param username + * the requesting user + */ + protected void logIncomingConnection(final String username) { + final String message = LDAP_CONNECTION_MESSAGE + username; + LOG.info(message); + Accounter.output(message); + } + + /** + * Extracts the username from <code>token</code> + * + * @param token Which possibly contains a username + * @return the username if it can be extracted + * @throws ClassCastException + * The incoming token is not username/password (i.e., X.509 + * certificate) + */ + public static String getUsername(AuthenticationToken token) throws ClassCastException { + if (null == token) { + return null; + } + return (String) token.getPrincipal(); + } +} diff --git a/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/realm/RadiusRealm.java b/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/realm/RadiusRealm.java new file mode 100644 index 00000000..51d4bfbf --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/realm/RadiusRealm.java @@ -0,0 +1,37 @@ +/* + * 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 + */ +package org.opendaylight.aaa.shiro.realm; + +import org.apache.shiro.authc.AuthenticationException; +import org.apache.shiro.authc.AuthenticationInfo; +import org.apache.shiro.authc.AuthenticationToken; +import org.apache.shiro.authz.AuthorizationInfo; +import org.apache.shiro.realm.AuthorizingRealm; +import org.apache.shiro.subject.PrincipalCollection; + +/** + * Implementation of a Radius AuthorizingRealm. + * + * @author Ryan Goulding (ryandgoulding@gmail.com) + */ +public class RadiusRealm extends AuthorizingRealm { + + @Override + protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection arg0) { + // TODO use JRadius to extract Authorization Info + return null; + } + + @Override + protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken arg0) + throws AuthenticationException { + // TODO use JRadius to extract Authentication Info + return null; + } + +} diff --git a/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/realm/TACACSRealm.java b/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/realm/TACACSRealm.java new file mode 100644 index 00000000..38d7d91a --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/realm/TACACSRealm.java @@ -0,0 +1,38 @@ +/* + * 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 + */ + +package org.opendaylight.aaa.shiro.realm; + +import org.apache.shiro.authc.AuthenticationException; +import org.apache.shiro.authc.AuthenticationInfo; +import org.apache.shiro.authc.AuthenticationToken; +import org.apache.shiro.authz.AuthorizationInfo; +import org.apache.shiro.realm.AuthorizingRealm; +import org.apache.shiro.subject.PrincipalCollection; + +/** + * + * @author Ryan Goulding (ryandgoulding@gmail.com) + * + */ +public class TACACSRealm extends AuthorizingRealm { + + @Override + protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection arg0) { + // TODO Extract AuthorizationInfo using JNetLib + return null; + } + + @Override + protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken arg0) + throws AuthenticationException { + // TODO Extract AuthenticationInfo using JNetLib + return null; + } + +} diff --git a/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/realm/TokenAuthRealm.java b/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/realm/TokenAuthRealm.java new file mode 100644 index 00000000..f9ae5051 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/realm/TokenAuthRealm.java @@ -0,0 +1,369 @@ +/* + * 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 + */ + +package org.opendaylight.aaa.shiro.realm; + +import com.google.common.base.Strings; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.apache.shiro.authc.AuthenticationException; +import org.apache.shiro.authc.AuthenticationInfo; +import org.apache.shiro.authc.AuthenticationToken; +import org.apache.shiro.authc.SimpleAuthenticationInfo; +import org.apache.shiro.authc.UsernamePasswordToken; +import org.apache.shiro.authz.AuthorizationInfo; +import org.apache.shiro.authz.SimpleAuthorizationInfo; +import org.apache.shiro.codec.Base64; +import org.apache.shiro.realm.AuthorizingRealm; +import org.apache.shiro.subject.PrincipalCollection; +import org.opendaylight.aaa.api.Authentication; +import org.opendaylight.aaa.api.TokenAuth; +import org.opendaylight.aaa.basic.HttpBasicAuth; +import org.opendaylight.aaa.sts.ServiceLocator; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * TokenAuthRealm is an adapter between the AAA shiro subsystem and the existing + * <code>TokenAuth</code> mechanisms. Thus, one can enable use of + * <code>IDMStore</code> and <code>IDMMDSALStore</code>. + * + * @author Ryan Goulding (ryandgoulding@gmail.com) + */ +public class TokenAuthRealm extends AuthorizingRealm { + + private static final String USERNAME_DOMAIN_SEPARATOR = "@"; + + /** + * The unique identifying name for <code>TokenAuthRealm</code> + */ + private static final String TOKEN_AUTH_REALM_DEFAULT_NAME = "TokenAuthRealm"; + + /** + * The message that is displayed if no <code>TokenAuth</code> interface is + * available yet + */ + private static final String AUTHENTICATION_SERVICE_UNAVAILABLE_MESSAGE = "{\"error\":\"Authentication service unavailable\"}"; + + /** + * The message that is displayed if credentials are missing or malformed + */ + private static final String FATAL_ERROR_DECODING_CREDENTIALS = "{\"error\":\"Unable to decode credentials\"}"; + + /** + * The message that is displayed if non-Basic Auth is attempted + */ + private static final String FATAL_ERROR_BASIC_AUTH_ONLY = "{\"error\":\"Only basic authentication is supported by TokenAuthRealm\"}"; + + /** + * The purposefully generic message displayed if <code>TokenAuth</code> is + * unable to validate the given credentials + */ + private static final String UNABLE_TO_AUTHENTICATE = "{\"error\":\"Could not authenticate\"}"; + + private static final Logger LOG = LoggerFactory.getLogger(TokenAuthRealm.class); + + public TokenAuthRealm() { + super(); + super.setName(TOKEN_AUTH_REALM_DEFAULT_NAME); + } + + /* + * (non-Javadoc) + * + * Roles are derived from <code>TokenAuth.authenticate()</code>. Shiro roles + * are identical to existing IDM roles. + * + * @see + * org.apache.shiro.realm.AuthorizingRealm#doGetAuthorizationInfo(org.apache + * .shiro.subject.PrincipalCollection) + */ + @Override + protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { + final Object primaryPrincipal = principalCollection.getPrimaryPrincipal(); + final ODLPrincipal odlPrincipal; + try { + odlPrincipal = (ODLPrincipal) primaryPrincipal; + return new SimpleAuthorizationInfo(odlPrincipal.getRoles()); + } catch(ClassCastException e) { + LOG.error("Couldn't decode authorization request", e); + } + return new SimpleAuthorizationInfo(); + } + + /** + * Bridge new to old style <code>TokenAuth</code> interface. + * + * @param username The request username + * @param password The request password + * @param domain The request domain + * @return <code>username:password:domain</code> + */ + static String getUsernamePasswordDomainString(final String username, final String password, + final String domain) { + return username + HttpBasicAuth.AUTH_SEP + password + HttpBasicAuth.AUTH_SEP + domain; + } + + /** + * + * @param credentialToken + * @return Base64 encoded token + */ + static String getEncodedToken(final String credentialToken) { + return Base64.encodeToString(credentialToken.getBytes()); + } + + /** + * + * @param encodedToken + * @return Basic <code>encodedToken</code> + */ + static String getTokenAuthHeader(final String encodedToken) { + return HttpBasicAuth.BASIC_PREFIX + encodedToken; + } + + /** + * + * @param tokenAuthHeader + * @return a map with the basic auth header + */ + Map<String, List<String>> formHeadersWithToken(final String tokenAuthHeader) { + final Map<String, List<String>> headers = new HashMap<String, List<String>>(); + final List<String> headerValue = new ArrayList<String>(); + headerValue.add(tokenAuthHeader); + headers.put(HttpBasicAuth.AUTH_HEADER, headerValue); + return headers; + } + + /** + * Adapter between basic authentication mechanism and existing + * <code>TokenAuth</code> interface. + * + * @param username Username from the request + * @param password Password from the request + * @param domain Domain from the request + * @return input map for <code>TokenAuth.validate()</code> + */ + Map<String, List<String>> formHeaders(final String username, final String password, + final String domain) { + String usernamePasswordToken = getUsernamePasswordDomainString(username, password, domain); + String encodedToken = getEncodedToken(usernamePasswordToken); + String tokenAuthHeader = getTokenAuthHeader(encodedToken); + return formHeadersWithToken(tokenAuthHeader); + } + + /** + * Adapter to check for available <code>TokenAuth<code> implementations. + * + * @return + */ + boolean isTokenAuthAvailable() { + return ServiceLocator.getInstance().getAuthenticationService() != null; + } + + /* + * (non-Javadoc) + * + * Authenticates against any <code>TokenAuth</code> registered with the + * <code>ServiceLocator</code> + * + * @see + * org.apache.shiro.realm.AuthenticatingRealm#doGetAuthenticationInfo(org + * .apache.shiro.authc.AuthenticationToken) + */ + @Override + protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) + throws AuthenticationException { + + String username = ""; + String password = ""; + String domain = HttpBasicAuth.DEFAULT_DOMAIN; + + try { + final String qualifiedUser = extractUsername(authenticationToken); + if (qualifiedUser.contains(USERNAME_DOMAIN_SEPARATOR)) { + final String [] qualifiedUserArray = qualifiedUser.split(USERNAME_DOMAIN_SEPARATOR); + try { + username = qualifiedUserArray[0]; + domain = qualifiedUserArray[1]; + } catch (ArrayIndexOutOfBoundsException e) { + LOG.trace("Couldn't parse domain from {}; trying without one", + qualifiedUser, e); + } + } else { + username = qualifiedUser; + } + password = extractPassword(authenticationToken); + + } catch (NullPointerException e) { + throw new AuthenticationException(FATAL_ERROR_DECODING_CREDENTIALS, e); + } catch (ClassCastException e) { + throw new AuthenticationException(FATAL_ERROR_BASIC_AUTH_ONLY, e); + } + + // check to see if there are TokenAuth implementations available + if (!isTokenAuthAvailable()) { + throw new AuthenticationException(AUTHENTICATION_SERVICE_UNAVAILABLE_MESSAGE); + } + + // if the password is empty, this is an OAuth2 request, not a Basic HTTP + // Auth request + if (!Strings.isNullOrEmpty(password)) { + if (ServiceLocator.getInstance().getAuthenticationService().isAuthEnabled()) { + Map<String, List<String>> headers = formHeaders(username, password, domain); + // iterate over <code>TokenAuth</code> implementations and + // attempt to + // authentication with each one + final List<TokenAuth> tokenAuthCollection = ServiceLocator.getInstance() + .getTokenAuthCollection(); + for (TokenAuth ta : tokenAuthCollection) { + try { + LOG.debug("Authentication attempt using {}", ta.getClass().getName()); + final Authentication auth = ta.validate(headers); + if (auth != null) { + LOG.debug("Authentication attempt successful"); + ServiceLocator.getInstance().getAuthenticationService().set(auth); + final ODLPrincipal odlPrincipal = ODLPrincipal.createODLPrincipal(auth); + return new SimpleAuthenticationInfo(odlPrincipal, password.toCharArray(), + getName()); + } + } catch (AuthenticationException ae) { + LOG.debug("Authentication attempt unsuccessful"); + throw new AuthenticationException(UNABLE_TO_AUTHENTICATE, ae); + } + } + } + } + + // extract the authentication token and attempt validation of the token + final String token = extractUsername(authenticationToken); + final Authentication auth; + try { + auth = validate(token); + if (auth != null) { + final ODLPrincipal odlPrincipal = ODLPrincipal.createODLPrincipal(auth); + return new SimpleAuthenticationInfo(odlPrincipal, "", getName()); + } + } catch (AuthenticationException e) { + LOG.debug("Unknown OAuth2 Token Access Request", e); + } + + LOG.debug("Authentication failed: exhausted TokenAuth resources"); + return null; + } + + private Authentication validate(final String token) { + Authentication auth = ServiceLocator.getInstance().getTokenStore().get(token); + if (auth == null) { + throw new AuthenticationException("Could not validate the token " + token); + } else { + ServiceLocator.getInstance().getAuthenticationService().set(auth); + } + return auth; + } + + /** + * extract the username from an <code>AuthenticationToken</code> + * + * @param authenticationToken + * @return + * @throws ClassCastException + * @throws NullPointerException + */ + static String extractUsername(final AuthenticationToken authenticationToken) + throws ClassCastException, NullPointerException { + + return (String) authenticationToken.getPrincipal(); + } + + /** + * extract the password from an <code>AuthenticationToken</code> + * + * @param authenticationToken + * @return + * @throws ClassCastException + * @throws NullPointerException + */ + static String extractPassword(final AuthenticationToken authenticationToken) + throws ClassCastException, NullPointerException { + + final UsernamePasswordToken upt = (UsernamePasswordToken) authenticationToken; + return new String(upt.getPassword()); + } + + /** + * Since <code>TokenAuthRealm</code> is an <code>AuthorizingRealm</code>, it supports + * individual steps for authentication and authorization. In ODL's existing <code>TokenAuth</code> + * mechanism, authentication and authorization are currently done in a single monolithic step. + * <code>ODLPrincipal</code> is abstracted as a DTO between the two steps. It fulfills the + * responsibility of a <code>Principal</code>, since it contains identification information + * but no credential information. + * + * @author Ryan Goulding (ryandgoulding@gmail.com) + */ + private static class ODLPrincipal { + + private final String username; + private final String domain; + private final String userId; + private final Set<String> roles; + + private ODLPrincipal(final String username, final String domain, final String userId, final Set<String> roles) { + this.username = username; + this.domain = domain; + this.userId = userId; + this.roles = roles; + } + + /** + * A static factory method to create <code>ODLPrincipal</code> instances. + * + * @param username The authenticated user + * @param domain The domain <code>username</code> belongs to. + * @param userId The unique key for <code>username</code> + * @param roles The roles associated with <code>username</code>@<code>domain</code> + * @return A Principal for the given session; essentially a DTO. + */ + static ODLPrincipal createODLPrincipal(final String username, final String domain, + final String userId, final Set<String> roles) { + + return new ODLPrincipal(username, domain, userId, roles); + } + + /** + * A static factory method to create <code>ODLPrincipal</code> instances. + * + * @param auth Contains identifying information for the particular request. + * @return A Principal for the given session; essentially a DTO. + */ + static ODLPrincipal createODLPrincipal(final Authentication auth) { + return createODLPrincipal(auth.user(), auth.domain(), auth.userId(), auth.roles()); + } + + String getUsername() { + return this.username; + } + + String getDomain() { + return this.domain; + } + + String getUserId() { + return this.userId; + } + + Set<String> getRoles() { + return this.roles; + } + } +} diff --git a/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/web/env/KarafIniWebEnvironment.java b/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/web/env/KarafIniWebEnvironment.java new file mode 100644 index 00000000..acf4022c --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/web/env/KarafIniWebEnvironment.java @@ -0,0 +1,125 @@ +/* + * 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 + */ + +package org.opendaylight.aaa.shiro.web.env; + +import java.io.File; +import java.io.FileNotFoundException; +import java.util.Collection; +import org.apache.shiro.config.Ini; +import org.apache.shiro.config.Ini.Section; +import org.apache.shiro.web.env.IniWebEnvironment; +import org.opendaylight.aaa.shiro.accounting.Accounter; +import org.opendaylight.aaa.shiro.authorization.DefaultRBACRules; +import org.opendaylight.aaa.shiro.authorization.RBACRule; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Identical to <code>IniWebEnvironment</code> except the Ini is loaded from + * <code>$KARAF_HOME/etc/shiro.ini</code>. + * + * @author Ryan Goulding (ryandgoulding@gmail.com) + * + */ +public class KarafIniWebEnvironment extends IniWebEnvironment { + + private static final Logger LOG = LoggerFactory.getLogger(KarafIniWebEnvironment.class); + public static final String DEFAULT_SHIRO_INI_FILE = "etc/shiro.ini"; + public static final String SHIRO_FILE_PREFIX = "file:/"; + + public KarafIniWebEnvironment() { + } + + @Override + public void init() { + // Initialize the Shiro environment from etc/shiro.ini then delegate to + // the parent class + Ini ini; + try { + ini = createDefaultShiroIni(); + // appendCustomIniRules(ini); + setIni(ini); + } catch (FileNotFoundException e) { + final String ERROR_MESSAGE = "Could not find etc/shiro.ini"; + LOG.error(ERROR_MESSAGE, e); + } + super.init(); + } + + /** + * A hook for installing custom default RBAC rules for security purposes. + * + * @param ini + */ + private void appendCustomIniRules(final Ini ini) { + final String INSTALL_MESSAGE = "Installing the RBAC rule: %s"; + Section urlSection = getOrCreateUrlSection(ini); + Collection<RBACRule> rbacRules = DefaultRBACRules.getInstance().getRBACRules(); + for (RBACRule rbacRule : rbacRules) { + urlSection.put(rbacRule.getUrlPattern(), rbacRule.getRolesInShiroFormat()); + Accounter.output(String.format(INSTALL_MESSAGE, rbacRule)); + } + } + + /** + * Extracts the url section of the Ini file, or creates one if it doesn't + * already exist + * + * @param ini + * @return + */ + private Section getOrCreateUrlSection(final Ini ini) { + final String URL_SECTION_TITLE = "urls"; + Section urlSection = ini.getSection(URL_SECTION_TITLE); + if (urlSection == null) { + LOG.debug("shiro.ini does not contain a [urls] section; creating one"); + urlSection = ini.addSection(URL_SECTION_TITLE); + } else { + LOG.debug("shiro.ini contains a [urls] section; appending rules to existing"); + } + return urlSection; + } + + /** + * + * @return Ini associated with <code>$KARAF_HOME/etc/shiro.ini</code> + * @throws FileNotFoundException + */ + static Ini createDefaultShiroIni() throws FileNotFoundException { + return createShiroIni(DEFAULT_SHIRO_INI_FILE); + } + + /** + * + * @param path + * the file path, which is either absolute or relative to + * <code>$KARAF_HOME</code> + * @return Ini loaded from <code>path</code> + */ + static Ini createShiroIni(final String path) throws FileNotFoundException { + File f = new File(path); + Ini ini = new Ini(); + final String fileBasedIniPath = createFileBasedIniPath(f.getAbsolutePath()); + ini.loadFromPath(fileBasedIniPath); + return ini; + } + + /** + * + * @param path + * the file path, which is either absolute or relative to + * <code>$KARAF_HOME</code> + * @return <code>file:/$KARAF_HOME/etc/shiro.ini</code> + */ + static String createFileBasedIniPath(final String path) { + String fileBasedIniPath = SHIRO_FILE_PREFIX + path; + LOG.debug(fileBasedIniPath); + return fileBasedIniPath; + } +} diff --git a/odl-aaa-moon/aaa/aaa-shiro/src/main/resources/WEB-INF/web.xml b/odl-aaa-moon/aaa/aaa-shiro/src/main/resources/WEB-INF/web.xml new file mode 100644 index 00000000..63288c23 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-shiro/src/main/resources/WEB-INF/web.xml @@ -0,0 +1,48 @@ +<?xml version="1.0" encoding="ISO-8859-1"?> +<web-app xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" + version="3.0"> + + <servlet> + <servlet-name>MOON</servlet-name> + <servlet-class>org.opendaylight.aaa.shiro.moon.MoonTokenEndpoint</servlet-class> + <load-on-startup>1</load-on-startup> + </servlet> + + <servlet-mapping> + <servlet-name>MOON</servlet-name> + <url-pattern>/token</url-pattern> + </servlet-mapping> + <servlet-mapping> + <servlet-name>MOON</servlet-name> + <url-pattern>/revoke</url-pattern> + </servlet-mapping> + <servlet-mapping> + <servlet-name>MOON</servlet-name> + <url-pattern>/validate</url-pattern> + </servlet-mapping> + <servlet-mapping> + <servlet-name>MOON</servlet-name> + <url-pattern>/*</url-pattern> + </servlet-mapping> + + <!-- Shiro Filter --> + <context-param> + <param-name>shiroEnvironmentClass</param-name> + <param-value>org.opendaylight.aaa.shiro.web.env.KarafIniWebEnvironment</param-value> + </context-param> + + <listener> + <listener-class>org.apache.shiro.web.env.EnvironmentLoaderListener</listener-class> + </listener> + + <filter> + <filter-name>ShiroFilter</filter-name> + <filter-class>org.opendaylight.aaa.shiro.filters.AAAFilter</filter-class> + </filter> + + <filter-mapping> + <filter-name>ShiroFilter</filter-name> + <url-pattern>/*</url-pattern> + </filter-mapping> +</web-app>
\ No newline at end of file diff --git a/odl-aaa-moon/aaa/aaa-shiro/src/main/resources/shiro.ini b/odl-aaa-moon/aaa/aaa-shiro/src/main/resources/shiro.ini new file mode 100644 index 00000000..b48abe96 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-shiro/src/main/resources/shiro.ini @@ -0,0 +1,106 @@ +# +# 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 +# + +############################################################################### +# shiro.ini # +# # +# Configuration of OpenDaylight's aaa-shiro feature. Provided Realm # +# implementations include: # +# - TokenAuthRealm (enabled by default) # +# - ODLJndiLdapRealm (disabled by default) # +# - ODLJndiLdapRealmAuthNOnly (disabled by default) # +# Basic user configuration through shiro.ini is disabled for security # +# purposes. # +############################################################################### + + + +[main] +############################################################################### +# realms # +# # +# This section is dedicated to setting up realms for OpenDaylight. Realms # +# are essentially different methods for providing AAA. ODL strives to provide# +# highly-configurable AAA by providing pluggable infrastructure. By deafult, # +# TokenAuthRealm is enabled out of the box (which bridges to the existing AAA # +# mechanisms). More than one realm can be enabled, and the realms are # +# tried Round-Robin until: # +# 1) a realm successfully authenticates the incoming request # +# 2) all realms are exhausted, and 401 is returned # +############################################################################### + +# ODL provides a few LDAP implementations, which are disabled out of the box. +# ODLJndiLdapRealm includes authorization functionality based on LDAP elements +# extracted through and LDAP search. This requires a bit of knowledge about +# how your LDAP system is setup. An example is provided below: +#ldapRealm = org.opendaylight.aaa.shiro.realm.ODLJndiLdapRealm +#ldapRealm.userDnTemplate = uid={0},ou=People,dc=DOMAIN,dc=TLD +#ldapRealm.contextFactory.url = ldap://<URL>:389 +#ldapRealm.searchBase = dc=DOMAIN,dc=TLD +#ldapRealm.ldapAttributeForComparison = objectClass + +# ODL also provides ODLJndiLdapRealmAuthNOnly. Essentially, this allows +# access through AAAFilter to any user that can authenticate against the +# provided LDAP server. +#ldapRealm = org.opendaylight.aaa.shiro.realm.ODLJndiLdapRealmAuthNOnly +#ldapRealm.userDnTemplate = uid={0},ou=People,dc=DOMAIN,dc=TLD +#ldapRealm.contextFactory.url = ldap://<URL>:389 + +# Bridge to existing h2/idmlight/mdsal authentication/authorization mechanisms. +# This realm is enabled by default, and utilizes h2-store by default. +#tokenAuthRealm = org.opendaylight.aaa.shiro.realm.TokenAuthRealm +# Defining moon realm +moonAuthRealm = org.opendaylight.aaa.shiro.realm.MoonRealm + +# The CSV list of enabled realms. In order to enable a realm, add it to the +# list below: +#securityManager.realms = $tokenAuthRealm +# Configure the Shiro Security Manager to use Moon Realm +securityManager.realms = $moonAuthRealm + +# adds a custom AuthenticationFilter to support OAuth2 for backwards +# compatibility. To disable OAuth2 access, just comment out the next line +# and authcBasic will default to BasicHttpAuthenticationFilter, a +# Shiro-provided class. +authcBasic = org.opendaylight.aaa.shiro.filters.ODLHttpAuthenticationFilter +# OAuth2 Filer for moon token AuthN +rest = org.opendaylight.aaa.shiro.filters.MoonOAuthFilter + +# add in AuthenticationListener, a Listener that records whether +# authentication attempts are successful or unsuccessful. This audit +# information is disabled by default, to avoid log flooding. To enable, +# issue the following in karaf: +# >log:set DEBUG org.opendaylight.aaa.shiro.filters.AuthenticationListener +accountingListener = org.opendaylight.aaa.shiro.filters.AuthenticationListener +securityManager.authenticator.authenticationListeners = $accountingListener + + + +[urls] +############################################################################### +# url authorization section # +# # +# This section is dedicated to defining url-based authorization according to: # +# http://shiro.apache.org/web.html # +############################################################################### + +# Restrict AAA endpoints to users w/ admin role +/v1/users/** = authcBasic +/v1/domains/** = authcBasic +/v1/roles/** = authcBasic + +#Filter OAuth2 request$ +/token = rest + +# General access through AAAFilter requires valid credentials (AuthN only). +/** = authcBasic + +# Access to the credential store is limited to the valid users who have the +# admin role. The following line is only needed if the mdsal store is enabled +#(the mdsal store is disabled by default). +/config/aaa-authn-model** = authcBasic,roles[admin] diff --git a/odl-aaa-moon/aaa/aaa-shiro/src/test/java/org/opendaylight/aaa/shiro/ServiceProxyTest.java b/odl-aaa-moon/aaa/aaa-shiro/src/test/java/org/opendaylight/aaa/shiro/ServiceProxyTest.java new file mode 100644 index 00000000..2d9c8976 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-shiro/src/test/java/org/opendaylight/aaa/shiro/ServiceProxyTest.java @@ -0,0 +1,45 @@ +/* + * 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 + */ + +package org.opendaylight.aaa.shiro; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; +import org.opendaylight.aaa.shiro.filters.AAAFilter; + +/** + * @author Ryan Goulding (ryandgoulding@gmail.com) + */ +public class ServiceProxyTest { + + @Test + public void testGetInstance() { + // ensures that singleton pattern is working + assertNotNull(ServiceProxy.getInstance()); + } + + @Test + public void testGetSetEnabled() { + // combines set and get tests. These are important in this instance, + // because getEnabled allows an optional callback Filter. + ServiceProxy.getInstance().setEnabled(true); + assertTrue(ServiceProxy.getInstance().getEnabled(null)); + + AAAFilter testFilter = new AAAFilter(); + // register the filter + ServiceProxy.getInstance().getEnabled(testFilter); + assertTrue(testFilter.isEnabled()); + + ServiceProxy.getInstance().setEnabled(false); + assertFalse(ServiceProxy.getInstance().getEnabled(testFilter)); + assertFalse(testFilter.isEnabled()); + } +} diff --git a/odl-aaa-moon/aaa/aaa-shiro/src/test/java/org/opendaylight/aaa/shiro/TestAppender.java b/odl-aaa-moon/aaa/aaa-shiro/src/test/java/org/opendaylight/aaa/shiro/TestAppender.java new file mode 100644 index 00000000..ec9375dc --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-shiro/src/test/java/org/opendaylight/aaa/shiro/TestAppender.java @@ -0,0 +1,67 @@ +/* + * 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.shiro; + +import ch.qos.logback.classic.spi.LoggingEvent; +import ch.qos.logback.core.AppenderBase; + +import java.util.List; +import java.util.Vector; + +/** + * A custom slf4j <code>Appender</code> which stores <code>LoggingEvent</code>(s) in memory + * for future retrieval. This is useful from inside test resources. This class is specified + * within <code>logback-test.xml</code>. + * + * @author Ryan Goulding (ryandgoulding@gmail.com) + */ +public class TestAppender extends AppenderBase<LoggingEvent> { + + /** + * stores all log events in memory, instead of file + */ + private List<LoggingEvent> events = new Vector<>(); + + /** + * Since junit maven & junit instantiate the logging appender (as provided + * by logback-test.xml), singleton is not possible. The next best thing is to track the + * current instance so it can be retrieved by Test instances. + */ + private static volatile TestAppender currentInstance; + + /** + * keeps track of the current instance + */ + public TestAppender() { + currentInstance = this; + } + + @Override + protected void append(final LoggingEvent e) { + events.add(e); + } + + /** + * Extract the log. + * + * @return the in-memory representation of <code>LoggingEvent</code>(s) + */ + public List<LoggingEvent> getEvents() { + return events; + } + + /** + * A way to extract the appender from Test instances. + * + * @return <code>this</code> + */ + public static TestAppender getCurrentInstance() { + return currentInstance; + } +} diff --git a/odl-aaa-moon/aaa/aaa-shiro/src/test/java/org/opendaylight/aaa/shiro/authorization/DefaultRBACRulesTest.java b/odl-aaa-moon/aaa/aaa-shiro/src/test/java/org/opendaylight/aaa/shiro/authorization/DefaultRBACRulesTest.java new file mode 100644 index 00000000..38658f0c --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-shiro/src/test/java/org/opendaylight/aaa/shiro/authorization/DefaultRBACRulesTest.java @@ -0,0 +1,43 @@ +/* + * 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 + */ + +package org.opendaylight.aaa.shiro.authorization; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import com.google.common.collect.Sets; +import java.util.Collection; +import org.junit.Test; + +/** + * A few basic test cases for the DefualtRBACRules singleton container. + * + * @author Ryan Goulding (ryandgoulding@gmail.com) + * + */ +public class DefaultRBACRulesTest { + + @Test + public void testGetInstance() { + assertNotNull(DefaultRBACRules.getInstance()); + assertEquals(DefaultRBACRules.getInstance(), DefaultRBACRules.getInstance()); + } + + @Test + public void testGetRBACRules() { + Collection<RBACRule> rbacRules = DefaultRBACRules.getInstance().getRBACRules(); + assertNotNull(rbacRules); + + // check that a copy was returned + int originalSize = rbacRules.size(); + rbacRules.add(RBACRule.createAuthorizationRule("fakeurl/*", Sets.newHashSet("admin"))); + assertEquals(originalSize, DefaultRBACRules.getInstance().getRBACRules().size()); + } + +} diff --git a/odl-aaa-moon/aaa/aaa-shiro/src/test/java/org/opendaylight/aaa/shiro/authorization/RBACRuleTest.java b/odl-aaa-moon/aaa/aaa-shiro/src/test/java/org/opendaylight/aaa/shiro/authorization/RBACRuleTest.java new file mode 100644 index 00000000..825fe626 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-shiro/src/test/java/org/opendaylight/aaa/shiro/authorization/RBACRuleTest.java @@ -0,0 +1,106 @@ +/* + * 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 + */ + +package org.opendaylight.aaa.shiro.authorization; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import com.google.common.collect.Sets; +import java.util.Collection; +import java.util.HashSet; +import org.junit.Test; + +public class RBACRuleTest { + + private static final String BASIC_RBAC_RULE_URL_PATTERN = "/*"; + private static final Collection<String> BASIC_RBAC_RULE_ROLES = Sets.newHashSet("admin"); + private RBACRule basicRBACRule = RBACRule.createAuthorizationRule(BASIC_RBAC_RULE_URL_PATTERN, + BASIC_RBAC_RULE_ROLES); + + private static final String COMPLEX_RBAC_RULE_URL_PATTERN = "/auth/v1/"; + private static final Collection<String> COMPLEX_RBAC_RULE_ROLES = Sets.newHashSet("admin", + "user"); + private RBACRule complexRBACRule = RBACRule.createAuthorizationRule( + COMPLEX_RBAC_RULE_URL_PATTERN, COMPLEX_RBAC_RULE_ROLES); + + @Test + public void testCreateAuthorizationRule() { + // positive test cases + assertNotNull(RBACRule.createAuthorizationRule(BASIC_RBAC_RULE_URL_PATTERN, + BASIC_RBAC_RULE_ROLES)); + assertNotNull(RBACRule.createAuthorizationRule(COMPLEX_RBAC_RULE_URL_PATTERN, + COMPLEX_RBAC_RULE_ROLES)); + + // negative test cases + // both null + assertNull(RBACRule.createAuthorizationRule(null, null)); + + // url pattern is null + assertNull(RBACRule.createAuthorizationRule(null, BASIC_RBAC_RULE_ROLES)); + // url pattern is empty string + assertNull(RBACRule.createAuthorizationRule("", BASIC_RBAC_RULE_ROLES)); + + // roles is null + assertNull(RBACRule.createAuthorizationRule(BASIC_RBAC_RULE_URL_PATTERN, null)); + // roles is empty collection + assertNull(RBACRule.createAuthorizationRule(COMPLEX_RBAC_RULE_URL_PATTERN, + new HashSet<String>())); + } + + @Test + public void testGetUrlPattern() { + assertEquals(BASIC_RBAC_RULE_URL_PATTERN, basicRBACRule.getUrlPattern()); + assertEquals(COMPLEX_RBAC_RULE_URL_PATTERN, complexRBACRule.getUrlPattern()); + } + + @Test + public void testGetRoles() { + assertTrue(BASIC_RBAC_RULE_ROLES.containsAll(basicRBACRule.getRoles())); + basicRBACRule.getRoles().clear(); + // test that getRoles() produces a new object + assertFalse(basicRBACRule.getRoles().isEmpty()); + assertTrue(basicRBACRule.getRoles().containsAll(BASIC_RBAC_RULE_ROLES)); + + assertTrue(COMPLEX_RBAC_RULE_ROLES.containsAll(complexRBACRule.getRoles())); + complexRBACRule.getRoles().add("newRole"); + // test that getRoles() produces a new object + assertFalse(complexRBACRule.getRoles().contains("newRole")); + assertTrue(complexRBACRule.getRoles().containsAll(COMPLEX_RBAC_RULE_ROLES)); + } + + @Test + public void testGetRolesInShiroFormat() { + final String BASIC_RBAC_RULE_EXPECTED_SHIRO_FORMAT = "roles[admin]"; + assertEquals(BASIC_RBAC_RULE_EXPECTED_SHIRO_FORMAT, basicRBACRule.getRolesInShiroFormat()); + + // set ordering is not predictable, so both formats must be considered + final String COMPLEX_RBAC_RULE_EXPECTED_SHIRO_FORMAT_1 = "roles[admin, user]"; + final String COMPLEX_RBAC_RULE_EXPECTED_SHIRO_FORMAT_2 = "roles[user, admin]"; + assertTrue(COMPLEX_RBAC_RULE_EXPECTED_SHIRO_FORMAT_1.equals(complexRBACRule + .getRolesInShiroFormat()) + || COMPLEX_RBAC_RULE_EXPECTED_SHIRO_FORMAT_2.equals(complexRBACRule + .getRolesInShiroFormat())); + } + + @Test + public void testToString() { + final String BASIC_RBAC_RULE_EXPECTED_SHIRO_FORMAT = "/*=roles[admin]"; + assertEquals(BASIC_RBAC_RULE_EXPECTED_SHIRO_FORMAT, basicRBACRule.toString()); + + // set ordering is not predictable,s o both formats must be considered + final String COMPLEX_RBAC_RULE_EXPECTED_SHIRO_FORMAT_1 = "/auth/v1/=roles[admin, user]"; + final String COMPLEX_RBAC_RULE_EXPECTED_SHIRO_FORMAT_2 = "/auth/v1/=roles[user, admin]"; + assertTrue(COMPLEX_RBAC_RULE_EXPECTED_SHIRO_FORMAT_1.equals(complexRBACRule.toString()) + || COMPLEX_RBAC_RULE_EXPECTED_SHIRO_FORMAT_2.equals(complexRBACRule.toString())); + } + +} diff --git a/odl-aaa-moon/aaa/aaa-shiro/src/test/java/org/opendaylight/aaa/shiro/filters/AuthenticationListenerTest.java b/odl-aaa-moon/aaa/aaa-shiro/src/test/java/org/opendaylight/aaa/shiro/filters/AuthenticationListenerTest.java new file mode 100644 index 00000000..1c823525 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-shiro/src/test/java/org/opendaylight/aaa/shiro/filters/AuthenticationListenerTest.java @@ -0,0 +1,72 @@ +/* + * 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.shiro.filters; + +import static org.junit.Assert.*; + +import ch.qos.logback.classic.spi.LoggingEvent; + +import java.util.List; + +import org.apache.shiro.authc.AuthenticationException; +import org.apache.shiro.authc.SimpleAuthenticationInfo; +import org.apache.shiro.authc.UsernamePasswordToken; +import org.junit.Test; +import org.opendaylight.aaa.shiro.TestAppender; +import org.opendaylight.aaa.shiro.filters.AuthenticationListener; + +/** + * Test AuthenticationListener, which is responsible for logging Accounting events. + * + * @author Ryan Goulding (ryandgoulding@gmail.com) + */ +public class AuthenticationListenerTest { + + @Test + public void testOnSuccess() throws Exception { + // sets up a successful authentication attempt + final AuthenticationListener authenticationListener = new AuthenticationListener(); + final UsernamePasswordToken authenticationToken = new UsernamePasswordToken(); + authenticationToken.setUsername("successfulUser1"); + authenticationToken.setHost("successfulHost1"); + final SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(); + // the following call produces accounting output + authenticationListener.onSuccess(authenticationToken, simpleAuthenticationInfo); + + // grab the latest log output and make sure it is in line with what is expected + final List<LoggingEvent> loggingEvents = TestAppender.getCurrentInstance().getEvents(); + // the latest logging event is the one we need to inspect + final int whichLoggingEvent = loggingEvents.size() - 1; + final LoggingEvent latestLoggingEvent = loggingEvents.get(whichLoggingEvent); + final String latestLogMessage = latestLoggingEvent.getMessage(); + assertEquals("Successful authentication attempt by successfulUser1 from successfulHost1", + latestLogMessage); + } + + @Test + public void testOnFailure() throws Exception { + // variables for an unsucessful authentication attempt + final AuthenticationListener authenticationListener = new AuthenticationListener(); + final UsernamePasswordToken authenticationToken = new UsernamePasswordToken(); + authenticationToken.setUsername("unsuccessfulUser1"); + authenticationToken.setHost("unsuccessfulHost1"); + final AuthenticationException authenticationException = + new AuthenticationException("test auth exception"); + // produces unsuccessful authentication attempt output + authenticationListener.onFailure(authenticationToken, authenticationException); + + // grab the latest log output and ensure it is in line with what is expected + final List<LoggingEvent> loggingEvents = TestAppender.getCurrentInstance().getEvents(); + final int whichLoggingEvent = loggingEvents.size() - 1; + final LoggingEvent latestLoggingEvent = loggingEvents.get(whichLoggingEvent); + final String latestLogMessage = latestLoggingEvent.getMessage(); + assertEquals("Unsuccessful authentication attempt by unsuccessfulUser1 from unsuccessfulHost1", + latestLogMessage); + } +}
\ No newline at end of file diff --git a/odl-aaa-moon/aaa/aaa-shiro/src/test/java/org/opendaylight/aaa/shiro/filters/AuthenticationTokenUtilsTest.java b/odl-aaa-moon/aaa/aaa-shiro/src/test/java/org/opendaylight/aaa/shiro/filters/AuthenticationTokenUtilsTest.java new file mode 100644 index 00000000..09331c52 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-shiro/src/test/java/org/opendaylight/aaa/shiro/filters/AuthenticationTokenUtilsTest.java @@ -0,0 +1,124 @@ +/* + * 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.shiro.filters; + +import static org.junit.Assert.*; + +import org.apache.shiro.authc.AuthenticationToken; +import org.apache.shiro.authc.UsernamePasswordToken; +import org.junit.Test; +import org.opendaylight.aaa.shiro.filters.AuthenticationTokenUtils; + +/** + * Tests authentication token output utilities. + * + * @author Ryan Goulding (ryandgoulding@gmail.com) + */ +public class AuthenticationTokenUtilsTest { + + /** + * A sample non-UsernamePasswordToken implementation for testing. + */ + private final class NotUsernamePasswordToken implements AuthenticationToken { + + @Override + public Object getPrincipal() { + return null; + } + + @Override + public Object getCredentials() { + return null; + } + } + + @Test + public void testIsUsernamePasswordToken() throws Exception { + // null test + final AuthenticationToken nullUsernamePasswordToken = null; + assertFalse(AuthenticationTokenUtils.isUsernamePasswordToken(nullUsernamePasswordToken)); + + // alternate implementation of AuthenticationToken + final AuthenticationToken notUsernamePasswordToken = new NotUsernamePasswordToken(); + assertFalse(AuthenticationTokenUtils.isUsernamePasswordToken(notUsernamePasswordToken)); + + // positive test case + final AuthenticationToken positiveUsernamePasswordToken = new UsernamePasswordToken(); + assertTrue(AuthenticationTokenUtils.isUsernamePasswordToken(positiveUsernamePasswordToken)); + + } + + @Test + public void testExtractUsername() throws Exception { + // null test + final AuthenticationToken nullAuthenticationToken = null; + assertEquals(AuthenticationTokenUtils.DEFAULT_TOKEN, + AuthenticationTokenUtils.extractUsername(nullAuthenticationToken)); + + // non-UsernamePasswordToken test + final AuthenticationToken notUsernamePasswordToken = new NotUsernamePasswordToken(); + assertEquals(AuthenticationTokenUtils.DEFAULT_TOKEN, + AuthenticationTokenUtils.extractUsername(notUsernamePasswordToken)); + + // null username test + final UsernamePasswordToken nullUsername = new UsernamePasswordToken(); + nullUsername.setUsername(null); + assertEquals(AuthenticationTokenUtils.DEFAULT_USERNAME, + AuthenticationTokenUtils.extractUsername(nullUsername)); + + // positive test + final UsernamePasswordToken positiveUsernamePasswordToken = new UsernamePasswordToken(); + final String testUsername = "testUser1"; + positiveUsernamePasswordToken.setUsername(testUsername); + assertEquals(testUsername, AuthenticationTokenUtils.extractUsername(positiveUsernamePasswordToken)); + } + + @Test + public void testExtractHostname() throws Exception { + // null test + final AuthenticationToken nullAuthenticationToken = null; + assertEquals(AuthenticationTokenUtils.DEFAULT_HOSTNAME, + AuthenticationTokenUtils.extractHostname(nullAuthenticationToken)); + + // non-UsernamePasswordToken test + final AuthenticationToken notUsernamePasswordToken = new NotUsernamePasswordToken(); + assertEquals(AuthenticationTokenUtils.DEFAULT_HOSTNAME, + AuthenticationTokenUtils.extractHostname(notUsernamePasswordToken)); + + // null hostname test + final UsernamePasswordToken nullHostname = new UsernamePasswordToken(); + nullHostname.setHost(null); + assertEquals(AuthenticationTokenUtils.DEFAULT_HOSTNAME, + AuthenticationTokenUtils.extractHostname(nullHostname)); + + // positive test + final UsernamePasswordToken positiveUsernamePasswordToken = new UsernamePasswordToken(); + final String testUsername = "testHostname1"; + positiveUsernamePasswordToken.setHost(testUsername); + assertEquals(testUsername, AuthenticationTokenUtils.extractHostname(positiveUsernamePasswordToken)); + } + + @Test + public void testGenerateUnsuccessfulAuthenticationMessage() throws Exception { + final UsernamePasswordToken unsuccessfulToken = new UsernamePasswordToken(); + unsuccessfulToken.setUsername("unsuccessfulUser1"); + unsuccessfulToken.setHost("unsuccessfulHost1"); + assertEquals("Unsuccessful authentication attempt by unsuccessfulUser1 from unsuccessfulHost1", + AuthenticationTokenUtils.generateUnsuccessfulAuthenticationMessage(unsuccessfulToken)); + } + + @Test + public void testGenerateSuccessfulAuthenticationMessage() throws Exception { + final UsernamePasswordToken successfulToken = new UsernamePasswordToken(); + successfulToken.setUsername("successfulUser1"); + successfulToken.setHost("successfulHost1"); + assertEquals("Successful authentication attempt by successfulUser1 from successfulHost1", + AuthenticationTokenUtils.generateSuccessfulAuthenticationMessage(successfulToken)); + } +} diff --git a/odl-aaa-moon/aaa/aaa-shiro/src/test/java/org/opendaylight/aaa/shiro/realm/ODLJndiLdapRealmTest.java b/odl-aaa-moon/aaa/aaa-shiro/src/test/java/org/opendaylight/aaa/shiro/realm/ODLJndiLdapRealmTest.java new file mode 100644 index 00000000..22ce203f --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-shiro/src/test/java/org/opendaylight/aaa/shiro/realm/ODLJndiLdapRealmTest.java @@ -0,0 +1,246 @@ +/* + * 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 + */ + +package org.opendaylight.aaa.shiro.realm; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Set; +import java.util.Vector; +import javax.naming.NamingEnumeration; +import javax.naming.NamingException; +import javax.naming.directory.BasicAttributes; +import javax.naming.directory.SearchControls; +import javax.naming.directory.SearchResult; +import javax.naming.ldap.LdapContext; +import org.apache.shiro.authc.AuthenticationToken; +import org.apache.shiro.authc.UsernamePasswordToken; +import org.apache.shiro.authz.AuthorizationInfo; +import org.apache.shiro.realm.ldap.LdapContextFactory; +import org.apache.shiro.subject.PrincipalCollection; +import org.junit.Test; + +/** + * @author Ryan Goulding (ryandgoulding@gmail.com) + */ +public class ODLJndiLdapRealmTest { + + /** + * throw-away anonymous test class + */ + class TestNamingEnumeration implements NamingEnumeration<SearchResult> { + + /** + * state variable + */ + boolean first = true; + + /** + * returned the first time <code>next()</code> or + * <code>nextElement()</code> is called. + */ + SearchResult searchResult = new SearchResult("testuser", null, new BasicAttributes( + "objectClass", "engineering")); + + /** + * returns true the first time, then false for subsequent calls + */ + @Override + public boolean hasMoreElements() { + return first; + } + + /** + * returns <code>searchResult</code> then null for subsequent calls + */ + @Override + public SearchResult nextElement() { + if (first) { + first = false; + return searchResult; + } + return null; + } + + /** + * does nothing because close() doesn't require any special behavior + */ + @Override + public void close() throws NamingException { + } + + /** + * returns true the first time, then false for subsequent calls + */ + @Override + public boolean hasMore() throws NamingException { + return first; + } + + /** + * returns <code>searchResult</code> then null for subsequent calls + */ + @Override + public SearchResult next() throws NamingException { + if (first) { + first = false; + return searchResult; + } + return null; + } + }; + + /** + * throw away test class + * + * @author ryan + */ + class TestPrincipalCollection implements PrincipalCollection { + /** + * + */ + private static final long serialVersionUID = -1236759619455574475L; + + Vector<String> collection = new Vector<String>(); + + public TestPrincipalCollection(String element) { + collection.add(element); + } + + @Override + public Iterator<String> iterator() { + return collection.iterator(); + } + + @Override + public List<String> asList() { + return collection; + } + + @Override + public Set<String> asSet() { + HashSet<String> set = new HashSet<String>(); + set.addAll(collection); + return set; + } + + @Override + public <T> Collection<T> byType(Class<T> arg0) { + return null; + } + + @Override + public Collection<String> fromRealm(String arg0) { + return collection; + } + + @Override + public Object getPrimaryPrincipal() { + return collection.firstElement(); + } + + @Override + public Set<String> getRealmNames() { + return null; + } + + @Override + public boolean isEmpty() { + return collection.isEmpty(); + } + + @Override + public <T> T oneByType(Class<T> arg0) { + // TODO Auto-generated method stub + return null; + } + }; + + @Test + public void testGetUsernameAuthenticationToken() { + AuthenticationToken authenticationToken = null; + assertNull(ODLJndiLdapRealm.getUsername(authenticationToken)); + AuthenticationToken validAuthenticationToken = new UsernamePasswordToken("test", + "testpassword"); + assertEquals("test", ODLJndiLdapRealm.getUsername(validAuthenticationToken)); + } + + @Test + public void testGetUsernamePrincipalCollection() { + PrincipalCollection pc = null; + assertNull(new ODLJndiLdapRealm().getUsername(pc)); + TestPrincipalCollection tpc = new TestPrincipalCollection("testuser"); + String username = new ODLJndiLdapRealm().getUsername(tpc); + assertEquals("testuser", username); + } + + @Test + public void testQueryForAuthorizationInfoPrincipalCollectionLdapContextFactory() + throws NamingException { + LdapContext ldapContext = mock(LdapContext.class); + // emulates an ldap search and returns the mocked up test class + when( + ldapContext.search((String) any(), (String) any(), + (SearchControls) any())).thenReturn(new TestNamingEnumeration()); + LdapContextFactory ldapContextFactory = mock(LdapContextFactory.class); + when(ldapContextFactory.getSystemLdapContext()).thenReturn(ldapContext); + AuthorizationInfo authorizationInfo = new ODLJndiLdapRealm().queryForAuthorizationInfo( + new TestPrincipalCollection("testuser"), ldapContextFactory); + assertNotNull(authorizationInfo); + assertFalse(authorizationInfo.getRoles().isEmpty()); + assertTrue(authorizationInfo.getRoles().contains("engineering")); + } + + @Test + public void testBuildAuthorizationInfo() { + assertNull(ODLJndiLdapRealm.buildAuthorizationInfo(null)); + Set<String> roleNames = new HashSet<String>(); + roleNames.add("engineering"); + AuthorizationInfo authorizationInfo = ODLJndiLdapRealm.buildAuthorizationInfo(roleNames); + assertNotNull(authorizationInfo); + assertFalse(authorizationInfo.getRoles().isEmpty()); + assertTrue(authorizationInfo.getRoles().contains("engineering")); + } + + @Test + public void testGetRoleNamesForUser() throws NamingException { + ODLJndiLdapRealm ldapRealm = new ODLJndiLdapRealm(); + LdapContext ldapContext = mock(LdapContext.class); + + // emulates an ldap search and returns the mocked up test class + when( + ldapContext.search((String) any(), (String) any(), + (SearchControls) any())).thenReturn(new TestNamingEnumeration()); + + // extracts the roles for "testuser" and ensures engineering is returned + Set<String> roles = ldapRealm.getRoleNamesForUser("testuser", ldapContext); + assertFalse(roles.isEmpty()); + assertTrue(roles.iterator().next().equals("engineering")); + } + + @Test + public void testCreateSearchControls() { + SearchControls searchControls = ODLJndiLdapRealm.createSearchControls(); + assertNotNull(searchControls); + int expectedSearchScope = SearchControls.SUBTREE_SCOPE; + int actualSearchScope = searchControls.getSearchScope(); + assertEquals(expectedSearchScope, actualSearchScope); + } + +} diff --git a/odl-aaa-moon/aaa/aaa-shiro/src/test/java/org/opendaylight/aaa/shiro/realm/TokenAuthRealmTest.java b/odl-aaa-moon/aaa/aaa-shiro/src/test/java/org/opendaylight/aaa/shiro/realm/TokenAuthRealmTest.java new file mode 100644 index 00000000..f2eb92b5 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-shiro/src/test/java/org/opendaylight/aaa/shiro/realm/TokenAuthRealmTest.java @@ -0,0 +1,139 @@ +/* + * 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 + */ + +package org.opendaylight.aaa.shiro.realm; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.google.common.collect.Lists; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.apache.shiro.authc.AuthenticationToken; +import org.junit.Test; + +/** + * + * @author Ryan Goulding (ryandgoulding@gmail.com) + * + */ +public class TokenAuthRealmTest extends TokenAuthRealm { + + private TokenAuthRealm testRealm = new TokenAuthRealm(); + + @Test + public void testTokenAuthRealm() { + assertEquals("TokenAuthRealm", testRealm.getName()); + } + + @Test(expected = NullPointerException.class) + public void testDoGetAuthorizationInfoPrincipalCollectionNullCacheToken() { + testRealm.doGetAuthorizationInfo(null); + } + + @Test + public void testGetUsernamePasswordDomainString() { + final String username = "user"; + final String password = "password"; + final String domain = "domain"; + final String expectedUsernamePasswordString = "user:password:domain"; + assertEquals(expectedUsernamePasswordString, getUsernamePasswordDomainString(username, password, domain)); + } + + @Test + public void testGetEncodedToken() { + final String stringToEncode = "admin1:admin1"; + final byte[] bytesToEncode = stringToEncode.getBytes(); + final String expectedToken = org.apache.shiro.codec.Base64.encodeToString(bytesToEncode); + assertEquals(expectedToken, getEncodedToken(stringToEncode)); + } + + @Test + public void testGetTokenAuthHeader() { + final String encodedCredentials = getEncodedToken(getUsernamePasswordDomainString("user1", + "password", "sdn")); + final String expectedTokenAuthHeader = "Basic " + encodedCredentials; + assertEquals(expectedTokenAuthHeader, getTokenAuthHeader(encodedCredentials)); + } + + @Test + public void testFormHeadersWithToken() { + final String authHeader = getEncodedToken(getTokenAuthHeader(getUsernamePasswordDomainString( + "user1", "password", "sdn"))); + final Map<String, List<String>> expectedHeaders = new HashMap<String, List<String>>(); + expectedHeaders.put("Authorization", Lists.newArrayList(authHeader)); + final Map<String, List<String>> actualHeaders = formHeadersWithToken(authHeader); + List<String> value; + for (String key : expectedHeaders.keySet()) { + value = expectedHeaders.get(key); + assertTrue(actualHeaders.get(key).equals(value)); + } + } + + @Test + public void testFormHeaders() { + final String username = "basicUser"; + final String password = "basicPassword"; + final String domain = "basicDomain"; + final String authHeader = getTokenAuthHeader(getEncodedToken(getUsernamePasswordDomainString( + username, password, domain))); + final Map<String, List<String>> expectedHeaders = new HashMap<String, List<String>>(); + expectedHeaders.put("Authorization", Lists.newArrayList(authHeader)); + final Map<String, List<String>> actualHeaders = formHeaders(username, password, domain); + List<String> value; + for (String key : expectedHeaders.keySet()) { + value = expectedHeaders.get(key); + assertTrue(actualHeaders.get(key).equals(value)); + } + } + + @Test + public void testIsTokenAuthAvailable() { + assertFalse(testRealm.isTokenAuthAvailable()); + } + + @Test(expected = org.apache.shiro.authc.AuthenticationException.class) + public void testDoGetAuthenticationInfoAuthenticationToken() { + testRealm.doGetAuthenticationInfo(null); + } + + @Test + public void testExtractUsernameNullUsername() { + AuthenticationToken at = mock(AuthenticationToken.class); + when(at.getPrincipal()).thenReturn(null); + assertNull(extractUsername(at)); + } + + @Test(expected = ClassCastException.class) + public void testExtractPasswordNullPassword() { + AuthenticationToken at = mock(AuthenticationToken.class); + when(at.getPrincipal()).thenReturn("username"); + when(at.getCredentials()).thenReturn(null); + extractPassword(at); + } + + @Test(expected = ClassCastException.class) + public void testExtractUsernameBadUsernameClass() { + AuthenticationToken at = mock(AuthenticationToken.class); + when(at.getPrincipal()).thenReturn(new Integer(1)); + extractUsername(at); + } + + @Test(expected = ClassCastException.class) + public void testExtractPasswordBadPasswordClass() { + AuthenticationToken at = mock(AuthenticationToken.class); + when(at.getPrincipal()).thenReturn("username"); + when(at.getCredentials()).thenReturn(new Integer(1)); + extractPassword(at); + } +} diff --git a/odl-aaa-moon/aaa/aaa-shiro/src/test/java/org/opendaylight/aaa/shiro/web/env/KarafIniWebEnvironmentTest.java b/odl-aaa-moon/aaa/aaa-shiro/src/test/java/org/opendaylight/aaa/shiro/web/env/KarafIniWebEnvironmentTest.java new file mode 100644 index 00000000..141d0ce5 --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-shiro/src/test/java/org/opendaylight/aaa/shiro/web/env/KarafIniWebEnvironmentTest.java @@ -0,0 +1,76 @@ +/* + * 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 + */ + +package org.opendaylight.aaa.shiro.web.env; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import org.apache.shiro.config.Ini; +import org.apache.shiro.config.Ini.Section; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; + +/** + * @author Ryan Goulding (ryandgoulding@gmail.com) + */ +public class KarafIniWebEnvironmentTest { + private static File iniFile; + + @BeforeClass + public static void setup() throws IOException { + iniFile = createShiroIniFile(); + assertTrue(iniFile.exists()); + } + + @AfterClass + public static void teardown() { + iniFile.delete(); + } + + private static String createFakeShiroIniContents() { + return "[users]\n" + "admin=admin, ROLE_ADMIN \n" + "[roles]\n" + "ROLE_ADMIN = *\n" + + "[urls]\n" + "/** = authcBasic"; + } + + private static File createShiroIniFile() throws IOException { + File shiroIni = File.createTempFile("shiro", "ini"); + FileWriter writer = new FileWriter(shiroIni); + writer.write(createFakeShiroIniContents()); + writer.flush(); + writer.close(); + return shiroIni; + } + + @Test + public void testCreateShiroIni() throws IOException { + Ini ini = KarafIniWebEnvironment.createShiroIni(iniFile.getAbsolutePath()); + assertNotNull(ini); + assertNotNull(ini.getSection("users")); + assertNotNull(ini.getSection("roles")); + assertNotNull(ini.getSection("urls")); + Section usersSection = ini.getSection("users"); + assertTrue(usersSection.containsKey("admin")); + assertTrue(usersSection.get("admin").contains("admin")); + assertTrue(usersSection.get("admin").contains("ROLE_ADMIN")); + } + + @Test + public void testCreateFileBasedIniPath() { + String testPath = "/shiro.ini"; + String expectedFileBasedIniPath = KarafIniWebEnvironment.SHIRO_FILE_PREFIX + testPath; + String actualFileBasedIniPath = KarafIniWebEnvironment.createFileBasedIniPath(testPath); + assertEquals(expectedFileBasedIniPath, actualFileBasedIniPath); + } + +} diff --git a/odl-aaa-moon/aaa/aaa-shiro/src/test/resources/logback-test.xml b/odl-aaa-moon/aaa/aaa-shiro/src/test/resources/logback-test.xml new file mode 100644 index 00000000..68ceeabc --- /dev/null +++ b/odl-aaa-moon/aaa/aaa-shiro/src/test/resources/logback-test.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<configuration> + + <appender name="TEST-APPENDER" class="org.opendaylight.aaa.shiro.TestAppender"> + <layout class="ch.qos.logback.classic.PatternLayout"> + <Pattern> + %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n + </Pattern> + </layout> + </appender> + + <logger name="org.opendaylight.aaa.shiro.authc" level="debug" + additivity="false"> + <appender-ref ref="TEST-APPENDER" /> + </logger> + + <root level="debug"> + <appender-ref ref="TEST-APPENDER" /> + </root> + +</configuration> |