/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.apache.hadoop.security;

import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.charset.StandardCharsets;

import org.apache.hadoop.classification.VisibleForTesting;
import org.apache.commons.io.IOUtils;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.security.alias.CredentialProviderFactory;
import org.apache.hadoop.security.alias.JavaKeyStoreProvider;
import org.apache.hadoop.security.alias.LocalJavaKeyStoreProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Utility methods for both key and credential provider APIs.
 *
 */
public final class ProviderUtils {
  @VisibleForTesting
  public static final String NO_PASSWORD_WARN =
      "WARNING: You have accepted the use of the default provider password\n" +
      "by not configuring a password in one of the two following locations:\n";
  @VisibleForTesting
  public static final String NO_PASSWORD_ERROR =
      "ERROR: The provider cannot find a password in the expected " +
      "locations.\nPlease supply a password using one of the " +
      "following two mechanisms:\n";
  @VisibleForTesting
  public static final String NO_PASSWORD_CONT =
      "Continuing with the default provider password.\n";
  @VisibleForTesting
  public static final String NO_PASSWORD_INSTRUCTIONS_DOC =
      "Please review the documentation regarding provider passwords in\n" +
      "the keystore passwords section of the Credential Provider API\n";

  private static final Logger LOG =
      LoggerFactory.getLogger(ProviderUtils.class);

  /**
   * Hidden ctor to ensure that this utility class isn't
   * instantiated explicitly.
   */
  private ProviderUtils() {
    // hide ctor for checkstyle compliance
  }

  /**
   * Convert a nested URI to decode the underlying path. The translation takes
   * the authority and parses it into the underlying scheme and authority.
   * For example, "myscheme://hdfs@nn/my/path" is converted to
   * "hdfs://nn/my/path".
   * @param nestedUri the URI from the nested URI
   * @return the unnested path
   */
  public static Path unnestUri(URI nestedUri) {
    StringBuilder result = new StringBuilder();
    String authority = nestedUri.getAuthority();
    if (authority != null) {
      String[] parts = nestedUri.getAuthority().split("@", 2);
      result.append(parts[0])
          .append("://");
      if (parts.length == 2) {
        result.append(parts[1]);
      }
    }
    result.append(nestedUri.getPath());
    if (nestedUri.getQuery() != null) {
      result.append("?");
      result.append(nestedUri.getQuery());
    }
    if (nestedUri.getFragment() != null) {
      result.append("#");
      result.append(nestedUri.getFragment());
    }
    return new Path(result.toString());
  }

  /**
   * Mangle given local java keystore file URI to allow use as a
   * LocalJavaKeyStoreProvider.
   * @param localFile absolute URI with file scheme and no authority component.
   *                  i.e. return of File.toURI,
   *                  e.g. file:///home/larry/creds.jceks
   * @return URI of the form localjceks://file/home/larry/creds.jceks
   * @throws IllegalArgumentException if localFile isn't not a file uri or if it
   *                                  has an authority component.
   * @throws URISyntaxException if the wrapping process violates RFC 2396
   */
  public static URI nestURIForLocalJavaKeyStoreProvider(final URI localFile)
      throws URISyntaxException {
    if (!("file".equals(localFile.getScheme()))) {
      throw new IllegalArgumentException("passed URI had a scheme other than " +
          "file.");
    }
    if (localFile.getAuthority() != null) {
      throw new IllegalArgumentException("passed URI must not have an " +
          "authority component. For non-local keystores, please use " +
          JavaKeyStoreProvider.class.getName());
    }
    return new URI(LocalJavaKeyStoreProvider.SCHEME_NAME,
        "//file" + localFile.getSchemeSpecificPart(), localFile.getFragment());
  }

  /**
   * There are certain integrations of the credential provider API in
   * which a recursive dependency between the provider and the hadoop
   * filesystem abstraction causes a problem. These integration points
   * need to leverage this utility method to remove problematic provider
   * types from the existing provider path within the configuration.
   *
   * @param config the existing configuration with provider path
   * @param fileSystemClass the class which providers must be compatible
   * @return Configuration clone with new provider path
   * @throws IOException raised on errors performing I/O.
   */
  public static Configuration excludeIncompatibleCredentialProviders(
      Configuration config, Class<? extends FileSystem> fileSystemClass)
          throws IOException {

    String providerPath = config.get(
        CredentialProviderFactory.CREDENTIAL_PROVIDER_PATH);

    if (providerPath == null) {
      return config;
    }
    StringBuilder newProviderPath = new StringBuilder();
    String[] providers = providerPath.split(",");
    Path path = null;
    for (String provider: providers) {
      try {
        path = unnestUri(new URI(provider));
        Class<? extends FileSystem> clazz = null;
        try {
          String scheme = path.toUri().getScheme();
          clazz = FileSystem.getFileSystemClass(scheme, config);
        } catch (IOException ioe) {
          // not all providers are filesystem based
          // for instance user:/// will not be able to
          // have a filesystem class associated with it.
          if (newProviderPath.length() > 0) {
            newProviderPath.append(",");
          }
          newProviderPath.append(provider);
        }
        if (clazz != null) {
          if (fileSystemClass.isAssignableFrom(clazz)) {
            LOG.debug("Filesystem based provider excluded from provider " +
                "path due to recursive dependency: {}", provider);
          } else {
            if (newProviderPath.length() > 0) {
              newProviderPath.append(",");
            }
            newProviderPath.append(provider);
          }
        }
      } catch (URISyntaxException e) {
        LOG.warn("Credential Provider URI is invalid." + provider);
      }
    }

    String effectivePath = newProviderPath.toString();
    if (effectivePath.equals(providerPath)) {
      return config;
    }

    Configuration conf = new Configuration(config);
    if (effectivePath.equals("")) {
      conf.unset(CredentialProviderFactory.CREDENTIAL_PROVIDER_PATH);
    } else {
      conf.set(CredentialProviderFactory.CREDENTIAL_PROVIDER_PATH,
          effectivePath);
    }
    return conf;
  }

  /**
   * The password is either found in the environment or in a file. This
   * routine implements the logic for locating the password in these
   * locations.
   *
   * @param envWithPass  The name of the environment variable that might
   *                     contain the password. Must not be null.
   * @param fileWithPass The name of a file that could contain the password.
   *                     Can be null.
   * @return The password as a char []; null if not found.
   * @throws IOException If fileWithPass is non-null and points to a
   * nonexistent file or a file that fails to open and be read properly.
   */
  public static char[] locatePassword(String envWithPass, String fileWithPass)
      throws IOException {
    char[] pass = null;
    if (System.getenv().containsKey(envWithPass)) {
      pass = System.getenv(envWithPass).toCharArray();
    }
    if (pass == null) {
      if (fileWithPass != null) {
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        URL pwdFile = cl.getResource(fileWithPass);
        if (pwdFile == null) {
          // Provided Password file does not exist
          throw new IOException("Password file does not exist");
        }
        try (InputStream is = pwdFile.openStream()) {
          pass = IOUtils.toString(is, StandardCharsets.UTF_8).trim().toCharArray();
        }
      }
    }
    return pass;
  }

  private static String noPasswordInstruction(String envKey, String fileKey) {
    return
        "    * In the environment variable " + envKey + "\n" +
        "    * In a file referred to by the configuration entry\n" +
        "      " + fileKey + ".\n" +
        NO_PASSWORD_INSTRUCTIONS_DOC;
  }

  public static String noPasswordWarning(String envKey, String fileKey) {
    return NO_PASSWORD_WARN + noPasswordInstruction(envKey, fileKey) +
        NO_PASSWORD_CONT;
  }

  public static String noPasswordError(String envKey, String fileKey) {
    return NO_PASSWORD_ERROR + noPasswordInstruction(envKey, fileKey);
  }
}
