/*
 * Copyright 2002-2014 the original author or authors.
 *
 * Licensed 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
 *
 *      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.springframework.security.ldap.authentication;

import java.util.Collection;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.springframework.context.MessageSource;
import org.springframework.context.MessageSourceAware;
import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.ldap.core.DirContextOperations;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.SpringSecurityMessageSource;
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
import org.springframework.security.core.authority.mapping.NullAuthoritiesMapper;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.ldap.userdetails.LdapUserDetailsMapper;
import org.springframework.security.ldap.userdetails.UserDetailsContextMapper;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

/**
 * Base class for the standard {@code LdapAuthenticationProvider} and the
 * {@code ActiveDirectoryLdapAuthenticationProvider}.
 *
 * @author Luke Taylor
 * @since 3.1
 */
public abstract class AbstractLdapAuthenticationProvider
		implements AuthenticationProvider, MessageSourceAware {
	protected final Log logger = LogFactory.getLog(getClass());
	protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
	private boolean useAuthenticationRequestCredentials = true;
	private GrantedAuthoritiesMapper authoritiesMapper = new NullAuthoritiesMapper();
	protected UserDetailsContextMapper userDetailsContextMapper = new LdapUserDetailsMapper();

	public Authentication authenticate(Authentication authentication)
			throws AuthenticationException {
		Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
				this.messages.getMessage("LdapAuthenticationProvider.onlySupports",
						"Only UsernamePasswordAuthenticationToken is supported"));

		final UsernamePasswordAuthenticationToken userToken = (UsernamePasswordAuthenticationToken) authentication;

		String username = userToken.getName();
		String password = (String) authentication.getCredentials();

		if (this.logger.isDebugEnabled()) {
			this.logger.debug("Processing authentication request for user: " + username);
		}

		if (!StringUtils.hasLength(username)) {
			throw new BadCredentialsException(this.messages.getMessage(
					"LdapAuthenticationProvider.emptyUsername", "Empty Username"));
		}

		if (!StringUtils.hasLength(password)) {
			throw new BadCredentialsException(this.messages.getMessage(
					"AbstractLdapAuthenticationProvider.emptyPassword",
					"Empty Password"));
		}

		Assert.notNull(password, "Null password was supplied in authentication token");

		DirContextOperations userData = doAuthentication(userToken);

		UserDetails user = this.userDetailsContextMapper.mapUserFromContext(userData,
				authentication.getName(),
				loadUserAuthorities(userData, authentication.getName(),
						(String) authentication.getCredentials()));

		return createSuccessfulAuthentication(userToken, user);
	}

	protected abstract DirContextOperations doAuthentication(
			UsernamePasswordAuthenticationToken auth);

	protected abstract Collection<? extends GrantedAuthority> loadUserAuthorities(
			DirContextOperations userData, String username, String password);

	/**
	 * Creates the final {@code Authentication} object which will be returned from the
	 * {@code authenticate} method.
	 *
	 * @param authentication the original authentication request token
	 * @param user the <tt>UserDetails</tt> instance returned by the configured
	 * <tt>UserDetailsContextMapper</tt>.
	 * @return the Authentication object for the fully authenticated user.
	 */
	protected Authentication createSuccessfulAuthentication(
			UsernamePasswordAuthenticationToken authentication, UserDetails user) {
		Object password = this.useAuthenticationRequestCredentials
				? authentication.getCredentials() : user.getPassword();

		UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(
				user, password,
				this.authoritiesMapper.mapAuthorities(user.getAuthorities()));
		result.setDetails(authentication.getDetails());

		return result;
	}

	public boolean supports(Class<?> authentication) {
		return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
	}

	/**
	 * Determines whether the supplied password will be used as the credentials in the
	 * successful authentication token. If set to false, then the password will be
	 * obtained from the UserDetails object created by the configured
	 * {@code UserDetailsContextMapper}. Often it will not be possible to read the
	 * password from the directory, so defaults to true.
	 *
	 * @param useAuthenticationRequestCredentials
	 */
	public void setUseAuthenticationRequestCredentials(
			boolean useAuthenticationRequestCredentials) {
		this.useAuthenticationRequestCredentials = useAuthenticationRequestCredentials;
	}

	public void setMessageSource(MessageSource messageSource) {
		this.messages = new MessageSourceAccessor(messageSource);
	}

	public void setAuthoritiesMapper(GrantedAuthoritiesMapper authoritiesMapper) {
		this.authoritiesMapper = authoritiesMapper;
	}

	/**
	 * Allows a custom strategy to be used for creating the <tt>UserDetails</tt> which
	 * will be stored as the principal in the <tt>Authentication</tt> returned by the
	 * {@link #createSuccessfulAuthentication(org.springframework.security.authentication.UsernamePasswordAuthenticationToken, org.springframework.security.core.userdetails.UserDetails)}
	 * method.
	 *
	 * @param userDetailsContextMapper the strategy instance. If not set, defaults to a
	 * simple <tt>LdapUserDetailsMapper</tt>.
	 */
	public void setUserDetailsContextMapper(
			UserDetailsContextMapper userDetailsContextMapper) {
		Assert.notNull(userDetailsContextMapper,
				"UserDetailsContextMapper must not be null");
		this.userDetailsContextMapper = userDetailsContextMapper;
	}

	/**
	 * Provides access to the injected {@code UserDetailsContextMapper} strategy for use
	 * by subclasses.
	 */
	protected UserDetailsContextMapper getUserDetailsContextMapper() {
		return this.userDetailsContextMapper;
	}
}
