001 /*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements. See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership. The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License. You may obtain a copy of the License at
009 *
010 * http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing,
013 * software distributed under the License is distributed on an
014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 * KIND, either express or implied. See the License for the
016 * specific language governing permissions and limitations
017 * under the License.
018 */
019 package org.apache.shiro.realm.activedirectory;
020
021 import org.apache.shiro.authc.AuthenticationInfo;
022 import org.apache.shiro.authc.AuthenticationToken;
023 import org.apache.shiro.authc.SimpleAuthenticationInfo;
024 import org.apache.shiro.authc.UsernamePasswordToken;
025 import org.apache.shiro.authz.AuthorizationInfo;
026 import org.apache.shiro.authz.SimpleAuthorizationInfo;
027 import org.apache.shiro.realm.Realm;
028 import org.apache.shiro.realm.ldap.AbstractLdapRealm;
029 import org.apache.shiro.realm.ldap.LdapContextFactory;
030 import org.apache.shiro.realm.ldap.LdapUtils;
031 import org.apache.shiro.subject.PrincipalCollection;
032 import org.slf4j.Logger;
033 import org.slf4j.LoggerFactory;
034
035 import javax.naming.NamingEnumeration;
036 import javax.naming.NamingException;
037 import javax.naming.directory.Attribute;
038 import javax.naming.directory.Attributes;
039 import javax.naming.directory.SearchControls;
040 import javax.naming.directory.SearchResult;
041 import javax.naming.ldap.LdapContext;
042 import java.util.*;
043
044
045 /**
046 * A {@link Realm} that authenticates with an active directory LDAP
047 * server to determine the roles for a particular user. This implementation
048 * queries for the user's groups and then maps the group names to roles using the
049 * {@link #groupRolesMap}.
050 *
051 * @since 0.1
052 */
053 public class ActiveDirectoryRealm extends AbstractLdapRealm {
054
055 //TODO - complete JavaDoc
056
057 /*--------------------------------------------
058 | C O N S T A N T S |
059 ============================================*/
060
061 private static final Logger log = LoggerFactory.getLogger(ActiveDirectoryRealm.class);
062
063 private static final String ROLE_NAMES_DELIMETER = ",";
064
065 /*--------------------------------------------
066 | I N S T A N C E V A R I A B L E S |
067 ============================================*/
068
069 /**
070 * Mapping from fully qualified active directory
071 * group names (e.g. CN=Group,OU=Company,DC=MyDomain,DC=local)
072 * as returned by the active directory LDAP server to role names.
073 */
074 private Map<String, String> groupRolesMap;
075
076 /*--------------------------------------------
077 | C O N S T R U C T O R S |
078 ============================================*/
079
080 public void setGroupRolesMap(Map<String, String> groupRolesMap) {
081 this.groupRolesMap = groupRolesMap;
082 }
083
084 /*--------------------------------------------
085 | M E T H O D S |
086 ============================================*/
087
088
089 /**
090 * Builds an {@link AuthenticationInfo} object by querying the active directory LDAP context for the
091 * specified username. This method binds to the LDAP server using the provided username and password -
092 * which if successful, indicates that the password is correct.
093 * <p/>
094 * This method can be overridden by subclasses to query the LDAP server in a more complex way.
095 *
096 * @param token the authentication token provided by the user.
097 * @param ldapContextFactory the factory used to build connections to the LDAP server.
098 * @return an {@link AuthenticationInfo} instance containing information retrieved from LDAP.
099 * @throws NamingException if any LDAP errors occur during the search.
100 */
101 protected AuthenticationInfo queryForAuthenticationInfo(AuthenticationToken token, LdapContextFactory ldapContextFactory) throws NamingException {
102
103 UsernamePasswordToken upToken = (UsernamePasswordToken) token;
104
105 // Binds using the username and password provided by the user.
106 LdapContext ctx = null;
107 try {
108 ctx = ldapContextFactory.getLdapContext(upToken.getUsername(), String.valueOf(upToken.getPassword()));
109 } finally {
110 LdapUtils.closeContext(ctx);
111 }
112
113 return buildAuthenticationInfo(upToken.getUsername(), upToken.getPassword());
114 }
115
116 protected AuthenticationInfo buildAuthenticationInfo(String username, char[] password) {
117 return new SimpleAuthenticationInfo(username, password, getName());
118 }
119
120
121 /**
122 * Builds an {@link org.apache.shiro.authz.AuthorizationInfo} object by querying the active directory LDAP context for the
123 * groups that a user is a member of. The groups are then translated to role names by using the
124 * configured {@link #groupRolesMap}.
125 * <p/>
126 * This implementation expects the <tt>principal</tt> argument to be a String username.
127 * <p/>
128 * Subclasses can override this method to determine authorization data (roles, permissions, etc) in a more
129 * complex way. Note that this default implementation does not support permissions, only roles.
130 *
131 * @param principals the principal of the Subject whose account is being retrieved.
132 * @param ldapContextFactory the factory used to create LDAP connections.
133 * @return the AuthorizationInfo for the given Subject principal.
134 * @throws NamingException if an error occurs when searching the LDAP server.
135 */
136 protected AuthorizationInfo queryForAuthorizationInfo(PrincipalCollection principals, LdapContextFactory ldapContextFactory) throws NamingException {
137
138 String username = (String) getAvailablePrincipal(principals);
139
140 // Perform context search
141 LdapContext ldapContext = ldapContextFactory.getSystemLdapContext();
142
143 Set<String> roleNames;
144
145 try {
146 roleNames = getRoleNamesForUser(username, ldapContext);
147 } finally {
148 LdapUtils.closeContext(ldapContext);
149 }
150
151 return buildAuthorizationInfo(roleNames);
152 }
153
154 protected AuthorizationInfo buildAuthorizationInfo(Set<String> roleNames) {
155 return new SimpleAuthorizationInfo(roleNames);
156 }
157
158 private Set<String> getRoleNamesForUser(String username, LdapContext ldapContext) throws NamingException {
159 Set<String> roleNames;
160 roleNames = new LinkedHashSet<String>();
161
162 SearchControls searchCtls = new SearchControls();
163 searchCtls.setSearchScope(SearchControls.SUBTREE_SCOPE);
164
165 String userPrincipalName = username;
166 if (principalSuffix != null) {
167 userPrincipalName += principalSuffix;
168 }
169
170 //SHIRO-115 - prevent potential code injection:
171 String searchFilter = "(&(objectClass=*)(userPrincipalName={0}))";
172 Object[] searchArguments = new Object[]{userPrincipalName};
173
174 NamingEnumeration answer = ldapContext.search(searchBase, searchFilter, searchArguments, searchCtls);
175
176 while (answer.hasMoreElements()) {
177 SearchResult sr = (SearchResult) answer.next();
178
179 if (log.isDebugEnabled()) {
180 log.debug("Retrieving group names for user [" + sr.getName() + "]");
181 }
182
183 Attributes attrs = sr.getAttributes();
184
185 if (attrs != null) {
186 NamingEnumeration ae = attrs.getAll();
187 while (ae.hasMore()) {
188 Attribute attr = (Attribute) ae.next();
189
190 if (attr.getID().equals("memberOf")) {
191
192 Collection<String> groupNames = LdapUtils.getAllAttributeValues(attr);
193
194 if (log.isDebugEnabled()) {
195 log.debug("Groups found for user [" + username + "]: " + groupNames);
196 }
197
198 Collection<String> rolesForGroups = getRoleNamesForGroups(groupNames);
199 roleNames.addAll(rolesForGroups);
200 }
201 }
202 }
203 }
204 return roleNames;
205 }
206
207 /**
208 * This method is called by the default implementation to translate Active Directory group names
209 * to role names. This implementation uses the {@link #groupRolesMap} to map group names to role names.
210 *
211 * @param groupNames the group names that apply to the current user.
212 * @return a collection of roles that are implied by the given role names.
213 */
214 protected Collection<String> getRoleNamesForGroups(Collection<String> groupNames) {
215 Set<String> roleNames = new HashSet<String>(groupNames.size());
216
217 if (groupRolesMap != null) {
218 for (String groupName : groupNames) {
219 String strRoleNames = groupRolesMap.get(groupName);
220 if (strRoleNames != null) {
221 for (String roleName : strRoleNames.split(ROLE_NAMES_DELIMETER)) {
222
223 if (log.isDebugEnabled()) {
224 log.debug("User is member of group [" + groupName + "] so adding role [" + roleName + "]");
225 }
226
227 roleNames.add(roleName);
228
229 }
230 }
231 }
232 }
233 return roleNames;
234 }
235
236 }