/*
 * Copyright 2008-2016 MongoDB, Inc.
 *
 * 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 com.mongodb;

import com.mongodb.annotations.Immutable;

import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

import static com.mongodb.AuthenticationMechanism.GSSAPI;
import static com.mongodb.AuthenticationMechanism.MONGODB_CR;
import static com.mongodb.AuthenticationMechanism.MONGODB_X509;
import static com.mongodb.AuthenticationMechanism.PLAIN;
import static com.mongodb.AuthenticationMechanism.SCRAM_SHA_1;
import static com.mongodb.assertions.Assertions.notNull;

/**
 * Represents credentials to authenticate to a mongo server,as well as the source of the credentials and the authentication mechanism to
 * use.
 *
 * @since 2.11
 */
@Immutable
public final class MongoCredential {

    private final AuthenticationMechanism mechanism;
    private final String userName;
    private final String source;
    private final char[] password;
    private final Map<String, Object> mechanismProperties;

    /**
     * The MongoDB Challenge Response mechanism.
     * @mongodb.driver.manual core/authentication/#mongodb-cr-authentication MONGODB-CR
     */
    public static final String MONGODB_CR_MECHANISM = AuthenticationMechanism.MONGODB_CR.getMechanismName();

    /**
     * The GSSAPI mechanism.  See the <a href="http://tools.ietf.org/html/rfc4752">RFC</a>.
     *
     * @mongodb.server.release 2.4
     * @mongodb.driver.manual core/authentication/#kerberos-authentication GSSAPI
     */
    public static final String GSSAPI_MECHANISM = AuthenticationMechanism.GSSAPI.getMechanismName();

    /**
     * The PLAIN mechanism.  See the <a href="http://www.ietf.org/rfc/rfc4616.txt">RFC</a>.
     *
     * @since 2.12
     * @mongodb.server.release 2.6
     * @mongodb.driver.manual core/authentication/#ldap-proxy-authority-authentication PLAIN
     */
    public static final String PLAIN_MECHANISM = AuthenticationMechanism.PLAIN.getMechanismName();

    /**
     * The MongoDB X.509
     *
     * @since 2.12
     * @mongodb.server.release 2.6
     * @mongodb.driver.manual core/authentication/#x-509-certificate-authentication X-509
     */
    public static final String MONGODB_X509_MECHANISM = AuthenticationMechanism.MONGODB_X509.getMechanismName();

    /**
     * The SCRAM-SHA-1 Mechanism.
     *
     * @since 2.13
     * @mongodb.server.release 3.0
     * @mongodb.driver.manual core/authentication/#authentication-scram-sha-1 SCRAM-SHA-1
     */
    public static final String SCRAM_SHA_1_MECHANISM = AuthenticationMechanism.SCRAM_SHA_1.getMechanismName();

    /**
     * Mechanism property key for overriding the service name for GSSAPI authentication.
     *
     * @see #createGSSAPICredential(String)
     * @see #withMechanismProperty(String, Object)
     * @since 3.3
     */
    public static final String SERVICE_NAME_KEY = "SERVICE_NAME";

    /**
     * Mechanism property key for specifying whether to canonicalize the host name for GSSAPI authentication.
     *
     * @see #createGSSAPICredential(String)
     * @see #withMechanismProperty(String, Object)
     * @since 3.3
     */
    public static final String CANONICALIZE_HOST_NAME_KEY = "CANONICALIZE_HOST_NAME";

    /**
     * Mechanism property key for overriding the SaslClient properties for GSSAPI authentication.
     *
     * The value of this property must be a {@code Map<String, Object>}.  In most cases there is no need to set this mechanism property.
     * But if an application does:
     * <ul>
     * <li>Generally it must set the {@link javax.security.sasl.Sasl#CREDENTIALS} property to an instance of
     * {@link org.ietf.jgss.GSSCredential}.</li>
     * <li>It's recommended that it set the {@link javax.security.sasl.Sasl#MAX_BUFFER} property to "0" to ensure compatibility with all
     * versions of MongoDB.</li>
     * </ul>
     *
     * @see #createGSSAPICredential(String)
     * @see #withMechanismProperty(String, Object)
     * @see javax.security.sasl.Sasl
     * @see javax.security.sasl.Sasl#CREDENTIALS
     * @see javax.security.sasl.Sasl#MAX_BUFFER
     * @since 3.3
     */
    public static final String JAVA_SASL_CLIENT_PROPERTIES_KEY = "JAVA_SASL_CLIENT_PROPERTIES";

    /**
     * Mechanism property key for overriding the {@link javax.security.auth.Subject} under which GSSAPI authentication executes.
     *
     * @see #createGSSAPICredential(String)
     * @see #withMechanismProperty(String, Object)
     * @since 3.3
     */
    public static final String JAVA_SUBJECT_KEY = "JAVA_SUBJECT";

    /**
     * Creates a MongoCredential instance with an unspecified mechanism.  The client will negotiate the best mechanism based on the
     * version of the server that the client is authenticating to.  If the server version is 3.0 or higher,
     * the driver will authenticate using the SCRAM-SHA-1 mechanism.  Otherwise, the driver will authenticate using the MONGODB_CR
     * mechanism.
     *
     *
     * @param userName the user name
     * @param database the database where the user is defined
     * @param password the user's password
     * @return the credential
     *
     * @since 2.13
     * @mongodb.driver.manual core/authentication/#mongodb-cr-authentication MONGODB-CR
     * @mongodb.driver.manual core/authentication/#authentication-scram-sha-1 SCRAM-SHA-1
     */
    public static MongoCredential createCredential(final String userName, final String database, final char[] password) {
        return new MongoCredential(null, userName, database, password);
    }

    /**
     * Creates a MongoCredential instance for the SCRAM-SHA-1 SASL mechanism. Use this method only if you want to ensure that
     * the driver uses the SCRAM-SHA-1 mechanism regardless of whether the server you are connecting to supports the
     * authentication mechanism.  Otherwise use the {@link #createCredential(String, String, char[])} method to allow the driver to
     * negotiate the best mechanism based on the server version.
     *
     *
     * @param userName the non-null user name
     * @param source the source where the user is defined.
     * @param password the non-null user password
     * @return the credential
     * @see #createCredential(String, String, char[])
     *
     * @since 2.13
     * @mongodb.server.release 3.0
     * @mongodb.driver.manual core/authentication/#authentication-scram-sha-1 SCRAM-SHA-1
     */
    public static MongoCredential createScramSha1Credential(final String userName, final String source, final char[] password) {
        return new MongoCredential(SCRAM_SHA_1, userName, source, password);
    }

    /**
     * Creates a MongoCredential instance for the MongoDB Challenge Response protocol. Use this method only if you want to ensure that
     * the driver uses the MONGODB_CR mechanism regardless of whether the server you are connecting to supports a more secure
     * authentication mechanism.  Otherwise use the {@link #createCredential(String, String, char[])} method to allow the driver to
     * negotiate the best mechanism based on the server version.
     *
     * @param userName the user name
     * @param database the database where the user is defined
     * @param password the user's password
     * @return the credential
     * @see #createCredential(String, String, char[])
     * @mongodb.driver.manual core/authentication/#mongodb-cr-authentication MONGODB-CR
     */
    public static MongoCredential createMongoCRCredential(final String userName, final String database, final char[] password) {
        return new MongoCredential(MONGODB_CR, userName, database, password);
    }

    /**
     * Creates a MongoCredential instance for the MongoDB X.509 protocol.
     *
     * @param userName the user name
     * @return the credential
     *
     * @since 2.12
     * @mongodb.server.release 2.6
     * @mongodb.driver.manual core/authentication/#x-509-certificate-authentication X-509
     */
    public static MongoCredential createMongoX509Credential(final String userName) {
        return new MongoCredential(MONGODB_X509, userName, "$external", null);
    }

    /**
     * Creates a MongoCredential instance for the MongoDB X.509 protocol where the distinguished subject name of the client certificate
     * acts as the userName.
     * <p>
     *     Available on MongoDB server versions &gt;= 3.4.
     * </p>
     * @return the credential
     *
     * @since 3.4
     * @mongodb.server.release 3.4
     * @mongodb.driver.manual core/authentication/#x-509-certificate-authentication X-509
     */
    public static MongoCredential createMongoX509Credential() {
        return new MongoCredential(MONGODB_X509, null, "$external", null);
    }

    /**
     * Creates a MongoCredential instance for the PLAIN SASL mechanism.
     *
     * @param userName the non-null user name
     * @param source   the source where the user is defined.  This can be either {@code "$external"} or the name of a database.
     * @param password the non-null user password
     * @return the credential
     *
     * @since 2.12
     * @mongodb.server.release 2.6
     * @mongodb.driver.manual core/authentication/#ldap-proxy-authority-authentication PLAIN
     */
    public static MongoCredential createPlainCredential(final String userName, final String source, final char[] password) {
        return new MongoCredential(PLAIN, userName, source, password);
    }

    /**
     * Creates a MongoCredential instance for the GSSAPI SASL mechanism.
     * <p>
     * To override the default service name of {@code "mongodb"}, add a mechanism property with the name {@code "SERVICE_NAME"}.
     * <p>
     * To force canonicalization of the host name prior to authentication, add a mechanism property with the name
     * {@code "CANONICALIZE_HOST_NAME"} with the value{@code true}.
     * <p>
     * To override the {@link javax.security.auth.Subject} with which the authentication executes, add a mechanism property with the name
     * {@code "JAVA_SUBJECT"} with the value of a {@code Subject} instance.
     * <p>
     * To override the properties of the {@link javax.security.sasl.SaslClient} with which the authentication executes, add a mechanism
     * property with the name {@code "JAVA_SASL_CLIENT_PROPERTIES"} with the value of a {@code Map<String, Object} instance containing the
     * necessary properties.  This can be useful if the application is customizing the default
     * {@link javax.security.sasl.SaslClientFactory}.
     *
     * @param userName the non-null user name
     * @return the credential
     * @mongodb.server.release 2.4
     * @mongodb.driver.manual core/authentication/#kerberos-authentication GSSAPI
     * @see #withMechanismProperty(String, Object)
     * @see #SERVICE_NAME_KEY
     * @see #CANONICALIZE_HOST_NAME_KEY
     * @see #JAVA_SUBJECT_KEY
     * @see #JAVA_SASL_CLIENT_PROPERTIES_KEY
     */
    public static MongoCredential createGSSAPICredential(final String userName) {
        return new MongoCredential(GSSAPI, userName, "$external", null);
    }

    /**
     * Creates a new MongoCredential as a copy of this instance, with the specified mechanism property added.
     *
     * @param key   the key to the property, which is treated as case-insensitive
     * @param value the value of the property
     * @param <T>   the property type
     * @return the credential
     * @since 2.12
     */
    public <T> MongoCredential withMechanismProperty(final String key, final T value) {
        return new MongoCredential(this, key, value);
    }

    /**
     * Constructs a new instance using the given mechanism, userName, source, and password
     *
     * @param mechanism the authentication mechanism
     * @param userName  the user name
     * @param source    the source of the user name, typically a database name
     * @param password  the password
     */
    MongoCredential(final AuthenticationMechanism mechanism, final String userName, final String source, final char[] password) {
        if (mechanism != MONGODB_X509 && userName == null) {
            throw new IllegalArgumentException("username can not be null");
        }

        if (mechanism == null && password == null) {
            throw new IllegalArgumentException("Password can not be null when the authentication mechanism is unspecified");
        }

        if ((mechanism == PLAIN || mechanism == MONGODB_CR || mechanism == SCRAM_SHA_1) && password == null) {
            throw new IllegalArgumentException("Password can not be null for " + mechanism + " mechanism");
        }

        if ((mechanism == GSSAPI || mechanism == MONGODB_X509) && password != null) {
            throw new IllegalArgumentException("Password must be null for the " + mechanism + " mechanism");
        }

        this.mechanism = mechanism;
        this.userName = userName;
        this.source = notNull("source", source);

        this.password = password != null ? password.clone() : null;
        this.mechanismProperties = Collections.emptyMap();
    }

    /**
     * Constructs a new instance using the given credential plus an additional mechanism property.
     *
     * @param from                   the credential to copy from
     * @param mechanismPropertyKey   the new mechanism property key
     * @param mechanismPropertyValue the new mechanism property value
     * @param <T>                    the mechanism property type
     */
    <T> MongoCredential(final MongoCredential from, final String mechanismPropertyKey, final T mechanismPropertyValue) {
        notNull("mechanismPropertyKey", mechanismPropertyKey);

        this.mechanism = from.mechanism;
        this.userName = from.userName;
        this.source = from.source;
        this.password = from.password;
        this.mechanismProperties = new HashMap<String, Object>(from.mechanismProperties);
        this.mechanismProperties.put(mechanismPropertyKey.toLowerCase(), mechanismPropertyValue);
    }

    /**
     * Gets the mechanism
     *
     * @return the mechanism.
     */
    public String getMechanism() {
        return mechanism == null ? null : mechanism.getMechanismName();
    }

    /**
     * Gets the mechanism
     *
     * @return the mechanism.
     * @since 3.0
     */
    public AuthenticationMechanism getAuthenticationMechanism() {
        return mechanism;
    }

    /**
     * Gets the user name
     *
     * @return the user name.  Can never be null.
     */
    public String getUserName() {
        return userName;
    }

    /**
     * Gets the source of the user name, typically the name of the database where the user is defined.
     *
     * @return the user name.  Can never be null.
     */
    public String getSource() {
        return source;
    }

    /**
     * Gets the password.
     *
     * @return the password.  Can be null for some mechanisms.
     */
    public char[] getPassword() {
        if (password == null) {
            return null;
        }
        return password.clone();
    }

    /**
     * Get the value of the given key to a mechanism property, or defaultValue if there is no mapping.
     *
     * @param key          the mechanism property key, which is treated as case-insensitive
     * @param defaultValue the default value, if no mapping exists
     * @param <T>          the value type
     * @return the mechanism property value
     * @since 2.12
     */
    @SuppressWarnings("unchecked")
    public <T> T getMechanismProperty(final String key, final T defaultValue) {
        notNull("key", key);

        T value = (T) mechanismProperties.get(key.toLowerCase());
        return (value == null) ? defaultValue : value;

    }

    @Override
    public boolean equals(final Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }

        MongoCredential that = (MongoCredential) o;

        if (mechanism != that.mechanism) {
            return false;
        }
        if (!Arrays.equals(password, that.password)) {
            return false;
        }
        if (!source.equals(that.source)) {
            return false;
        }
        if (userName != null ? !userName.equals(that.userName) : that.userName != null) {
            return false;
        }
        if (!mechanismProperties.equals(that.mechanismProperties)) {
            return false;
        }

        return true;
    }

    @Override
    public int hashCode() {
        int result = mechanism != null ? mechanism.hashCode() : 0;
        result = 31 * result + (userName != null ? userName.hashCode() : 0);
        result = 31 * result + source.hashCode();
        result = 31 * result + (password != null ? Arrays.hashCode(password) : 0);
        result = 31 * result + mechanismProperties.hashCode();
        return result;
    }

    @Override
    public String toString() {
        return "MongoCredential{"
               + "mechanism=" + mechanism
               + ", userName='" + userName + '\''
               + ", source='" + source + '\''
               + ", password=<hidden>"
               + ", mechanismProperties=" + mechanismProperties
               + '}';
    }
}

