/* * 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 * org.apache.shiro.realm.ldap.JndiLdapRealm which includes * additional Authorization capabilities. To enable this Realm, add the * following to shiro.ini: * *#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 * * The values above are specific to the deployed LDAP domain. If the defaults * are not sufficient, alternatives can be derived through enabling * TRACE level logging. To enable TRACE level * logging, issue the following command in the karaf shell: * log:set TRACE org.opendaylight.aaa.shiro.realm.ODLJndiLdapRealm * * @author Ryan Goulding (ryandgoulding@gmail.com) * @see org.apache.shiro.realm.ldap.JndiLdapRealm * @see Shiro * documentation */ 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 * ldapAttributeForComparison. */ 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 * super.getUserDnSuffix(). */ 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 * DEFAULT_LDAP_ATTRIBUTE_FOR_COMPARISON. */ 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 token * * @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 principals * * @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., * * /** = authcBasic, roles[person] * * @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 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 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 shiro.ini * @return A set of roles * @throws NamingException If the ldap search fails */ protected Set 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 roleNames = new LinkedHashSet(); final SearchControls searchControls = createSearchControls(); LOG.debug("Asking the configured LDAP about which groups uid=\"{}\" belongs to using " + "searchBase=\"{}\" ldapAttributeForComparison=\"{}\"", username, searchBase, ldapAttributeForComparison); final NamingEnumeration 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 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 groupNamesExtractedFromLdap = LdapUtils.getAllAttributeValues(attr); final Collection 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 shiro.ini configuration. * * @param searchBase The desired value for searchBase */ public void setSearchBase(final String searchBase) { // public for injection reasons this.searchBase = searchBase; } /** * Injected from shiro.ini configuration. * * @param ldapAttributeForComparison The attribute from which groups are extracted */ public void setLdapAttributeForComparison(final String ldapAttributeForComparison) { // public for injection reasons this.ldapAttributeForComparison = ldapAttributeForComparison; } }