/* * Licensed to Jasig under one or more contributor license * agreements. See the NOTICE file distributed with this work * for additional information regarding copyright ownership. * Jasig licenses this file to you under the Apache License, * Version 2.0 (the "License"); you may not use this file * except in compliance with the License. You may obtain a * copy of the License at the following location: * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.jasig.cas.userdetails; import java.util.ArrayList; import java.util.Collection; import javax.validation.constraints.NotNull; import org.ldaptive.ConnectionFactory; import org.ldaptive.LdapAttribute; import org.ldaptive.LdapEntry; import org.ldaptive.LdapException; import org.ldaptive.Response; import org.ldaptive.SearchExecutor; import org.ldaptive.SearchFilter; import org.ldaptive.SearchResult; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; /** * Provides a simple {@link UserDetailsService} implementation that obtains user details from an LDAP search. * Two searches are performed by this component for every user details lookup: * * <ol> * <li>Search for an entry to resolve the username. In most cases the search should return exactly one result, * but the {@link #setAllowMultipleResults(boolean)} property may be toggled to change that behavior.</li> * <li>Search for groups of which the user is a member. This search commonly occurs on a separate directory * branch than that of the user search.</li> * </ol> * * @author Marvin S. Addison * @author Misagh Moayyed * @since 4.0 */ public class LdapUserDetailsService implements UserDetailsService { /** Default role prefix. */ public static final String DEFAULT_ROLE_PREFIX = "ROLE_"; /** Placeholder for unknown password given to user details. */ public static final String UNKNOWN_PASSWORD = "<UNKNOWN>"; /** Logger instance. */ private final Logger logger = LoggerFactory.getLogger(getClass()); /** Source of LDAP connections. */ @NotNull private final ConnectionFactory connectionFactory; /** Executes the search query for user data. */ @NotNull private final SearchExecutor userSearchExecutor; /** Executes the search query for roles. */ @NotNull private final SearchExecutor roleSearchExecutor; /** Specify the name of LDAP attribute to use as principal identifier. */ @NotNull private final String userAttributeName; /** Specify the name of LDAP attribute to be used as the basis for role granted authorities. */ @NotNull private final String roleAttributeName; /** Prefix appended to the uppercased {@link #roleAttributeName} per the normal Spring Security convention. */ @NotNull private String rolePrefix = DEFAULT_ROLE_PREFIX; /** Flag that indicates whether multiple search results are allowed for a given credential. */ private boolean allowMultipleResults = false; /** * Creates a new instance with the given required parameters. * * @param factory Source of LDAP connections for searches. * @param userSearchExecutor Executes the LDAP search for user data. * @param roleSearchExecutor Executes the LDAP search for role data. * @param userAttributeName Name of LDAP attribute that contains username for user details. * @param roleAttributeName Name of LDAP attribute that contains role membership data for the user. */ public LdapUserDetailsService( final ConnectionFactory factory, final SearchExecutor userSearchExecutor, final SearchExecutor roleSearchExecutor, final String userAttributeName, final String roleAttributeName) { this.connectionFactory = factory; this.userSearchExecutor = userSearchExecutor; this.roleSearchExecutor = roleSearchExecutor; this.userAttributeName = userAttributeName; this.roleAttributeName = roleAttributeName; } /** * Sets the prefix appended to the uppercase {@link #roleAttributeName} per the normal Spring Security convention. * The default value {@value #DEFAULT_ROLE_PREFIX} is sufficient in most cases. * * @param rolePrefix Role prefix. */ public void setRolePrefix(final String rolePrefix) { this.rolePrefix = rolePrefix; } /** * Sets whether to allow multiple search results for user details given a username. * This is false by default, which is sufficient and secure for more deployments. * Setting this to true may have security consequences. * * @param allowMultiple True to allow multiple search results in which case the first result * returned is used to construct user details, or false to indicate that * a runtime exception should be raised on multiple search results for user details. */ public void setAllowMultipleResults(final boolean allowMultiple) { this.allowMultipleResults = allowMultiple; } @Override public UserDetails loadUserByUsername(final String username) { final SearchResult userResult; try { logger.debug("Attempting to get details for user {}.", username); final Response<SearchResult> response = this.userSearchExecutor.search( this.connectionFactory, createSearchFilter(this.userSearchExecutor, username)); logger.debug("LDAP user search response: {}", response); userResult = response.getResult(); } catch (final LdapException e) { throw new RuntimeException("LDAP error fetching details for user.", e); } if (userResult.size() == 0) { throw new UsernameNotFoundException(username + " not found."); } if (userResult.size() > 1 && !this.allowMultipleResults) { throw new IllegalStateException( "Found multiple results for user which is not allowed (allowMultipleResults=false)."); } final String userDn = userResult.getEntry().getDn(); final LdapAttribute userAttribute = userResult.getEntry().getAttribute(this.userAttributeName); if (userAttribute == null) { throw new IllegalStateException(this.userAttributeName + " attribute not found in results."); } final String id = userAttribute.getStringValue(); final SearchResult roleResult; try { logger.debug("Attempting to get roles for user {}.", userDn); final Response<SearchResult> response = this.roleSearchExecutor.search( this.connectionFactory, createSearchFilter(this.roleSearchExecutor, userDn)); logger.debug("LDAP role search response: {}", response); roleResult = response.getResult(); } catch (final LdapException e) { throw new RuntimeException("LDAP error fetching roles for user.", e); } LdapAttribute roleAttribute; final Collection<SimpleGrantedAuthority> roles = new ArrayList<SimpleGrantedAuthority>(roleResult.size()); for (final LdapEntry entry : roleResult.getEntries()) { roleAttribute = entry.getAttribute(this.roleAttributeName); if (roleAttribute == null) { logger.warn("Role attribute not found on entry {}", entry); continue; } roles.add(new SimpleGrantedAuthority(this.rolePrefix + roleAttribute.getStringValue().toUpperCase())); } return new User(id, UNKNOWN_PASSWORD, roles); } /** * Constructs a new search filter using {@link SearchExecutor#searchFilter} as a template and * the username as a parameter. * * @param username Username parameter of search query. * * @return Search filter with parameters applied. */ private SearchFilter createSearchFilter(final SearchExecutor executor, final String username) { final SearchFilter filter = new SearchFilter(); filter.setFilter(executor.getSearchFilter().getFilter()); filter.setParameter(0, username); logger.debug("Constructed LDAP search filter [{}]", filter.format()); return filter; } }