Merge "Fix the package name"
[aaa.git] / aaa-shiro / impl / src / main / java / org / opendaylight / aaa / shiro / realm / ODLJndiLdapRealm.java
1 /*
2  * Copyright (c) 2015, 2016 Brocade Communications Systems, Inc. and others.  All rights reserved.
3  *
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
7  */
8
9 package org.opendaylight.aaa.shiro.realm;
10
11 import java.util.Collection;
12 import java.util.HashSet;
13 import java.util.LinkedHashSet;
14 import java.util.Map;
15 import java.util.Set;
16
17 import javax.naming.NamingEnumeration;
18 import javax.naming.NamingException;
19 import javax.naming.directory.Attribute;
20 import javax.naming.directory.Attributes;
21 import javax.naming.directory.SearchControls;
22 import javax.naming.directory.SearchResult;
23 import javax.naming.ldap.LdapContext;
24
25 import org.apache.shiro.authc.AuthenticationException;
26 import org.apache.shiro.authc.AuthenticationInfo;
27 import org.apache.shiro.authc.AuthenticationToken;
28 import org.apache.shiro.authz.AuthorizationInfo;
29 import org.apache.shiro.authz.SimpleAuthorizationInfo;
30 import org.apache.shiro.realm.ldap.JndiLdapRealm;
31 import org.apache.shiro.realm.ldap.LdapContextFactory;
32 import org.apache.shiro.realm.ldap.LdapUtils;
33 import org.apache.shiro.subject.PrincipalCollection;
34 import org.apache.shiro.util.Nameable;
35 import org.opendaylight.aaa.shiro.accounting.Accounter;
36 import org.opendaylight.aaa.shiro.realm.mapping.api.GroupsToRolesMappingStrategy;
37 import org.opendaylight.aaa.shiro.realm.mapping.impl.BestAttemptGroupToRolesMappingStrategy;
38 import org.slf4j.Logger;
39 import org.slf4j.LoggerFactory;
40
41 /**
42  * An extended implementation of
43  * <code>org.apache.shiro.realm.ldap.JndiLdapRealm</code> which includes
44  * additional Authorization capabilities.  To enable this Realm, add the
45  * following to <code>shiro.ini</code>:
46  *
47  *<code>#ldapRealm = org.opendaylight.aaa.shiro.realm.ODLJndiLdapRealmAuthNOnly
48  *#ldapRealm.userDnTemplate = uid={0},ou=People,dc=DOMAIN,dc=TLD
49  *#ldapRealm.contextFactory.url = ldap://URL:389
50  *#ldapRealm.searchBase = dc=DOMAIN,dc=TLD
51  *#ldapRealm.ldapAttributeForComparison = objectClass
52  *# The CSV list of enabled realms.  In order to enable a realm, add it to the
53  *# list below:
54  * securityManager.realms = $tokenAuthRealm, $ldapRealm</code>
55  *
56  * The values above are specific to the deployed LDAP domain.  If the defaults
57  * are not sufficient, alternatives can be derived through enabling
58  * <code>TRACE</code> level logging.  To enable <code>TRACE</code> level
59  * logging, issue the following command in the karaf shell:
60  * <code>log:set TRACE org.opendaylight.aaa.shiro.realm.ODLJndiLdapRealm</code>
61  *
62  * @author Ryan Goulding (ryandgoulding@gmail.com)
63  * @see <code>org.apache.shiro.realm.ldap.JndiLdapRealm</code>
64  * @see <a
65  *      href="https://shiro.apache.org/static/1.2.3/apidocs/org/apache/shiro/realm/ldap/JndiLdapRealm.html">Shiro
66  *      documentation</a>
67  */
68 public class ODLJndiLdapRealm extends JndiLdapRealm implements Nameable {
69
70     private static final Logger LOG = LoggerFactory.getLogger(ODLJndiLdapRealm.class);
71
72     /**
73      * When an LDAP Authorization lookup is made for a user account, a list of
74      * attributes are returned.  The attributes are used to determine LDAP
75      * grouping, which is equivalent to ODL role(s).  The default value is
76      * set to "objectClass", which is common attribute for LDAP systems.
77      * The actual value may be configured through setting
78      * <code>ldapAttributeForComparison</code>.
79      */
80     private static final String DEFAULT_LDAP_ATTRIBUTE_FOR_COMPARISON = "objectClass";
81
82     /**
83      * The LDAP nomenclature for user ID, which is used in the authorization query process.
84      */
85     private static final String UID = "uid";
86
87     /**
88      * When multiple roles are specified in groupRolesMap, this delimiter separates the individual roles.
89      */
90     private static final String ROLE_NAMES_DELIMITER = ",";
91
92     /**
93      * Strategy to determine how groups are mapped to roles.
94      */
95     private static final GroupsToRolesMappingStrategy GROUPS_TO_ROLES_MAPPING_STRATEGY =
96             new BestAttemptGroupToRolesMappingStrategy();
97
98     /**
99      * The searchBase for the ldap query, which indicates the LDAP realms to
100      * search.  By default, this is set to the
101      * <code>super.getUserDnSuffix()</code>.
102      */
103     private String searchBase = super.getUserDnSuffix();
104
105     /**
106      * When an LDAP Authorization lookup is made for a user account, a list of
107      * attributes is returned.  The attributes are used to determine LDAP
108      * grouping, which is equivalent to ODL role(s).  The default is set to
109      * <code>DEFAULT_LDAP_ATTRIBUTE_FOR_COMPARISON</code>.
110      */
111     private String ldapAttributeForComparison = DEFAULT_LDAP_ATTRIBUTE_FOR_COMPARISON;
112
113     private Map<String, String> groupRolesMap;
114
115     /*
116      * Adds debugging information surrounding creation of ODLJndiLdapRealm
117      */
118     public ODLJndiLdapRealm() {
119         LOG.debug("Creating ODLJndiLdapRealm");
120     }
121
122     /*
123      * (non-Javadoc) Overridden to expose important audit trail information for
124      * accounting.
125      *
126      * @see
127      * org.apache.shiro.realm.ldap.JndiLdapRealm#doGetAuthenticationInfo(org
128      * .apache.shiro.authc.AuthenticationToken)
129      */
130     @Override
131     protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)
132             throws AuthenticationException {
133
134         // Delegates all AuthN lookup responsibility to the super class
135         try {
136             final String username = getUsername(token);
137             logIncomingConnection(username);
138             return super.doGetAuthenticationInfo(token);
139         } catch (ClassCastException e) {
140             LOG.info("Couldn't service the LDAP connection", e);
141         }
142         return null;
143     }
144
145     /**
146      * Logs an incoming LDAP connection
147      *
148      * @param username
149      *            the requesting user
150      */
151     protected void logIncomingConnection(final String username) {
152         LOG.info("AAA LDAP connection from {}", username);
153         Accounter.output("AAA LDAP connection from " + username);
154     }
155
156     /**
157      * Extracts the username from <code>token</code>
158      *
159      * @param token Encoded token which could contain a username
160      * @return The extracted username
161      * @throws ClassCastException
162      *             The incoming token is not username/password (i.e., X.509
163      *             certificate)
164      */
165     public static String getUsername(AuthenticationToken token) throws ClassCastException {
166         if (null == token) {
167             return null;
168         }
169         return (String) token.getPrincipal();
170     }
171
172     @Override
173     protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
174
175         AuthorizationInfo ai = null;
176         try {
177             ai = this.queryForAuthorizationInfo(principals, getContextFactory());
178         } catch (NamingException e) {
179             LOG.error("Unable to query for AuthZ info", e);
180         }
181         return ai;
182     }
183
184     /**
185      * extracts a username from <code>principals</code>
186      *
187      * @param principals A single principal extracted for the username
188      * @return The username if possible
189      * @throws ClassCastException
190      *             the PrincipalCollection contains an element that is not in
191      *             username/password form (i.e., X.509 certificate)
192      */
193     protected String getUsername(final PrincipalCollection principals) throws ClassCastException {
194
195         if (null == principals) {
196             return null;
197         }
198         return (String) getAvailablePrincipal(principals);
199     }
200
201     /*
202      * (non-Javadoc)
203      *
204      * This method is only called if doGetAuthenticationInfo(...) completes successfully AND
205      * the requested endpoint has an RBAC restriction.  To add an RBAC restriction, edit the
206      * etc/shiro.ini file and add a url to the url section.  E.g.,
207      *
208      * <code>/** = authcBasic, roles[person]</code>
209      *
210      * @see org.apache.shiro.realm.ldap.JndiLdapRealm#queryForAuthorizationInfo(org.apache.shiro.subject.PrincipalCollection, org.apache.shiro.realm.ldap.LdapContextFactory)
211      */
212     @Override
213     protected AuthorizationInfo queryForAuthorizationInfo(PrincipalCollection principals,
214             LdapContextFactory ldapContextFactory) throws NamingException {
215
216         AuthorizationInfo authorizationInfo = null;
217         try {
218             final String username = getUsername(principals);
219             final LdapContext ldapContext = ldapContextFactory.getSystemLdapContext();
220             final Set<String> roleNames;
221
222             try {
223                 roleNames = getRoleNamesForUser(username, ldapContext);
224                 authorizationInfo = buildAuthorizationInfo(roleNames);
225             } finally {
226                 LdapUtils.closeContext(ldapContext);
227             }
228         } catch (ClassCastException e) {
229             LOG.error("Unable to extract a valid user", e);
230         }
231         return authorizationInfo;
232     }
233
234     public static AuthorizationInfo buildAuthorizationInfo(final Set<String> roleNames) {
235         if (null == roleNames) {
236             return null;
237         }
238         return new SimpleAuthorizationInfo(roleNames);
239     }
240
241     /**
242      * extracts the Set of roles associated with a user based on the username
243      * and ldap context (server).
244      *
245      * @param username The username for the request
246      * @param ldapContext The specific system context provided by <code>shiro.ini</code>
247      * @return A set of roles
248      * @throws NamingException If the ldap search fails
249      */
250     protected Set<String> getRoleNamesForUser(final String username, final LdapContext ldapContext)
251             throws NamingException {
252
253         final Set<String> roleNames = new LinkedHashSet<String>();
254         final SearchControls searchControls = createSearchControls();
255
256         LOG.debug("Asking the configured LDAP about which groups uid=\"{}\" belongs to using "
257                 + "searchBase=\"{}\" ldapAttributeForComparison=\"{}\"",
258                 username, searchBase, ldapAttributeForComparison);
259         final NamingEnumeration<SearchResult> answer = ldapContext.search(searchBase,
260                 String.format("%s=%s", UID, username), searchControls);
261
262         while (answer.hasMoreElements()) {
263             final SearchResult searchResult = answer.next();
264             final Attributes attrs = searchResult.getAttributes();
265             if (attrs != null) {
266                 final NamingEnumeration<? extends Attribute> ae = attrs.getAll();
267                 while (ae.hasMore()) {
268                     final Attribute attr = ae.next();
269                     LOG.debug("LDAP returned \"{}\" attribute for \"{}\"", attr.getID(), username);
270                     if (attr.getID().equals(ldapAttributeForComparison)) {
271                         final Collection<String> groupNamesExtractedFromLdap = LdapUtils.getAllAttributeValues(attr);
272                         final Map<String, Set<String>> groupsToRoles = this.GROUPS_TO_ROLES_MAPPING_STRATEGY.mapGroupsToRoles(
273                                 groupNamesExtractedFromLdap, ROLE_NAMES_DELIMITER, groupRolesMap);
274
275                         final Collection<String> roleNamesFromLdapGroups;
276                         // map the groups
277                         if (groupRolesMap != null) {
278                             roleNamesFromLdapGroups = new HashSet<>();
279                             for (String  rolesKey : groupsToRoles.keySet()) {
280                                 roleNamesFromLdapGroups.addAll(groupsToRoles.get(rolesKey));
281                             }
282                             if (LOG.isDebugEnabled()) {
283                                 for (String group : groupsToRoles.keySet()) {
284                                     LOG.debug("Mapped the \"{}\" LDAP group to \"{}\" ODL role for \"{}\"", group,
285                                             groupsToRoles.get(group), username);
286                                 }
287                             }
288                         } else {
289                             LOG.debug("Since groupRolesMap was unspecified, no mapping is attempted so " +
290                                     "the role names are set to the extracted group names");
291                             roleNamesFromLdapGroups = groupNamesExtractedFromLdap;
292                             if (LOG.isDebugEnabled()) {
293                                 for (String group : groupNamesExtractedFromLdap) {
294                                     LOG.debug("Mapped the \"{}\" LDAP group to \"{}\" ODL role for \"{}\"",
295                                             group, group, username);
296                                 }
297                             }
298                         }
299
300                         roleNames.addAll(roleNamesFromLdapGroups);
301                     }
302                 }
303             }
304         }
305         return roleNames;
306     }
307
308     /**
309      * A utility method to help create the search controls for the LDAP lookup
310      *
311      * @return A generic set of search controls for LDAP scoped to subtree
312      */
313     protected static SearchControls createSearchControls() {
314         SearchControls searchControls = new SearchControls();
315         searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE);
316         return searchControls;
317     }
318
319     @Override
320     public String getUserDnSuffix() {
321         return super.getUserDnSuffix();
322     }
323
324     /**
325      * Injected from <code>shiro.ini</code> configuration.
326      *
327      * @param searchBase The desired value for searchBase
328      */
329     public void setSearchBase(final String searchBase) {
330         // public for injection reasons
331         this.searchBase = searchBase;
332     }
333
334     /**
335      * Injected from <code>shiro.ini</code> configuration.
336      *
337      * @param ldapAttributeForComparison The attribute from which groups are extracted
338      */
339     public void setLdapAttributeForComparison(final String ldapAttributeForComparison) {
340         // public for injection reasons
341         this.ldapAttributeForComparison = ldapAttributeForComparison;
342     }
343
344     /**
345      * Injected from <code>shiro.ini</code> configuration.
346      *
347      * @param groupRolesMap Something like <code>"ldapAdmin":"admin,user","organizationalPerson":"user"</code>
348      */
349     public void setGroupRolesMap(final Map<String, String> groupRolesMap) {
350         this.groupRolesMap = groupRolesMap;
351     }
352 }