/* * 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 java.io.File; import java.io.IOException; import java.util.Map; import java.util.concurrent.ConcurrentMap; 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.controller.KerberosHelper; import org.apache.ambari.server.controller.utilities.KerberosChecker; import org.apache.ambari.server.orm.dao.HostDAO; import org.apache.ambari.server.orm.dao.KerberosPrincipalHostDAO; import org.apache.ambari.server.orm.entities.HostEntity; import org.apache.ambari.server.serveraction.ActionLog; import org.apache.ambari.server.utils.ShellCommandUtil; import org.apache.ambari.server.utils.StageUtils; import org.apache.commons.codec.digest.DigestUtils; import org.apache.commons.io.FileUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.inject.Inject; /** * ConfigureAmbariIdentitiesServerAction is a ServerAction implementation that creates keytab files as * instructed. * <p/> * This class mainly relies on the KerberosServerAction to iterate through metadata identifying * the Kerberos keytab files that need to be created. For each identity in the metadata, this * implementation's * {@link KerberosServerAction#processIdentity(Map, String, KerberosOperationHandler, Map, Map)} * is invoked attempting the creation of the relevant keytab file. */ public class ConfigureAmbariIdentitiesServerAction extends KerberosServerAction { private static final String KEYTAB_PATTERN = "keyTab=\"(.+)?\""; private static final String PRINCIPAL_PATTERN = "principal=\"(.+)?\""; private final static Logger LOG = LoggerFactory.getLogger(ConfigureAmbariIdentitiesServerAction.class); @Inject private KerberosPrincipalHostDAO kerberosPrincipalHostDAO; @Inject private HostDAO hostDAO; /** * Called to execute this action. Upon invocation, calls * {@link KerberosServerAction#processIdentities(Map)} )} * to iterate through the Kerberos identity metadata and call * {@link ConfigureAmbariIdentitiesServerAction#processIdentities(Map)} * for each identity to process. * * @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 action * @throws AmbariException * @throws InterruptedException */ @Override public CommandReport execute(ConcurrentMap<String, Object> requestSharedDataContext) throws AmbariException, InterruptedException { return processIdentities(requestSharedDataContext); } /** * Creates keytab file for ambari-server identity. * <p/> * It is expected that the {@link CreatePrincipalsServerAction} * (or similar) and {@link CreateKeytabFilesServerAction} has executed before this action. * * @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 */ @Override protected CommandReport processIdentity(Map<String, String> identityRecord, String evaluatedPrincipal, KerberosOperationHandler operationHandler, Map<String, String> kerberosConfiguration, Map<String, Object> requestSharedDataContext) throws AmbariException { CommandReport commandReport = null; if (identityRecord != null) { String message; String dataDirectory = getDataDirectoryPath(); if (dataDirectory == null) { message = "The data directory has not been set. Generated keytab files can not be stored."; LOG.error(message); commandReport = createCommandReport(1, HostRoleStatus.FAILED, "{}", actionLog.getStdOut(), actionLog.getStdErr()); } else { String hostName = identityRecord.get(KerberosIdentityDataFileReader.HOSTNAME); if (hostName != null && hostName.equalsIgnoreCase(KerberosHelper.AMBARI_SERVER_HOST_NAME)) { String destKeytabFilePath = identityRecord.get(KerberosIdentityDataFileReader.KEYTAB_FILE_PATH); File hostDirectory = new File(dataDirectory, hostName); File srcKeytabFile = new File(hostDirectory, DigestUtils.sha1Hex(destKeytabFilePath)); if (srcKeytabFile.exists()) { String ownerAccess = identityRecord.get(KerberosIdentityDataFileReader.KEYTAB_FILE_OWNER_ACCESS); boolean ownerWritable = "w".equalsIgnoreCase(ownerAccess) || "rw".equalsIgnoreCase(ownerAccess); boolean ownerReadable = "r".equalsIgnoreCase(ownerAccess) || "rw".equalsIgnoreCase(ownerAccess); String groupAccess = identityRecord.get(KerberosIdentityDataFileReader.KEYTAB_FILE_OWNER_ACCESS); boolean groupWritable = "w".equalsIgnoreCase(groupAccess) || "rw".equalsIgnoreCase(groupAccess); boolean groupReadable = "r".equalsIgnoreCase(groupAccess) || "rw".equalsIgnoreCase(groupAccess); installAmbariServerIdentity(evaluatedPrincipal, srcKeytabFile.getAbsolutePath(), destKeytabFilePath, identityRecord.get(KerberosIdentityDataFileReader.KEYTAB_FILE_OWNER_NAME), ownerReadable, ownerWritable, identityRecord.get(KerberosIdentityDataFileReader.KEYTAB_FILE_GROUP_NAME), groupReadable, groupWritable, actionLog); if ("AMBARI_SERVER_SELF".equals(identityRecord.get(KerberosIdentityDataFileReader.COMPONENT))) { // Create/update the JAASFile... configureJAAS(evaluatedPrincipal, destKeytabFilePath, actionLog); } } } } } return commandReport; } /** * Installs the Ambari Server Kerberos identity by copying its keytab file to the specified location * and then creating the Ambari Server JAAS File. * * @param principal the ambari server principal name * @param srcKeytabFilePath the source location of the ambari server keytab file * @param destKeytabFilePath the destination location of the ambari server keytab file * @param ownerName the username for the owner of the generated keytab file * @param ownerReadable true if the owner should be able to read this file; otherwise false * @param ownerWritable true if the owner should be able to write to this file; otherwise false * @param groupName the name of the group for the generated keytab file * @param groupReadable true if the group should be able to read this file; otherwise false * @param groupWritable true if the group should be able to write to this file; otherwise false * @param actionLog the logger * @return true if success; false otherwise * @throws AmbariException */ public boolean installAmbariServerIdentity(String principal, String srcKeytabFilePath, String destKeytabFilePath, String ownerName, boolean ownerReadable, boolean ownerWritable, String groupName, boolean groupReadable, boolean groupWritable, ActionLog actionLog) throws AmbariException { try { // Copy the keytab file into place (creating the parent directory, if necessary... copyFile(srcKeytabFilePath, destKeytabFilePath); setFileACL(destKeytabFilePath, ownerName, ownerReadable, ownerWritable, groupName, groupReadable, groupWritable); String ambariServerHostName = StageUtils.getHostName(); HostEntity ambariServerHostEntity = hostDAO.findByName(ambariServerHostName); Long ambariServerHostID = (ambariServerHostEntity == null) ? null : ambariServerHostEntity.getHostId(); if (ambariServerHostID == null) { String message = String.format("Failed to add the kerberos_principal_host record for %s on " + "the Ambari server host since the host id for Ambari server host, %s, was not found." + " This is not an error if an Ambari agent is not installed on the Ambari server host.", principal, ambariServerHostName); LOG.warn(message); if (actionLog != null) { actionLog.writeStdErr(message); } } else if (!kerberosPrincipalHostDAO.exists(principal, ambariServerHostID)) { kerberosPrincipalHostDAO.create(principal, ambariServerHostID); } if (actionLog != null) { actionLog.writeStdOut(String.format("Created Ambari server keytab file for %s at %s", principal, destKeytabFilePath)); } } catch (InterruptedException | IOException e) { throw new AmbariException(e.getLocalizedMessage(), e); } return true; } /** * Configure Ambari's JAAS file to reflect the principal name and keytab file for Ambari's Kerberos * identity. * * @param principal the Ambari server's principal name * @param keytabFilePath the absolute path to the Ambari server's keytab file * @param actionLog the logger */ public void configureJAAS(String principal, String keytabFilePath, ActionLog actionLog) { String jaasConfPath = getJAASConfFilePath(); if (jaasConfPath != null) { File jaasConfigFile = new File(jaasConfPath); try { String jaasConfig = FileUtils.readFileToString(jaasConfigFile); File oldJaasConfigFile = new File(jaasConfPath + ".bak"); FileUtils.writeStringToFile(oldJaasConfigFile, jaasConfig); jaasConfig = jaasConfig.replaceFirst(KEYTAB_PATTERN, "keyTab=\"" + keytabFilePath + "\""); jaasConfig = jaasConfig.replaceFirst(PRINCIPAL_PATTERN, "principal=\"" + principal + "\""); FileUtils.writeStringToFile(jaasConfigFile, jaasConfig); String message = String.format("JAAS config file %s modified successfully for principal %s.", jaasConfigFile.getName(), principal); if (actionLog != null) { actionLog.writeStdOut(message); } } catch (IOException e) { String message = String.format("Failed to configure JAAS file %s for %s - %s", jaasConfigFile, principal, e.getMessage()); if (actionLog != null) { actionLog.writeStdErr(message); } LOG.error(message, e); } } else { String message = String.format("Failed to configure JAAS, config file should be passed to Ambari server as: " + "%s.", KerberosChecker.JAVA_SECURITY_AUTH_LOGIN_CONFIG); if (actionLog != null) { actionLog.writeStdErr(message); } LOG.error(message); } } /** * Copies the specified source file to the specified destination path, creating any needed parent * directories. * <p> * This method is mocked in unit tests to avoid dealing with ShellCommandUtil in a mocked env. * * @param srcKeytabFilePath the source location of the ambari server keytab file * @param destKeytabFilePath the destination location of the ambari server keytab file * @throws IOException * @throws InterruptedException * @throws AmbariException * @see ShellCommandUtil#mkdir(String, boolean); * @see ShellCommandUtil#copyFile(String, String, boolean, boolean) */ void copyFile(String srcKeytabFilePath, String destKeytabFilePath) throws IOException, InterruptedException { ShellCommandUtil.Result result; // Create the parent directory if necessary (using sudo) File destKeytabFile = new File(destKeytabFilePath); result = ShellCommandUtil.mkdir(destKeytabFile.getParent(), true); if (!result.isSuccessful()) { throw new AmbariException(result.getStderr()); } // Copy the file (using sudo) result = ShellCommandUtil.copyFile(srcKeytabFilePath, destKeytabFilePath, true, true); if (!result.isSuccessful()) { throw new AmbariException(result.getStderr()); } } /** * Sets the access control list for this specified file. * <p> * The owner and group for the file is set as well as the owner's and group's ability to read and write * the file. * <p> * The result of the operation to set the group for the file is ignored since it is possible that * the group does not exist when performing this operation. It is expected this issue will be remedied * when the group becomes available. * <p> * Access for other users is denied and the file is assumed to not be executeable by anyone. * * @param filePath the path to the file * @param ownerName the username for the owner of the generated keytab file * @param ownerWritable true if the owner should be able to write to this file; otherwise false * @param ownerReadable true if the owner should be able to read this file; otherwise false * @param groupName the name of the group for the generated keytab file * @param groupWritable true if the group should be able to write to this file; otherwise false * @param groupReadable true if the group should be able to read this file; otherwise false * @throws AmbariException if an error occurs setting the permissions on the fils */ void setFileACL(String filePath, String ownerName, boolean ownerReadable, boolean ownerWritable, String groupName, boolean groupReadable, boolean groupWritable) throws AmbariException { ShellCommandUtil.Result result; result = ShellCommandUtil.setFileOwner(filePath, ownerName); if (result.isSuccessful()) { result = ShellCommandUtil.setFileGroup(filePath, groupName); if (!result.isSuccessful()) { // Ignore, but log, this it is possible that the group does not exist when performing this operation LOG.warn("Failed to set the group for the file at {} to {}: {}", filePath, groupName, result.getStderr()); } result = ShellCommandUtil.setFileMode(filePath, ownerReadable, ownerWritable, false, groupReadable, groupWritable, false, false, false, false); } if (!result.isSuccessful()) { throw new AmbariException(result.getStderr()); } } /** * Gets the location of Ambari's JAAS config file. * <p> * This method is mocked in unit tests to avoid having to alter the System properties in * order to locate the test JAAS config file. * * @return the path to Ambari's JAAS config file */ String getJAASConfFilePath() { return System.getProperty(KerberosChecker.JAVA_SECURITY_AUTH_LOGIN_CONFIG); } }