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;
020
021import org.apache.commons.io.IOUtils;
022import org.apache.directory.server.kerberos.shared.keytab.Keytab;
023import org.apache.directory.server.kerberos.shared.keytab.KeytabEntry;
024import org.apache.directory.shared.kerberos.components.EncryptionKey;
025import org.apache.hadoop.conf.Configuration;
026import org.apache.hadoop.conf.Configured;
027import org.apache.hadoop.io.Text;
028import org.apache.hadoop.security.authentication.util.KerberosName;
029import org.apache.hadoop.security.token.Token;
030import org.apache.hadoop.security.token.TokenIdentifier;
031import org.apache.hadoop.util.ExitUtil;
032import org.apache.hadoop.util.Shell;
033import org.apache.hadoop.util.StringUtils;
034import org.apache.hadoop.util.Tool;
035import org.apache.hadoop.util.ToolRunner;
036import org.slf4j.Logger;
037import org.slf4j.LoggerFactory;
038
039import javax.crypto.Cipher;
040import java.io.Closeable;
041import java.io.File;
042import java.io.FileInputStream;
043import java.io.IOException;
044import java.io.InputStream;
045import java.io.PrintWriter;
046import java.lang.reflect.InvocationTargetException;
047import java.net.InetAddress;
048import java.security.NoSuchAlgorithmException;
049import java.util.ArrayList;
050import java.util.Arrays;
051import java.util.Collection;
052import java.util.Collections;
053import java.util.Date;
054import java.util.LinkedList;
055import java.util.List;
056import java.util.regex.Pattern;
057
058import static org.apache.hadoop.fs.CommonConfigurationKeysPublic.*;
059import static org.apache.hadoop.security.UserGroupInformation.*;
060import static org.apache.hadoop.security.authentication.util.KerberosUtil.*;
061import static org.apache.hadoop.util.StringUtils.popOption;
062import static org.apache.hadoop.util.StringUtils.popOptionWithArgument;
063
064/**
065 * Kerberos diagnostics
066 *
067 * This operation expands some of the diagnostic output of the security code,
068 * but not all. For completeness
069 *
070 * Set the environment variable {@code HADOOP_JAAS_DEBUG=true}
071 * Set the log level for {@code org.apache.hadoop.security=DEBUG}
072 */
073public class KDiag extends Configured implements Tool, Closeable {
074
075  private static final Logger LOG = LoggerFactory.getLogger(KDiag.class);
076  /**
077   * Location of the kerberos ticket cache as passed down via an environment
078   * variable. This is what kinit will use by default: {@value}
079   */
080  public static final String KRB5_CCNAME = "KRB5CCNAME";
081  public static final String JAVA_SECURITY_KRB5_CONF
082    = "java.security.krb5.conf";
083  public static final String JAVA_SECURITY_KRB5_REALM
084    = "java.security.krb5.realm";
085  public static final String JAVA_SECURITY_KRB5_KDC_ADDRESS
086    = "java.security.krb5.kdc";
087  public static final String SUN_SECURITY_KRB5_DEBUG
088    = "sun.security.krb5.debug";
089  public static final String SUN_SECURITY_SPNEGO_DEBUG
090    = "sun.security.spnego.debug";
091  public static final String SUN_SECURITY_JAAS_FILE
092    = "java.security.auth.login.config";
093  public static final String KERBEROS_KINIT_COMMAND
094    = "hadoop.kerberos.kinit.command";
095
096  public static final String HADOOP_AUTHENTICATION_IS_DISABLED
097      = "Hadoop authentication is disabled";
098  public static final String UNSET = "(unset)";
099
100  /**
101   * String seen in {@code getDefaultRealm()} exceptions if the user has
102   * no realm: {@value}.
103   */
104  public static final String NO_DEFAULT_REALM = "Cannot locate default realm";
105
106  /**
107   * The exit code for a failure of the diagnostics: 41 == HTTP 401 == unauth.
108   */
109  public static final int KDIAG_FAILURE = 41;
110  public static final String DFS_DATA_TRANSFER_SASLPROPERTIES_RESOLVER_CLASS
111      = "dfs.data.transfer.saslproperties.resolver.class";
112  public static final String DFS_DATA_TRANSFER_PROTECTION
113      = "dfs.data.transfer.protection";
114  public static final String ETC_KRB5_CONF = "/etc/krb5.conf";
115  public static final String ETC_NTP = "/etc/ntp.conf";
116  public static final String HADOOP_JAAS_DEBUG = "HADOOP_JAAS_DEBUG";
117
118  private PrintWriter out;
119  private File keytab;
120  private String principal;
121  private long minKeyLength = 256;
122  private boolean securityRequired;
123  private boolean nofail = false;
124  private boolean nologin = false;
125  private boolean jaas = false;
126  private boolean checkShortName = false;
127
128  /**
129   * A pattern that recognizes simple/non-simple names. Per KerberosName
130   */
131  private static final Pattern nonSimplePattern = Pattern.compile("[/@]");
132
133  /**
134   * Flag set to true if a {@link #verify(boolean, String, String, Object...)}
135   * probe failed.
136   */
137  private boolean probeHasFailed = false;
138
139  public static final String CAT_CONFIG = "CONFIG";
140  public static final String CAT_JAAS = "JAAS";
141  public static final String CAT_JVM = "JVM";
142  public static final String CAT_KERBEROS = "KERBEROS";
143  public static final String CAT_LOGIN = "LOGIN";
144  public static final String CAT_OS = "JAAS";
145  public static final String CAT_SASL = "SASL";
146  public static final String CAT_UGI = "UGI";
147
148  public static final String ARG_KEYLEN = "--keylen";
149  public static final String ARG_KEYTAB = "--keytab";
150  public static final String ARG_JAAS = "--jaas";
151  public static final String ARG_NOFAIL = "--nofail";
152  public static final String ARG_NOLOGIN = "--nologin";
153  public static final String ARG_OUTPUT = "--out";
154  public static final String ARG_PRINCIPAL = "--principal";
155  public static final String ARG_RESOURCE = "--resource";
156
157  public static final String ARG_SECURE = "--secure";
158
159  public static final String ARG_VERIFYSHORTNAME = "--verifyshortname";
160
161  @SuppressWarnings("IOResourceOpenedButNotSafelyClosed")
162  public KDiag(Configuration conf,
163      PrintWriter out,
164      File keytab,
165      String principal,
166      long minKeyLength,
167      boolean securityRequired) {
168    super(conf);
169    this.keytab = keytab;
170    this.principal = principal;
171    this.out = out;
172    this.minKeyLength = minKeyLength;
173    this.securityRequired = securityRequired;
174  }
175
176  public KDiag() {
177  }
178
179  @Override
180  public void close() throws IOException {
181    flush();
182    if (out != null) {
183      out.close();
184    }
185  }
186
187  @Override
188  public int run(String[] argv) throws Exception {
189    List<String> args = new LinkedList<>(Arrays.asList(argv));
190    String keytabName = popOptionWithArgument(ARG_KEYTAB, args);
191    if (keytabName != null) {
192      keytab = new File(keytabName);
193    }
194    principal = popOptionWithArgument(ARG_PRINCIPAL, args);
195    String outf = popOptionWithArgument(ARG_OUTPUT, args);
196    String mkl = popOptionWithArgument(ARG_KEYLEN, args);
197    if (mkl != null) {
198      minKeyLength = Integer.parseInt(mkl);
199    }
200    securityRequired = popOption(ARG_SECURE, args);
201    nofail = popOption(ARG_NOFAIL, args);
202    jaas = popOption(ARG_JAAS, args);
203    nologin = popOption(ARG_NOLOGIN, args);
204    checkShortName = popOption(ARG_VERIFYSHORTNAME, args);
205
206    // look for list of resources
207    String resource;
208    while (null != (resource = popOptionWithArgument(ARG_RESOURCE, args))) {
209      // loading a resource
210      LOG.info("Loading resource {}", resource);
211      try (InputStream in =
212               getClass().getClassLoader().getResourceAsStream(resource)) {
213        if (verify(in != null, CAT_CONFIG, "No resource %s", resource)) {
214          Configuration.addDefaultResource(resource);
215        }
216      }
217    }
218    // look for any leftovers
219    if (!args.isEmpty()) {
220      println("Unknown arguments in command:");
221      for (String s : args) {
222        println("  \"%s\"", s);
223      }
224      println();
225      println(usage());
226      return -1;
227    }
228    if (outf != null) {
229      println("Printing output to %s", outf);
230      out = new PrintWriter(new File(outf), "UTF-8");
231    }
232    execute();
233    return probeHasFailed ? KDIAG_FAILURE : 0;
234  }
235
236  private String usage() {
237    return "KDiag: Diagnose Kerberos Problems\n"
238      + arg("-D", "key=value", "Define a configuration option")
239      + arg(ARG_JAAS, "",
240      "Require a JAAS file to be defined in " + SUN_SECURITY_JAAS_FILE)
241      + arg(ARG_KEYLEN, "<keylen>",
242      "Require a minimum size for encryption keys supported by the JVM."
243      + " Default value : "+ minKeyLength)
244      + arg(ARG_KEYTAB, "<keytab> " + ARG_PRINCIPAL + " <principal>",
245          "Login from a keytab as a specific principal")
246      + arg(ARG_NOFAIL, "", "Do not fail on the first problem")
247      + arg(ARG_NOLOGIN, "", "Do not attempt to log in")
248      + arg(ARG_OUTPUT, "<file>", "Write output to a file")
249      + arg(ARG_RESOURCE, "<resource>", "Load an XML configuration resource")
250      + arg(ARG_SECURE, "", "Require the hadoop configuration to be secure")
251      + arg(ARG_VERIFYSHORTNAME, ARG_PRINCIPAL + " <principal>",
252      "Verify the short name of the specific principal does not contain '@' or '/'");
253  }
254
255  private String arg(String name, String params, String meaning) {
256    return String.format("  [%s%s%s] : %s",
257        name, (!params.isEmpty() ? " " : ""), params, meaning) + ".\n";
258  }
259
260  /**
261   * Execute diagnostics.
262   * <p>
263   * Things it would be nice if UGI made accessible
264   * <ol>
265   *   <li>A way to enable JAAS debug programatically</li>
266   *   <li>Access to the TGT</li>
267   * </ol>
268   * @return true if security was enabled and all probes were successful
269   * @throws KerberosDiagsFailure explicitly raised failure
270   * @throws Exception other security problems
271   */
272  @SuppressWarnings("deprecation")
273  public boolean execute() throws Exception {
274
275    title("Kerberos Diagnostics scan at %s",
276        new Date(System.currentTimeMillis()));
277
278    // check that the machine has a name
279    println("Hostname = %s",
280        InetAddress.getLocalHost().getCanonicalHostName());
281
282    println("%s = %d", ARG_KEYLEN, minKeyLength);
283    println("%s = %s", ARG_KEYTAB, keytab);
284    println("%s = %s", ARG_PRINCIPAL, principal);
285    println("%s = %s", ARG_VERIFYSHORTNAME, checkShortName);
286
287    // Fail fast on a JVM without JCE installed.
288    validateKeyLength();
289
290    // look at realm
291    println("JVM Kerberos Login Module = %s", getKrb5LoginModuleName());
292
293    title("Core System Properties");
294    for (String prop : new String[]{
295      "user.name",
296      "java.version",
297      "java.vendor",
298      JAVA_SECURITY_KRB5_CONF,
299      JAVA_SECURITY_KRB5_REALM,
300      JAVA_SECURITY_KRB5_KDC_ADDRESS,
301      SUN_SECURITY_KRB5_DEBUG,
302      SUN_SECURITY_SPNEGO_DEBUG,
303      SUN_SECURITY_JAAS_FILE
304    }) {
305      printSysprop(prop);
306    }
307    endln();
308
309    title("All System Properties");
310    ArrayList<String> propList = new ArrayList<>(
311        System.getProperties().stringPropertyNames());
312    Collections.sort(propList, String.CASE_INSENSITIVE_ORDER);
313    for (String s : propList) {
314      printSysprop(s);
315    }
316    endln();
317
318    title("Environment Variables");
319    for (String env : new String[]{
320      HADOOP_JAAS_DEBUG,
321      KRB5_CCNAME,
322      HADOOP_USER_NAME,
323      HADOOP_PROXY_USER,
324      HADOOP_TOKEN_FILE_LOCATION,
325      "HADOOP_SECURE_LOG",
326      "HADOOP_OPTS",
327      "HADOOP_CLIENT_OPTS",
328    }) {
329      printEnv(env);
330    }
331    endln();
332
333    title("Configuration Options");
334    for (String prop : new String[]{
335      KERBEROS_KINIT_COMMAND,
336      HADOOP_SECURITY_AUTHENTICATION,
337      HADOOP_SECURITY_AUTHORIZATION,
338      "hadoop.kerberos.min.seconds.before.relogin",    // not in 2.6
339      "hadoop.security.dns.interface",   // not in 2.6
340      "hadoop.security.dns.nameserver",  // not in 2.6
341      HADOOP_RPC_PROTECTION,
342      HADOOP_SECURITY_SASL_PROPS_RESOLVER_CLASS,
343      HADOOP_SECURITY_CRYPTO_CODEC_CLASSES_KEY_PREFIX,
344      HADOOP_SECURITY_GROUP_MAPPING,
345      "hadoop.security.impersonation.provider.class",    // not in 2.6
346      DFS_DATA_TRANSFER_PROTECTION, // HDFS
347      DFS_DATA_TRANSFER_SASLPROPERTIES_RESOLVER_CLASS // HDFS
348    }) {
349      printConfOpt(prop);
350    }
351
352    // check that authentication is enabled
353    Configuration conf = getConf();
354    if (isSimpleAuthentication(conf)) {
355      println(HADOOP_AUTHENTICATION_IS_DISABLED);
356      failif(securityRequired, CAT_CONFIG, HADOOP_AUTHENTICATION_IS_DISABLED);
357      // no security, warn
358      LOG.warn("Security is not enabled for the Hadoop cluster");
359    } else {
360      if (isSimpleAuthentication(new Configuration())) {
361        LOG.warn("The default cluster security is insecure");
362        failif(securityRequired, CAT_CONFIG, HADOOP_AUTHENTICATION_IS_DISABLED);
363      }
364    }
365
366
367    // now the big test: login, then try again
368    boolean krb5Debug = getAndSet(SUN_SECURITY_KRB5_DEBUG);
369    boolean spnegoDebug = getAndSet(SUN_SECURITY_SPNEGO_DEBUG);
370
371    try {
372      UserGroupInformation.setConfiguration(conf);
373      validateKrb5File();
374      printDefaultRealm();
375      validateSasl(HADOOP_SECURITY_SASL_PROPS_RESOLVER_CLASS);
376      if (conf.get(DFS_DATA_TRANSFER_SASLPROPERTIES_RESOLVER_CLASS) != null) {
377        validateSasl(DFS_DATA_TRANSFER_SASLPROPERTIES_RESOLVER_CLASS);
378      }
379      validateKinitExecutable();
380      validateJAAS(jaas);
381      validateNTPConf();
382
383      if (checkShortName) {
384        validateShortName();
385      }
386
387      if (!nologin) {
388        title("Logging in");
389        if (keytab != null) {
390          dumpKeytab(keytab);
391          loginFromKeytab();
392        } else {
393          UserGroupInformation loginUser = getLoginUser();
394          dumpUGI("Log in user", loginUser);
395          validateUGI("Login user", loginUser);
396          println("Ticket based login: %b", isLoginTicketBased());
397          println("Keytab based login: %b", isLoginKeytabBased());
398        }
399      }
400
401      return true;
402    } finally {
403      // restore original system properties
404      System.setProperty(SUN_SECURITY_KRB5_DEBUG,
405        Boolean.toString(krb5Debug));
406      System.setProperty(SUN_SECURITY_SPNEGO_DEBUG,
407        Boolean.toString(spnegoDebug));
408    }
409  }
410
411  /**
412   * Is the authentication method of this configuration "simple"?
413   * @param conf configuration to check
414   * @return true if auth is simple (i.e. not kerberos)
415   */
416  protected boolean isSimpleAuthentication(Configuration conf) {
417    return SecurityUtil.getAuthenticationMethod(conf)
418        .equals(AuthenticationMethod.SIMPLE);
419  }
420
421  /**
422   * Fail fast on a JVM without JCE installed.
423   *
424   * This is a recurrent problem
425   * (that is: it keeps creeping back with JVM updates);
426   * a fast failure is the best tactic.
427   * @throws NoSuchAlgorithmException
428   */
429
430  protected void validateKeyLength() throws NoSuchAlgorithmException {
431    int aesLen = Cipher.getMaxAllowedKeyLength("AES");
432    println("Maximum AES encryption key length %d bits", aesLen);
433    verify(minKeyLength <= aesLen,
434        CAT_JVM,
435        "Java Cryptography Extensions are not installed on this JVM."
436            + " Maximum supported key length %s - minimum required %d",
437        aesLen, minKeyLength);
438  }
439
440  /**
441   * Verify whether auth_to_local rules transform a principal name
442   * <p>
443   * Having a local user name "[email protected]" may be harmless, so it is noted at
444   * info. However if what was intended is a transformation to "bar"
445   * it can be difficult to debug, hence this check.
446   */
447  protected void validateShortName() {
448    failif(principal == null, CAT_KERBEROS, "No principal defined");
449
450    try {
451      KerberosName kn = new KerberosName(principal);
452      String result = kn.getShortName();
453      if (nonSimplePattern.matcher(result).find()) {
454        warn(CAT_KERBEROS, principal + " short name: " + result
455                + " still contains @ or /");
456      }
457    } catch (IOException e) {
458      throw new KerberosDiagsFailure(CAT_KERBEROS, e,
459              "Failed to get short name for " + principal, e);
460    } catch (IllegalArgumentException e) {
461      error(CAT_KERBEROS, "KerberosName(" + principal + ") failed: %s\n%s",
462              e, StringUtils.stringifyException(e));
463    }
464  }
465
466  /**
467   * Get the default realm.
468   * <p>
469   * Not having a default realm may be harmless, so is noted at info.
470   * All other invocation failures are downgraded to warn, as
471   * follow-on actions may still work.
472   * Failure to invoke the method via introspection is considered a failure,
473   * as it's a sign of JVM compatibility issues that may have other 
474   * consequences
475   */
476  protected void printDefaultRealm() {
477    try {
478      String defaultRealm = getDefaultRealm();
479      println("Default Realm = %s", defaultRealm);
480      if (defaultRealm == null) {
481        warn(CAT_KERBEROS, "Host has no default realm");
482      }
483    } catch (ClassNotFoundException
484        | IllegalAccessException
485        | NoSuchMethodException e) {
486      throw new KerberosDiagsFailure(CAT_JVM, e,
487          "Failed to invoke krb5.Config.getDefaultRealm: %s: " +e, e);
488    } catch (InvocationTargetException e) {
489      Throwable cause = e.getCause() != null ? e.getCause() : e;
490      if (cause.toString().contains(NO_DEFAULT_REALM)) {
491        // exception raised if there is no default realm. This is not
492        // always a problem, so downgrade to a message.
493        warn(CAT_KERBEROS, "Host has no default realm");
494        LOG.debug(cause.toString(), cause);
495      } else {
496        error(CAT_KERBEROS, "Kerberos.getDefaultRealm() failed: %s\n%s",
497            cause, StringUtils.stringifyException(cause));
498      }
499    }
500  }
501
502  /**
503   * Locate the {@code krb5.conf} file and dump it.
504   *
505   * No-op on windows.
506   * @throws IOException problems reading the file.
507   */
508  private void validateKrb5File() throws IOException {
509    if (!Shell.WINDOWS) {
510      title("Locating Kerberos configuration file");
511      String krbPath = ETC_KRB5_CONF;
512      String jvmKrbPath = System.getProperty(JAVA_SECURITY_KRB5_CONF);
513      if (jvmKrbPath != null && !jvmKrbPath.isEmpty()) {
514        println("Setting kerberos path from sysprop %s: \"%s\"",
515          JAVA_SECURITY_KRB5_CONF, jvmKrbPath);
516        krbPath = jvmKrbPath;
517      }
518
519      String krb5name = System.getenv(KRB5_CCNAME);
520      if (krb5name != null) {
521        println("Setting kerberos path from environment variable %s: \"%s\"",
522          KRB5_CCNAME, krb5name);
523        krbPath = krb5name;
524        if (jvmKrbPath != null) {
525          println("Warning - both %s and %s were set - %s takes priority",
526            JAVA_SECURITY_KRB5_CONF, KRB5_CCNAME, KRB5_CCNAME);
527        }
528      }
529
530      File krbFile = new File(krbPath);
531      println("Kerberos configuration file = %s", krbFile);
532      dump(krbFile);
533      endln();
534    }
535  }
536
537  /**
538   * Dump a keytab: list all principals.
539   *
540   * @param keytabFile the keytab file
541   * @throws IOException IO problems
542   */
543  private void dumpKeytab(File keytabFile) throws IOException {
544    title("Examining keytab %s", keytabFile);
545    File kt = keytabFile.getCanonicalFile();
546    verifyFileIsValid(kt, CAT_KERBEROS, "keytab");
547    List<KeytabEntry> entries = Keytab.read(kt).getEntries();
548    println("keytab entry count: %d", entries.size());
549    for (KeytabEntry entry : entries) {
550      EncryptionKey key = entry.getKey();
551      println(" %s: version=%d expires=%s encryption=%s",
552          entry.getPrincipalName(),
553          entry.getKeyVersion(),
554          entry.getTimeStamp(),
555          key.getKeyType());
556    }
557    endln();
558  }
559
560  /**
561   * Log in from a keytab, dump the UGI, validate it, then try and log in again.
562   *
563   * That second-time login catches JVM/Hadoop compatibility problems.
564   * @throws IOException Keytab loading problems
565   */
566  private void loginFromKeytab() throws IOException {
567    UserGroupInformation ugi;
568    String identity;
569    if (keytab != null) {
570      File kt = keytab.getCanonicalFile();
571      println("Using keytab %s principal %s", kt, principal);
572      identity = principal;
573
574      failif(principal == null, CAT_KERBEROS, "No principal defined");
575      ugi = loginUserFromKeytabAndReturnUGI(principal, kt.getPath());
576      dumpUGI(identity, ugi);
577      validateUGI(principal, ugi);
578
579      title("Attempting to relogin");
580      try {
581        // package scoped -hence the reason why this class must be in the
582        // hadoop.security package
583        setShouldRenewImmediatelyForTests(true);
584        // attempt a new login
585        ugi.reloginFromKeytab();
586      } catch (IllegalAccessError e) {
587        // if you've built this class into an independent JAR, package-access
588        // may fail. Downgrade
589        warn(CAT_UGI, "Failed to reset UGI -and so could not try to relogin");
590        LOG.debug("Failed to reset UGI: {}", e, e);
591      }
592    } else {
593      println("No keytab: attempting to log in is as current user");
594    }
595  }
596
597  /**
598   * Dump a UGI.
599   *
600   * @param title title of this section
601   * @param ugi UGI to dump
602   * @throws IOException
603   */
604  private void dumpUGI(String title, UserGroupInformation ugi)
605    throws IOException {
606    title(title);
607    println("UGI instance = %s", ugi);
608    println("Has kerberos credentials: %b", ugi.hasKerberosCredentials());
609    println("Authentication method: %s", ugi.getAuthenticationMethod());
610    println("Real Authentication method: %s",
611      ugi.getRealAuthenticationMethod());
612    title("Group names");
613    for (String name : ugi.getGroupNames()) {
614      println(name);
615    }
616    title("Credentials");
617    List<Text> secretKeys = ugi.getCredentials().getAllSecretKeys();
618    title("Secret keys");
619    if (!secretKeys.isEmpty()) {
620      for (Text secret: secretKeys) {
621        println("%s", secret);
622      }
623    } else {
624      println("(none)");
625    }
626
627    dumpTokens(ugi);
628  }
629
630  /**
631   * Validate the UGI: verify it is kerberized.
632   * @param messagePrefix message in exceptions
633   * @param user user to validate
634   */
635  private void validateUGI(String messagePrefix, UserGroupInformation user) {
636    if (verify(user.getAuthenticationMethod() == AuthenticationMethod.KERBEROS,
637        CAT_LOGIN, "User %s is not authenticated by Kerberos", user)) {
638      verify(user.hasKerberosCredentials(),
639          CAT_LOGIN, "%s: No kerberos credentials for %s", messagePrefix, user);
640      verify(user.getAuthenticationMethod() != null,
641          CAT_LOGIN, "%s: Null AuthenticationMethod for %s", messagePrefix,
642          user);
643    }
644  }
645
646  /**
647   * A cursory look at the {@code kinit} executable.
648   *
649   * If it is an absolute path: it must exist with a size > 0.
650   * If it is just a command, it has to be on the path. There's no check
651   * for that -but the PATH is printed out.
652   */
653  private void validateKinitExecutable() {
654    String kinit = getConf().getTrimmed(KERBEROS_KINIT_COMMAND, "");
655    if (!kinit.isEmpty()) {
656      File kinitPath = new File(kinit);
657      println("%s = %s", KERBEROS_KINIT_COMMAND, kinitPath);
658      if (kinitPath.isAbsolute()) {
659        verifyFileIsValid(kinitPath, CAT_KERBEROS, KERBEROS_KINIT_COMMAND);
660      } else {
661        println("Executable %s is relative -must be on the PATH", kinit);
662        printEnv("PATH");
663      }
664    }
665  }
666
667  /**
668   * Try to load the SASL resolver.
669   * @param saslPropsResolverKey key for the SASL resolver
670   */
671  private void validateSasl(String saslPropsResolverKey) {
672    title("Resolving SASL property %s", saslPropsResolverKey);
673    String saslPropsResolver = getConf().getTrimmed(saslPropsResolverKey);
674    try {
675      Class<? extends SaslPropertiesResolver> resolverClass =
676          getConf().getClass(
677          saslPropsResolverKey,
678          SaslPropertiesResolver.class,
679          SaslPropertiesResolver.class);
680      println("Resolver is %s", resolverClass);
681    } catch (RuntimeException e) {
682      throw new KerberosDiagsFailure(CAT_SASL, e,
683          "Failed to load %s class %s",
684          saslPropsResolverKey, saslPropsResolver);
685    }
686  }
687
688  /**
689   * Validate any JAAS entry referenced in the {@link #SUN_SECURITY_JAAS_FILE}
690   * property.
691   * @param jaasRequired is JAAS required
692   */
693  private void validateJAAS(boolean jaasRequired) throws IOException {
694    String jaasFilename = System.getProperty(SUN_SECURITY_JAAS_FILE);
695    if (jaasRequired) {
696      verify(jaasFilename != null, CAT_JAAS,
697          "No JAAS file specified in " + SUN_SECURITY_JAAS_FILE);
698    }
699    if (jaasFilename != null) {
700      title("JAAS");
701      File jaasFile = new File(jaasFilename);
702      println("JAAS file is defined in %s: %s",
703          SUN_SECURITY_JAAS_FILE, jaasFile);
704      verifyFileIsValid(jaasFile, CAT_JAAS,
705          "JAAS file defined in " + SUN_SECURITY_JAAS_FILE);
706      dump(jaasFile);
707      endln();
708    }
709  }
710
711  private void validateNTPConf() throws IOException {
712    if (!Shell.WINDOWS) {
713      File ntpfile = new File(ETC_NTP);
714      if (ntpfile.exists()
715          && verifyFileIsValid(ntpfile, CAT_OS,
716          "NTP file: " + ntpfile)) {
717        title("NTP");
718        dump(ntpfile);
719        endln();
720      }
721    }
722  }
723
724
725  /**
726   * Verify that a file is valid: it is a file, non-empty and readable.
727   * @param file file
728   * @param category category for exceptions
729   * @param text text message
730   * @return true if the validation held; false if it did not <i>and</i>
731   * {@link #nofail} has disabled raising exceptions.
732   */
733  private boolean verifyFileIsValid(File file, String category, String text) {
734    return verify(file.exists(), category,
735        "%s file does not exist: %s",
736        text, file)
737     && verify(file.isFile(), category,
738        "%s path does not refer to a file: %s", text, file)
739     && verify(file.length() != 0, category,
740        "%s file is empty: %s", text, file)
741      && verify(file.canRead(), category,
742        "%s file is not readable: %s", text, file);
743  }
744
745  /**
746   * Dump all tokens of a UGI.
747   * @param ugi UGI to examine
748   */
749  public void dumpTokens(UserGroupInformation ugi) {
750    Collection<Token<? extends TokenIdentifier>> tokens
751      = ugi.getCredentials().getAllTokens();
752    title("Token Count: %d", tokens.size());
753    for (Token<? extends TokenIdentifier> token : tokens) {
754      println("Token %s", token.getKind());
755    }
756    endln();
757  }
758
759  /**
760   * Set the System property to true; return the old value for caching.
761   *
762   * @param sysprop property
763   * @return the previous value
764   */
765  private boolean getAndSet(String sysprop) {
766    boolean old = Boolean.getBoolean(sysprop);
767    System.setProperty(sysprop, "true");
768    return old;
769  }
770
771  /**
772   * Flush all active output channels, including {@Code System.err},
773   * so as to stay in sync with any JRE log messages.
774   */
775  private void flush() {
776    if (out != null) {
777      out.flush();
778    } else {
779      System.out.flush();
780    }
781    System.err.flush();
782  }
783
784  /**
785   * Print a line of output. This goes to any output file, or
786   * is logged at info. The output is flushed before and after, to
787   * try and stay in sync with JRE logging.
788   *
789   * @param format format string
790   * @param args any arguments
791   */
792  private void println(String format, Object... args) {
793    flush();
794    String msg = String.format(format, args);
795    if (out != null) {
796      out.println(msg);
797    } else {
798      System.out.println(msg);
799    }
800    flush();
801  }
802
803  /**
804   * Print a new line
805   */
806  private void println() {
807    println("");
808  }
809
810  /**
811   * Print something at the end of a section
812   */
813  private void endln() {
814    println();
815    println("-----");
816  }
817
818  /**
819   * Print a title entry.
820   *
821   * @param format format string
822   * @param args any arguments
823   */
824  private void title(String format, Object... args) {
825    println();
826    println();
827    println("== " + String.format(format, args) + " ==");
828    println();
829  }
830
831  /**
832   * Print a system property, or {@link #UNSET} if unset.
833   * @param property property to print
834   */
835  private void printSysprop(String property) {
836    println("%s = \"%s\"", property,
837        System.getProperty(property, UNSET));
838  }
839
840  /**
841   * Print a configuration option, or {@link #UNSET} if unset.
842   *
843   * @param option option to print
844   */
845  private void printConfOpt(String option) {
846    println("%s = \"%s\"", option, getConf().get(option, UNSET));
847  }
848
849  /**
850   * Print an environment variable's name and value; printing
851   * {@link #UNSET} if it is not set.
852   * @param variable environment variable
853   */
854  private void printEnv(String variable) {
855    String env = System.getenv(variable);
856    println("%s = \"%s\"", variable, env != null ? env : UNSET);
857  }
858
859  /**
860   * Dump any file to standard out.
861   * @param file file to dump
862   * @throws IOException IO problems
863   */
864  private void dump(File file) throws IOException {
865    try (FileInputStream in = new FileInputStream(file)) {
866      for (String line : IOUtils.readLines(in)) {
867        println(line);
868      }
869    }
870  }
871
872  /**
873   * Format and raise a failure.
874   *
875   * @param category category for exception
876   * @param message string formatting message
877   * @param args any arguments for the formatting
878   * @throws KerberosDiagsFailure containing the formatted text
879   */
880  private void fail(String category, String message, Object... args)
881    throws KerberosDiagsFailure {
882    error(category, message, args);
883    throw new KerberosDiagsFailure(category, message, args);
884  }
885
886  /**
887   * Assert that a condition must hold.
888   *
889   * If not, an exception is raised, or, if {@link #nofail} is set,
890   * an error will be logged and the method return false.
891   *
892   * @param condition condition which must hold
893   * @param category category for exception
894   * @param message string formatting message
895   * @param args any arguments for the formatting
896   * @return true if the verification succeeded, false if it failed but
897   * an exception was not raised.
898   * @throws KerberosDiagsFailure containing the formatted text
899   *         if the condition was met
900   */
901  private boolean verify(boolean condition,
902      String category,
903      String message,
904      Object... args)
905    throws KerberosDiagsFailure {
906    if (!condition) {
907      // condition not met: fail or report
908      probeHasFailed = true;
909      if (!nofail) {
910        fail(category, message, args);
911      } else {
912        error(category, message, args);
913      }
914      return false;
915    } else {
916      // condition is met
917      return true;
918    }
919  }
920
921  /**
922   * Print a message as an error
923   * @param category error category
924   * @param message format string
925   * @param args list of arguments
926   */
927  private void error(String category, String message, Object...args) {
928    println("ERROR: %s: %s", category, String.format(message, args));
929  }
930  /**
931   * Print a message as an warning
932   * @param category error category
933   * @param message format string
934   * @param args list of arguments
935   */
936  private void warn(String category, String message, Object...args) {
937    println("WARNING: %s: %s", category, String.format(message, args));
938  }
939
940  /**
941   * Conditional failure with string formatted arguments.
942   * There is no chek for the {@link #nofail} value.
943   * @param condition failure condition
944   * @param category category for exception
945   * @param message string formatting message
946   * @param args any arguments for the formatting
947   * @throws KerberosDiagsFailure containing the formatted text
948   *         if the condition was met
949   */
950  private void failif(boolean condition,
951      String category,
952      String message,
953      Object... args)
954      throws KerberosDiagsFailure {
955    if (condition) {
956      fail(category, message, args);
957    }
958  }
959
960  /**
961   * Inner entry point, with no logging or system exits.
962   *
963   * @param conf configuration
964   * @param argv argument list
965   * @return an exception
966   * @throws Exception
967   */
968  public static int exec(Configuration conf, String... argv) throws Exception {
969    try(KDiag kdiag = new KDiag()) {
970      return ToolRunner.run(conf, kdiag, argv);
971    }
972  }
973
974  /**
975   * Main entry point.
976   * @param argv args list
977   */
978  public static void main(String[] argv) {
979    try {
980      ExitUtil.terminate(exec(new Configuration(), argv));
981    } catch (ExitUtil.ExitException e) {
982      LOG.error(e.toString());
983      System.exit(e.status);
984    } catch (Exception e) {
985      LOG.error(e.toString(), e);
986      ExitUtil.halt(-1, e);
987    }
988  }
989
990  /**
991   * Diagnostics failures return the exit code 41, "unauthorized".
992   *
993   * They have a category, initially for testing: the category can be
994   * validated without having to match on the entire string.
995   */
996  public static class KerberosDiagsFailure extends ExitUtil.ExitException {
997    private final String category;
998
999    public KerberosDiagsFailure(String category, String message) {
1000      super(KDIAG_FAILURE, category + ": " + message);
1001      this.category = category;
1002    }
1003
1004    public KerberosDiagsFailure(String category,
1005        String message,
1006        Object... args) {
1007      this(category, String.format(message, args));
1008    }
1009
1010    public KerberosDiagsFailure(String category, Throwable throwable,
1011        String message, Object... args) {
1012      this(category, message, args);
1013      initCause(throwable);
1014    }
1015
1016    public String getCategory() {
1017      return category;
1018    }
1019  }
1020}