|
|
@@ -22,20 +22,25 @@ import com.google.inject.Inject;
|
|
|
import org.apache.ambari.server.AmbariException;
|
|
|
import org.apache.ambari.server.actionmanager.HostRoleStatus;
|
|
|
import org.apache.ambari.server.agent.CommandReport;
|
|
|
+import org.apache.ambari.server.configuration.Configuration;
|
|
|
import org.apache.ambari.server.orm.dao.KerberosPrincipalDAO;
|
|
|
import org.apache.ambari.server.orm.dao.KerberosPrincipalHostDAO;
|
|
|
import org.apache.ambari.server.orm.entities.KerberosPrincipalEntity;
|
|
|
import org.apache.commons.codec.digest.DigestUtils;
|
|
|
-import org.apache.commons.io.FileUtils;
|
|
|
+import org.apache.directory.server.kerberos.shared.keytab.Keytab;
|
|
|
import org.slf4j.Logger;
|
|
|
import org.slf4j.LoggerFactory;
|
|
|
|
|
|
import java.io.File;
|
|
|
import java.io.IOException;
|
|
|
+import java.util.HashMap;
|
|
|
+import java.util.HashSet;
|
|
|
import java.util.Map;
|
|
|
+import java.util.Set;
|
|
|
import java.util.concurrent.ConcurrentMap;
|
|
|
|
|
|
import static org.apache.ambari.server.serveraction.kerberos.KerberosActionDataFile.HOSTNAME;
|
|
|
+import static org.apache.ambari.server.serveraction.kerberos.KerberosActionDataFile.KEYTAB_FILE_IS_CACHABLE;
|
|
|
import static org.apache.ambari.server.serveraction.kerberos.KerberosActionDataFile.KEYTAB_FILE_PATH;
|
|
|
|
|
|
/**
|
|
|
@@ -63,6 +68,18 @@ public class CreateKeytabFilesServerAction extends KerberosServerAction {
|
|
|
@Inject
|
|
|
private KerberosPrincipalHostDAO kerberosPrincipalHostDAO;
|
|
|
|
|
|
+ /**
|
|
|
+ * Configuration used to get the configured properties such as the keytab file cache directory
|
|
|
+ */
|
|
|
+ @Inject
|
|
|
+ private Configuration configuration;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * A map of data used to track what has been processed in order to optimize the creation of keytabs
|
|
|
+ * such as knowing when to create a cached keytab file or use a cached keytab file.
|
|
|
+ */
|
|
|
+ Map<String, Set<String>> visitedIdentities = new HashMap<String, Set<String>>();
|
|
|
+
|
|
|
/**
|
|
|
* Called to execute this action. Upon invocation, calls
|
|
|
* {@link org.apache.ambari.server.serveraction.kerberos.KerberosServerAction#processIdentities(java.util.Map)} )}
|
|
|
@@ -126,9 +143,7 @@ public class CreateKeytabFilesServerAction extends KerberosServerAction {
|
|
|
CommandReport commandReport = null;
|
|
|
|
|
|
if (identityRecord != null) {
|
|
|
- String message = String.format("Creating keytab file for %s", evaluatedPrincipal);
|
|
|
- LOG.info(message);
|
|
|
- actionLog.writeStdOut(message);
|
|
|
+ String message;
|
|
|
|
|
|
if (operationHandler == null) {
|
|
|
message = String.format("Failed to create keytab file for %s, missing KerberosOperationHandler", evaluatedPrincipal);
|
|
|
@@ -143,84 +158,157 @@ public class CreateKeytabFilesServerAction extends KerberosServerAction {
|
|
|
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);
|
|
|
-
|
|
|
- // 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 (password == null) {
|
|
|
- if (kerberosPrincipalHostDAO.exists(evaluatedPrincipal, host)) {
|
|
|
- // There is nothing to do for this since it must already exist and we don't want to
|
|
|
- // regenerate the keytab
|
|
|
- message = String.format("Skipping keytab file for %s, missing password indicates nothing to do", evaluatedPrincipal);
|
|
|
- LOG.debug(message);
|
|
|
- } else {
|
|
|
- KerberosPrincipalEntity principalEntity = kerberosPrincipalDAO.find(evaluatedPrincipal);
|
|
|
- String cachedKeytabPath = (principalEntity == null) ? null : principalEntity.getCachedKeytabPath();
|
|
|
-
|
|
|
- if (cachedKeytabPath == null) {
|
|
|
- message = String.format("Failed to create keytab file for %s, missing password", evaluatedPrincipal);
|
|
|
- actionLog.writeStdErr(message);
|
|
|
- LOG.error(message);
|
|
|
- commandReport = createCommandReport(1, HostRoleStatus.FAILED, "{}", actionLog.getStdOut(), actionLog.getStdErr());
|
|
|
+ Set<String> visitedPrincipalKeys = visitedIdentities.get(evaluatedPrincipal);
|
|
|
+ String visitationKey = String.format("%s|%s", host, keytabFilePath);
|
|
|
+
|
|
|
+ if ((visitedPrincipalKeys == null) || !visitedPrincipalKeys.contains(visitationKey)) {
|
|
|
+ // Look up the current evaluatedPrincipal's password.
|
|
|
+ // If found create the keytab file, else try to find it in the cache.
|
|
|
+ String password = principalPasswordMap.get(evaluatedPrincipal);
|
|
|
+
|
|
|
+ message = String.format("Creating keytab file for %s on host %s", evaluatedPrincipal, host);
|
|
|
+ LOG.info(message);
|
|
|
+ actionLog.writeStdOut(message);
|
|
|
+
|
|
|
+ // 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()) {
|
|
|
+ // Make sure only Ambari has access to this directory.
|
|
|
+ ensureAmbariOnlyAccess(hostDirectory);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (hostDirectory.exists()) {
|
|
|
+ File destinationKeytabFile = new File(hostDirectory, DigestUtils.sha1Hex(keytabFilePath));
|
|
|
+
|
|
|
+ if (password == null) {
|
|
|
+ if (kerberosPrincipalHostDAO.exists(evaluatedPrincipal, host)) {
|
|
|
+ // There is nothing to do for this since it must already exist and we don't want to
|
|
|
+ // regenerate the keytab
|
|
|
+ message = String.format("Skipping keytab file for %s, missing password indicates nothing to do", evaluatedPrincipal);
|
|
|
+ LOG.debug(message);
|
|
|
} else {
|
|
|
+ KerberosPrincipalEntity principalEntity = kerberosPrincipalDAO.find(evaluatedPrincipal);
|
|
|
+ String cachedKeytabPath = (principalEntity == null) ? null : principalEntity.getCachedKeytabPath();
|
|
|
+
|
|
|
+ if (cachedKeytabPath == null) {
|
|
|
+ message = String.format("Failed to create keytab for %s, missing cached file", evaluatedPrincipal);
|
|
|
+ actionLog.writeStdErr(message);
|
|
|
+ LOG.error(message);
|
|
|
+ commandReport = createCommandReport(1, HostRoleStatus.FAILED, "{}", actionLog.getStdOut(), actionLog.getStdErr());
|
|
|
+ } else {
|
|
|
+ try {
|
|
|
+ operationHandler.createKeytabFile(new File(cachedKeytabPath), destinationKeytabFile);
|
|
|
+ } catch (KerberosOperationException e) {
|
|
|
+ message = String.format("Failed to create keytab file for %s - %s", evaluatedPrincipal, e.getMessage());
|
|
|
+ actionLog.writeStdErr(message);
|
|
|
+ LOG.error(message, e);
|
|
|
+ commandReport = createCommandReport(1, HostRoleStatus.FAILED, "{}", actionLog.getStdOut(), actionLog.getStdErr());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ Keytab keytab = null;
|
|
|
+
|
|
|
+ // Possibly get the keytab from the cache
|
|
|
+ if (visitedPrincipalKeys != null) {
|
|
|
+ // Since we have visited this principal before, attempt to pull the keytab from the
|
|
|
+ // cache...
|
|
|
+ KerberosPrincipalEntity principalEntity = kerberosPrincipalDAO.find(evaluatedPrincipal);
|
|
|
+ String cachedKeytabPath = (principalEntity == null) ? null : principalEntity.getCachedKeytabPath();
|
|
|
+
|
|
|
+ if (cachedKeytabPath != null) {
|
|
|
+ try {
|
|
|
+ keytab = Keytab.read(new File(cachedKeytabPath));
|
|
|
+ } catch (IOException e) {
|
|
|
+ message = String.format("Failed to read the cached keytab for %s, recreating if possible - %s",
|
|
|
+ evaluatedPrincipal, e.getMessage());
|
|
|
+
|
|
|
+ if (LOG.isDebugEnabled()) {
|
|
|
+ LOG.warn(message, e);
|
|
|
+ } else {
|
|
|
+ LOG.warn(message, e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // If the keytab was not retrieved from the cache... create it.
|
|
|
+ if (keytab == null) {
|
|
|
+ Integer keyNumber = principalKeyNumberMap.get(evaluatedPrincipal);
|
|
|
+
|
|
|
try {
|
|
|
- FileUtils.copyFile(new File(cachedKeytabPath), keytabFile);
|
|
|
- message = String.format("Using cached keytab file for %s at %s", evaluatedPrincipal, keytabFile.getAbsolutePath());
|
|
|
- LOG.debug(message);
|
|
|
- } catch (IOException e) {
|
|
|
- message = String.format("Failed to use cached keytab file for %s at %s: %s", evaluatedPrincipal, keytabFile.getAbsolutePath(), e.getMessage());
|
|
|
+ keytab = operationHandler.createKeytab(evaluatedPrincipal, password, keyNumber);
|
|
|
+
|
|
|
+ // If the current identity does not represent a service, copy it to a secure location
|
|
|
+ // and store that location so it can be reused rather than recreate it.
|
|
|
+ KerberosPrincipalEntity principalEntity = kerberosPrincipalDAO.find(evaluatedPrincipal);
|
|
|
+ if (principalEntity != null) {
|
|
|
+ if (!principalEntity.isService() && ("true".equalsIgnoreCase(identityRecord.get(KEYTAB_FILE_IS_CACHABLE)))) {
|
|
|
+ File cachedKeytabFile = cacheKeytab(evaluatedPrincipal, keytab);
|
|
|
+ String previousCachedFilePath = principalEntity.getCachedKeytabPath();
|
|
|
+ String cachedKeytabFilePath = ((cachedKeytabFile == null) || !cachedKeytabFile.exists())
|
|
|
+ ? null
|
|
|
+ : cachedKeytabFile.getAbsolutePath();
|
|
|
+
|
|
|
+ principalEntity.setCachedKeytabPath(cachedKeytabFilePath);
|
|
|
+ kerberosPrincipalDAO.merge(principalEntity);
|
|
|
+
|
|
|
+ if(previousCachedFilePath != null) {
|
|
|
+ if(!new File(previousCachedFilePath).delete()) {
|
|
|
+ LOG.debug(String.format("Failed to remove orphaned cache file %s", previousCachedFilePath));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (KerberosOperationException e) {
|
|
|
+ message = String.format("Failed to create keytab file for %s - %s", evaluatedPrincipal, e.getMessage());
|
|
|
actionLog.writeStdErr(message);
|
|
|
- LOG.warn(message);
|
|
|
+ LOG.error(message, e);
|
|
|
commandReport = createCommandReport(1, HostRoleStatus.FAILED, "{}", actionLog.getStdOut(), actionLog.getStdErr());
|
|
|
}
|
|
|
}
|
|
|
- }
|
|
|
- } else {
|
|
|
- Integer keyNumber = principalKeyNumberMap.get(evaluatedPrincipal);
|
|
|
|
|
|
- try {
|
|
|
- if (operationHandler.createKeytabFile(evaluatedPrincipal, password, keyNumber, keytabFile)) {
|
|
|
- message = String.format("Successfully created keytab file for %s at %s", evaluatedPrincipal, keytabFile.getAbsolutePath());
|
|
|
- LOG.debug(message);
|
|
|
+ if (keytab != null) {
|
|
|
+ try {
|
|
|
+ if (operationHandler.createKeytabFile(keytab, destinationKeytabFile)) {
|
|
|
+ ensureAmbariOnlyAccess(destinationKeytabFile);
|
|
|
|
|
|
- // If the current identity does not represent a service, store the location of the
|
|
|
- // keytab file so it can be reused rather than recreate it.
|
|
|
- // Note: for now we are using the keytab's destination directory on the Ambari
|
|
|
- // server
|
|
|
- KerberosPrincipalEntity principalEntity = kerberosPrincipalDAO.find(evaluatedPrincipal);
|
|
|
- if (principalEntity != null) {
|
|
|
- if (!principalEntity.isService()) {
|
|
|
- principalEntity.setCachedKeytabPath(keytabFilePath);
|
|
|
- kerberosPrincipalDAO.merge(principalEntity);
|
|
|
+ message = String.format("Successfully created keytab file for %s at %s", evaluatedPrincipal, destinationKeytabFile.getAbsolutePath());
|
|
|
+ LOG.debug(message);
|
|
|
+ } else {
|
|
|
+ message = String.format("Failed to create keytab file for %s at %s", evaluatedPrincipal, destinationKeytabFile.getAbsolutePath());
|
|
|
+ actionLog.writeStdErr(message);
|
|
|
+ LOG.error(message);
|
|
|
+ commandReport = createCommandReport(1, HostRoleStatus.FAILED, "{}", actionLog.getStdOut(), actionLog.getStdErr());
|
|
|
}
|
|
|
+ } catch (KerberosOperationException e) {
|
|
|
+ message = String.format("Failed to create keytab file for %s - %s", evaluatedPrincipal, e.getMessage());
|
|
|
+ actionLog.writeStdErr(message);
|
|
|
+ LOG.error(message, e);
|
|
|
+ commandReport = createCommandReport(1, HostRoleStatus.FAILED, "{}", actionLog.getStdOut(), actionLog.getStdErr());
|
|
|
}
|
|
|
- } else {
|
|
|
- message = String.format("Failed to create keytab file for %s at %s", evaluatedPrincipal, keytabFile.getAbsolutePath());
|
|
|
- actionLog.writeStdErr(message);
|
|
|
- LOG.error(message);
|
|
|
- commandReport = createCommandReport(1, HostRoleStatus.FAILED, "{}", actionLog.getStdOut(), actionLog.getStdErr());
|
|
|
}
|
|
|
- } catch (KerberosOperationException e) {
|
|
|
- message = String.format("Failed to create keytab file for %s - %s", evaluatedPrincipal, e.getMessage());
|
|
|
- actionLog.writeStdErr(message);
|
|
|
- LOG.error(message, e);
|
|
|
- commandReport = createCommandReport(1, HostRoleStatus.FAILED, "{}", actionLog.getStdOut(), actionLog.getStdErr());
|
|
|
}
|
|
|
+ } else {
|
|
|
+ message = String.format("Failed to create keytab file for %s, the container directory does not exist: %s",
|
|
|
+ evaluatedPrincipal, hostDirectory.getAbsolutePath());
|
|
|
+ actionLog.writeStdErr(message);
|
|
|
+ LOG.error(message);
|
|
|
+ commandReport = createCommandReport(1, HostRoleStatus.FAILED, "{}", actionLog.getStdOut(), actionLog.getStdErr());
|
|
|
}
|
|
|
- } else {
|
|
|
- message = String.format("Failed to create keytab file for %s, the container directory does not exist: %s",
|
|
|
- evaluatedPrincipal, hostDirectory.getAbsolutePath());
|
|
|
- actionLog.writeStdErr(message);
|
|
|
- LOG.error(message);
|
|
|
- commandReport = createCommandReport(1, HostRoleStatus.FAILED, "{}", actionLog.getStdOut(), actionLog.getStdErr());
|
|
|
+
|
|
|
+ if(visitedPrincipalKeys == null) {
|
|
|
+ visitedPrincipalKeys = new HashSet<String>();
|
|
|
+ visitedIdentities.put(evaluatedPrincipal, visitedPrincipalKeys);
|
|
|
+ }
|
|
|
+
|
|
|
+ visitedPrincipalKeys.add(visitationKey);
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ LOG.debug(String.format("Skipping previously processed keytab for %s on host %s", evaluatedPrincipal, host));
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
@@ -228,4 +316,83 @@ public class CreateKeytabFilesServerAction extends KerberosServerAction {
|
|
|
|
|
|
return commandReport;
|
|
|
}
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Cache a keytab given its relative principal name and the keytab data.
|
|
|
+ * <p/>
|
|
|
+ * The specified keytab is stored in a file in a location derived using the configured keytab
|
|
|
+ * cache directory and the seeded hash of the principal name - this is to add a slight level
|
|
|
+ * of obscurity so that it cannot be determined what keytab data is in the file based on its name.
|
|
|
+ * The file is the set readable by only the Ambari server process owner.
|
|
|
+ *
|
|
|
+ * @param principal the principal name related to the keytab data
|
|
|
+ * @param keytab the keytab data to cache
|
|
|
+ * @return a File pointing to the cached keytab file
|
|
|
+ * @throws AmbariException if a failure occurs while creating the cache file containing the the keytab data
|
|
|
+ */
|
|
|
+ private File cacheKeytab(String principal, Keytab keytab) throws AmbariException {
|
|
|
+ File cacheDirectory = configuration.getKerberosKeytabCacheDir();
|
|
|
+
|
|
|
+ if (cacheDirectory == null) {
|
|
|
+ String message = "The Kerberos keytab cache directory is not configured in the Ambari properties";
|
|
|
+ LOG.error(message);
|
|
|
+ throw new AmbariException(message);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!cacheDirectory.exists()) {
|
|
|
+ // If the cache directory does not exist, create it and ensure only Ambari has access to it
|
|
|
+ if (cacheDirectory.mkdirs()) {
|
|
|
+ ensureAmbariOnlyAccess(cacheDirectory);
|
|
|
+
|
|
|
+ if (!cacheDirectory.exists()) {
|
|
|
+ String message = String.format("Failed to create the keytab cache directory %s",
|
|
|
+ cacheDirectory.getAbsolutePath());
|
|
|
+ LOG.error(message);
|
|
|
+ throw new AmbariException(message);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ File cachedKeytabFile = new File(cacheDirectory, DigestUtils.sha1Hex(principal + String.valueOf(System.currentTimeMillis())));
|
|
|
+
|
|
|
+ try {
|
|
|
+ keytab.write(cachedKeytabFile);
|
|
|
+ ensureAmbariOnlyAccess(cachedKeytabFile);
|
|
|
+ } catch (IOException e) {
|
|
|
+ String message = String.format("Failed to write the keytab for %s to the cache location (%s)",
|
|
|
+ principal, cachedKeytabFile.getAbsolutePath());
|
|
|
+ LOG.error(message, e);
|
|
|
+ throw new AmbariException(message, e);
|
|
|
+ }
|
|
|
+
|
|
|
+ return cachedKeytabFile;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Ensures that the owner of the Ambari server process is the only local user account able to
|
|
|
+ * read and write to the specified file or read, write to, and execute the specified directory.
|
|
|
+ *
|
|
|
+ * @param file the file or directory for which to modify access
|
|
|
+ */
|
|
|
+ private void ensureAmbariOnlyAccess(File file) {
|
|
|
+ if (file.exists()) {
|
|
|
+ if (!file.setReadable(false, false) || !file.setReadable(true, true)) {
|
|
|
+ LOG.warn(String.format("Failed to set %s readable only by Ambari", file.getAbsolutePath()));
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!file.setWritable(false, false) || !file.setWritable(true, true)) {
|
|
|
+ LOG.warn(String.format("Failed to set %s writable only by Ambari", file.getAbsolutePath()));
|
|
|
+ }
|
|
|
+
|
|
|
+ if (file.isDirectory()) {
|
|
|
+ if (!file.setExecutable(false, false) && !file.setExecutable(true, true)) {
|
|
|
+ LOG.warn(String.format("Failed to set %s executable by Ambari", file.getAbsolutePath()));
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ if (!file.setExecutable(false, false)) {
|
|
|
+ LOG.warn(String.format("Failed to set %s not executable", file.getAbsolutePath()));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|