View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *     http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.shiro.realm.activedirectory;
20  
21  import org.apache.shiro.authc.AuthenticationInfo;
22  import org.apache.shiro.authc.AuthenticationToken;
23  import org.apache.shiro.authc.SimpleAuthenticationInfo;
24  import org.apache.shiro.authc.UsernamePasswordToken;
25  import org.apache.shiro.authz.AuthorizationInfo;
26  import org.apache.shiro.authz.SimpleAuthorizationInfo;
27  import org.apache.shiro.realm.Realm;
28  import org.apache.shiro.realm.ldap.AbstractLdapRealm;
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.slf4j.Logger;
33  import org.slf4j.LoggerFactory;
34  
35  import javax.naming.NamingEnumeration;
36  import javax.naming.NamingException;
37  import javax.naming.directory.Attribute;
38  import javax.naming.directory.Attributes;
39  import javax.naming.directory.SearchControls;
40  import javax.naming.directory.SearchResult;
41  import javax.naming.ldap.LdapContext;
42  import java.util.Collection;
43  import java.util.HashSet;
44  import java.util.LinkedHashSet;
45  import java.util.Locale;
46  import java.util.Map;
47  import java.util.Set;
48  
49  
50  /**
51   * A {@link Realm} that authenticates with an active directory LDAP
52   * server to determine the roles for a particular user.  This implementation
53   * queries for the user's groups and then maps the group names to roles using the
54   * {@link #groupRolesMap}.
55   *
56   * @since 0.1
57   */
58  public class ActiveDirectoryRealm extends AbstractLdapRealm {
59  
60      /*--------------------------------------------
61      |             C O N S T A N T S             |
62      ============================================*/
63  
64      private static final Logger LOGGER = LoggerFactory.getLogger(ActiveDirectoryRealm.class);
65  
66      private static final String ROLE_NAMES_DELIMETER = ",";
67  
68      /*--------------------------------------------
69      |    I N S T A N C E   V A R I A B L E S    |
70      ============================================*/
71  
72      /**
73       * Mapping from fully qualified active directory
74       * group names (e.g. CN=Group,OU=Company,DC=MyDomain,DC=local)
75       * as returned by the active directory LDAP server to role names.
76       */
77      private Map<String, String> groupRolesMap;
78  
79      /*--------------------------------------------
80      |         C O N S T R U C T O R S           |
81      ============================================*/
82  
83      public void setGroupRolesMap(Map<String, String> groupRolesMap) {
84          this.groupRolesMap = groupRolesMap;
85      }
86  
87      /*--------------------------------------------
88      |               M E T H O D S               |
89      ============================================*/
90  
91      /**
92       * Builds an {@link AuthenticationInfo} object by querying the active directory LDAP context for the
93       * specified username.  This method binds to the LDAP server using the provided username and password -
94       * which, if successful, indicates that the password is correct.
95       * <p/>
96       * This method can be overridden by subclasses to query the LDAP server in a more complex way.
97       *
98       * @param token              the authentication token provided by the user.
99       * @param ldapContextFactory the factory used to build connections to the LDAP server.
100      * @return an {@link AuthenticationInfo} instance containing information retrieved from LDAP.
101      * @throws NamingException if any LDAP errors occur during the search.
102      */
103     protected AuthenticationInfo queryForAuthenticationInfo(AuthenticationToken token, LdapContextFactory ldapContextFactory)
104             throws NamingException {
105 
106         UsernamePasswordToken upToken = (UsernamePasswordToken) token;
107 
108         // Binds using the username and password provided by the user.
109         LdapContext ctx = null;
110         try {
111             ctx = ldapContextFactory.getLdapContext(upToken.getUsername(), String.valueOf(upToken.getPassword()));
112         } finally {
113             LdapUtils.closeContext(ctx);
114         }
115 
116         return buildAuthenticationInfo(upToken.getUsername(), upToken.getPassword());
117     }
118 
119     protected AuthenticationInfo buildAuthenticationInfo(String username, char[] password) {
120         return new SimpleAuthenticationInfo(username, password, getName());
121     }
122 
123 
124     /**
125      * Builds an {@link org.apache.shiro.authz.AuthorizationInfo} object by querying the active directory LDAP context for the
126      * groups that a user is a member of.  The groups are then translated to role names by using the
127      * configured {@link #groupRolesMap}.
128      * <p/>
129      * This implementation expects the <tt>principal</tt> argument to be a String username.
130      * <p/>
131      * Subclasses can override this method to determine authorization data (roles, permissions, etc.) in a more
132      * complex way.  Note that this default implementation does not support permissions, only roles.
133      *
134      * @param principals         the principal of the Subject whose account is being retrieved.
135      * @param ldapContextFactory the factory used to create LDAP connections.
136      * @return the AuthorizationInfo for the given Subject principal.
137      * @throws NamingException if an error occurs when searching the LDAP server.
138      */
139     protected AuthorizationInfo queryForAuthorizationInfo(PrincipalCollection principals,
140                                                           LdapContextFactory ldapContextFactory) throws NamingException {
141 
142         String username = (String) getAvailablePrincipal(principals);
143 
144         // Perform context search
145         LdapContext ldapContext = ldapContextFactory.getSystemLdapContext();
146 
147         Set<String> roleNames;
148 
149         try {
150             roleNames = getRoleNamesForUser(username, ldapContext);
151         } finally {
152             LdapUtils.closeContext(ldapContext);
153         }
154 
155         return buildAuthorizationInfo(roleNames);
156     }
157 
158     protected AuthorizationInfo buildAuthorizationInfo(Set<String> roleNames) {
159         return new SimpleAuthorizationInfo(roleNames);
160     }
161 
162     protected Set<String> getRoleNamesForUser(String username, LdapContext ldapContext) throws NamingException {
163         Set<String> roleNames;
164         roleNames = new LinkedHashSet<String>();
165 
166         SearchControls searchControls = new SearchControls();
167         searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE);
168 
169         String userPrincipalName = username;
170         if (principalSuffix != null
171                 && !userPrincipalName.toLowerCase(Locale.ROOT).endsWith(principalSuffix.toLowerCase(Locale.ROOT))) {
172             userPrincipalName += principalSuffix;
173         }
174 
175         Object[] searchArguments = new Object[] {userPrincipalName};
176 
177         NamingEnumeration answer = ldapContext.search(searchBase, searchFilter, searchArguments, searchControls);
178 
179         while (answer.hasMoreElements()) {
180             SearchResult sr = (SearchResult) answer.next();
181 
182             if (LOGGER.isDebugEnabled()) {
183                 LOGGER.debug("Retrieving group names for user [" + sr.getName() + "]");
184             }
185 
186             Attributes attrs = sr.getAttributes();
187 
188             if (attrs != null) {
189                 NamingEnumeration ae = attrs.getAll();
190                 while (ae.hasMore()) {
191                     Attribute attr = (Attribute) ae.next();
192 
193                     if (attr.getID().equals("memberOf")) {
194 
195                         Collection<String> groupNames = LdapUtils.getAllAttributeValues(attr);
196 
197                         if (LOGGER.isDebugEnabled()) {
198                             LOGGER.debug("Groups found for user [" + username + "]: " + groupNames);
199                         }
200 
201                         Collection<String> rolesForGroups = getRoleNamesForGroups(groupNames);
202                         roleNames.addAll(rolesForGroups);
203                     }
204                 }
205             }
206         }
207         return roleNames;
208     }
209 
210     /**
211      * This method is called by the default implementation to translate Active Directory group names
212      * to role names.  This implementation uses the {@link #groupRolesMap} to map group names to role names.
213      *
214      * @param groupNames the group names that apply to the current user.
215      * @return a collection of roles that are implied by the given role names.
216      */
217     protected Collection<String> getRoleNamesForGroups(Collection<String> groupNames) {
218         Set<String> roleNames = new HashSet<String>(groupNames.size());
219 
220         if (groupRolesMap != null) {
221             for (String groupName : groupNames) {
222                 String strRoleNames = groupRolesMap.get(groupName);
223                 if (strRoleNames != null) {
224                     for (String roleName : strRoleNames.split(ROLE_NAMES_DELIMETER)) {
225 
226                         if (LOGGER.isDebugEnabled()) {
227                             LOGGER.debug("User is member of group [" + groupName + "] so adding role [" + roleName + "]");
228                         }
229 
230                         roleNames.add(roleName);
231 
232                     }
233                 }
234             }
235         }
236         return roleNames;
237     }
238 
239 }