7d0bafd79a75484fd4700c617a08108318b3c992
[moon.git] /
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.LinkedHashSet;
13 import java.util.Set;
14
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;
22
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;
36
37 /**
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>:
42  *
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
49  *# list below:
50  * securityManager.realms = $tokenAuthRealm, $ldapRealm</code>
51  *
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>
57  *
58  * @author Ryan Goulding (ryandgoulding@gmail.com)
59  * @see <code>org.apache.shiro.realm.ldap.JndiLdapRealm</code>
60  * @see <a
61  *      href="https://shiro.apache.org/static/1.2.3/apidocs/org/apache/shiro/realm/ldap/JndiLdapRealm.html">Shiro
62  *      documentation</a>
63  */
64 public class ODLJndiLdapRealm extends JndiLdapRealm implements Nameable {
65
66     private static final Logger LOG = LoggerFactory.getLogger(ODLJndiLdapRealm.class);
67
68     /**
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>.
75      */
76     private static final String DEFAULT_LDAP_ATTRIBUTE_FOR_COMPARISON = "objectClass";
77
78     /**
79      * The LDAP nomenclature for user ID, which is used in the authorization query process.
80      */
81     private static final String UID = "uid";
82
83     /**
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>.
87      */
88     private String searchBase = super.getUserDnSuffix();
89
90     /**
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>.
95      */
96     private String ldapAttributeForComparison = DEFAULT_LDAP_ATTRIBUTE_FOR_COMPARISON;
97
98     /*
99      * Adds debugging information surrounding creation of ODLJndiLdapRealm
100      */
101     public ODLJndiLdapRealm() {
102         super();
103         final String DEBUG_MESSAGE = "Creating ODLJndiLdapRealm";
104         LOG.debug(DEBUG_MESSAGE);
105     }
106
107     /*
108      * (non-Javadoc) Overridden to expose important audit trail information for
109      * accounting.
110      *
111      * @see
112      * org.apache.shiro.realm.ldap.JndiLdapRealm#doGetAuthenticationInfo(org
113      * .apache.shiro.authc.AuthenticationToken)
114      */
115     @Override
116     protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)
117             throws AuthenticationException {
118
119         // Delegates all AuthN lookup responsibility to the super class
120         try {
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);
126         }
127         return null;
128     }
129
130     /**
131      * Logs an incoming LDAP connection
132      *
133      * @param username
134      *            the requesting user
135      */
136     protected void logIncomingConnection(final String username) {
137         LOG.info("AAA LDAP connection from {}", username);
138         Accounter.output("AAA LDAP connection from " + username);
139     }
140
141     /**
142      * Extracts the username from <code>token</code>
143      *
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
148      *             certificate)
149      */
150     public static String getUsername(AuthenticationToken token) throws ClassCastException {
151         if (null == token) {
152             return null;
153         }
154         return (String) token.getPrincipal();
155     }
156
157     @Override
158     protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
159
160         AuthorizationInfo ai = null;
161         try {
162             ai = this.queryForAuthorizationInfo(principals, getContextFactory());
163         } catch (NamingException e) {
164             LOG.error("Unable to query for AuthZ info", e);
165         }
166         return ai;
167     }
168
169     /**
170      * extracts a username from <code>principals</code>
171      *
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)
177      */
178     protected String getUsername(final PrincipalCollection principals) throws ClassCastException {
179
180         if (null == principals) {
181             return null;
182         }
183         return (String) getAvailablePrincipal(principals);
184     }
185
186     /*
187      * (non-Javadoc)
188      *
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.,
192      *
193      * <code>/** = authcBasic, roles[person]</code>
194      *
195      * @see org.apache.shiro.realm.ldap.JndiLdapRealm#queryForAuthorizationInfo(org.apache.shiro.subject.PrincipalCollection, org.apache.shiro.realm.ldap.LdapContextFactory)
196      */
197     @Override
198     protected AuthorizationInfo queryForAuthorizationInfo(PrincipalCollection principals,
199             LdapContextFactory ldapContextFactory) throws NamingException {
200
201         AuthorizationInfo authorizationInfo = null;
202         try {
203             final String username = getUsername(principals);
204             final LdapContext ldapContext = ldapContextFactory.getSystemLdapContext();
205             final Set<String> roleNames;
206
207             try {
208                 roleNames = getRoleNamesForUser(username, ldapContext);
209                 authorizationInfo = buildAuthorizationInfo(roleNames);
210             } finally {
211                 LdapUtils.closeContext(ldapContext);
212             }
213         } catch (ClassCastException e) {
214             LOG.error("Unable to extract a valid user", e);
215         }
216         return authorizationInfo;
217     }
218
219     public static AuthorizationInfo buildAuthorizationInfo(final Set<String> roleNames) {
220         if (null == roleNames) {
221             return null;
222         }
223         return new SimpleAuthorizationInfo(roleNames);
224     }
225
226     /**
227      * extracts the Set of roles associated with a user based on the username
228      * and ldap context (server).
229      *
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
234      */
235     protected Set<String> getRoleNamesForUser(final String username, final LdapContext ldapContext)
236             throws NamingException {
237
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>();
241
242         final SearchControls searchControls = createSearchControls();
243
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);
249
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();
254             if (attrs != null) {
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);
270                             }
271                         }
272                         roleNames.addAll(roleNamesFromLdapGroups);
273                     }
274                 }
275             }
276         }
277         return roleNames;
278     }
279
280     /**
281      * A utility method to help create the search controls for the LDAP lookup
282      *
283      * @return A generic set of search controls for LDAP scoped to subtree
284      */
285     protected static SearchControls createSearchControls() {
286         SearchControls searchControls = new SearchControls();
287         searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE);
288         return searchControls;
289     }
290
291     @Override
292     public String getUserDnSuffix() {
293         return super.getUserDnSuffix();
294     }
295
296     /**
297      * Injected from <code>shiro.ini</code> configuration.
298      *
299      * @param searchBase The desired value for searchBase
300      */
301     public void setSearchBase(final String searchBase) {
302         // public for injection reasons
303         this.searchBase = searchBase;
304     }
305
306     /**
307      * Injected from <code>shiro.ini</code> configuration.
308      *
309      * @param ldapAttributeForComparison The attribute from which groups are extracted
310      */
311     public void setLdapAttributeForComparison(final String ldapAttributeForComparison) {
312         // public for injection reasons
313         this.ldapAttributeForComparison = ldapAttributeForComparison;
314     }
315 }