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    }