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 * @author Tim Veil
052 * @author Jeremy Haile
053 * @since 0.1
054 */
055 public class ActiveDirectoryRealm extends AbstractLdapRealm {
056
057 //TODO - complete JavaDoc
058
059 /*--------------------------------------------
060 | C O N S T A N T S |
061 ============================================*/
062
063 private static final Logger log = LoggerFactory.getLogger(ActiveDirectoryRealm.class);
064
065 private static final String ROLE_NAMES_DELIMETER = ",";
066
067 /*--------------------------------------------
068 | I N S T A N C E V A R I A B L E S |
069 ============================================*/
070
071 /**
072 * Mapping from fully qualified active directory
073 * group names (e.g. CN=Group,OU=Company,DC=MyDomain,DC=local)
074 * as returned by the active directory LDAP server to role names.
075 */
076 private Map<String, String> groupRolesMap;
077
078 /*--------------------------------------------
079 | C O N S T R U C T O R S |
080 ============================================*/
081
082 public void setGroupRolesMap(Map<String, String> groupRolesMap) {
083 this.groupRolesMap = groupRolesMap;
084 }
085
086 /*--------------------------------------------
087 | M E T H O D S |
088 ============================================*/
089
090
091 /**
092 * Builds an {@link AuthenticationInfo} object by querying the active directory LDAP context for the
093 * specified username. This method binds to the LDAP server using the provided username and password -
094 * which if successful, indicates that the password is correct.
095 * <p/>
096 * This method can be overridden by subclasses to query the LDAP server in a more complex way.
097 *
098 * @param token the authentication token provided by the user.
099 * @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) throws NamingException {
104
105 UsernamePasswordToken upToken = (UsernamePasswordToken) token;
106
107 // Binds using the username and password provided by the user.
108 LdapContext ctx = null;
109 try {
110 ctx = ldapContextFactory.getLdapContext(upToken.getUsername(), String.valueOf(upToken.getPassword()));
111 } finally {
112 LdapUtils.closeContext(ctx);
113 }
114
115 return buildAuthenticationInfo(upToken.getUsername(), upToken.getPassword());
116 }
117
118 protected AuthenticationInfo buildAuthenticationInfo(String username, char[] password) {
119 return new SimpleAuthenticationInfo(username, password, getName());
120 }
121
122
123 /**
124 * Builds an {@link org.apache.shiro.authz.AuthorizationInfo} object by querying the active directory LDAP context for the
125 * groups that a user is a member of. The groups are then translated to role names by using the
126 * configured {@link #groupRolesMap}.
127 * <p/>
128 * This implementation expects the <tt>principal</tt> argument to be a String username.
129 * <p/>
130 * Subclasses can override this method to determine authorization data (roles, permissions, etc) in a more
131 * complex way. Note that this default implementation does not support permissions, only roles.
132 *
133 * @param principals the principal of the Subject whose account is being retrieved.
134 * @param ldapContextFactory the factory used to create LDAP connections.
135 * @return the AuthorizationInfo for the given Subject principal.
136 * @throws NamingException if an error occurs when searching the LDAP server.
137 */
138 protected AuthorizationInfo queryForAuthorizationInfo(PrincipalCollection principals, LdapContextFactory ldapContextFactory) throws NamingException {
139
140 String username = (String) getAvailablePrincipal(principals);
141
142 // Perform context search
143 LdapContext ldapContext = ldapContextFactory.getSystemLdapContext();
144
145 Set<String> roleNames;
146
147 try {
148 roleNames = getRoleNamesForUser(username, ldapContext);
149 } finally {
150 LdapUtils.closeContext(ldapContext);
151 }
152
153 return buildAuthorizationInfo(roleNames);
154 }
155
156 protected AuthorizationInfo buildAuthorizationInfo(Set<String> roleNames) {
157 return new SimpleAuthorizationInfo(roleNames);
158 }
159
160 private Set<String> getRoleNamesForUser(String username, LdapContext ldapContext) throws NamingException {
161 Set<String> roleNames;
162 roleNames = new LinkedHashSet<String>();
163
164 SearchControls searchCtls = new SearchControls();
165 searchCtls.setSearchScope(SearchControls.SUBTREE_SCOPE);
166
167 String userPrincipalName = username;
168 if (principalSuffix != null) {
169 userPrincipalName += principalSuffix;
170 }
171
172 //SHIRO-115 - prevent potential code injection:
173 String searchFilter = "(&(objectClass=*)(userPrincipalName={0}))";
174 Object[] searchArguments = new Object[]{userPrincipalName};
175
176 NamingEnumeration answer = ldapContext.search(searchBase, searchFilter, searchArguments, searchCtls);
177
178 while (answer.hasMoreElements()) {
179 SearchResult sr = (SearchResult) answer.next();
180
181 if (log.isDebugEnabled()) {
182 log.debug("Retrieving group names for user [" + sr.getName() + "]");
183 }
184
185 Attributes attrs = sr.getAttributes();
186
187 if (attrs != null) {
188 NamingEnumeration ae = attrs.getAll();
189 while (ae.hasMore()) {
190 Attribute attr = (Attribute) ae.next();
191
192 if (attr.getID().equals("memberOf")) {
193
194 Collection<String> groupNames = LdapUtils.getAllAttributeValues(attr);
195
196 if (log.isDebugEnabled()) {
197 log.debug("Groups found for user [" + username + "]: " + groupNames);
198 }
199
200 Collection<String> rolesForGroups = getRoleNamesForGroups(groupNames);
201 roleNames.addAll(rolesForGroups);
202 }
203 }
204 }
205 }
206 return roleNames;
207 }
208
209 /**
210 * This method is called by the default implementation to translate Active Directory group names
211 * to role names. This implementation uses the {@link #groupRolesMap} to map group names to role names.
212 *
213 * @param groupNames the group names that apply to the current user.
214 * @return a collection of roles that are implied by the given role names.
215 */
216 protected Collection<String> getRoleNamesForGroups(Collection<String> groupNames) {
217 Set<String> roleNames = new HashSet<String>(groupNames.size());
218
219 if (groupRolesMap != null) {
220 for (String groupName : groupNames) {
221 String strRoleNames = groupRolesMap.get(groupName);
222 if (strRoleNames != null) {
223 for (String roleName : strRoleNames.split(ROLE_NAMES_DELIMETER)) {
224
225 if (log.isDebugEnabled()) {
226 log.debug("User is member of group [" + groupName + "] so adding role [" + roleName + "]");
227 }
228
229 roleNames.add(roleName);
230
231 }
232 }
233 }
234 }
235 return roleNames;
236 }
237
238 }