2 * Copyright (c) 2015, 2016 Brocade Communications Systems, Inc. and others. All rights reserved.
4 * This program and the accompanying materials are made available under the
5 * terms of the Eclipse Public License v1.0 which accompanies this distribution,
6 * and is available at http://www.eclipse.org/legal/epl-v10.html
9 package org.opendaylight.aaa.shiro.realm;
11 import java.util.Collection;
12 import java.util.LinkedHashSet;
15 import javax.naming.NamingEnumeration;
16 import javax.naming.NamingException;
17 import javax.naming.directory.Attribute;
18 import javax.naming.directory.Attributes;
19 import javax.naming.directory.SearchControls;
20 import javax.naming.directory.SearchResult;
21 import javax.naming.ldap.LdapContext;
23 import org.apache.shiro.authc.AuthenticationException;
24 import org.apache.shiro.authc.AuthenticationInfo;
25 import org.apache.shiro.authc.AuthenticationToken;
26 import org.apache.shiro.authz.AuthorizationInfo;
27 import org.apache.shiro.authz.SimpleAuthorizationInfo;
28 import org.apache.shiro.realm.ldap.JndiLdapRealm;
29 import org.apache.shiro.realm.ldap.LdapContextFactory;
30 import org.apache.shiro.realm.ldap.LdapUtils;
31 import org.apache.shiro.subject.PrincipalCollection;
32 import org.apache.shiro.util.Nameable;
33 import org.opendaylight.aaa.shiro.accounting.Accounter;
34 import org.slf4j.Logger;
35 import org.slf4j.LoggerFactory;
38 * An extended implementation of
39 * <code>org.apache.shiro.realm.ldap.JndiLdapRealm</code> which includes
40 * additional Authorization capabilities. To enable this Realm, add the
41 * following to <code>shiro.ini</code>:
43 *<code>#ldapRealm = org.opendaylight.aaa.shiro.realm.ODLJndiLdapRealmAuthNOnly
44 *#ldapRealm.userDnTemplate = uid={0},ou=People,dc=DOMAIN,dc=TLD
45 *#ldapRealm.contextFactory.url = ldap://URL:389
46 *#ldapRealm.searchBase = dc=DOMAIN,dc=TLD
47 *#ldapRealm.ldapAttributeForComparison = objectClass
48 *# The CSV list of enabled realms. In order to enable a realm, add it to the
50 * securityManager.realms = $tokenAuthRealm, $ldapRealm</code>
52 * The values above are specific to the deployed LDAP domain. If the defaults
53 * are not sufficient, alternatives can be derived through enabling
54 * <code>TRACE</code> level logging. To enable <code>TRACE</code> level
55 * logging, issue the following command in the karaf shell:
56 * <code>log:set TRACE org.opendaylight.aaa.shiro.realm.ODLJndiLdapRealm</code>
58 * @author Ryan Goulding (ryandgoulding@gmail.com)
59 * @see <code>org.apache.shiro.realm.ldap.JndiLdapRealm</code>
61 * href="https://shiro.apache.org/static/1.2.3/apidocs/org/apache/shiro/realm/ldap/JndiLdapRealm.html">Shiro
64 public class ODLJndiLdapRealm extends JndiLdapRealm implements Nameable {
66 private static final Logger LOG = LoggerFactory.getLogger(ODLJndiLdapRealm.class);
69 * When an LDAP Authorization lookup is made for a user account, a list of
70 * attributes are returned. The attributes are used to determine LDAP
71 * grouping, which is equivalent to ODL role(s). The default value is
72 * set to "objectClass", which is common attribute for LDAP systems.
73 * The actual value may be configured through setting
74 * <code>ldapAttributeForComparison</code>.
76 private static final String DEFAULT_LDAP_ATTRIBUTE_FOR_COMPARISON = "objectClass";
79 * The LDAP nomenclature for user ID, which is used in the authorization query process.
81 private static final String UID = "uid";
84 * The searchBase for the ldap query, which indicates the LDAP realms to
85 * search. By default, this is set to the
86 * <code>super.getUserDnSuffix()</code>.
88 private String searchBase = super.getUserDnSuffix();
91 * When an LDAP Authorization lookup is made for a user account, a list of
92 * attributes is returned. The attributes are used to determine LDAP
93 * grouping, which is equivalent to ODL role(s). The default is set to
94 * <code>DEFAULT_LDAP_ATTRIBUTE_FOR_COMPARISON</code>.
96 private String ldapAttributeForComparison = DEFAULT_LDAP_ATTRIBUTE_FOR_COMPARISON;
99 * Adds debugging information surrounding creation of ODLJndiLdapRealm
101 public ODLJndiLdapRealm() {
103 final String DEBUG_MESSAGE = "Creating ODLJndiLdapRealm";
104 LOG.debug(DEBUG_MESSAGE);
108 * (non-Javadoc) Overridden to expose important audit trail information for
112 * org.apache.shiro.realm.ldap.JndiLdapRealm#doGetAuthenticationInfo(org
113 * .apache.shiro.authc.AuthenticationToken)
116 protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)
117 throws AuthenticationException {
119 // Delegates all AuthN lookup responsibility to the super class
121 final String username = getUsername(token);
122 logIncomingConnection(username);
123 return super.doGetAuthenticationInfo(token);
124 } catch (ClassCastException e) {
125 LOG.info("Couldn't service the LDAP connection", e);
131 * Logs an incoming LDAP connection
134 * the requesting user
136 protected void logIncomingConnection(final String username) {
137 LOG.info("AAA LDAP connection from {}", username);
138 Accounter.output("AAA LDAP connection from " + username);
142 * Extracts the username from <code>token</code>
144 * @param token Encoded token which could contain a username
145 * @return The extracted username
146 * @throws ClassCastException
147 * The incoming token is not username/password (i.e., X.509
150 public static String getUsername(AuthenticationToken token) throws ClassCastException {
154 return (String) token.getPrincipal();
158 protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
160 AuthorizationInfo ai = null;
162 ai = this.queryForAuthorizationInfo(principals, getContextFactory());
163 } catch (NamingException e) {
164 LOG.error("Unable to query for AuthZ info", e);
170 * extracts a username from <code>principals</code>
172 * @param principals A single principal extracted for the username
173 * @return The username if possible
174 * @throws ClassCastException
175 * the PrincipalCollection contains an element that is not in
176 * username/password form (i.e., X.509 certificate)
178 protected String getUsername(final PrincipalCollection principals) throws ClassCastException {
180 if (null == principals) {
183 return (String) getAvailablePrincipal(principals);
189 * This method is only called if doGetAuthenticationInfo(...) completes successfully AND
190 * the requested endpoint has an RBAC restriction. To add an RBAC restriction, edit the
191 * etc/shiro.ini file and add a url to the url section. E.g.,
193 * <code>/** = authcBasic, roles[person]</code>
195 * @see org.apache.shiro.realm.ldap.JndiLdapRealm#queryForAuthorizationInfo(org.apache.shiro.subject.PrincipalCollection, org.apache.shiro.realm.ldap.LdapContextFactory)
198 protected AuthorizationInfo queryForAuthorizationInfo(PrincipalCollection principals,
199 LdapContextFactory ldapContextFactory) throws NamingException {
201 AuthorizationInfo authorizationInfo = null;
203 final String username = getUsername(principals);
204 final LdapContext ldapContext = ldapContextFactory.getSystemLdapContext();
205 final Set<String> roleNames;
208 roleNames = getRoleNamesForUser(username, ldapContext);
209 authorizationInfo = buildAuthorizationInfo(roleNames);
211 LdapUtils.closeContext(ldapContext);
213 } catch (ClassCastException e) {
214 LOG.error("Unable to extract a valid user", e);
216 return authorizationInfo;
219 public static AuthorizationInfo buildAuthorizationInfo(final Set<String> roleNames) {
220 if (null == roleNames) {
223 return new SimpleAuthorizationInfo(roleNames);
227 * extracts the Set of roles associated with a user based on the username
228 * and ldap context (server).
230 * @param username The username for the request
231 * @param ldapContext The specific system context provided by <code>shiro.ini</code>
232 * @return A set of roles
233 * @throws NamingException If the ldap search fails
235 protected Set<String> getRoleNamesForUser(final String username, final LdapContext ldapContext)
236 throws NamingException {
238 // Stores the role names, which are equivalent to the set of group names extracted
239 // from the LDAP query.
240 final Set<String> roleNames = new LinkedHashSet<String>();
242 final SearchControls searchControls = createSearchControls();
244 LOG.debug("Asking the configured LDAP about which groups uid=\"{}\" belongs to using "
245 + "searchBase=\"{}\" ldapAttributeForComparison=\"{}\"",
246 username, searchBase, ldapAttributeForComparison);
247 final NamingEnumeration<SearchResult> answer = ldapContext.search(searchBase,
248 String.format("%s=%s", UID, username), searchControls);
250 // Cursor based traversal over the LDAP query result
251 while (answer.hasMoreElements()) {
252 final SearchResult searchResult = answer.next();
253 final Attributes attrs = searchResult.getAttributes();
255 // Extract the attributes from the LDAP search.
256 // attrs.getAttr(String) was not chosen, since all attributes should be exposed
257 // in trace logging should the operator wish to use an alternate attribute.
258 final NamingEnumeration<? extends Attribute> ae = attrs.getAll();
259 while (ae.hasMore()) {
260 final Attribute attr = ae.next();
261 LOG.trace("LDAP returned \"{}\" attribute for \"{}\"", attr.getID(), username);
262 if (attr.getID().equals(ldapAttributeForComparison)) {
263 // Stresses the point that LDAP groups are EQUIVALENT to ODL role names
264 // TODO make this configurable via a Strategy pattern so more interesting mappings can be made
265 final Collection<String> groupNamesExtractedFromLdap = LdapUtils.getAllAttributeValues(attr);
266 final Collection<String> roleNamesFromLdapGroups = groupNamesExtractedFromLdap;
267 if (LOG.isTraceEnabled()) {
268 for (String roleName : roleNamesFromLdapGroups) {
269 LOG.trace("Mapped the \"{}\" LDAP group to ODL role for \"{}\"", roleName, username);
272 roleNames.addAll(roleNamesFromLdapGroups);
281 * A utility method to help create the search controls for the LDAP lookup
283 * @return A generic set of search controls for LDAP scoped to subtree
285 protected static SearchControls createSearchControls() {
286 SearchControls searchControls = new SearchControls();
287 searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE);
288 return searchControls;
292 public String getUserDnSuffix() {
293 return super.getUserDnSuffix();
297 * Injected from <code>shiro.ini</code> configuration.
299 * @param searchBase The desired value for searchBase
301 public void setSearchBase(final String searchBase) {
302 // public for injection reasons
303 this.searchBase = searchBase;
307 * Injected from <code>shiro.ini</code> configuration.
309 * @param ldapAttributeForComparison The attribute from which groups are extracted
311 public void setLdapAttributeForComparison(final String ldapAttributeForComparison) {
312 // public for injection reasons
313 this.ldapAttributeForComparison = ldapAttributeForComparison;