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 019package org.apache.hadoop.security.alias; 020 021import java.io.Console; 022import java.io.IOException; 023import java.io.PrintStream; 024import java.security.InvalidParameterException; 025import java.security.NoSuchAlgorithmException; 026import java.util.Arrays; 027import java.util.List; 028 029import com.google.common.annotations.VisibleForTesting; 030import org.apache.hadoop.conf.Configuration; 031import org.apache.hadoop.conf.Configured; 032import org.apache.hadoop.util.Tool; 033import org.apache.hadoop.util.ToolRunner; 034 035/** 036 * This program is the CLI utility for the CredentialProvider facilities in 037 * Hadoop. 038 */ 039public class CredentialShell extends Configured implements Tool { 040 final static private String USAGE_PREFIX = "Usage: hadoop credential " + 041 "[generic options]\n"; 042 final static private String COMMANDS = 043 " [-help]\n" + 044 " [" + CreateCommand.USAGE + "]\n" + 045 " [" + DeleteCommand.USAGE + "]\n" + 046 " [" + ListCommand.USAGE + "]\n"; 047 @VisibleForTesting 048 public static final String NO_VALID_PROVIDERS = 049 "There are no valid (non-transient) providers configured.\n" + 050 "No action has been taken. Use the -provider option to specify\n" + 051 "a provider. If you want to use a transient provider then you\n" + 052 "MUST use the -provider argument."; 053 054 private boolean interactive = true; 055 private Command command = null; 056 057 /** If true, fail if the provider requires a password and none is given. */ 058 private boolean strict = false; 059 060 /** Allows stdout to be captured if necessary. */ 061 @VisibleForTesting 062 public PrintStream out = System.out; 063 /** Allows stderr to be captured if necessary. */ 064 @VisibleForTesting 065 public PrintStream err = System.err; 066 067 private boolean userSuppliedProvider = false; 068 private String value = null; 069 private PasswordReader passwordReader; 070 private boolean isHelp = false; 071 072 @Override 073 public int run(String[] args) throws Exception { 074 int exitCode = 0; 075 try { 076 exitCode = init(args); 077 if (exitCode != 0) { 078 return exitCode; 079 } 080 if (!isHelp) { 081 if (command.validate()) { 082 command.execute(); 083 } else { 084 exitCode = 1; 085 } 086 } 087 } catch (Exception e) { 088 e.printStackTrace(err); 089 return 1; 090 } 091 return exitCode; 092 } 093 094 /** 095 * Parse the command line arguments and initialize the data. 096 * <pre> 097 * % hadoop credential create alias [-provider providerPath] 098 * % hadoop credential list [-provider providerPath] 099 * % hadoop credential delete alias [-provider providerPath] [-f] 100 * </pre> 101 * @param args 102 * @return 0 if the argument(s) were recognized, 1 otherwise 103 * @throws IOException 104 */ 105 protected int init(String[] args) throws IOException { 106 // no args should print the help message 107 if (0 == args.length) { 108 printCredShellUsage(); 109 ToolRunner.printGenericCommandUsage(System.err); 110 return 1; 111 } 112 113 for (int i = 0; i < args.length; i++) { // parse command line 114 if (args[i].equals("create")) { 115 if (i == args.length - 1) { 116 printCredShellUsage(); 117 return 1; 118 } 119 String alias = args[++i]; 120 command = new CreateCommand(alias); 121 if (alias.equals("-help")) { 122 printCredShellUsage(); 123 return 0; 124 } 125 } else if (args[i].equals("delete")) { 126 if (i == args.length - 1) { 127 printCredShellUsage(); 128 return 1; 129 } 130 String alias = args[++i]; 131 command = new DeleteCommand(alias); 132 if (alias.equals("-help")) { 133 printCredShellUsage(); 134 return 0; 135 } 136 } else if (args[i].equals("list")) { 137 command = new ListCommand(); 138 } else if (args[i].equals("-provider")) { 139 if (i == args.length - 1) { 140 printCredShellUsage(); 141 return 1; 142 } 143 userSuppliedProvider = true; 144 getConf().set(CredentialProviderFactory.CREDENTIAL_PROVIDER_PATH, 145 args[++i]); 146 } else if (args[i].equals("-f") || (args[i].equals("-force"))) { 147 interactive = false; 148 } else if (args[i].equals("-strict")) { 149 strict = true; 150 } else if (args[i].equals("-v") || (args[i].equals("-value"))) { 151 value = args[++i]; 152 } else if (args[i].equals("-help")) { 153 printCredShellUsage(); 154 return 0; 155 } else { 156 printCredShellUsage(); 157 ToolRunner.printGenericCommandUsage(System.err); 158 return 1; 159 } 160 } 161 return 0; 162 } 163 164 private void printCredShellUsage() { 165 isHelp = true; 166 out.println(USAGE_PREFIX + COMMANDS); 167 if (command != null) { 168 out.println(command.getUsage()); 169 } else { 170 out.println("=========================================================" + 171 "======"); 172 out.println(CreateCommand.USAGE + ":\n\n" + CreateCommand.DESC); 173 out.println("=========================================================" + 174 "======"); 175 out.println(DeleteCommand.USAGE + ":\n\n" + DeleteCommand.DESC); 176 out.println("=========================================================" + 177 "======"); 178 out.println(ListCommand.USAGE + ":\n\n" + ListCommand.DESC); 179 } 180 } 181 182 private abstract class Command { 183 protected CredentialProvider provider = null; 184 185 public boolean validate() { 186 return true; 187 } 188 189 protected CredentialProvider getCredentialProvider() { 190 CredentialProvider prov = null; 191 List<CredentialProvider> providers; 192 try { 193 providers = CredentialProviderFactory.getProviders(getConf()); 194 if (userSuppliedProvider) { 195 prov = providers.get(0); 196 } else { 197 for (CredentialProvider p : providers) { 198 if (!p.isTransient()) { 199 prov = p; 200 break; 201 } 202 } 203 } 204 } catch (IOException e) { 205 e.printStackTrace(err); 206 } 207 if (prov == null) { 208 out.println(NO_VALID_PROVIDERS); 209 } 210 return prov; 211 } 212 213 protected void printProviderWritten() { 214 out.println("Provider " + provider.toString() + " has been updated."); 215 } 216 217 protected void warnIfTransientProvider() { 218 if (provider.isTransient()) { 219 out.println("WARNING: you are modifying a transient provider."); 220 } 221 } 222 223 public abstract void execute() throws Exception; 224 225 public abstract String getUsage(); 226 } 227 228 private class ListCommand extends Command { 229 public static final String USAGE = 230 "list [-provider provider-path] [-strict]"; 231 public static final String DESC = 232 "The list subcommand displays the aliases contained within \n" + 233 "a particular provider - as configured in core-site.xml or\n" + 234 "indicated through the -provider argument. If -strict is supplied,\n" + 235 "fail immediately if the provider requires a password and none is\n" + 236 "provided."; 237 238 public boolean validate() { 239 provider = getCredentialProvider(); 240 return (provider != null); 241 } 242 243 public void execute() throws IOException { 244 List<String> aliases; 245 try { 246 aliases = provider.getAliases(); 247 out.println("Listing aliases for CredentialProvider: " + 248 provider.toString()); 249 for (String alias : aliases) { 250 out.println(alias); 251 } 252 } catch (IOException e) { 253 out.println("Cannot list aliases for CredentialProvider: " + 254 provider.toString() 255 + ": " + e.getMessage()); 256 throw e; 257 } 258 } 259 260 @Override 261 public String getUsage() { 262 return USAGE + ":\n\n" + DESC; 263 } 264 } 265 266 private class DeleteCommand extends Command { 267 public static final String USAGE = 268 "delete <alias> [-f] [-provider provider-path] [-strict]"; 269 public static final String DESC = 270 "The delete subcommand deletes the credential\n" + 271 "specified as the <alias> argument from within the provider\n" + 272 "indicated through the -provider argument. The command asks for\n" + 273 "confirmation unless the -f option is specified. If -strict is\n" + 274 "supplied, fail immediately if the provider requires a password\n" + 275 "and none is given."; 276 277 private String alias = null; 278 private boolean cont = true; 279 280 public DeleteCommand(String alias) { 281 this.alias = alias; 282 } 283 284 @Override 285 public boolean validate() { 286 provider = getCredentialProvider(); 287 if (provider == null) { 288 return false; 289 } 290 if (alias == null) { 291 out.println("There is no alias specified. Please provide the" + 292 "mandatory <alias>. See the usage description with -help."); 293 return false; 294 } 295 if (interactive) { 296 try { 297 cont = ToolRunner 298 .confirmPrompt("You are about to DELETE the credential " + 299 alias + " from CredentialProvider " + provider.toString() + 300 ". Continue? "); 301 if (!cont) { 302 out.println("Nothing has been be deleted."); 303 } 304 return cont; 305 } catch (IOException e) { 306 out.println(alias + " will not be deleted."); 307 e.printStackTrace(err); 308 } 309 } 310 return true; 311 } 312 313 public void execute() throws IOException { 314 warnIfTransientProvider(); 315 out.println("Deleting credential: " + alias + 316 " from CredentialProvider: " + provider.toString()); 317 if (cont) { 318 try { 319 provider.deleteCredentialEntry(alias); 320 out.println("Credential " + alias + 321 " has been successfully deleted."); 322 provider.flush(); 323 printProviderWritten(); 324 } catch (IOException e) { 325 out.println("Credential " + alias + " has NOT been deleted."); 326 throw e; 327 } 328 } 329 } 330 331 @Override 332 public String getUsage() { 333 return USAGE + ":\n\n" + DESC; 334 } 335 } 336 337 private class CreateCommand extends Command { 338 public static final String USAGE = "create <alias> [-value alias-value] " + 339 "[-provider provider-path] [-strict]"; 340 public static final String DESC = 341 "The create subcommand creates a new credential for the name\n" + 342 "specified as the <alias> argument within the provider indicated\n" + 343 "through the -provider argument. If -strict is supplied, fail\n" + 344 "immediately if the provider requires a password and none is given.\n" + 345 "If -value is provided, use that for the value of the credential\n" + 346 "instead of prompting the user."; 347 348 private String alias = null; 349 350 public CreateCommand(String alias) { 351 this.alias = alias; 352 } 353 354 public boolean validate() { 355 boolean rc = true; 356 try { 357 provider = getCredentialProvider(); 358 if (provider == null) { 359 rc = false; 360 } else if (provider.needsPassword()) { 361 if (strict) { 362 out.println(provider.noPasswordError()); 363 rc = false; 364 } else { 365 out.println(provider.noPasswordWarning()); 366 } 367 } 368 } catch (IOException e) { 369 e.printStackTrace(err); 370 } 371 if (alias == null) { 372 out.println("There is no alias specified. Please provide the" + 373 "mandatory <alias>. See the usage description with -help."); 374 rc = false; 375 } 376 return rc; 377 } 378 379 public void execute() throws IOException, NoSuchAlgorithmException { 380 warnIfTransientProvider(); 381 try { 382 char[] credential = null; 383 if (value != null) { 384 // testing only 385 credential = value.toCharArray(); 386 } else { 387 credential = promptForCredential(); 388 } 389 provider.createCredentialEntry(alias, credential); 390 provider.flush(); 391 out.println(alias + " has been successfully created."); 392 printProviderWritten(); 393 } catch (InvalidParameterException e) { 394 out.println("Credential " + alias + " has NOT been created. " + 395 e.getMessage()); 396 throw e; 397 } catch (IOException e) { 398 out.println("Credential " + alias + " has NOT been created. " + 399 e.getMessage()); 400 throw e; 401 } 402 } 403 404 @Override 405 public String getUsage() { 406 return USAGE + ":\n\n" + DESC; 407 } 408 } 409 410 protected char[] promptForCredential() throws IOException { 411 PasswordReader c = getPasswordReader(); 412 if (c == null) { 413 throw new IOException("No console available for prompting user."); 414 } 415 416 char[] cred = null; 417 418 boolean noMatch; 419 do { 420 char[] newPassword1 = c.readPassword("Enter alias password: "); 421 char[] newPassword2 = c.readPassword("Enter alias password again: "); 422 noMatch = !Arrays.equals(newPassword1, newPassword2); 423 if (noMatch) { 424 if (newPassword1 != null) { 425 Arrays.fill(newPassword1, ' '); 426 } 427 c.format("Passwords don't match. Try again.%n"); 428 } else { 429 cred = newPassword1; 430 } 431 if (newPassword2 != null) { 432 Arrays.fill(newPassword2, ' '); 433 } 434 } while (noMatch); 435 return cred; 436 } 437 438 public PasswordReader getPasswordReader() { 439 if (passwordReader == null) { 440 passwordReader = new PasswordReader(); 441 } 442 return passwordReader; 443 } 444 445 public void setPasswordReader(PasswordReader reader) { 446 passwordReader = reader; 447 } 448 449 /** To facilitate testing since Console is a final class. */ 450 public static class PasswordReader { 451 public char[] readPassword(String prompt) { 452 Console console = System.console(); 453 char[] pass = console.readPassword(prompt); 454 return pass; 455 } 456 457 public void format(String message) { 458 Console console = System.console(); 459 console.format(message); 460 } 461 } 462 463 464 /** 465 * Main program. 466 * 467 * @param args 468 * Command line arguments 469 * @throws Exception 470 */ 471 public static void main(String[] args) throws Exception { 472 int res = ToolRunner.run(new Configuration(), new CredentialShell(), args); 473 System.exit(res); 474 } 475}