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 }