blob: db210e08c5e532390d69a5ce9a3bc36b3b4b4cf0 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.ambari.server.serveraction.kerberos;
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.agent.ExecutionCommand;
import org.apache.ambari.server.controller.KerberosHelper;
import org.apache.ambari.server.security.credential.PrincipalKeyCredential;
import org.apache.ambari.server.serveraction.AbstractServerAction;
import org.apache.ambari.server.state.Cluster;
import org.apache.ambari.server.state.Clusters;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static org.apache.ambari.server.serveraction.kerberos.KerberosIdentityDataFileReader.DATA_FILE_NAME;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
/**
* KerberosServerAction is an abstract class to be implemented by Kerberos-related
* {@link org.apache.ambari.server.serveraction.ServerAction} implementations.
* <p/>
* This class provides helper methods used to get common properties from the command parameters map
* and iterate through the Kerberos identity metadata file
* (see {@link org.apache.ambari.server.serveraction.kerberos.KerberosIdentityDataFileReader}).
*/
public abstract class KerberosServerAction extends AbstractServerAction {
/**
* A (command parameter) property name used to hold the authenticated user's name for use in
* operations that record the acting user.
*/
public static final String AUTHENTICATED_USER_NAME = "authenticated_user_name";
/**
* A (command parameter) property name used to hold the absolute path to the directory that is to
* be used to store transient data while the request is being processed. This is expected to be
* a temporary directory.
*/
public static final String DATA_DIRECTORY = "data_directory";
/**
* A (command parameter) property name used to hold the default Kerberos realm value.
*/
public static final String DEFAULT_REALM = "default_realm";
/**
* A (command parameter) property name used to hold the (serialized) service/component filter map.
*/
public static final String SERVICE_COMPONENT_FILTER = "service_component_filter";
/**
* A (command parameter) property name used to hold the (serialized) host filter list.
*/
public static final String HOST_FILTER = "host_filter";
/**
* A (command parameter) property name used to hold the (serialized) identity filter list.
*/
public static final String IDENTITY_FILTER = "identity_filter";
/**
* A (command parameter) property name used to hold the relevant KDC type value. See
* {@link org.apache.ambari.server.serveraction.kerberos.KDCType} for valid values
*/
public static final String KDC_TYPE = "kdc_type";
/**
* A (command parameter) property name used to hold a boolean value indicating whether configurations
* should be process to see if they need to be updated
*/
public static final String UPDATE_CONFIGURATIONS = "update_configurations";
/**
* A (command parameter) property name used to hold the note to set when applying any
* configuration changes
*/
public static final String UPDATE_CONFIGURATION_NOTE = "update_configuration_note";
/**
* The prefix to use for the data directory name.
*/
public static final String DATA_DIRECTORY_PREFIX = ".ambari_";
/*
* 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
*/
public static final String KEYTAB_CONTENT_BASE64 = "keytab_content_base64";
/*
* Key used in kerberosCommandParams in ExecutionCommand to indicate whether to generate key keytabs
* for all principals ("true") or only those that are missing ("false")
*/
public static final String REGENERATE_ALL = "regenerate_all";
/*
* Key used in kerberosCommandParams in ExecutionCommand to indicate whether to include Ambari server indetity
* ("true") or ignore it ("false")
*/
public static final String INCLUDE_AMBARI_IDENTITY = "include_ambari_identity";
private static final Logger LOG = LoggerFactory.getLogger(KerberosServerAction.class);
/**
* The Cluster that this ServerAction implementation is executing on
*/
@Inject
private Clusters clusters = null;
/**
* The KerberosOperationHandlerFactory to use to obtain KerberosOperationHandler instances
* <p/>
* This is needed to help with test cases to mock a KerberosOperationHandler
*/
@Inject
private KerberosOperationHandlerFactory kerberosOperationHandlerFactory;
/**
* The KerberosIdentityDataFileReaderFactory to use to obtain KerberosIdentityDataFileReader instances
*/
@Inject
private KerberosIdentityDataFileReaderFactory kerberosIdentityDataFileReaderFactory;
/**
* KerberosHelper
*/
@Inject
private KerberosHelper kerberosHelper;
/**
* Given a (command parameter) Map and a property name, attempts to safely retrieve the requested
* data.
*
* @param commandParameters a Map containing the dictionary of data to interrogate
* @param propertyName a String declaring the name of the item from commandParameters to retrieve
* @return a String or null, depending on the property value and if it existed in commandParameters
*/
protected static String getCommandParameterValue(Map<String, String> commandParameters, String propertyName) {
return ((commandParameters == null) || (propertyName == null)) ? null : commandParameters.get(propertyName);
}
/**
* Given a (command parameter) Map, attempts to safely retrieve the "default_realm" property.
*
* @param commandParameters a Map containing the dictionary of data to interrogate
* @return a String indicating the default realm or null (if not found or set)
*/
protected static String getDefaultRealm(Map<String, String> commandParameters) {
return getCommandParameterValue(commandParameters, DEFAULT_REALM);
}
/**
* Given a (command parameter) Map, attempts to safely retrieve the "kdc_type" property.
* <p/>
* If not found, {@link org.apache.ambari.server.serveraction.kerberos.KDCType#MIT_KDC} will be
* returned as a default value.
*
* @param commandParameters a Map containing the dictionary of data to interrogate
* @return a KDCType or null (if not found or set)
*/
protected static KDCType getKDCType(Map<String, String> commandParameters) {
String kdcType = getCommandParameterValue(commandParameters, KDC_TYPE);
return ((kdcType == null) || kdcType.isEmpty())
? KDCType.MIT_KDC
: KDCType.translate(kdcType);
}
/**
* Given a (command parameter) Map, attempts to safely retrieve the "data_directory" property.
*
* @param commandParameters a Map containing the dictionary of data to interrogate
* @return a String indicating the data directory or null (if not found or set)
*/
protected static String getDataDirectoryPath(Map<String, String> commandParameters) {
return getCommandParameterValue(commandParameters, DATA_DIRECTORY);
}
/**
* Sets the shared principal-to-password Map used to store principals and generated password for
* use within the current request context.
*
* @param requestSharedDataContext a Map to be used as shared data among all ServerActions related
* to a given request
* @param principalPasswordMap A Map of principals and password to store
*/
protected static void setPrincipalPasswordMap(Map<String, Object> requestSharedDataContext,
Map<String, String> principalPasswordMap) {
if (requestSharedDataContext != null) {
requestSharedDataContext.put(PRINCIPAL_PASSWORD_MAP, principalPasswordMap);
}
}
/**
* Gets the shared principal-to-password Map used to store principals and generated password 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-password
*/
protected static Map<String, String> getPrincipalPasswordMap(Map<String, Object> requestSharedDataContext) {
if (requestSharedDataContext == null) {
return null;
} else {
Object map = requestSharedDataContext.get(PRINCIPAL_PASSWORD_MAP);
if (map == null) {
map = new HashMap<String, String>();
requestSharedDataContext.put(PRINCIPAL_PASSWORD_MAP, map);
}
return (Map<String, String>) map;
}
}
/**
* 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;
}
}
/**
* Returns the relevant cluster's name
* <p/>
* Using the data from the execution command, retrieve the relevant cluster's name.
*
* @return a String declaring the relevant cluster's name
* @throws AmbariException if the cluster's name is not available
*/
protected String getClusterName() throws AmbariException {
ExecutionCommand executionCommand = getExecutionCommand();
String clusterName = (executionCommand == null) ? null : executionCommand.getClusterName();
if ((clusterName == null) || clusterName.isEmpty()) {
throw new AmbariException("Failed to retrieve the cluster name from the execution command");
}
return clusterName;
}
/**
* Returns the relevant Cluster object
*
* @return the relevant Cluster
* @throws AmbariException if the Cluster object cannot be retrieved
*/
protected Cluster getCluster() throws AmbariException {
Cluster cluster = clusters.getCluster(getClusterName());
if (cluster == null) {
throw new AmbariException(String.format("Failed to retrieve cluster for %s", getClusterName()));
}
return cluster;
}
/**
* The Clusters object for this KerberosServerAction
*
* @return a Clusters object
*/
protected Clusters getClusters() {
return clusters;
}
/**
* Attempts to safely retrieve the "data_directory" property from the this action's relevant
* command parameters Map.
*
* @return a String indicating the data directory or null (if not found or set)
*/
protected String getDataDirectoryPath() {
return getDataDirectoryPath(getCommandParameters());
}
/**
* Iterates through the Kerberos identity metadata from the
* {@link org.apache.ambari.server.serveraction.kerberos.KerberosIdentityDataFileReader} and calls
* the implementing class to handle each identity found.
* <p/>
* Using the "data_directory" value from this action's command parameters map, creates a
* {@link KerberosIdentityDataFileReader} to parse
* the relative identity.dat file and iterate through its "records". Each "record" is process using
* {@link #processRecord(Map, String, KerberosOperationHandler, Map, Map)}.
*
* @param requestSharedDataContext a Map to be used a shared data among all ServerActions related
* to a given request
* @return a CommandReport indicating the result of this operation
* @throws AmbariException
*/
protected CommandReport processIdentities(Map<String, Object> requestSharedDataContext)
throws AmbariException {
CommandReport commandReport = null;
Map<String, String> commandParameters = getCommandParameters();
actionLog.writeStdOut("Processing identities...");
LOG.info("Processing identities...");
if (commandParameters != null) {
// Grab the relevant data from this action's command parameters map
PrincipalKeyCredential administratorCredential = kerberosHelper.getKDCAdministratorCredentials(getClusterName());
String defaultRealm = getDefaultRealm(commandParameters);
KDCType kdcType = getKDCType(commandParameters);
String dataDirectoryPath = getDataDirectoryPath(commandParameters);
if (dataDirectoryPath != null) {
File dataDirectory = new File(dataDirectoryPath);
// If the data directory exists, attempt to process further, else assume there is no work to do
if (dataDirectory.exists()) {
if (!dataDirectory.isDirectory() || !dataDirectory.canRead()) {
String message = String.format("Failed to process the identities, the data directory is not accessible: %s",
dataDirectory.getAbsolutePath());
actionLog.writeStdErr(message);
LOG.error(message);
throw new AmbariException(message);
}
// The "identity data" file may or may not exist in the data directory, depending on if
// there is work to do or not.
File identityDataFile = new File(dataDirectory, DATA_FILE_NAME);
if (identityDataFile.exists()) {
if (!identityDataFile.canRead()) {
String message = String.format("Failed to process the identities, cannot read the index file: %s",
identityDataFile.getAbsolutePath());
actionLog.writeStdErr(message);
LOG.error(message);
throw new AmbariException(message);
}
KerberosOperationHandler handler = kerberosOperationHandlerFactory.getKerberosOperationHandler(kdcType);
if (handler == null) {
String message = String.format("Failed to process the identities, a KDC operation handler was not found for the KDC type of : %s",
kdcType.toString());
actionLog.writeStdErr(message);
LOG.error(message);
throw new AmbariException(message);
}
Map<String, String> kerberosConfiguration = getConfiguration("kerberos-env");
try {
handler.open(administratorCredential, defaultRealm, kerberosConfiguration);
} catch (KerberosOperationException e) {
String message = String.format("Failed to process the identities, could not properly open the KDC operation handler: %s",
e.getMessage());
actionLog.writeStdErr(message);
LOG.error(message);
throw new AmbariException(message, e);
}
// Create the data file reader to parse and iterate through the records
KerberosIdentityDataFileReader reader = null;
try {
reader = kerberosIdentityDataFileReaderFactory.createKerberosIdentityDataFileReader(identityDataFile);
for (Map<String, String> record : reader) {
// Process the current record
commandReport = processRecord(record, defaultRealm, handler, kerberosConfiguration, requestSharedDataContext);
// If the principal processor returns a CommandReport, than it is time to stop since
// an error condition has probably occurred, else all is assumed to be well.
if (commandReport != null) {
break;
}
}
} catch (AmbariException e) {
// Catch this separately from IOException since the reason it was thrown was not the same
// Note: AmbariException is an IOException, so there may be some confusion
throw new AmbariException(e.getMessage(), e);
} catch (IOException e) {
String message = String.format("Failed to process the identities, cannot read the index file: %s",
identityDataFile.getAbsolutePath());
actionLog.writeStdErr(message);
LOG.error(message, e);
throw new AmbariException(message, e);
} finally {
if (reader != null) {
// The reader needs to be closed, if it fails to close ignore the exception since
// there is little we can or care to do about it now.
try {
reader.close();
} catch (IOException e) {
// Ignore this...
}
}
// The KerberosOperationHandler needs to be closed, if it fails to close ignore the
// exception since there is little we can or care to do about it now.
try {
handler.close();
} catch (KerberosOperationException e) {
// Ignore this...
}
}
}
}
}
}
actionLog.writeStdOut("Processing identities completed.");
LOG.info("Processing identities completed.");
// If commandReport is null, we can assume this operation was a success, so return a successful
// CommandReport; else return the previously created CommandReport.
return (commandReport == null)
? createCommandReport(0, HostRoleStatus.COMPLETED, "{}", actionLog.getStdOut(), actionLog.getStdErr())
: commandReport;
}
/**
* Processes an identity as necessary.
* <p/>
* This method is called from {@link #processIdentities(Map)} for each
* identity "record" found in the Kerberos identity metadata file. After processing, it is expected
* that the return value is null on success and a CommandReport (indicating the error) on failure.
*
* @param identityRecord a Map containing the data for the current identity record
* @param evaluatedPrincipal a String indicating the relevant principal
* @param operationHandler a KerberosOperationHandler used to perform Kerberos-related
* tasks for specific Kerberos implementations
* (MIT, Active Directory, etc...)
* @param kerberosConfiguration a Map of configuration properties from kerberos-env
* @param requestSharedDataContext a Map to be used a shared data among all ServerActions related
* to a given request @return a CommandReport, indicating an error
* condition; or null, indicating a success condition
* @throws AmbariException if an error occurs while processing the identity record
*/
protected abstract CommandReport processIdentity(Map<String, String> identityRecord,
String evaluatedPrincipal,
KerberosOperationHandler operationHandler,
Map<String, String> kerberosConfiguration,
Map<String, Object> requestSharedDataContext)
throws AmbariException;
/**
* Process and prepares an identity record to be handled by the implementing class.
* <p/>
* Given the data from the record Map, attempts to replace variables in the principal pattern to
* generate a concrete principal value to further process. This "evaluated principal" is then passed to
* {@link #processIdentity(Map, String, KerberosOperationHandler, Map, Map)}
* to be handled as needed.
*
* @param record a Map containing the data for the current identity record
* @param defaultRealm a String declaring the default Kerberos realm
* @param operationHandler a KerberosOperationHandler used to perform Kerberos-related
* tasks for specific Kerberos implementations
* (MIT, Active Directory, etc...)
* @param kerberosConfiguration a Map of configuration properties from kerberos-env
* @param requestSharedDataContext a Map to be used a shared data among all ServerActions related
* to a given request @return a CommandReport, indicating an error
* condition; or null, indicating a success condition
* @throws AmbariException if an error occurs while processing the identity record
*/
private CommandReport processRecord(Map<String, String> record, String defaultRealm,
KerberosOperationHandler operationHandler,
Map<String, String> kerberosConfiguration, Map<String, Object> requestSharedDataContext)
throws AmbariException {
CommandReport commandReport = null;
if (record != null) {
String principal = record.get(KerberosIdentityDataFileReader.PRINCIPAL);
String host = record.get(KerberosIdentityDataFileReader.HOSTNAME);
if (principal != null) {
// Evaluate the principal "pattern" found in the record to generate the "evaluated principal"
// by replacing the _HOST and _REALM variables.
String evaluatedPrincipal = principal.replace("_HOST", host).replace("_REALM", defaultRealm);
commandReport = processIdentity(record, evaluatedPrincipal, operationHandler, kerberosConfiguration, requestSharedDataContext);
}
}
return commandReport;
}
}