/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License, Version 1.0 only
 * (the "License").  You may not use this file except in compliance
 * with the License.
 *
 * You can obtain a copy of the license at
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE
 * or https://OpenDS.dev.java.net/OpenDS.LICENSE.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE.  If applicable,
 * add the following below this CDDL HEADER, with the fields enclosed
 * by brackets "[]" replaced with your own identifying information:
 *      Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 *
 *
 *      Copyright 2006-2009 Sun Microsystems, Inc.
 */
package org.opends.server.extensions;



import static org.opends.messages.ExtensionMessages.*;
import static org.opends.server.loggers.ErrorLogger.*;
import static org.opends.server.loggers.debug.DebugLogger.*;
import static org.opends.server.util.ServerConstants.*;
import static org.opends.server.util.StaticUtils.*;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.UnsupportedCallbackException;
import javax.security.auth.login.LoginContext;
import javax.security.auth.login.LoginException;
import javax.security.sasl.Sasl;
import javax.security.sasl.SaslException;

import org.ietf.jgss.GSSException;
import org.opends.messages.Message;
import org.opends.messages.MessageBuilder;
import org.opends.server.admin.server.ConfigurationChangeListener;
import org.opends.server.admin.std.meta.
  GSSAPISASLMechanismHandlerCfgDefn.QualityOfProtection;
import org.opends.server.admin.std.server.GSSAPISASLMechanismHandlerCfg;
import org.opends.server.admin.std.server.SASLMechanismHandlerCfg;
import org.opends.server.api.ClientConnection;
import org.opends.server.api.IdentityMapper;
import org.opends.server.api.SASLMechanismHandler;
import org.opends.server.config.ConfigException;
import org.opends.server.core.BindOperation;
import org.opends.server.core.DirectoryServer;
import org.opends.server.loggers.debug.DebugTracer;
import org.opends.server.types.ConfigChangeResult;
import org.opends.server.types.DN;
import org.opends.server.types.DebugLogLevel;
import org.opends.server.types.DirectoryException;
import org.opends.server.types.Entry;
import org.opends.server.types.InitializationException;
import org.opends.server.types.ResultCode;



/**
 * This class provides an implementation of a SASL mechanism that
 * authenticates clients through Kerberos over GSSAPI.
 */
public class GSSAPISASLMechanismHandler extends
    SASLMechanismHandler<GSSAPISASLMechanismHandlerCfg> implements
    ConfigurationChangeListener<GSSAPISASLMechanismHandlerCfg>, CallbackHandler
{

  // The tracer object for the debug logger.
  private static final DebugTracer TRACER = getTracer();

  // The DN of the configuration entry for this SASL mechanism
  // handler.
  private DN configEntryDN;

  // The current configuration for this SASL mechanism handler.
  private GSSAPISASLMechanismHandlerCfg configuration;

  // The identity mapper that will be used to map identities.
  private IdentityMapper<?> identityMapper;

  // The properties to use when creating a SASL server to process the
  // GSSAPI authentication.
  private HashMap<String, String> saslProps;

  // The fully qualified domain name used when creating the SASL
  // server.
  private String serverFQDN;

  // The login context used to perform server-side authentication.
  private LoginContext loginContext;



  /**
   * Creates a new instance of this SASL mechanism handler. No
   * initialization should be done in this method, as it should all be
   * performed in the <CODE>initializeSASLMechanismHandler</CODE>
   * method.
   */
  public GSSAPISASLMechanismHandler()
  {
    super();
  }



  /**
   * {@inheritDoc}
   */
  @Override()
  public void initializeSASLMechanismHandler(
      GSSAPISASLMechanismHandlerCfg configuration) throws ConfigException,
      InitializationException {
    try {
      initialize(configuration);
      DirectoryServer.registerSASLMechanismHandler(SASL_MECHANISM_GSSAPI, this);
      configuration.addGSSAPIChangeListener(this);
      this.configuration = configuration;
      Message msg = INFO_GSSAPI_STARTED.get();
      logError(msg);
    }
    catch (UnknownHostException unhe)
    {
      if (debugEnabled())
      {
        TRACER.debugCaught(DebugLogLevel.ERROR, unhe);
      }
      Message message = ERR_SASL_CANNOT_GET_SERVER_FQDN.get(String
          .valueOf(configEntryDN), getExceptionMessage(unhe));
      throw new InitializationException(message, unhe);
    }
    catch (IOException ioe)
    {
      if (debugEnabled())
      {
        TRACER.debugCaught(DebugLogLevel.ERROR, ioe);
      }
      Message message = ERR_SASLGSSAPI_CANNOT_CREATE_JAAS_CONFIG
          .get(getExceptionMessage(ioe));
      throw new InitializationException(message, ioe);
    }
    catch (LoginException le)
    {
      if (debugEnabled())
      {
        TRACER.debugCaught(DebugLogLevel.ERROR, le);
      }
      Message message = ERR_SASLGSSAPI_CANNOT_CREATE_LOGIN_CONTEXT
          .get(getExceptionMessage(le));
      throw new InitializationException(message, le);
    }
  }



  /**
   * Checks to make sure that the ds-cfg-kdc-address and dc-cfg-realm
   * are both defined in the configuration. If only one is set, then
   * that is an error. If both are defined, or, both are null that is
   * fine.
   *
   * @param configuration
   *          The configuration to use.
   * @throws InitializationException
   *           If the properties violate the requirements.
   */
  private void getKdcRealm(GSSAPISASLMechanismHandlerCfg configuration)
      throws InitializationException
  {
    String kdcAddress = configuration.getKdcAddress();
    String realm = configuration.getRealm();
    if ((kdcAddress != null && realm == null)
        || (kdcAddress == null && realm != null))
    {
      Message message = ERR_SASLGSSAPI_KDC_REALM_NOT_DEFINED.get();
      throw new InitializationException(message);
    }
    else if (kdcAddress != null && realm != null)
    {
      System.setProperty(KRBV_PROPERTY_KDC, kdcAddress);
      System.setProperty(KRBV_PROPERTY_REALM, realm);

    }
  }



  /**
   * During login, callbacks are usually used to prompt for passwords.
   * All of the GSSAPI login information is provided in the properties
   * and login.conf file, so callbacks are ignored.
   *
   * @param callbacks
   *          An array of callbacks to process.
   * @throws UnsupportedCallbackException
   *           if an error occurs.
   */
  public void handle(Callback[] callbacks) throws UnsupportedCallbackException
  {
  }



  /**
   * Returns the fully qualified name either defined in the
   * configuration, or, determined by examining the system
   * configuration.
   *
   * @param configuration
   *          The configuration to check.
   * @return The fully qualified hostname of the server.
   * @throws UnknownHostException
   *           If the name cannot be determined from the system
   *           configuration.
   */
  private String getFQDN(GSSAPISASLMechanismHandlerCfg configuration)
      throws UnknownHostException
  {
    String serverName = configuration.getServerFqdn();
    if (serverName == null)
    {
      serverName = InetAddress.getLocalHost().getCanonicalHostName();
    }
    return serverName;
  }



  /**
   * Create a login context or login using the principal and keytab
   * information specified in the configuration.
   *
   * @throws LoginException
   *           If a login context cannot be created.
   */
  private void login() throws LoginException
  {
    loginContext = new LoginContext(GSSAPISASLMechanismHandler.class.getName(),
        this);
    loginContext.login();
  }



  /**
   * Logout of the current login context.
   */
  private void logout()
  {
    try
    {
      loginContext.logout();
    }
    catch (LoginException e)
    {
      if (debugEnabled())
      {
        TRACER.debugCaught(DebugLogLevel.ERROR, e);
      }
    }
  }



  /**
   * Creates an login.conf file from information in the specified
   * configuration. This file is used during the login phase.
   *
   * @param configuration
   *          The new configuration to use.
   * @return The filename of the new configuration file.
   * @throws IOException
   *           If the configuration file cannot be created.
   */
  private String configureLoginConfFile(
      GSSAPISASLMechanismHandlerCfg configuration)
  throws IOException, InitializationException {
    String configFileName;
    File tempFile = File.createTempFile("login", "conf");
    configFileName = tempFile.getAbsolutePath();
    tempFile.deleteOnExit();
    BufferedWriter w = new BufferedWriter(new FileWriter(tempFile, false));
    w.write(getClass().getName() + " {");
    w.newLine();
    w.write("  com.sun.security.auth.module.Krb5LoginModule required "
        + "storeKey=true useKeyTab=true doNotPrompt=true ");
    String keyTabFilePath = configuration.getKeytab();
    if(keyTabFilePath == null) {
      String home = System.getProperty("user.home");
      String sep = System.getProperty("file.separator");
      keyTabFilePath = home+sep+"krb5.keytab";
    }
    File keyTabFile = new File(keyTabFilePath);
    if(!keyTabFile.exists()) {
      Message msg = ERR_SASL_GSSAPI_KEYTAB_INVALID.get(keyTabFilePath);
      throw new InitializationException(msg);
    }
    w.write("keyTab=\"" + keyTabFile + "\" ");
    StringBuilder principal = new StringBuilder();
    String principalName = configuration.getPrincipalName();
    String realm = configuration.getRealm();
    if (principalName != null)
      principal.append("principal=\"" + principalName);
    else
      principal.append("principal=\"ldap/" + serverFQDN);
    if (realm != null)
      principal.append("@" + realm);
    w.write(principal.toString());
    Message msg = INFO_GSSAPI_PRINCIPAL_NAME.get(principal.toString());
    logError(msg);
    w.write("\";");
    w.newLine();
    w.write("};");
    w.newLine();
    w.flush();
    w.close();
    return configFileName;
  }



  /**
   * {@inheritDoc}
   */
  @Override()
  public void finalizeSASLMechanismHandler() {
    logout();
    if(configuration != null)
    configuration.removeGSSAPIChangeListener(this);
    DirectoryServer.deregisterSASLMechanismHandler(SASL_MECHANISM_GSSAPI);
    clearProperties();
    Message msg = INFO_GSSAPI_STOPPED.get();
    logError(msg);
  }


private void clearProperties() {
  System.clearProperty(KRBV_PROPERTY_KDC);
  System.clearProperty(KRBV_PROPERTY_REALM);
  System.clearProperty(JAAS_PROPERTY_CONFIG_FILE);
  System.clearProperty(JAAS_PROPERTY_SUBJECT_CREDS_ONLY);
}

  /**
   * {@inheritDoc}
   */
  @Override()
  public void processSASLBind(BindOperation bindOp)
  {
    ClientConnection clientConnection = bindOp.getClientConnection();
    if (clientConnection == null)
    {
      Message message = ERR_SASLGSSAPI_NO_CLIENT_CONNECTION.get();
      bindOp.setAuthFailureReason(message);
      bindOp.setResultCode(ResultCode.INVALID_CREDENTIALS);
      return;
    }
    ClientConnection clientConn = bindOp.getClientConnection();
    SASLContext saslContext = (SASLContext) clientConn.getSASLAuthStateInfo();
    if (saslContext == null) {
      try {
        //If the connection is secure already (i.e., TLS), then make the
        //receive buffers sizes match.
        if(clientConn.isSecure()) {
          HashMap<String, String>secProps =
                                  new HashMap<String,String>(saslProps);
          int maxBuf = clientConn.getAppBufferSize();
          secProps.put(Sasl.MAX_BUFFER, Integer.toString(maxBuf));
          saslContext = SASLContext.createSASLContext(secProps, serverFQDN,
                                  SASL_MECHANISM_GSSAPI, identityMapper);
        } else {
          saslContext = SASLContext.createSASLContext(saslProps, serverFQDN,
                                  SASL_MECHANISM_GSSAPI, identityMapper);
        }
      } catch (SaslException ex) {
        if (debugEnabled())
          TRACER.debugCaught(DebugLogLevel.ERROR, ex);
        Message msg;
        GSSException gex = (GSSException) ex.getCause();
        if(gex != null) {
          msg = ERR_SASL_CONTEXT_CREATE_ERROR.get(SASL_MECHANISM_GSSAPI,
              getGSSExceptionMessage(gex));
        } else {
          msg = ERR_SASL_CONTEXT_CREATE_ERROR.get(SASL_MECHANISM_GSSAPI,
              getExceptionMessage(ex));
        }
        clientConn.setSASLAuthStateInfo(null);
        bindOp.setAuthFailureReason(msg);
        bindOp.setResultCode(ResultCode.INVALID_CREDENTIALS);
        return;
      }
    }
    saslContext.performAuthentication(loginContext, bindOp);
  }


  /**
   * Get the underlying GSSException messages that really tell what the
   * problem is. The major code is the GSS-API status and the minor is the
   * mechanism specific error.
   *
   * @param gex The GSSExcption thrown.
   *
   * @return The message containing the major and (optional) minor codes and
   *         strings.
   */
  public static Message getGSSExceptionMessage(GSSException gex) {
    MessageBuilder message = new MessageBuilder();
    message.append("major code (" + Integer.valueOf(gex.getMajor()).toString()
                  + ") " +  gex.getMajorString());
    if(gex.getMinor() != 0)
      message.append(", minor code (" +
                      Integer.valueOf(gex.getMinor()).toString()
                       +  ") "  + gex.getMinorString());
    return message.toMessage();
  }

  /**
   * Retrieves the user account for the user associated with the
   * provided authorization ID.
   *
   * @param bindOperation
   *          The bind operation from which the provided authorization
   *          ID was derived.
   * @param authzID
   *          The authorization ID for which to retrieve the
   *          associated user.
   * @return The user entry for the user with the specified
   *         authorization ID, or {@code null} if none is identified.
   * @throws DirectoryException
   *           If a problem occurs while searching the directory for
   *           the associated user, or if multiple matching entries
   *           are found.
   */
  public Entry getUserForAuthzID(BindOperation bindOperation, String authzID)
      throws DirectoryException
  {
    return identityMapper.getEntryForID(authzID);
  }



  /**
   * {@inheritDoc}
   */
  @Override()
  public boolean isPasswordBased(String mechanism)
  {
    // This is not a password-based mechanism.
    return false;
  }



  /**
   * {@inheritDoc}
   */
  @Override()
  public boolean isSecure(String mechanism)
  {
    // This may be considered a secure mechanism.
    return true;
  }



  /**
   * {@inheritDoc}
   */
  @Override()
  public boolean isConfigurationAcceptable(
      SASLMechanismHandlerCfg configuration, List<Message> unacceptableReasons)
  {
    GSSAPISASLMechanismHandlerCfg newConfig =
      (GSSAPISASLMechanismHandlerCfg) configuration;
    return isConfigurationChangeAcceptable(newConfig, unacceptableReasons);
  }



  /**
   * {@inheritDoc}
   */
  public boolean isConfigurationChangeAcceptable(
      GSSAPISASLMechanismHandlerCfg newConfiguration,
      List<Message> unacceptableReasons) {
    boolean returnCode = true;
    boolean newStateEnabled = newConfiguration.isEnabled();
    boolean oldStateEnabled = false;
    if(this.configuration != null)
      oldStateEnabled = configuration.isEnabled();
    if(newStateEnabled) {
      try {
       if(oldStateEnabled)
         finalizeSASLMechanismHandler();
       initialize(newConfiguration);
       finalizeSASLMechanismHandler();
      } catch (InitializationException ex) {
        if (debugEnabled())
          TRACER.debugCaught(DebugLogLevel.ERROR, ex);
        Message message = ex.getMessageObject();
        unacceptableReasons.add(message);
        clearProperties();
        returnCode = false;
      } catch (UnknownHostException ex) {
        if (debugEnabled())
          TRACER.debugCaught(DebugLogLevel.ERROR, ex);
        Message message = ERR_SASL_CANNOT_GET_SERVER_FQDN.get(String
            .valueOf(configEntryDN), getExceptionMessage(ex));
        unacceptableReasons.add(message);
        clearProperties();
        returnCode = false;
      } catch (IOException ex) {
        if (debugEnabled())
          TRACER.debugCaught(DebugLogLevel.ERROR, ex);
        Message message = ERR_SASLGSSAPI_CANNOT_CREATE_JAAS_CONFIG
            .get(getExceptionMessage(ex));
        unacceptableReasons.add(message);
        clearProperties();
        returnCode = false;
      } catch (LoginException ex) {
        if (debugEnabled())
          TRACER.debugCaught(DebugLogLevel.ERROR, ex);
        Message message = ERR_SASLGSSAPI_CANNOT_CREATE_LOGIN_CONTEXT
            .get(getExceptionMessage(ex));
        unacceptableReasons.add(message);
        clearProperties();
        returnCode = false;
      }
    } else {
      if(oldStateEnabled)
       this.finalizeSASLMechanismHandler();
    }
    return returnCode;
  }



  /**
   * {@inheritDoc}
   */
  public ConfigChangeResult applyConfigurationChange(
      GSSAPISASLMechanismHandlerCfg configuration) {
    ResultCode resultCode = ResultCode.SUCCESS;
    boolean adminActionRequired = false;
    ArrayList<Message> messages = new ArrayList<Message>();
    return new ConfigChangeResult(resultCode, adminActionRequired, messages);
  }

/**
 * Try to initialize the GSSAPI mechanism handler with the specified config.
 *
 * @param config The configuration to use.
 *
 * @throws UnknownHostException If a host name does not resolve.
 * @throws IOException If there was a problem creating the login file.
 * @throws LoginException If the context could not login.
 * @throws InitializationException If the keytab file does not exist.
 */
private void initialize(GSSAPISASLMechanismHandlerCfg config)
throws UnknownHostException, IOException,
       LoginException, InitializationException {
    configEntryDN = config.dn();
    DN identityMapperDN = config.getIdentityMapperDN();
    identityMapper = DirectoryServer.getIdentityMapper(identityMapperDN);
    serverFQDN = getFQDN(config);
    Message msg = INFO_GSSAPI_SERVER_FQDN.get(serverFQDN);
    logError(msg);
    saslProps = new HashMap<String, String>();
    saslProps.put(Sasl.QOP, getQOP(config));
    saslProps.put(Sasl.REUSE, "false");
    String configFileName = configureLoginConfFile(config);
    System.setProperty(JAAS_PROPERTY_CONFIG_FILE, configFileName);
    System.setProperty(JAAS_PROPERTY_SUBJECT_CREDS_ONLY, "false");
    getKdcRealm(config);
    login();
}

  /**
   * Retrieves the QOP (quality-of-protection) from the specified
   * configuration.
   *
   * @param configuration
   *          The new configuration to use.
   * @return A string representing the quality-of-protection.
   */
  private String getQOP(GSSAPISASLMechanismHandlerCfg configuration)
  {
    QualityOfProtection QOP = configuration.getQualityOfProtection();
    if (QOP.equals(QualityOfProtection.CONFIDENTIALITY))
      return "auth-conf";
    else if (QOP.equals(QualityOfProtection.INTEGRITY))
      return "auth-int";
    else
      return "auth";
  }
}
