001    /**
002     * Licensed to the Apache Software Foundation (ASF) under one
003     * or more contributor license agreements.  See the NOTICE file
004     * distributed with this work for additional information
005     * regarding copyright ownership.  The ASF licenses this file
006     * to you under the Apache License, Version 2.0 (the
007     * "License"); you may not use this file except in compliance
008     * with the License.  You may obtain a copy of the License at
009     *
010     *     http://www.apache.org/licenses/LICENSE-2.0
011     *
012     * Unless required by applicable law or agreed to in writing, software
013     * distributed under the License is distributed on an "AS IS" BASIS,
014     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015     * See the License for the specific language governing permissions and
016     * limitations under the License.
017     */
018    
019    package org.apache.hadoop.security.alias;
020    
021    import java.io.Console;
022    import java.io.IOException;
023    import java.io.PrintStream;
024    import java.security.InvalidParameterException;
025    import java.security.NoSuchAlgorithmException;
026    import java.util.Arrays;
027    import java.util.List;
028    
029    import org.apache.hadoop.conf.Configuration;
030    import org.apache.hadoop.conf.Configured;
031    import org.apache.hadoop.util.Tool;
032    import org.apache.hadoop.util.ToolRunner;
033    
034    /**
035     * This program is the CLI utility for the CredentialProvider facilities in 
036     * Hadoop.
037     */
038    public class CredentialShell extends Configured implements Tool {
039      final static private String USAGE_PREFIX = "Usage: hadoop credential " +
040                    "[generic options]\n";
041      final static private String COMMANDS =
042          "   [--help]\n" +
043          "   [" + CreateCommand.USAGE + "]\n" +
044          "   [" + DeleteCommand.USAGE + "]\n" +
045          "   [" + ListCommand.USAGE + "]\n";
046    
047      private boolean interactive = false;
048      private Command command = null;
049    
050      /** allows stdout to be captured if necessary */
051      public PrintStream out = System.out;
052      /** allows stderr to be captured if necessary */
053      public PrintStream err = System.err;
054    
055      private boolean userSuppliedProvider = false;
056      private String value = null;
057      private PasswordReader passwordReader;
058    
059      @Override
060      public int run(String[] args) throws Exception {
061        int exitCode = 0;
062        try {
063          exitCode = init(args);
064          if (exitCode != 0) {
065            return exitCode;
066          }
067          if (command.validate()) {
068              command.execute();
069          } else {
070            exitCode = 1;
071          }
072        } catch (Exception e) {
073          e.printStackTrace(err);
074          return 1;
075        }
076        return exitCode;
077      }
078    
079      /**
080       * Parse the command line arguments and initialize the data
081       * <pre>
082       * % hadoop credential create alias [-provider providerPath]
083       * % hadoop credential list [-provider providerPath]
084       * % hadoop credential delete alias [-provider providerPath] [-i]
085       * </pre>
086       * @param args
087       * @return 0 if the argument(s) were recognized, 1 otherwise
088       * @throws IOException
089       */
090      protected int init(String[] args) throws IOException {
091        // no args should print the help message
092        if (0 == args.length) {
093          printCredShellUsage();
094          ToolRunner.printGenericCommandUsage(System.err);
095          return 1;
096        }
097    
098        for (int i = 0; i < args.length; i++) { // parse command line
099          if (args[i].equals("create")) {
100            String alias = args[++i];
101            command = new CreateCommand(alias);
102            if (alias.equals("-help")) {
103              printCredShellUsage();
104              return 0;
105            }
106          } else if (args[i].equals("delete")) {
107            String alias = args[++i];
108            command = new DeleteCommand(alias);
109            if (alias.equals("-help")) {
110              printCredShellUsage();
111              return 0;
112            }
113          } else if (args[i].equals("list")) {
114            command = new ListCommand();
115          } else if (args[i].equals("-provider")) {
116            userSuppliedProvider = true;
117            getConf().set(CredentialProviderFactory.CREDENTIAL_PROVIDER_PATH, 
118                args[++i]);
119          } else if (args[i].equals("-i") || (args[i].equals("-interactive"))) {
120            interactive = true;
121          } else if (args[i].equals("-v") || (args[i].equals("-value"))) {
122            value = args[++i];
123          } else if (args[i].equals("-help")) {
124            printCredShellUsage();
125            return 0;
126          } else {
127            printCredShellUsage();
128            ToolRunner.printGenericCommandUsage(System.err);
129            return 1;
130          }
131        }
132        return 0;
133      }
134    
135      private void printCredShellUsage() {
136        out.println(USAGE_PREFIX + COMMANDS);
137        if (command != null) {
138          out.println(command.getUsage());
139        }
140        else {
141          out.println("=========================================================" +
142                    "======");
143          out.println(CreateCommand.USAGE + ":\n\n" + CreateCommand.DESC);
144          out.println("=========================================================" +
145              "======");
146          out.println(DeleteCommand.USAGE + ":\n\n" + DeleteCommand.DESC);
147          out.println("=========================================================" +
148              "======");
149          out.println(ListCommand.USAGE + ":\n\n" + ListCommand.DESC);
150        }
151      }
152    
153      private abstract class Command {
154        protected CredentialProvider provider = null;
155    
156        public boolean validate() {
157          return true;
158        }
159    
160        protected CredentialProvider getCredentialProvider() {
161          CredentialProvider provider = null;
162          List<CredentialProvider> providers;
163          try {
164            providers = CredentialProviderFactory.getProviders(getConf());
165            if (userSuppliedProvider) {
166              provider = providers.get(0);
167            }
168            else {
169              for (CredentialProvider p : providers) {
170                if (!p.isTransient()) {
171                  provider = p;
172                  break;
173                }
174              }
175            }
176          } catch (IOException e) {
177            e.printStackTrace(err);
178          }
179          return provider;
180        }
181    
182        protected void printProviderWritten() {
183            out.println(provider.getClass().getName() + " has been updated.");
184        }
185    
186        protected void warnIfTransientProvider() {
187          if (provider.isTransient()) {
188            out.println("WARNING: you are modifying a transient provider.");
189          }
190        }
191    
192        public abstract void execute() throws Exception;
193    
194        public abstract String getUsage();
195      }
196    
197      private class ListCommand extends Command {
198        public static final String USAGE = "list [-provider] [-help]";
199        public static final String DESC =
200            "The list subcommand displays the aliases contained within \n" +
201            "a particular provider - as configured in core-site.xml or " +
202            "indicated\nthrough the -provider argument.";
203    
204        public boolean validate() {
205          boolean rc = true;
206          provider = getCredentialProvider();
207          if (provider == null) {
208            out.println("There are no non-transient CredentialProviders configured.\n"
209                + "Consider using the -provider option to indicate the provider\n"
210                + "to use. If you want to list a transient provider then you\n"
211                + "you MUST use the -provider argument.");
212            rc = false;
213          }
214          return rc;
215        }
216    
217        public void execute() throws IOException {
218          List<String> aliases;
219          try {
220            aliases = provider.getAliases();
221            out.println("Listing aliases for CredentialProvider: " + provider.toString());
222            for (String alias : aliases) {
223              out.println(alias);
224            }
225          } catch (IOException e) {
226            out.println("Cannot list aliases for CredentialProvider: " + provider.toString()
227                + ": " + e.getMessage());
228            throw e;
229          }
230        }
231    
232        @Override
233        public String getUsage() {
234          return USAGE + ":\n\n" + DESC;
235        }
236      }
237    
238      private class DeleteCommand extends Command {
239        public static final String USAGE = "delete <alias> [-provider] [-help]";
240        public static final String DESC =
241            "The delete subcommand deletes the credenital\n" +
242            "specified as the <alias> argument from within the provider\n" +
243            "indicated through the -provider argument";
244    
245        String alias = null;
246        boolean cont = true;
247    
248        public DeleteCommand(String alias) {
249          this.alias = alias;
250        }
251    
252        @Override
253        public boolean validate() {
254          provider = getCredentialProvider();
255          if (provider == null) {
256            out.println("There are no valid CredentialProviders configured.\n"
257                + "Nothing will be deleted.\n"
258                + "Consider using the -provider option to indicate the provider"
259                + " to use.");
260            return false;
261          }
262          if (alias == null) {
263            out.println("There is no alias specified. Please provide the" +
264                "mandatory <alias>. See the usage description with -help.");
265            return false;
266          }
267          if (interactive) {
268            try {
269              cont = ToolRunner
270                  .confirmPrompt("You are about to DELETE the credential: " + 
271                      alias + " from CredentialProvider " + provider.toString() +
272                      ". Continue?:");
273              if (!cont) {
274                out.println("Nothing has been be deleted.");
275              }
276              return cont;
277            } catch (IOException e) {
278              out.println(alias + " will not be deleted.");
279              e.printStackTrace(err);
280            }
281          }
282          return true;
283        }
284    
285        public void execute() throws IOException {
286          warnIfTransientProvider();
287          out.println("Deleting credential: " + alias + " from CredentialProvider: "
288              + provider.toString());
289          if (cont) {
290            try {
291              provider.deleteCredentialEntry(alias);
292              out.println(alias + " has been successfully deleted.");
293              provider.flush();
294              printProviderWritten();
295            } catch (IOException e) {
296              out.println(alias + "has NOT been deleted.");
297              throw e;
298            }
299          }
300        }
301    
302        @Override
303        public String getUsage() {
304          return USAGE + ":\n\n" + DESC;
305        }
306      }
307    
308      private class CreateCommand extends Command {
309        public static final String USAGE = "create <alias> [-provider] [-help]";
310        public static final String DESC =
311            "The create subcommand creates a new credential for the name specified\n" +
312            "as the <alias> argument within the provider indicated through\n" +
313            "the -provider argument.";
314    
315        String alias = null;
316    
317        public CreateCommand(String alias) {
318          this.alias = alias;
319        }
320    
321        public boolean validate() {
322          boolean rc = true;
323          provider = getCredentialProvider();
324          if (provider == null) {
325            out.println("There are no valid CredentialProviders configured." +
326                            "\nCredential will not be created.\n"
327                + "Consider using the -provider option to indicate the provider" +
328                " to use.");
329            rc = false;
330          }
331          if (alias == null) {
332            out.println("There is no alias specified. Please provide the" +
333                "mandatory <alias>. See the usage description with -help.");
334            rc = false;
335          }
336          return rc;
337        }
338    
339        public void execute() throws IOException, NoSuchAlgorithmException {
340          warnIfTransientProvider();
341          try {
342            char[] credential = null;
343            if (value != null) {
344              // testing only
345              credential = value.toCharArray();
346            }
347            else {
348               credential = promptForCredential();
349            }
350            provider.createCredentialEntry(alias, credential);
351            out.println(alias + " has been successfully created.");
352            provider.flush();
353            printProviderWritten();
354          } catch (InvalidParameterException e) {
355            out.println(alias + " has NOT been created. " + e.getMessage());
356            throw e;
357          } catch (IOException e) {
358            out.println(alias + " has NOT been created. " + e.getMessage());
359            throw e;
360          }
361        }
362    
363        @Override
364        public String getUsage() {
365          return USAGE + ":\n\n" + DESC;
366        }
367      }
368      
369      protected char[] promptForCredential() throws IOException {
370        PasswordReader c = getPasswordReader();
371        if (c == null) {
372          throw new IOException("No console available for prompting user.");
373        }
374        
375        char[] cred = null;
376    
377        boolean noMatch;
378        do {
379          char[] newPassword1 = c.readPassword("Enter password: ");
380          char[] newPassword2 = c.readPassword("Enter password again: ");
381          noMatch = !Arrays.equals(newPassword1, newPassword2);
382          if (noMatch) {
383            if (newPassword1 != null) Arrays.fill(newPassword1, ' ');
384            c.format("Passwords don't match. Try again.%n");
385          } else {
386            cred = newPassword1;
387          }
388          if (newPassword2 != null) Arrays.fill(newPassword2, ' ');
389        } while (noMatch);
390        return cred;
391      }
392      
393      public PasswordReader getPasswordReader() {
394        if (passwordReader == null) {
395          passwordReader = new PasswordReader();
396        }
397        return passwordReader;
398      }
399      
400      public void setPasswordReader(PasswordReader reader) {
401        passwordReader = reader;
402      }
403      
404      // to facilitate testing since Console is a final class...
405      public static class PasswordReader {
406        public char[] readPassword(String prompt) {
407          Console console = System.console();
408          char[] pass = console.readPassword(prompt);
409          return pass;
410        }
411    
412        public void format(String message) {
413          Console console = System.console();
414          console.format(message);
415        }
416      }
417      
418      
419      /**
420       * Main program.
421       *
422       * @param args
423       *          Command line arguments
424       * @throws Exception
425       */
426      public static void main(String[] args) throws Exception {
427        int res = ToolRunner.run(new Configuration(), new CredentialShell(), args);
428        System.exit(res);
429      }
430    }