diff options
Diffstat (limited to 'upstream/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/realm')
6 files changed, 960 insertions, 0 deletions
diff --git a/upstream/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/realm/MoonRealm.java b/upstream/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/upstream/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/upstream/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/realm/ODLJndiLdapRealm.java b/upstream/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/upstream/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/upstream/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/realm/ODLJndiLdapRealmAuthNOnly.java b/upstream/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/upstream/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/upstream/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/realm/RadiusRealm.java b/upstream/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/upstream/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/upstream/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/realm/TACACSRealm.java b/upstream/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/upstream/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/upstream/odl-aaa-moon/aaa/aaa-shiro/src/main/java/org/opendaylight/aaa/shiro/realm/TokenAuthRealm.java b/upstream/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/upstream/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; + } + } +} |