content: /*
   * 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.ambari.server.credentialapi;
  
  import java.io.IOException;
  import java.util.ArrayList;
  import java.util.List;
  
  import org.apache.commons.lang.StringUtils;
  import org.apache.hadoop.conf.Configuration;
  import org.apache.hadoop.conf.Configured;
  import org.apache.hadoop.security.alias.CredentialProvider;
  import org.apache.hadoop.security.alias.CredentialProviderFactory;
  import org.apache.hadoop.security.alias.CredentialShell;
  import org.apache.hadoop.security.alias.JavaKeyStoreProvider;
  import org.apache.hadoop.util.Tool;
  import org.apache.hadoop.util.ToolRunner;
  
  /**
   * Command line utility that wraps over CredentialShell. Extends the
   * create command to overwrite a credential if it exists. Also
   * provides the ability to get the decrypted password.
   *
   * The CLI structure and code is the same as CredentialShell.
   */
  public class CredentialUtil extends Configured implements Tool {
    /**
     * List of supported commands.
     */
    final static private String COMMANDS =
            "   [--help]\n" +
                    "   [" + CreateCommand.USAGE + "]\n" +
                    "   [" + DeleteCommand.USAGE + "]\n" +
                    "   [" + ListCommand.USAGE + "]\n" +
                    "   [" + GetCommand.USAGE + "]\n";
  
    /**
     * JCEKS provider prefix.
     */
    public static final String jceksPrefix = JavaKeyStoreProvider.SCHEME_NAME + "://file";
  
    /**
     * Local JCEKS provider prefix.
     */
    public static final String localJceksPrefix = "localjceks://file";
  
    /**
     * Password alias
     */
    private String alias = null;
  
    /**
     * Password specified using the -value option
     */
    private String value = null;
  
    /**
     * Provider specified using the -provider option
     */
    protected CredentialProvider provider;
  
    /**
     * When creating a credential, overwrite the credential if it exists.
     * If -n option is specified, this will be set to false.
     */
    private boolean overwrite = true;
  
    /**
     * Prompt for user confirmation before deleting/overwriting a credential.
     * If -f option is specified, it will be set to false. In the case
     * of a create command, it will be set to false if -n or -f is specified.
     */
    private boolean interactive = true;
  
    /**
     * One of the supported credential commands.
     */
    private Command command = null;
  
    /**
     * Main program.
     *
     * @param args Command line arguments
     * @throws Exception
     */
    public static void main(String[] args) throws Exception {
      int res = ToolRunner.run(new Configuration(), new CredentialUtil(), args);
      System.exit(res);
    }
  
    /**
     * Called by ToolRunner.run(). This is the entry point to the tool.
     * Parses the command line arguments and executes the appropriate command.
     *
     * @param args - Arguments supplied by the user.
     * @return - 0 if successful. 1 in case of a failure.
     * @throws Exception - If something goes wrong during command execution.
     */
    @Override
    public int run(String[] args) throws Exception {
      int exitCode = 1;
  
      for (int i = 0; i < args.length; ++i) {
        if (args[i].equals("create")) {
          if (i == args.length - 1) {
            return 1;
          }
          command = new CreateCommand();
          alias = args[++i];
          if (alias.equals("-h") || alias.equals("-help")) {
            printUsage();
            return 0;
          }
        } else if (args[i].equals("get")) {
          if (i == args.length - 1) {
            return 1;
          }
          command = new GetCommand();
          alias = args[++i];
          if (alias.equals("-h") || alias.equals("-help")) {
            printUsage();
            return 0;
          }
        } else if (args[i].equals("delete")) {
          if (i == args.length - 1) {
            printUsage();
            return 1;
          }
          command = new DeleteCommand();
          alias = args[++i];
          if (alias.equals("-help")) {
            printUsage();
            return 0;
          }
        } else if (args[i].equals("list")) {
          if (i < args.length - 1) {
            alias = args[i + 1];
          }
          command = new ListCommand();
          if (alias.equals("-h") || alias.equals("-help")) {
            printUsage();
            return 0;
          }
          alias = "not required";
        } else if (args[i].equals("-provider")) {
          if (i == args.length - 1) {
            return 1;
          }
          String providerPath = getNormalizedPath(args[++i]);
          getConf().set(CredentialProviderFactory.CREDENTIAL_PROVIDER_PATH, providerPath);
          provider = getCredentialProvider();
        } else if (args[i].equals("-f") || args[i].equals("-force")) {
          interactive = false;
          overwrite = true;
        } else if (args[i].equals("-n")) {
          interactive = false;
          overwrite = false;
        } else if (args[i].equals("-v") || args[i].equals("-value")) {
          value = args[++i];
        } else if (args[i].equals("-h") || args[i].equals("-help")) {
          printUsage();
          return 0;
        } else {
          printUsage();
          ToolRunner.printGenericCommandUsage(System.err);
          return 1;
        }
      }
  
      if (command == null) {
        printUsage();
      }
      else if (command.validate()) {
        exitCode = command.execute();
      }
  
      return exitCode;
    }
  
    /**
     * Prints a command specific usage or overall tool usage.
     */
    protected void printUsage() {
      System.out.println(getUsagePrefix() + COMMANDS);
      if (command != null) {
        System.out.println(command.getUsage());
      }
      else {
        System.out.println("=========================================================" +
                "======");
        System.out.println(CreateCommand.USAGE + ":\n\n" + CreateCommand.DESC);
        System.out.println("=========================================================" +
                "======");
        System.out.println(DeleteCommand.USAGE + ":\n\n" + DeleteCommand.DESC);
        System.out.println("=========================================================" +
                "======");
        System.out.println(ListCommand.USAGE + ":\n\n" + ListCommand.DESC);
        System.out.println("=========================================================" +
                "======");
        System.out.println(GetCommand.USAGE + ":\n\n" + GetCommand.DESC);
      }
    }
  
    /**
     * Overridden by the command line driver to provide name of the tool.
     *
     * @return - CLI specific information, like tool name, year, copyright, etc.
     */
    protected String getUsagePrefix() {
      return "Usage: ";
    }
  
    /*
     * Normalize the providerPath to jceks://file/<file_path> or localjceks://file/<file_path>
     */
    private static String getNormalizedPath(String providerPath) {
      if (providerPath != null) {
        String jceksPath;
  
        if (providerPath.startsWith("/")) {
          providerPath = providerPath.substring(1);
        }
  
        jceksPath = StringUtils.lowerCase(providerPath.trim());
  
        if (!jceksPath.startsWith(StringUtils.lowerCase(jceksPrefix)) &&
                !jceksPath.startsWith(localJceksPrefix)) {
          providerPath = jceksPrefix + "/" + providerPath;
        }
      }
  
      return providerPath;
    }
  
    /**
     * Gets the provider object for the user specified provider.
     *
     * @return - A credential provider.
     */
    private CredentialProvider getCredentialProvider() {
      CredentialProvider provider = null;
  
      List<CredentialProvider> providers;
      try {
        providers = CredentialProviderFactory.getProviders(getConf());
        provider = providers.get(0);
      } catch (IOException e) {
        e.printStackTrace(System.err);
      }
  
      return provider;
    }
  
    /**
     * CredentialCommand base class
     */
    private abstract class Command {
      /**
       * Validates the user input.
       *
       * @return - True if inputs are valid. False otherwise.
       */
      public boolean validate() {
        boolean rc = true;
  
        if (alias == null || alias.isEmpty()) {
          System.out.println("There is no alias specified. Please provide the" +
                  "mandatory <alias>. See the usage description with -help.");
          rc = false;
        }
  
        if (provider == null) {
          System.out.println("There are no valid CredentialProviders configured." +
                  "\nCredential will not be created.\n"
                  + "Consider using the -provider option to indicate the provider" +
                  " to use.");
          rc = false;
        }
  
        return rc;
      }
  
      /**
       * Gets command usage and description.
       *
       * @return
       */
      public abstract String getUsage();
  
      /**
       * Called by run(). Implemented by the concrete command classes.
       *
       * @return - 0 if successful. 1 on failure.
       * @throws Exception - If something goes wrong.
       */
      public abstract int execute() throws Exception;
    }
  
    /**
     * Gets the credential for the specified alias from the
     * specified provider.
     */
    private class GetCommand extends Command {
      public static final String USAGE = "get <alias> [-provider provider-path]";
      public static final String DESC =
              "The get subcommand gets the credential for the specified alias\n" +
                      "from the provider specified through the -provider argument.\n";
  
      /**
       * Executes the get command. Prints the clear text password on the command line.
       *
       * @return - 0 on success; 1 on failure.
       * @throws IOException
       */
      @Override
      public int execute() throws IOException {
        int exitCode = 0;
  
        try {
          String credential = getCredential();
          if (credential == null) {
            exitCode = 1;
          } else {
            System.out.println(credential);
          }
        } catch (IOException ex) {
          System.out.println("Cannot get the credential for the specified alias."
                  + ": " + ex.getMessage());
          throw ex;
        }
  
        return exitCode;
      }
  
      /**
       * Gets the clear text password from the credential provider.
       *
       * @return - Decrypted password for the specified alias.
       * @throws IOException
       */
      private String getCredential() throws IOException {
        String credential = null;
        CredentialProvider.CredentialEntry credEntry = provider.getCredentialEntry(alias);
  
        if (credEntry != null) {
          char[] password = credEntry.getCredential();
          if (password != null) {
            credential = String.valueOf(password);
          }
        }
  
        return credential;
      }
  
      /**
       * Usage and description.
       *
       * @return
       */
      @Override
      public String getUsage() {
        return USAGE + ":\n\n" + DESC;
      }
    }
  
    /**
     * Creates a new credential for the alias specified or overwrites an
     * existing credential
     */
    private class CreateCommand extends Command {
      /**
       * Usage summary
       */
      public static final String USAGE =
              "create <alias> [-value credential] [-provider provider-path] [-f | -n]";
  
      /**
       * Command description
       */
      public static final String DESC =
              "The create subcommand creates a new credential or overwrites\n" +
                      "an existing credential for the name specified\n" +
                      "as the <alias> argument within the provider indicated through\n" +
                      "the -provider argument. The command asks for confirmation to\n" +
                      "overwrite the existing credential unless the -f option is specified.\n" +
                      "Specify -n to not overwrite if the credential exists.\nThe option specified last wins.";
  
      /**
       * Creates or updates the specified credential.
       *
       * @return - 0 on success; 1 on failure.
       * @throws Exception
       */
      @Override
      public int execute() throws Exception {
        int exitCode = 0;
        CredentialProvider.CredentialEntry credEntry = provider.getCredentialEntry(alias);
  
        if (credEntry != null) {
          /*
           * If credential already exists, overwrite if -f flag was specified.
           * overwrite is true if -f was specified.
           * overwrite is false if -n was specified.
           * if neither options were specified, prompt the user.
           */
          if (interactive) {
            // prompt the user to confirm or reject the overwrite
            overwrite = ToolRunner
                    .confirmPrompt("You are about to OVERWRITE the credential " +
                            alias + " from CredentialProvider " + provider +
                            ". Continue? ");
          }
  
          if (overwrite) {
            // delete the existing credential
            DeleteCommand deleteCommand = new DeleteCommand();
            exitCode = deleteCommand.execute();
          } else {
            // nothing to do
            return 0;
          }
        }
  
        // create new or overwrite existing credential if delete succeeded
        if (exitCode == 0) {
          exitCode = createCredential();
        }
  
        return exitCode;
      }
  
      /**
       * Usage and description.
       * @return
       */
      @Override
      public String getUsage() {
        return USAGE + ":\n\n" + DESC;
      }
  
      /**
       * Creates the specified credential. A credential with the same alias
       * should not exist. It must be deleted before this method is called.
       *
       * @return - 0 on success; 1 on failure.
       * @throws Exception - If the alias already exists.
       */
      private int createCredential() throws Exception {
        int exitCode;
        List<String> args = new ArrayList<>();
  
        args.add("create");
        args.add(alias);
        if (value != null) {
          args.add("-value");
          args.add(value);
        }
  
        String[] toolArgs = args.toArray(new String[args.size()]);
  
        exitCode = ToolRunner.run(getConf(), new CredentialShell(), toolArgs);
  
        return exitCode;
      }
    }
  
    /**
     * Deletes the credential specified by the alias from the
     * specified provider.
     */
    private class DeleteCommand extends Command {
      public static final String USAGE =
              "delete <alias> [-f] [-provider provider-path]";
      public static final String DESC =
              "The delete subcommand deletes the credential specified\n" +
                      "as the <alias> argument from within the provider indicated\n" +
                      "through the -provider argument. The command asks for\n" +
                      "confirmation unless the -f option is specified.";
  
      /**
       * Deletes the specified alias. Prompts for user confirmation
       * if -f option is not specified.
       * @return
       * @throws Exception
       */
      @Override
      public int execute() throws Exception {
        int exitCode;
        List<String> args = new ArrayList<>();
  
        args.add("delete");
        args.add(alias);
        if (!interactive) {
          args.add("-f");
        }
  
        String[] toolArgs = args.toArray(new String[args.size()]);
  
        exitCode = ToolRunner.run(getConf(), new CredentialShell(), toolArgs);
  
        return exitCode;
      }
  
      /**
       * Usage and description.
       *
       * @return
       */
      @Override
      public String getUsage() {
        return USAGE + ":\n\n" + DESC;
      }
    }
  
  
    /**
     * Lists all the aliases contained in the specified provider.
     */
    private class ListCommand extends Command {
      /**
       * Command usage
       */
      public static final String USAGE = "list [-provider provider-path]";
  
      /**
       * Command description
       */
      public static final String DESC =
              "The list subcommand displays the aliases contained within \n" +
                      "a particular provider - as configured in core-site.xml or\n " +
                      "indicated through the -provider argument.";
  
      /**
       * Executes the list command.
       *
       * @return - 0 if successful; 1 otherwise.
       * @throws Exception - If something goes wrong.
       */
      @Override
      public int execute() throws Exception {
        int exitCode;
        List<String> args = new ArrayList<>();
  
        args.add("list");
  
        String[] toolArgs = args.toArray(new String[args.size()]);
  
        exitCode = ToolRunner.run(getConf(), new CredentialShell(), toolArgs);
  
        return exitCode;
      }
  
      /**
       * Usage and description.
       *
       * @return
       */
      @Override
      public String getUsage() {
        return USAGE + ":\n\n" + DESC;
      }
    }
  }