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 */ 019package org.apache.shiro.realm.activedirectory; 020 021import org.apache.shiro.authc.AuthenticationInfo; 022import org.apache.shiro.authc.AuthenticationToken; 023import org.apache.shiro.authc.SimpleAuthenticationInfo; 024import org.apache.shiro.authc.UsernamePasswordToken; 025import org.apache.shiro.authz.AuthorizationInfo; 026import org.apache.shiro.authz.SimpleAuthorizationInfo; 027import org.apache.shiro.realm.Realm; 028import org.apache.shiro.realm.ldap.AbstractLdapRealm; 029import org.apache.shiro.realm.ldap.LdapContextFactory; 030import org.apache.shiro.realm.ldap.LdapUtils; 031import org.apache.shiro.subject.PrincipalCollection; 032import org.slf4j.Logger; 033import org.slf4j.LoggerFactory; 034 035import javax.naming.NamingEnumeration; 036import javax.naming.NamingException; 037import javax.naming.directory.Attribute; 038import javax.naming.directory.Attributes; 039import javax.naming.directory.SearchControls; 040import javax.naming.directory.SearchResult; 041import javax.naming.ldap.LdapContext; 042import java.util.Collection; 043import java.util.HashSet; 044import java.util.LinkedHashSet; 045import java.util.Locale; 046import java.util.Map; 047import java.util.Set; 048 049 050/** 051 * A {@link Realm} that authenticates with an active directory LDAP 052 * server to determine the roles for a particular user. This implementation 053 * queries for the user's groups and then maps the group names to roles using the 054 * {@link #groupRolesMap}. 055 * 056 * @since 0.1 057 */ 058public class ActiveDirectoryRealm extends AbstractLdapRealm { 059 060 /*-------------------------------------------- 061 | C O N S T A N T S | 062 ============================================*/ 063 064 private static final Logger LOGGER = LoggerFactory.getLogger(ActiveDirectoryRealm.class); 065 066 private static final String ROLE_NAMES_DELIMETER = ","; 067 068 /*-------------------------------------------- 069 | I N S T A N C E V A R I A B L E S | 070 ============================================*/ 071 072 /** 073 * Mapping from fully qualified active directory 074 * group names (e.g. CN=Group,OU=Company,DC=MyDomain,DC=local) 075 * as returned by the active directory LDAP server to role names. 076 */ 077 private Map<String, String> groupRolesMap; 078 079 /*-------------------------------------------- 080 | C O N S T R U C T O R S | 081 ============================================*/ 082 083 public void setGroupRolesMap(Map<String, String> groupRolesMap) { 084 this.groupRolesMap = groupRolesMap; 085 } 086 087 /*-------------------------------------------- 088 | M E T H O D S | 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) 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}