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    }