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.HashSet;
13 import java.util.LinkedHashSet;
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;
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;
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>:
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
54 * securityManager.realms = $tokenAuthRealm, $ldapRealm</code>
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>
62 * @author Ryan Goulding (ryandgoulding@gmail.com)
63 * @see <code>org.apache.shiro.realm.ldap.JndiLdapRealm</code>
65 * href="https://shiro.apache.org/static/1.2.3/apidocs/org/apache/shiro/realm/ldap/JndiLdapRealm.html">Shiro
68 public class ODLJndiLdapRealm extends JndiLdapRealm implements Nameable {
70 private static final Logger LOG = LoggerFactory.getLogger(ODLJndiLdapRealm.class);
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>.
80 private static final String DEFAULT_LDAP_ATTRIBUTE_FOR_COMPARISON = "objectClass";
83 * The LDAP nomenclature for user ID, which is used in the authorization query process.
85 private static final String UID = "uid";
88 * When multiple roles are specified in groupRolesMap, this delimiter separates the individual roles.
90 private static final String ROLE_NAMES_DELIMITER = ",";
93 * Strategy to determine how groups are mapped to roles.
95 private static final GroupsToRolesMappingStrategy GROUPS_TO_ROLES_MAPPING_STRATEGY =
96 new BestAttemptGroupToRolesMappingStrategy();
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>.
103 private String searchBase = super.getUserDnSuffix();
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>.
111 private String ldapAttributeForComparison = DEFAULT_LDAP_ATTRIBUTE_FOR_COMPARISON;
113 private Map<String, String> groupRolesMap;
116 * Adds debugging information surrounding creation of ODLJndiLdapRealm
118 public ODLJndiLdapRealm() {
119 LOG.debug("Creating ODLJndiLdapRealm");
123 * (non-Javadoc) Overridden to expose important audit trail information for
127 * org.apache.shiro.realm.ldap.JndiLdapRealm#doGetAuthenticationInfo(org
128 * .apache.shiro.authc.AuthenticationToken)
131 protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)
132 throws AuthenticationException {
134 // Delegates all AuthN lookup responsibility to the super class
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);
146 * Logs an incoming LDAP connection
149 * the requesting user
151 protected void logIncomingConnection(final String username) {
152 LOG.info("AAA LDAP connection from {}", username);
153 Accounter.output("AAA LDAP connection from " + username);
157 * Extracts the username from <code>token</code>
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
165 public static String getUsername(AuthenticationToken token) throws ClassCastException {
169 return (String) token.getPrincipal();
173 protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
175 AuthorizationInfo ai = null;
177 ai = this.queryForAuthorizationInfo(principals, getContextFactory());
178 } catch (NamingException e) {
179 LOG.error("Unable to query for AuthZ info", e);
185 * extracts a username from <code>principals</code>
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)
193 protected String getUsername(final PrincipalCollection principals) throws ClassCastException {
195 if (null == principals) {
198 return (String) getAvailablePrincipal(principals);
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.,
208 * <code>/** = authcBasic, roles[person]</code>
210 * @see org.apache.shiro.realm.ldap.JndiLdapRealm#queryForAuthorizationInfo(org.apache.shiro.subject.PrincipalCollection, org.apache.shiro.realm.ldap.LdapContextFactory)
213 protected AuthorizationInfo queryForAuthorizationInfo(PrincipalCollection principals,
214 LdapContextFactory ldapContextFactory) throws NamingException {
216 AuthorizationInfo authorizationInfo = null;
218 final String username = getUsername(principals);
219 final LdapContext ldapContext = ldapContextFactory.getSystemLdapContext();
220 final Set<String> roleNames;
223 roleNames = getRoleNamesForUser(username, ldapContext);
224 authorizationInfo = buildAuthorizationInfo(roleNames);
226 LdapUtils.closeContext(ldapContext);
228 } catch (ClassCastException e) {
229 LOG.error("Unable to extract a valid user", e);
231 return authorizationInfo;
234 public static AuthorizationInfo buildAuthorizationInfo(final Set<String> roleNames) {
235 if (null == roleNames) {
238 return new SimpleAuthorizationInfo(roleNames);
242 * extracts the Set of roles associated with a user based on the username
243 * and ldap context (server).
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
250 protected Set<String> getRoleNamesForUser(final String username, final LdapContext ldapContext)
251 throws NamingException {
253 final Set<String> roleNames = new LinkedHashSet<String>();
254 final SearchControls searchControls = createSearchControls();
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);
262 while (answer.hasMoreElements()) {
263 final SearchResult searchResult = answer.next();
264 final Attributes attrs = searchResult.getAttributes();
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);
275 final Collection<String> roleNamesFromLdapGroups;
277 if (groupRolesMap != null) {
278 roleNamesFromLdapGroups = new HashSet<>();
279 for (String rolesKey : groupsToRoles.keySet()) {
280 roleNamesFromLdapGroups.addAll(groupsToRoles.get(rolesKey));
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);
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);
300 roleNames.addAll(roleNamesFromLdapGroups);
309 * A utility method to help create the search controls for the LDAP lookup
311 * @return A generic set of search controls for LDAP scoped to subtree
313 protected static SearchControls createSearchControls() {
314 SearchControls searchControls = new SearchControls();
315 searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE);
316 return searchControls;
320 public String getUserDnSuffix() {
321 return super.getUserDnSuffix();
325 * Injected from <code>shiro.ini</code> configuration.
327 * @param searchBase The desired value for searchBase
329 public void setSearchBase(final String searchBase) {
330 // public for injection reasons
331 this.searchBase = searchBase;
335 * Injected from <code>shiro.ini</code> configuration.
337 * @param ldapAttributeForComparison The attribute from which groups are extracted
339 public void setLdapAttributeForComparison(final String ldapAttributeForComparison) {
340 // public for injection reasons
341 this.ldapAttributeForComparison = ldapAttributeForComparison;
345 * Injected from <code>shiro.ini</code> configuration.
347 * @param groupRolesMap Something like <code>"ldapAdmin":"admin,user","organizationalPerson":"user"</code>
349 public void setGroupRolesMap(final Map<String, String> groupRolesMap) {
350 this.groupRolesMap = groupRolesMap;