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}