Procházet zdrojové kódy

AMBARI-8851. Return correct key number value for dynamically created MIT KDC keytab files

Robert Levas před 10 roky
rodič
revize
ef3da6d483

+ 6 - 8
ambari-server/src/main/java/org/apache/ambari/server/serveraction/kerberos/ADKerberosOperationHandler.java

@@ -20,7 +20,6 @@ package org.apache.ambari.server.serveraction.kerberos;
 
 
 import org.apache.ambari.server.AmbariException;
-import org.apache.ambari.server.serveraction.kerberos.KerberosCredential;
 import org.apache.commons.logging.Log;
 import org.apache.commons.logging.LogFactory;
 
@@ -32,7 +31,6 @@ import java.io.UnsupportedEncodingException;
 import java.util.HashSet;
 import java.util.Properties;
 import java.util.Set;
-import java.util.StringTokenizer;
 
 /**
  * Implementation of <code>KerberosOperationHandler</code> to created principal in Active Directory
@@ -222,11 +220,11 @@ public class ADKerberosOperationHandler extends KerberosOperationHandler {
    *
    * @param principal a String containing the principal to add
    * @param password  a String containing the password to use when creating the principal
-   * @return true if the principal was successfully created; otherwise false
+   * @return an Integer declaring the generated key number
    * @throws AmbariException
    */
   @Override
-  public boolean createServicePrincipal(String principal, String password)
+  public Integer createServicePrincipal(String principal, String password)
     throws AmbariException {
     if (principal == null) {
       throw new AmbariException("principal is null");
@@ -275,7 +273,7 @@ public class ADKerberosOperationHandler extends KerberosOperationHandler {
     } catch (NamingException ne) {
       throw new AmbariException("Can not created principal : " + principal, ne);
     }
-    return true;
+    return 0;
   }
 
   /**
@@ -285,11 +283,11 @@ public class ADKerberosOperationHandler extends KerberosOperationHandler {
    *
    * @param principal a String containing the principal to update
    * @param password  a String containing the password to set
-   * @return true if the password was successfully updated; otherwise false
+   * @return an Integer declaring the new key number
    * @throws AmbariException
    */
   @Override
-  public boolean setPrincipalPassword(String principal, String password) throws AmbariException {
+  public Integer setPrincipalPassword(String principal, String password) throws AmbariException {
     if (principal == null) {
       throw new AmbariException("principal is null");
     }
@@ -316,7 +314,7 @@ public class ADKerberosOperationHandler extends KerberosOperationHandler {
     } catch (UnsupportedEncodingException ue) {
       throw new AmbariException("Unsupported encoding UTF-16LE", ue);
     }
-    return true;
+    return 0;
   }
 
   /**

+ 34 - 34
ambari-server/src/main/java/org/apache/ambari/server/serveraction/kerberos/CreateKeytabFilesServerAction.java

@@ -115,44 +115,44 @@ public class CreateKeytabFilesServerAction extends KerberosServerAction {
         commandReport = createCommandReport(1, HostRoleStatus.FAILED, "{}", "", message);
       } else {
         Map<String, String> principalPasswordMap = getPrincipalPasswordMap(requestSharedDataContext);
-
-        if (principalPasswordMap != null) {
-          String host = identityRecord.get(HOSTNAME);
-          String keytabFilePath = identityRecord.get(KEYTAB_FILE_PATH);
-
-          if ((host != null) && !host.isEmpty() && (keytabFilePath != null) && !keytabFilePath.isEmpty()) {
-            // Look up the current evaluatedPrincipal's password.
-            // If found create th keytab file, else skip it.
-            String password = principalPasswordMap.get(evaluatedPrincipal);
-
-            if (password == null) {
-              String message = String.format("Failed to create keytab file for %s, missing password", evaluatedPrincipal);
-              LOG.error(message);
-              commandReport = createCommandReport(1, HostRoleStatus.FAILED, "{}", "", message);
-            } else {
-              // Determine where to store the keytab file.  It should go into a host-specific
-              // directory under the previously determined data directory.
-              File hostDirectory = new File(getDataDirectoryPath(), host);
-
-              // Ensure the host directory exists...
-              if (hostDirectory.exists() || hostDirectory.mkdirs()) {
-                File keytabFile = new File(hostDirectory, DigestUtils.sha1Hex(keytabFilePath));
-
-                if (operationHandler.createKeytabFile(evaluatedPrincipal, password, keytabFile)) {
-                  LOG.debug("Successfully created keytab file for {} at {}",
-                      evaluatedPrincipal, keytabFile.getAbsolutePath());
-                } else {
-                  String message = String.format("Failed to create keytab file for %s at %s",
-                      evaluatedPrincipal, keytabFile.getAbsolutePath());
-                  LOG.error(message);
-                  commandReport = createCommandReport(1, HostRoleStatus.FAILED, "{}", "", message);
-                }
+        Map<String, Integer> principalKeyNumberMap = getPrincipalKeyNumberMap(requestSharedDataContext);
+
+        String host = identityRecord.get(HOSTNAME);
+        String keytabFilePath = identityRecord.get(KEYTAB_FILE_PATH);
+
+        if ((host != null) && !host.isEmpty() && (keytabFilePath != null) && !keytabFilePath.isEmpty()) {
+          // Look up the current evaluatedPrincipal's password.
+          // If found create th keytab file, else skip it.
+          String password = principalPasswordMap.get(evaluatedPrincipal);
+
+          if (password == null) {
+            String message = String.format("Failed to create keytab file for %s, missing password", evaluatedPrincipal);
+            LOG.error(message);
+            commandReport = createCommandReport(1, HostRoleStatus.FAILED, "{}", "", message);
+          } else {
+            // Determine where to store the keytab file.  It should go into a host-specific
+            // directory under the previously determined data directory.
+            File hostDirectory = new File(getDataDirectoryPath(), host);
+
+            // Ensure the host directory exists...
+            if (hostDirectory.exists() || hostDirectory.mkdirs()) {
+              File keytabFile = new File(hostDirectory, DigestUtils.sha1Hex(keytabFilePath));
+              Integer keyNumber = principalKeyNumberMap.get(evaluatedPrincipal);
+
+              if (operationHandler.createKeytabFile(evaluatedPrincipal, password, keyNumber, keytabFile)) {
+                LOG.debug("Successfully created keytab file for {} at {}",
+                    evaluatedPrincipal, keytabFile.getAbsolutePath());
               } else {
-                String message = String.format("Failed to create keytab file for %s, the container directory does not exist: %s",
-                    evaluatedPrincipal, hostDirectory.getAbsolutePath());
+                String message = String.format("Failed to create keytab file for %s at %s",
+                    evaluatedPrincipal, keytabFile.getAbsolutePath());
                 LOG.error(message);
                 commandReport = createCommandReport(1, HostRoleStatus.FAILED, "{}", "", message);
               }
+            } else {
+              String message = String.format("Failed to create keytab file for %s, the container directory does not exist: %s",
+                  evaluatedPrincipal, hostDirectory.getAbsolutePath());
+              LOG.error(message);
+              commandReport = createCommandReport(1, HostRoleStatus.FAILED, "{}", "", message);
             }
           }
         }

+ 11 - 4
ambari-server/src/main/java/org/apache/ambari/server/serveraction/kerberos/CreatePrincipalsServerAction.java

@@ -66,8 +66,9 @@ public class CreatePrincipalsServerAction extends KerberosServerAction {
    * If a password has not been previously created the current evaluatedPrincipal, create a "secure"
    * password using {@link KerberosOperationHandler#createSecurePassword()}.  Then if the principal
    * does not exist in the KDC, create it using the generated password; else if it does exist update
-   * its password.  Finally store the generated password in the shared principal-to-password map so
-   * that subsequent process may use it if necessary.
+   * its password.  Finally store the generated password in the shared principal-to-password map and
+   * store the new key numbers in the shared principal-to-key_number map so that subsequent process
+   * may use the data if necessary.
    *
    * @param identityRecord           a Map containing the data for the current identity record
    * @param evaluatedPrincipal       a String indicating the relevant principal
@@ -87,6 +88,7 @@ public class CreatePrincipalsServerAction extends KerberosServerAction {
     CommandReport commandReport = null;
 
     Map<String, String> principalPasswordMap = getPrincipalPasswordMap(requestSharedDataContext);
+    Map<String, Integer> principalKeyNumberMap = getPrincipalKeyNumberMap(requestSharedDataContext);
 
     String password = principalPasswordMap.get(evaluatedPrincipal);
 
@@ -98,8 +100,11 @@ public class CreatePrincipalsServerAction extends KerberosServerAction {
         // A new password/key would have been generated after exporting the keytab anyways.
         LOG.warn("Principal already exists, setting new password - {}", evaluatedPrincipal);
 
-        if (operationHandler.setPrincipalPassword(evaluatedPrincipal, password)) {
+        Integer keyNumber = operationHandler.setPrincipalPassword(evaluatedPrincipal, password);
+
+        if (keyNumber != null) {
           principalPasswordMap.put(evaluatedPrincipal, password);
+          principalKeyNumberMap.put(evaluatedPrincipal, keyNumber);
           LOG.debug("Successfully set password for principal {}", evaluatedPrincipal);
         } else {
           String message = String.format("Failed to set password for principal %s, unknown reason", evaluatedPrincipal);
@@ -108,9 +113,11 @@ public class CreatePrincipalsServerAction extends KerberosServerAction {
         }
       } else {
         LOG.debug("Creating new principal - {}", evaluatedPrincipal);
+        Integer keyNumber = operationHandler.createServicePrincipal(evaluatedPrincipal, password);
 
-        if (operationHandler.createServicePrincipal(evaluatedPrincipal, password)) {
+        if (keyNumber != null) {
           principalPasswordMap.put(evaluatedPrincipal, password);
+          principalKeyNumberMap.put(evaluatedPrincipal, keyNumber);
           LOG.debug("Successfully created new principal {}", evaluatedPrincipal);
         } else {
           String message = String.format("Failed to create principal %s, unknown reason", evaluatedPrincipal);

+ 66 - 99
ambari-server/src/main/java/org/apache/ambari/server/serveraction/kerberos/KerberosOperationHandler.java

@@ -37,8 +37,11 @@ import java.io.IOException;
 import java.io.OutputStream;
 import java.security.SecureRandom;
 import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
 /**
  * KerberosOperationHandler is an abstract class providing basic implementations of common Kerberos
@@ -61,6 +64,19 @@ public abstract class KerberosOperationHandler {
   private final static char[] SECURE_PASSWORD_CHARS =
       "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890?.!$%^*()-_+=~".toCharArray();
 
+
+  /**
+   * The default set of ciphers to use for creating keytab entries
+   */
+  private static final Set<EncryptionType> DEFAULT_CIPHERS = Collections.unmodifiableSet(
+      new HashSet<EncryptionType>() {{
+        add(EncryptionType.DES_CBC_MD5);
+        add(EncryptionType.DES3_CBC_SHA1_KD);
+        add(EncryptionType.RC4_HMAC);
+        add(EncryptionType.AES128_CTS_HMAC_SHA1_96);
+        add(EncryptionType.AES256_CTS_HMAC_SHA1_96);
+      }});
+
   private KerberosCredential administratorCredentials;
   private String defaultRealm;
 
@@ -165,10 +181,10 @@ public abstract class KerberosOperationHandler {
    *
    * @param principal a String containing the principal to add
    * @param password  a String containing the password to use when creating the principal
-   * @return true if the principal was successfully created; otherwise false
+   * @return an Integer declaring the generated key number
    * @throws AmbariException
    */
-  public abstract boolean createServicePrincipal(String principal, String password)
+  public abstract Integer createServicePrincipal(String principal, String password)
       throws AmbariException;
 
   /**
@@ -178,10 +194,10 @@ public abstract class KerberosOperationHandler {
    *
    * @param principal a String containing the principal to update
    * @param password  a String containing the password to set
-   * @return true if the password was successfully updated; otherwise false
+   * @return an Integer declaring the new key number
    * @throws AmbariException
    */
-  public abstract boolean setPrincipalPassword(String principal, String password)
+  public abstract Integer setPrincipalPassword(String principal, String password)
       throws AmbariException;
 
   /**
@@ -205,7 +221,7 @@ public abstract class KerberosOperationHandler {
    * @return true if the keytab file was successfully created; false otherwise
    * @throws AmbariException
    */
-  public boolean createKeytabFile(String principal, String password, File keytabFile)
+  public boolean createKeytabFile(String principal, String password, Integer keyNumber, File keytabFile)
       throws AmbariException {
     boolean success = false;
 
@@ -216,119 +232,70 @@ public abstract class KerberosOperationHandler {
     } else if (keytabFile == null) {
       throw new AmbariException(String.format("Failed to create keytab file for %s, missing file path", principal));
     } else {
-      // Create a set of keys and relevant keytab entries
-      Map<EncryptionType, EncryptionKey> keys = KerberosKeyFactory.getKerberosKeys(principal, password);
-
-      if (keys != null) {
-        KerberosTime timestamp = new KerberosTime();
-        List<KeytabEntry> keytabEntries = new ArrayList<KeytabEntry>();
-
-        Keytab keytab;
-
-        if (keytabFile.exists() && keytabFile.canRead() && (keytabFile.length() > 0)) {
-          // If the keytab file already exists, read it in and append the new keytabs to it so that
-          // potentially important data is not lost
-          try {
-            keytab = Keytab.read(keytabFile);
-          } catch (IOException e) {
-            // There was an issue reading in the existing keytab file... we might loose some keytabs
-            // but that is unlikely...
-            keytab = new Keytab();
-          }
+      Keytab keytab;
+      Set<EncryptionType> ciphers = new HashSet<EncryptionType>(DEFAULT_CIPHERS);
+      List<KeytabEntry> keytabEntries = new ArrayList<KeytabEntry>();
 
-          // In case there were any existing keytab entries, add them to the new entries list do
-          // they are not lost
-          List<KeytabEntry> existingEntries = keytab.getEntries();
-          if ((existingEntries != null) && !existingEntries.isEmpty()) {
-            keytabEntries.addAll(existingEntries);
-          }
-        } else {
+      if (keytabFile.exists() && keytabFile.canRead() && (keytabFile.length() > 0)) {
+        // If the keytab file already exists, read it in and append the new keytabs to it so that
+        // potentially important data is not lost
+        try {
+          keytab = Keytab.read(keytabFile);
+        } catch (IOException e) {
+          // There was an issue reading in the existing keytab file... we might loose some keytabs
+          // but that is unlikely...
           keytab = new Keytab();
         }
 
-        for (EncryptionKey encryptionKey : keys.values()) {
-          keytabEntries.add(new KeytabEntry(principal, 1, timestamp, (byte) 0, encryptionKey));
-        }
+        // In case there were any existing keytab entries, add them to the new entries list so
+        // they are not lost.  While at it, remove ciphers that already exist for the given principal
+        // so duplicate entries aren't added to the file.
+        List<KeytabEntry> existingEntries = keytab.getEntries();
+        if ((existingEntries != null) && !existingEntries.isEmpty()) {
 
-        keytab.setEntries(keytabEntries);
+          for (KeytabEntry entry : existingEntries) {
+            // Remove ciphers that will cause duplicate entries
+            if (principal.equals(entry.getPrincipalName())) {
+              ciphers.remove(entry.getKey().getKeyType());
+            }
 
-        try {
-          keytab.write(keytabFile);
-          success = true;
-        } catch (IOException e) {
-          String message = String.format("Failed to export keytab file for %s", principal);
-          LOG.error(message, e);
-
-          if (!keytabFile.delete()) {
-            keytabFile.deleteOnExit();
+            keytabEntries.add(entry);
           }
-
-          throw new AmbariException(message, e);
         }
+      } else {
+        keytab = new Keytab();
       }
-    }
-
-    return success;
-  }
-
-  /**
-   * Create a keytab file using the set of supplied principal-to-password map.
-   * <p/>
-   * If a file exists where filePath points to, it will be overwritten.
-   *
-   * @param credentials a Map of principals to password, each entry will be placed in the specified file
-   * @param keytabFile  a File containing the absolute path to the keytab file
-   * @return true if the keytab file was successfully created; false otherwise
-   * @throws AmbariException
-   */
-  public boolean createKeytabFile(Map<String, String> credentials, File keytabFile)
-      throws AmbariException {
-    boolean success = false;
-
-    if (credentials == null) {
-      throw new AmbariException("Failed to create keytab file, missing credentials");
-    } else if (keytabFile == null) {
-      throw new AmbariException("Failed to create keytab file, missing file path");
-    } else {
-      List<KeytabEntry> keytabEntries = new ArrayList<KeytabEntry>();
-      KerberosTime timestamp = new KerberosTime();
 
-      // For each set of credentials in the map, create a set of keys and relevant keytab entries
-      for (Map.Entry<String, String> entry : credentials.entrySet()) {
-        String principal = entry.getKey();
-        String password = entry.getValue();
+      if (ciphers.isEmpty()) {
+        // There are no new keys to create
+        success = true;
+      } else {
+        // Create a set of keys and relevant keytab entries
+        Map<EncryptionType, EncryptionKey> keys = KerberosKeyFactory.getKerberosKeys(principal, password, ciphers);
 
-        if (principal == null) {
-          LOG.warn("Missing principal, skipping entry");
-        } else if (password == null) {
-          LOG.warn("Missing password, skipping entry");
-        } else {
-          Map<EncryptionType, EncryptionKey> keys = KerberosKeyFactory.getKerberosKeys(principal, password);
+        if (keys != null) {
+          byte keyVersion = (keyNumber == null) ? 0 : keyNumber.byteValue();
+          KerberosTime timestamp = new KerberosTime();
 
           for (EncryptionKey encryptionKey : keys.values()) {
-            keytabEntries.add(new KeytabEntry(principal, 1, timestamp, (byte) 0, encryptionKey));
+            keytabEntries.add(new KeytabEntry(principal, 1, timestamp, keyVersion, encryptionKey));
           }
-        }
-      }
 
-      // If there are keytab entries, create and write the keytab file
-      if (!keytabEntries.isEmpty()) {
-        Keytab keytab = new Keytab();
+          keytab.setEntries(keytabEntries);
 
-        keytab.setEntries(keytabEntries);
+          try {
+            keytab.write(keytabFile);
+            success = true;
+          } catch (IOException e) {
+            String message = String.format("Failed to export keytab file for %s", principal);
+            LOG.error(message, e);
 
-        try {
-          keytab.write(keytabFile);
-          success = true;
-        } catch (IOException e) {
-          String message = String.format("Failed to export keytab file");
-          LOG.error(message, e);
+            if (!keytabFile.delete()) {
+              keytabFile.deleteOnExit();
+            }
 
-          if (!keytabFile.delete()) {
-            keytabFile.deleteOnExit();
+            throw new AmbariException(message, e);
           }
-
-          throw new AmbariException(message, e);
         }
       }
     }

+ 32 - 2
ambari-server/src/main/java/org/apache/ambari/server/serveraction/kerberos/KerberosServerAction.java

@@ -73,12 +73,16 @@ public abstract class KerberosServerAction extends AbstractServerAction {
    */
   public static final String DATA_DIRECTORY_PREFIX = ".ambari_";
 
-
   /*
-   * Kerberos action shared data entry names
+   * Kerberos action shared data entry name for the principal-to-password map
    */
   private static final String PRINCIPAL_PASSWORD_MAP = "principal_password_map";
 
+  /*
+   * Kerberos action shared data entry name for the principal-to-key_number map
+   */
+  private static final String PRINCIPAL_KEY_NUMBER_MAP = "principal_key_number_map";
+
   /*
   * Key used in kerberosCommandParams in ExecutionCommand for base64 encoded keytab content
   */
@@ -182,6 +186,32 @@ public abstract class KerberosServerAction extends AbstractServerAction {
     }
   }
 
+  /**
+   * Gets the shared principal-to-key_number Map used to store principals and key numbers for
+   * use within the current request context.
+   * <p/>
+   * If the requested Map is not found in requestSharedDataContext, one will be created and stored,
+   * ensuring that a Map will always be returned, assuming requestSharedDataContext is not null.
+   *
+   * @param requestSharedDataContext a Map to be used a shared data among all ServerActions related
+   *                                 to a given request
+   * @return A Map of principals-to-key_numbers
+   */
+  protected static Map<String, Integer> getPrincipalKeyNumberMap(Map<String, Object> requestSharedDataContext) {
+    if (requestSharedDataContext == null) {
+      return null;
+    } else {
+      Object map = requestSharedDataContext.get(PRINCIPAL_KEY_NUMBER_MAP);
+
+      if (map == null) {
+        map = new HashMap<String, String>();
+        requestSharedDataContext.put(PRINCIPAL_KEY_NUMBER_MAP, map);
+      }
+
+      return (Map<String, Integer>) map;
+    }
+  }
+
   /**
    * Given a (command parameter) Map, attempts to safely retrieve the "data_directory" property.
    *

+ 85 - 6
ambari-server/src/main/java/org/apache/ambari/server/serveraction/kerberos/MITKerberosOperationHandler.java

@@ -24,8 +24,12 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import java.io.File;
+import java.text.NumberFormat;
+import java.text.ParseException;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 
 /**
  * MITKerberosOperationHandler is an implementation of a KerberosOperationHandler providing
@@ -35,6 +39,13 @@ import java.util.List;
  * available
  */
 public class MITKerberosOperationHandler extends KerberosOperationHandler {
+
+  /**
+   * A regular expression pattern to use to parse the key number from the text captured from the
+   * get_principal kadmin command
+   */
+  private final static Pattern PATTERN_GET_KEY_NUMBER = Pattern.compile("^.*?Key: vno (\\d+).*$", Pattern.DOTALL);
+
   private final static Logger LOG = LoggerFactory.getLogger(MITKerberosOperationHandler.class);
 
 
@@ -105,11 +116,11 @@ public class MITKerberosOperationHandler extends KerberosOperationHandler {
    *
    * @param principal a String containing the principal add
    * @param password  a String containing the password to use when creating the principal
-   * @return true if the principal was successfully created; otherwise false
+   * @return an Integer declaring the generated key number
    * @throws AmbariException
    */
   @Override
-  public boolean createServicePrincipal(String principal, String password)
+  public Integer createServicePrincipal(String principal, String password)
       throws AmbariException {
 
     if ((principal == null) || principal.isEmpty()) {
@@ -141,7 +152,11 @@ public class MITKerberosOperationHandler extends KerberosOperationHandler {
 
             // If there is data from STDOUT, see if the following string exists:
             //    Principal "<principal>" created
-            return (stdOut != null) && stdOut.contains(String.format("Principal \"%s\" created", principal));
+            if ((stdOut != null) && stdOut.contains(String.format("Principal \"%s\" created", principal))) {
+              return getKeyNumber(principal);
+            } else {
+              throw new AmbariException(String.format("Failed to create service principal for %s", principal));
+            }
           } else {
             LOG.warn("Failed to create service principal for {}:\n\tExitCode: {}\n\tSTDOUT: {}\n\tSTDERR: {}",
                 principal, result.getExitCode(), result.getStdout(), result.getStderr());
@@ -167,11 +182,11 @@ public class MITKerberosOperationHandler extends KerberosOperationHandler {
    *
    * @param principal a String containing the principal to update
    * @param password  a String containing the password to set
-   * @return true if the password was successfully updated; otherwise false
+   * @return an Integer declaring the new key number
    * @throws AmbariException
    */
   @Override
-  public boolean setPrincipalPassword(String principal, String password) throws AmbariException {
+  public Integer setPrincipalPassword(String principal, String password) throws AmbariException {
     if ((principal == null) || principal.isEmpty()) {
       throw new AmbariException("Failed to set password - no principal specified");
     } else {
@@ -197,7 +212,7 @@ public class MITKerberosOperationHandler extends KerberosOperationHandler {
 
         if (result != null) {
           if (result.isSuccessful()) {
-            return true;
+            return getKeyNumber(principal);
           } else {
             LOG.warn("Failed to set password for {}:\n\tExitCode: {}\n\tSTDOUT: {}\n\tSTDERR: {}",
                 principal, result.getExitCode(), result.getStdout(), result.getStderr());
@@ -256,6 +271,70 @@ public class MITKerberosOperationHandler extends KerberosOperationHandler {
     }
   }
 
+  /**
+   * Retrieves the current key number assigned to the identity identified by the specified principal
+   *
+   * @param principal a String declaring the principal to look up
+   * @return an Integer declaring the current key number
+   * @throws AmbariException if an error occurs while looking up the relevant key number
+   */
+  private Integer getKeyNumber(String principal) throws AmbariException {
+    if ((principal == null) || principal.isEmpty()) {
+      throw new AmbariException("Failed to get key number for principal  - no principal specified");
+    } else {
+      // Create the kdamin query:  get_principal <principal>
+      String query = String.format("get_principal %s", principal);
+
+      try {
+        ShellCommandUtil.Result result = invokeKAdmin(query);
+
+        if (result != null) {
+          if (result.isSuccessful()) {
+            String stdOut = result.getStdout();
+
+            if (stdOut == null) {
+              LOG.warn("Failed to get key number for {}:\n\tExitCode: {}\n\tSTDOUT: NULL\n\tSTDERR: {}",
+                  principal, result.getExitCode(), result.getStderr());
+              throw new AmbariException(String.format("Failed to get key number for %s", principal));
+            }
+
+            Matcher matcher = PATTERN_GET_KEY_NUMBER.matcher(stdOut);
+
+            if (matcher.matches()) {
+              NumberFormat numberFormat = NumberFormat.getIntegerInstance();
+              String keyNumber = matcher.group(1);
+
+              numberFormat.setGroupingUsed(false);
+              try {
+                Number number = numberFormat.parse(keyNumber);
+                return (number == null) ? 0 : number.intValue();
+              } catch (ParseException e) {
+                LOG.warn("Failed to get key number for {} - invalid key number value ({}):\n\tExitCode: {}\n\tSTDOUT: NULL\n\tSTDERR: {}",
+                    principal, keyNumber, result.getExitCode(), result.getStderr());
+                throw new AmbariException(String.format("Failed to get key number for %s", principal));
+              }
+            } else {
+              LOG.warn("Failed to get key number for {} - unexpected STDOUT data:\n\tExitCode: {}\n\tSTDOUT: NULL\n\tSTDERR: {}",
+                  principal, result.getExitCode(), result.getStderr());
+              throw new AmbariException(String.format("Failed to get key number for %s", principal));
+            }
+          } else {
+            LOG.warn("Failed to get key number for {}:\n\tExitCode: {}\n\tSTDOUT: {}\n\tSTDERR: {}",
+                principal, result.getExitCode(), result.getStdout(), result.getStderr());
+            throw new AmbariException(String.format("Failed to get key number for %s", principal));
+          }
+        } else {
+          String message = String.format("Failed to get key number for %s - Unknown reason", principal);
+          LOG.warn(message);
+          throw new AmbariException(message);
+        }
+      } catch (AmbariException e) {
+        LOG.error(String.format("Failed to get key number for %s", principal), e);
+        throw e;
+      }
+    }
+  }
+
   /**
    * Invokes the kadmin shell command to issue queries
    *

+ 83 - 113
ambari-server/src/test/java/org/apache/ambari/server/serveraction/kerberos/AbstractKerberosOperationHandlerTest.java

@@ -24,16 +24,22 @@ import org.apache.commons.codec.binary.Base64;
 import org.apache.directory.server.kerberos.shared.keytab.Keytab;
 import org.apache.directory.server.kerberos.shared.keytab.KeytabEntry;
 import org.junit.After;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.Before;
+import org.junit.rules.TemporaryFolder;
 
 import java.io.File;
 import java.io.FileInputStream;
-import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
+import java.util.Set;
 
 public abstract class AbstractKerberosOperationHandlerTest {
 
+  @Rule
+  public TemporaryFolder folder = new TemporaryFolder();
+
   protected final KerberosOperationHandler handler;
 
   protected AbstractKerberosOperationHandlerTest(KerberosOperationHandler handler) {
@@ -71,13 +77,13 @@ public abstract class AbstractKerberosOperationHandlerTest {
       }
 
       @Override
-      public boolean createServicePrincipal(String principal, String password) throws AmbariException {
-        return false;
+      public Integer createServicePrincipal(String principal, String password) throws AmbariException {
+        return 0;
       }
 
       @Override
-      public boolean setPrincipalPassword(String principal, String password) throws AmbariException {
-        return false;
+      public Integer setPrincipalPassword(String principal, String password) throws AmbariException {
+        return 0;
       }
 
       @Override
@@ -117,158 +123,122 @@ public abstract class AbstractKerberosOperationHandlerTest {
   }
 
   @Test
-  public void testCreateKeytabFileAllAtOnce() throws Exception {
-
-    File file = File.createTempFile("ambari_ut_", ".dat");
+  public void testCreateKeytabFileOneAtATime() throws Exception {
+    File file = folder.newFile();
     final String principal1 = "principal1@REALM.COM";
     final String principal2 = "principal2@REALM.COM";
+    int count;
 
-    try {
-      Assert.assertTrue(handler.createKeytabFile(new HashMap<String, String>() {
-        {
-          put(principal1, handler.createSecurePassword());
-        }
-      }, file));
+    Assert.assertTrue(handler.createKeytabFile(principal1, handler.createSecurePassword(), 0, file));
 
-      Keytab keytab = Keytab.read(file);
-      Assert.assertNotNull(keytab);
+    Keytab keytab = Keytab.read(file);
+    Assert.assertNotNull(keytab);
 
-      List<KeytabEntry> entries = keytab.getEntries();
-      Assert.assertNotNull(entries);
-      Assert.assertFalse(entries.isEmpty());
+    List<KeytabEntry> entries = keytab.getEntries();
+    Assert.assertNotNull(entries);
+    Assert.assertFalse(entries.isEmpty());
 
-      for (KeytabEntry entry : entries) {
-        Assert.assertEquals(principal1, entry.getPrincipalName());
-      }
+    count = entries.size();
+
+    for (KeytabEntry entry : entries) {
+      Assert.assertEquals(principal1, entry.getPrincipalName());
+    }
 
-      Assert.assertTrue(handler.createKeytabFile(new HashMap<String, String>() {
-        {
-          put(principal1, handler.createSecurePassword());
-          put(principal2, handler.createSecurePassword());
-        }
-      }, file));
+    Assert.assertTrue(handler.createKeytabFile(principal2, handler.createSecurePassword(), 0, file));
 
-      keytab = Keytab.read(file);
-      Assert.assertNotNull(keytab);
+    keytab = Keytab.read(file);
+    Assert.assertNotNull(keytab);
 
-      entries = keytab.getEntries();
-      Assert.assertNotNull(entries);
-      Assert.assertFalse(entries.isEmpty());
-    } finally {
-      if (!file.delete()) {
-        file.deleteOnExit();
-      }
-    }
+    entries = keytab.getEntries();
+    Assert.assertNotNull(entries);
+    Assert.assertFalse(entries.isEmpty());
+
+    Assert.assertEquals(count * 2, entries.size());
   }
 
   @Test
-  public void testCreateKeytabFileOneAtATime() throws Exception {
-    File file = File.createTempFile("ambari_ut_", ".dat");
+  public void testEnsureKeytabFileContainsNoDuplicates() throws Exception {
+    File file = folder.newFile();
     final String principal1 = "principal1@REALM.COM";
     final String principal2 = "principal2@REALM.COM";
-    int count;
-
-    try {
-      Assert.assertTrue(handler.createKeytabFile(principal1, handler.createSecurePassword(), file));
-
-      Keytab keytab = Keytab.read(file);
-      Assert.assertNotNull(keytab);
+    Set<String> seenEntries = new HashSet<String>();
 
-      List<KeytabEntry> entries = keytab.getEntries();
-      Assert.assertNotNull(entries);
-      Assert.assertFalse(entries.isEmpty());
-
-      count = entries.size();
-
-      for (KeytabEntry entry : entries) {
-        Assert.assertEquals(principal1, entry.getPrincipalName());
-      }
+    Assert.assertTrue(handler.createKeytabFile(principal1, handler.createSecurePassword(), 0, file));
+    Assert.assertTrue(handler.createKeytabFile(principal2, handler.createSecurePassword(), 0, file));
 
-      Assert.assertTrue(handler.createKeytabFile(principal2, handler.createSecurePassword(), file));
+    // Attempt to add duplicate entries
+    Assert.assertTrue(handler.createKeytabFile(principal2, handler.createSecurePassword(), 0, file));
 
-      keytab = Keytab.read(file);
-      Assert.assertNotNull(keytab);
+    Keytab keytab = Keytab.read(file);
+    Assert.assertNotNull(keytab);
 
-      entries = keytab.getEntries();
-      Assert.assertNotNull(entries);
-      Assert.assertFalse(entries.isEmpty());
+    List<KeytabEntry> entries = keytab.getEntries();
+    Assert.assertNotNull(entries);
+    Assert.assertFalse(entries.isEmpty());
 
-      Assert.assertEquals(count * 2, entries.size());
-    } finally {
-      if (!file.delete()) {
-        file.deleteOnExit();
-      }
+    for (KeytabEntry entry : entries) {
+      String seenEntry = String.format("%s|%s", entry.getPrincipalName(), entry.getKey().getKeyType().toString());
+      Assert.assertFalse(seenEntries.contains(seenEntry));
+      seenEntries.add(seenEntry);
     }
   }
 
   @Test
   public void testCreateKeytabFileExceptions() throws Exception {
-    File file = File.createTempFile("ambari_ut_", ".dat");
+    File file = folder.newFile();
     final String principal1 = "principal1@REALM.COM";
 
     try {
-      try {
-        handler.createKeytabFile(null, handler.createSecurePassword(), file);
-        Assert.fail("AmbariException not thrown with null principal");
-      } catch (Throwable t) {
-        Assert.assertEquals(AmbariException.class, t.getClass());
-      }
+      handler.createKeytabFile(null, handler.createSecurePassword(), 0, file);
+      Assert.fail("AmbariException not thrown with null principal");
+    } catch (Throwable t) {
+      Assert.assertEquals(AmbariException.class, t.getClass());
+    }
 
-      try {
-        handler.createKeytabFile(principal1, null, file);
-        Assert.fail("AmbariException not thrown with null password");
-      } catch (Throwable t) {
-        Assert.assertEquals(AmbariException.class, t.getClass());
-      }
+    try {
+      handler.createKeytabFile(principal1, null, null, file);
+      Assert.fail("AmbariException not thrown with null password");
+    } catch (Throwable t) {
+      Assert.assertEquals(AmbariException.class, t.getClass());
+    }
 
-      try {
-        handler.createKeytabFile(principal1, handler.createSecurePassword(), null);
-        Assert.fail("AmbariException not thrown with null file");
-      } catch (Throwable t) {
-        Assert.assertEquals(AmbariException.class, t.getClass());
-      }
-    } finally {
-      if (!file.delete()) {
-        file.deleteOnExit();
-      }
+    try {
+      handler.createKeytabFile(principal1, handler.createSecurePassword(), 0, null);
+      Assert.fail("AmbariException not thrown with null file");
+    } catch (Throwable t) {
+      Assert.assertEquals(AmbariException.class, t.getClass());
     }
   }
 
   @Test
   public void testCreateKeytabFileFromBase64EncodedData() throws Exception {
-    File file = File.createTempFile("ambari_ut_", ".dat");
+    File file = folder.newFile();
     final String principal = "principal@REALM.COM";
 
-    try {
-      Assert.assertTrue(handler.createKeytabFile(principal, handler.createSecurePassword(), file));
+    Assert.assertTrue(handler.createKeytabFile(principal, handler.createSecurePassword(), 0, file));
 
-      FileInputStream fis = new FileInputStream(file);
-      byte[] data = new byte[(int) file.length()];
+    FileInputStream fis = new FileInputStream(file);
+    byte[] data = new byte[(int) file.length()];
 
-      Assert.assertEquals(data.length, fis.read(data));
-      fis.close();
+    Assert.assertEquals(data.length, fis.read(data));
+    fis.close();
 
-      File f = handler.createKeytabFile(Base64.encodeBase64String(data));
+    File f = handler.createKeytabFile(Base64.encodeBase64String(data));
 
-      try {
-        Keytab keytab = Keytab.read(f);
-        Assert.assertNotNull(keytab);
+    try {
+      Keytab keytab = Keytab.read(f);
+      Assert.assertNotNull(keytab);
 
-        List<KeytabEntry> entries = keytab.getEntries();
-        Assert.assertNotNull(entries);
-        Assert.assertFalse(entries.isEmpty());
+      List<KeytabEntry> entries = keytab.getEntries();
+      Assert.assertNotNull(entries);
+      Assert.assertFalse(entries.isEmpty());
 
-        for (KeytabEntry entry : entries) {
-          Assert.assertEquals(principal, entry.getPrincipalName());
-        }
-      } finally {
-        if (!f.delete()) {
-          f.deleteOnExit();
-        }
+      for (KeytabEntry entry : entries) {
+        Assert.assertEquals(principal, entry.getPrincipalName());
       }
     } finally {
-      if (!file.delete()) {
-        file.deleteOnExit();
+      if (!f.delete()) {
+        f.deleteOnExit();
       }
     }
   }

+ 4 - 4
ambari-server/src/test/java/org/apache/ambari/server/serveraction/kerberos/KerberosOperationHandlerTest.java

@@ -41,13 +41,13 @@ public class KerberosOperationHandlerTest extends AbstractKerberosOperationHandl
       }
 
       @Override
-      public boolean createServicePrincipal(String principal, String password) throws AmbariException {
-        return false;
+      public Integer createServicePrincipal(String principal, String password) throws AmbariException {
+        return 0;
       }
 
       @Override
-      public boolean setPrincipalPassword(String principal, String password) throws AmbariException {
-        return false;
+      public Integer setPrincipalPassword(String principal, String password) throws AmbariException {
+        return 0;
       }
 
       @Override