| /* |
| * 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; |
| } |
| } |