/*
* 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.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
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.audit.event.kerberos.ChangeSecurityStateKerberosAuditEvent;
import org.apache.ambari.server.state.Cluster;
import org.apache.ambari.server.state.Host;
import org.apache.ambari.server.state.SecurityState;
import org.apache.ambari.server.state.ServiceComponentHost;
import org.apache.ambari.server.utils.ShellCommandUtil;
import org.apache.ambari.server.utils.StageUtils;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class FinalizeKerberosServerAction extends KerberosServerAction {
private final static Logger LOG = LoggerFactory.getLogger(FinalizeKerberosServerAction.class);
/**
* Processes an identity as necessary.
* <p/>
* This implementation ensures that keytab files for the Ambari identities have the correct
* permissions. This is important in the event a secure cluster was created via Blueprints since
* some user accounts and groups may not have been available (at the OS level) when the keytab files
* were created.
*
* @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 null, always
* @throws AmbariException
*/
@Override
protected CommandReport processIdentity(Map<String, String> identityRecord, String evaluatedPrincipal,
KerberosOperationHandler operationHandler,
Map<String, String> kerberosConfiguration,
Map<String, Object> requestSharedDataContext)
throws AmbariException {
if (identityRecord != null) {
// If the record's HOSTNAME value is "ambari-server", rather than an actual hostname it will
// not match the Ambari server's host name. This will occur if the there is no agent installed
// on the Ambari server host. This is ok, since any keytab files installed on the Ambari server
// host will already have the permissions set so that only the Ambari server can read it.
// There is no need to update the permissions for those keytab files so that installed services
// can access them since no services will be installed on the host.
if (StageUtils.getHostName().equals(identityRecord.get(KerberosIdentityDataFile.HOSTNAME))) {
// If the principal name exists in one of the shared data maps, it has been processed by the
// current "Enable Kerberos" or "Add component" workflow and therefore should already have
// the correct permissions assigned. The relevant keytab files can be skipped.
Map<String, String> principalPasswordMap = getPrincipalPasswordMap(requestSharedDataContext);
if ((principalPasswordMap == null) || !principalPasswordMap.containsKey(evaluatedPrincipal)) {
String keytabFilePath = identityRecord.get(KerberosIdentityDataFile.KEYTAB_FILE_PATH);
if (!StringUtils.isEmpty(keytabFilePath)) {
Set<String> visited = (Set<String>) requestSharedDataContext.get(this.getClass().getName() + "_visited");
if (!visited.contains(keytabFilePath)) {
String ownerName = identityRecord.get(KerberosIdentityDataFile.KEYTAB_FILE_OWNER_NAME);
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 groupName = identityRecord.get(KerberosIdentityDataFile.KEYTAB_FILE_GROUP_NAME);
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);
ShellCommandUtil.Result result;
String message;
result = ShellCommandUtil.setFileOwner(keytabFilePath, ownerName);
if (result.isSuccessful()) {
message = String.format("Updated the owner of the keytab file at %s to %s",
keytabFilePath, ownerName);
LOG.info(message);
actionLog.writeStdOut(message);
} else {
message = String.format("Failed to update the owner of the keytab file at %s to %s: %s",
keytabFilePath, ownerName, result.getStderr());
LOG.error(message);
actionLog.writeStdOut(message);
actionLog.writeStdErr(message);
}
result = ShellCommandUtil.setFileGroup(keytabFilePath, groupName);
if (result.isSuccessful()) {
message = String.format("Updated the group of the keytab file at %s to %s",
keytabFilePath, groupName);
LOG.info(message);
actionLog.writeStdOut(message);
} else {
message = String.format("Failed to update the group of the keytab file at %s to %s: %s",
keytabFilePath, groupName, result.getStderr());
LOG.error(message);
actionLog.writeStdOut(message);
actionLog.writeStdErr(message);
}
result = ShellCommandUtil.setFileMode(keytabFilePath,
ownerReadable, ownerWritable, false,
groupReadable, groupWritable, false,
false, false, false);
if (result.isSuccessful()) {
message = String.format("Updated the access mode of the keytab file at %s to owner:'%s' and group:'%s'",
keytabFilePath, ownerAccess, groupAccess);
LOG.info(message);
actionLog.writeStdOut(message);
} else {
message = String.format("Failed to update the access mode of the keytab file at %s to owner:'%s' and group:'%s': %s",
keytabFilePath, ownerAccess, groupAccess, result.getStderr());
LOG.error(message);
actionLog.writeStdOut(message);
actionLog.writeStdErr(message);
}
visited.add(keytabFilePath);
}
}
}
}
}
return null;
}
/**
* @param requestSharedDataContext a Map to be used a shared data among all ServerActions related
* to a given request
* @return
* @throws AmbariException
* @throws InterruptedException
*/
@Override
public CommandReport execute(ConcurrentMap<String, Object> requestSharedDataContext) throws AmbariException, InterruptedException {
String dataDirectoryPath = getCommandParameterValue(DATA_DIRECTORY);
// Set the ServiceComponentHost from a transitional state to the desired endpoint state
Map<String, Host> hosts = getClusters().getHostsForCluster(getClusterName());
if ((hosts != null) && !hosts.isEmpty()) {
Cluster cluster = getCluster();
for (String hostname : hosts.keySet()) {
List<ServiceComponentHost> serviceComponentHosts = cluster.getServiceComponentHosts(hostname);
for (ServiceComponentHost sch : serviceComponentHosts) {
SecurityState securityState = sch.getSecurityState();
if (securityState.isTransitional()) {
String message = String.format("Setting securityState for %s/%s on host %s to state %s",
sch.getServiceName(), sch.getServiceComponentName(), sch.getHostName(),
sch.getDesiredSecurityState().toString());
LOG.info(message);
actionLog.writeStdOut(message);
sch.setSecurityState(sch.getDesiredSecurityState());
ChangeSecurityStateKerberosAuditEvent auditEvent = ChangeSecurityStateKerberosAuditEvent.builder()
.withTimestamp(System.currentTimeMillis())
.withService(sch.getServiceName())
.withComponent(sch.getServiceComponentName())
.withHostName(sch.getHostName())
.withState(sch.getDesiredSecurityState().toString())
.withRequestId(getHostRoleCommand().getRequestId())
.withTaskId(getHostRoleCommand().getTaskId())
.build();
auditLog(auditEvent);
}
}
}
}
if(getKDCType(getCommandParameters()) != KDCType.NONE) {
// Ensure the keytab files for the Ambari identities have the correct permissions
// This is important in the event a secure cluster was created via Blueprints since some
// user accounts and group may not have been created when the keytab files were created.
requestSharedDataContext.put(this.getClass().getName() + "_visited", new HashSet<String>());
processIdentities(requestSharedDataContext);
requestSharedDataContext.remove(this.getClass().getName() + "_visited");
}
// Make sure this is a relevant directory. We don't want to accidentally allow _ANY_ directory
// to be deleted.
if ((dataDirectoryPath != null) && dataDirectoryPath.contains("/" + DATA_DIRECTORY_PREFIX)) {
File dataDirectory = new File(dataDirectoryPath);
File dataDirectoryParent = dataDirectory.getParentFile();
// Make sure this directory has a parent and it is writeable, else we wont be able to
// delete the directory
if ((dataDirectoryParent != null) && dataDirectory.isDirectory() &&
dataDirectoryParent.isDirectory() && dataDirectoryParent.canWrite()) {
try {
FileUtils.deleteDirectory(dataDirectory);
} catch (IOException e) {
// We should log this exception, but don't let it fail the process since if we got to this
// KerberosServerAction it is expected that the the overall process was a success.
String message = String.format("The data directory (%s) was not deleted due to an error condition - {%s}",
dataDirectory.getAbsolutePath(), e.getMessage());
LOG.warn(message, e);
}
}
}
return createCommandReport(0, HostRoleStatus.COMPLETED, "{}", actionLog.getStdOut(), actionLog.getStdErr());
}
}