/*
 * Copyright 2002-2016 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.ppolicy;

import java.util.Hashtable;

import javax.naming.Context;
import javax.naming.directory.DirContext;
import javax.naming.ldap.Control;
import javax.naming.ldap.LdapContext;

import org.springframework.ldap.support.LdapUtils;
import org.springframework.security.ldap.DefaultSpringSecurityContextSource;

/**
 * Extended version of the <tt>DefaultSpringSecurityContextSource</tt> which adds support
 * for the use of {@link PasswordPolicyControl} to make use of user account data stored in
 * the directory.
 * <p>
 * When binding with specific username (not the <tt>userDn</tt>) property it will connect
 * first as the userDn, then reconnect as the user in order to retrieve any
 * password-policy control sent with the response, even if an exception occurs.
 *
 * @author Luke Taylor
 * @since 3.0
 */
public class PasswordPolicyAwareContextSource extends DefaultSpringSecurityContextSource {

	public PasswordPolicyAwareContextSource(String providerUrl) {
		super(providerUrl);
	}

	@Override
	public DirContext getContext(String principal, String credentials)
			throws PasswordPolicyException {
		if (principal.equals(userDn)) {
			return super.getContext(principal, credentials);
		}

		final boolean debug = logger.isDebugEnabled();

		if (debug) {
			logger.debug("Binding as '" + userDn + "', prior to reconnect as user '"
					+ principal + "'");
		}

		// First bind as manager user before rebinding as the specific principal.
		LdapContext ctx = (LdapContext) super.getContext(userDn, password);

		Control[] rctls = { new PasswordPolicyControl(false) };

		try {
			ctx.addToEnvironment(Context.SECURITY_PRINCIPAL, principal);
			ctx.addToEnvironment(Context.SECURITY_CREDENTIALS, credentials);
			ctx.reconnect(rctls);
		}
		catch (javax.naming.NamingException ne) {
			PasswordPolicyResponseControl ctrl = PasswordPolicyControlExtractor
					.extractControl(ctx);
			if (debug) {
				logger.debug("Failed to obtain context", ne);
				logger.debug("Password policy response: " + ctrl);
			}

			LdapUtils.closeContext(ctx);

			if (ctrl != null) {
				if (ctrl.isLocked()) {
					throw new PasswordPolicyException(ctrl.getErrorStatus());
				}
			}

			throw LdapUtils.convertLdapException(ne);
		}

		if (debug) {
			logger.debug("PPolicy control returned: "
					+ PasswordPolicyControlExtractor.extractControl(ctx));
		}

		return ctx;
	}

	@Override
	@SuppressWarnings("unchecked")
	protected Hashtable getAuthenticatedEnv(String principal, String credentials) {
		Hashtable env = super.getAuthenticatedEnv(principal, credentials);

		env.put(LdapContext.CONTROL_FACTORIES,
				PasswordPolicyControlFactory.class.getName());

		return env;
	}
}
