/*
  $Id: AbstractAuthenticator.java 1743 2010-11-19 17:00:18Z dfisher $

  Copyright (C) 2003-2010 Virginia Tech.
  All rights reserved.

  SEE LICENSE FOR MORE INFORMATION

  Author:  Middleware Services
  Email:   middleware@vt.edu
  Version: $Revision: 1743 $
  Updated: $Date: 2010-11-19 18:00:18 +0100 (Fri, 19 Nov 2010) $
*/
package edu.vt.middleware.ldap.auth;

import java.util.Arrays;
import javax.naming.AuthenticationException;
import javax.naming.NamingException;
import javax.naming.directory.Attributes;
import edu.vt.middleware.ldap.LdapConstants;
import edu.vt.middleware.ldap.LdapUtil;
import edu.vt.middleware.ldap.auth.handler.AuthenticationCriteria;
import edu.vt.middleware.ldap.auth.handler.AuthenticationHandler;
import edu.vt.middleware.ldap.auth.handler.AuthenticationResultHandler;
import edu.vt.middleware.ldap.auth.handler.AuthorizationHandler;
import edu.vt.middleware.ldap.handler.ConnectionHandler;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * <code>AbstractAuthenticator</code> provides basic functionality for
 * authenticating against an LDAP.
 *
 * @param  <T>  type of AuthenticatorConfig
 *
 * @author  Middleware Services
 * @version  $Revision: 1743 $ $Date: 2010-11-19 18:00:18 +0100 (Fri, 19 Nov 2010) $
 */
public abstract class AbstractAuthenticator<T extends AuthenticatorConfig>
{

  /** Log for this class. */
  protected final Log logger = LogFactory.getLog(this.getClass());

  /** Authenticator configuration environment. */
  protected T config;


  /**
   * This will set the config parameters of this <code>Authenticator</code>.
   *
   * @param  authConfig  <code>AuthenticatorConfig</code>
   */
  public void setAuthenticatorConfig(final T authConfig)
  {
    if (this.config != null) {
      this.config.checkImmutable();
    }
    this.config = authConfig;
  }


  /**
   * This will authenticate by binding to the LDAP with the supplied dn and
   * credential. See {@link #authenticateAndAuthorize( String, Object, boolean,
   * String[], AuthenticationResultHandler[], AuthorizationHandler[])}.
   *
   * @param  dn  <code>String</code> for bind
   * @param  credential  <code>Object</code> for bind
   * @param  authResultHandler  <code>AuthenticationResultHandler[]</code> to
   * post process authentication results
   * @param  authzHandler  <code>AuthorizationHandler[]</code> to process
   * authorization after authentication
   *
   * @return  <code>boolean</code> - whether the bind succeeded
   *
   * @throws  NamingException  if the authentication fails for any other reason
   * than invalid credentials
   */
  protected boolean authenticateAndAuthorize(
    final String dn,
    final Object credential,
    final AuthenticationResultHandler[] authResultHandler,
    final AuthorizationHandler[] authzHandler)
    throws NamingException
  {
    boolean success = false;
    try {
      this.authenticateAndAuthorize(
        dn,
        credential,
        false,
        null,
        authResultHandler,
        authzHandler);
      success = true;
    } catch (AuthenticationException e) {
      if (this.logger.isDebugEnabled()) {
        this.logger.debug("Authentication failed for dn: " + dn, e);
      }
    } catch (AuthorizationException e) {
      if (this.logger.isDebugEnabled()) {
        this.logger.debug("Authorization failed for dn: " + dn, e);
      }
    }
    return success;
  }


  /**
   * This will authenticate by binding to the LDAP with the supplied dn and
   * credential. Authentication will never succeed if {@link
   * AuthenticatorConfig#getAuthtype()} is set to 'none'. If retAttrs is null
   * and searchAttrs is true then all user attributes will be returned. If
   * retAttrs is an empty array and searchAttrs is true then no attributes will
   * be returned. This method throws AuthenticationException if authentication
   * fails and AuthorizationException if authorization fails.
   *
   * @param  dn  <code>String</code> for bind
   * @param  credential  <code>Object</code> for bind
   * @param  searchAttrs  <code>boolean</code> whether to perform attribute
   * search
   * @param  retAttrs  <code>String[]</code> user attributes to return
   * @param  authResultHandler  <code>AuthenticationResultHandler[]</code> to
   * post process authentication results
   * @param  authzHandler  <code>AuthorizationHandler[]</code> to process
   * authorization after authentication
   *
   * @return  <code>Attribute</code> - belonging to the supplied user, returns
   * null if searchAttrs is false
   *
   * @throws  NamingException  if any of the ldap operations fail
   * @throws  AuthenticationException  if authentication fails
   * @throws  AuthorizationException  if authorization fails
   */
  protected Attributes authenticateAndAuthorize(
    final String dn,
    final Object credential,
    final boolean searchAttrs,
    final String[] retAttrs,
    final AuthenticationResultHandler[] authResultHandler,
    final AuthorizationHandler[] authzHandler)
    throws NamingException
  {
    // check the authentication type
    final String authtype = this.config.getAuthtype();
    if (authtype.equalsIgnoreCase(LdapConstants.NONE_AUTHTYPE)) {
      throw new AuthenticationException(
        "Cannot authenticate dn, authtype is 'none'");
    }

    // check the credential
    if (!LdapUtil.checkCredential(credential)) {
      throw new AuthenticationException(
        "Cannot authenticate dn, invalid credential");
    }

    // check the dn
    if (dn == null || "".equals(dn)) {
      throw new AuthenticationException("Cannot authenticate dn, invalid dn");
    }

    Attributes userAttributes = null;

    // attempt to bind as this dn
    final ConnectionHandler ch = this.config.getConnectionHandler()
        .newInstance();
    try {
      final AuthenticationCriteria ac = new AuthenticationCriteria(dn);
      ac.setCredential(credential);
      try {
        final AuthenticationHandler authHandler = this.config
            .getAuthenticationHandler().newInstance();
        authHandler.authenticate(ch, ac);
        if (this.logger.isInfoEnabled()) {
          this.logger.info("Authentication succeeded for dn: " + dn);
        }
      } catch (AuthenticationException e) {
        if (this.logger.isInfoEnabled()) {
          this.logger.info("Authentication failed for dn: " + dn);
        }
        if (authResultHandler != null && authResultHandler.length > 0) {
          for (AuthenticationResultHandler ah : authResultHandler) {
            ah.process(ac, false);
          }
        }
        throw e;
      }
      // authentication succeeded, perform authorization if supplied
      if (authzHandler != null && authzHandler.length > 0) {
        for (AuthorizationHandler azh : authzHandler) {
          try {
            azh.process(ac, ch.getLdapContext());
            if (this.logger.isInfoEnabled()) {
              this.logger.info(
                "Authorization succeeded for dn: " + dn + " with handler: " +
                azh);
            }
          } catch (AuthenticationException e) {
            if (this.logger.isInfoEnabled()) {
              this.logger.info(
                "Authorization failed for dn: " + dn + " with handler: " + azh);
            }
            if (authResultHandler != null && authResultHandler.length > 0) {
              for (AuthenticationResultHandler ah : authResultHandler) {
                ah.process(ac, false);
              }
            }
            throw e;
          }
        }
      }
      if (searchAttrs) {
        if (this.logger.isDebugEnabled()) {
          this.logger.debug("Returning attributes: ");
          this.logger.debug(
            "    " +
            (retAttrs == null ? "all attributes" : Arrays.toString(retAttrs)));
        }
        userAttributes = ch.getLdapContext().getAttributes(dn, retAttrs);
      }
      if (authResultHandler != null && authResultHandler.length > 0) {
        for (AuthenticationResultHandler ah : authResultHandler) {
          ah.process(ac, true);
        }
      }
    } finally {
      ch.close();
    }

    return userAttributes;
  }


  /** This will close the connection on the underlying DN resolver. */
  public synchronized void close()
  {
    if (this.config.getDnResolver() != null) {
      this.config.getDnResolver().close();
    }
  }
}
