/*
 * The Alluxio Open Foundation licenses this work under the Apache License, version 2.0
 * (the "License"). You may not use this work except in compliance with the License, which is
 * available at www.apache.org/licenses/LICENSE-2.0
 *
 * This software is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
 * either express or implied, as more fully set forth in the License.
 *
 * See the NOTICE file distributed with this work for information regarding copyright ownership.
 */

package alluxio.security.authentication.plain;

import alluxio.security.authentication.AuthenticatedClientUser;

import com.google.common.base.Preconditions;

import java.util.Map;
import javax.annotation.Nullable;
import javax.annotation.concurrent.NotThreadSafe;
import javax.annotation.concurrent.ThreadSafe;
import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.NameCallback;
import javax.security.auth.callback.PasswordCallback;
import javax.security.sasl.AuthorizeCallback;
import javax.security.sasl.Sasl;
import javax.security.sasl.SaslException;
import javax.security.sasl.SaslServer;
import javax.security.sasl.SaslServerFactory;

/**
 * This class provides PLAIN SASL authentication.
 * <p/>
 * NOTE: When this SaslServer works on authentication (i.e., in the method
 * {@link #evaluateResponse(byte[])}, it always assigns authentication ID to authorization ID
 * currently.
 */
@NotThreadSafe
public final class PlainSaslServer implements SaslServer {
  /**
   * This ID represent the authorized client user, who has been authenticated successfully. It is
   * associated with the client connection thread for following action authorization usage.
   */
  private String mAuthorizationId;
  /** Whether an authentication is complete or not. */
  private boolean mCompleted;
  private CallbackHandler mHandler;

  PlainSaslServer(CallbackHandler handler) throws SaslException {
    mCompleted = false;
    mHandler = handler;
  }

  @Override
  public String getMechanismName() {
    return PlainSaslServerProvider.MECHANISM;
  }

  @Override
  @Nullable
  public byte[] evaluateResponse(byte[] response) throws SaslException {
    Preconditions.checkState(!mCompleted, "PLAIN authentication has completed");
    Preconditions.checkArgument(response != null, "Received null response");

    try {
      // parse the response
      // message = [authorizationId] UTF8NUL authenticationId UTF8NUL passwd'
      // authorizationId may be empty,then the authorizationId = authenticationId
      String payload;
      try {
        payload = new String(response, "UTF-8");
      } catch (Exception e) {
        throw new IllegalArgumentException("Received corrupt response", e);
      }
      String[] parts = payload.split("\u0000", 3);
      // validate response
      if (parts.length != 3) {
        throw new IllegalArgumentException("Invalid message format, parts must contain 3 items");
      }
      String authorizationId = parts[0];
      String authenticationId = parts[1];
      String passwd = parts[2];
      Preconditions.checkState(authenticationId != null && !authenticationId.isEmpty(),
          "No authentication identity provided");
      Preconditions.checkState(passwd != null && !passwd.isEmpty(), "No password provided");

      if (authorizationId == null || authorizationId.isEmpty()) {
        authorizationId = authenticationId;
      }

      NameCallback nameCallback = new NameCallback("User");
      nameCallback.setName(authenticationId);
      PasswordCallback passwordCallback = new PasswordCallback("Password", false);
      passwordCallback.setPassword(passwd.toCharArray());
      AuthorizeCallback authCallback = new AuthorizeCallback(authenticationId, authorizationId);

      Callback[] cbList = {nameCallback, passwordCallback, authCallback};
      mHandler.handle(cbList);
      if (!authCallback.isAuthorized()) {
        throw new SaslException("AuthorizeCallback authorized failure");
      }
      mAuthorizationId = authCallback.getAuthorizedID();
    } catch (Exception e) {
      throw new SaslException("Plain authentication failed: " + e.getMessage(), e);
    }
    mCompleted = true;
    return null;
  }

  @Override
  public boolean isComplete() {
    return mCompleted;
  }

  @Override
  public String getAuthorizationID() {
    checkNotComplete();
    return mAuthorizationId;
  }

  @Override
  public byte[] unwrap(byte[] incoming, int offset, int len) {
    throw new UnsupportedOperationException("PLAIN doesn't support wrap or unwrap operation");
  }

  @Override
  public byte[] wrap(byte[] outgoing, int offset, int len) {
    throw new UnsupportedOperationException("PLAIN doesn't support wrap or unwrap operation");
  }

  @Override
  @Nullable
  public Object getNegotiatedProperty(String propName) {
    checkNotComplete();
    return Sasl.QOP.equals(propName) ? "auth" : null;
  }

  @Override
  public void dispose() {
    if (mCompleted) {
      // clean up the user in threadlocal, when client connection is closed.
      AuthenticatedClientUser.remove();
    }

    mCompleted = false;
    mHandler = null;
    mAuthorizationId = null;
  }

  private void checkNotComplete() {
    if (!mCompleted) {
      throw new IllegalStateException("PLAIN authentication not completed");
    }
  }

  /**
   * This class is used to create an instances of {@link PlainSaslServer}. The parameter mechanism
   * must be "PLAIN" when this Factory is called, or null will be returned.
   */
  @ThreadSafe
  public static class Factory implements SaslServerFactory {

    /**
     * Constructs a new {@link Factory} for the {@link PlainSaslServer}.
     */
    public Factory() {}

    /**
     * Creates a {@link SaslServer} using the parameters supplied. It returns null if no SaslServer
     * can be created using the parameters supplied. Throws {@link SaslException} if it cannot
     * create a SaslServer because of an error.
     *
     * @param mechanism the name of a SASL mechanism. (e.g. "PLAIN")
     * @param protocol the non-null string name of the protocol for which the authentication is
     *        being performed
     * @param serverName the non-null fully qualified host name of the server to authenticate to
     * @param props the possibly null set of properties used to select the SASL mechanism and to
     *        configure the authentication exchange of the selected mechanism
     * @param callbackHandler the possibly null callback handler to used by the SASL mechanisms to
     *        do further operation
     * @return A possibly null SaslServer created using the parameters supplied. If null, this
     *         factory cannot produce a SaslServer using the parameters supplied.
     * @exception SaslException If it cannot create a SaslServer because of an error.
     */
    @Override
    public SaslServer createSaslServer(String mechanism, String protocol, String serverName,
        Map<String, ?> props, CallbackHandler callbackHandler) throws SaslException {
      Preconditions.checkArgument(PlainSaslServerProvider.MECHANISM.equals(mechanism));
      return new PlainSaslServer(callbackHandler);
    }

    @Override
    public String[] getMechanismNames(Map<String, ?> props) {
      return new String[] {PlainSaslServerProvider.MECHANISM};
    }
  }
}
