org.apache.ambari.server.serveraction.kerberos.CreateKeytabFilesServerAction.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.ambari.server.serveraction.kerberos.CreateKeytabFilesServerAction.java

Source

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

/**
 * CreateKeytabFilesServerAction 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(java.util.Map, String, KerberosOperationHandler, java.util.Map)}
 * is invoked attempting the creation of the relevant keytab file.
 */
public class CreateKeytabFilesServerAction extends KerberosServerAction {
    private final static Logger LOG = LoggerFactory.getLogger(CreateKeytabFilesServerAction.class);

    /**
     * KerberosPrincipalDAO used to set and get Kerberos principal details
     */
    @Inject
    private KerberosPrincipalDAO kerberosPrincipalDAO;

    /**
     * KerberosPrincipalHostDAO used to get Kerberos principal details
     */
    @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)} )}
     * to iterate through the Kerberos identity metadata and call
     * {@link org.apache.ambari.server.serveraction.kerberos.CreateKeytabFilesServerAction#processIdentities(java.util.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);
    }

    /**
     * For each identity, create a keytab and append to a new or existing keytab file.
     * <p/>
     * It is expected that the {@link org.apache.ambari.server.serveraction.kerberos.CreatePrincipalsServerAction}
     * (or similar) has executed before this action and a set of passwords has been created, map to
     * their relevant (evaluated) principals and stored in the requestSharedDataContext.
     * <p/>
     * If a password exists for the current evaluatedPrincipal, use a
     * {@link org.apache.ambari.server.serveraction.kerberos.KerberosOperationHandler} to generate
     * the keytab file. To help avoid filename collisions and to build a structure that is easy to
     * discover, each keytab file is stored in host-specific
     * ({@link org.apache.ambari.server.serveraction.kerberos.KerberosActionDataFile#HOSTNAME})
     * directory using the SHA1 hash of its destination file path
     * ({@link org.apache.ambari.server.serveraction.kerberos.KerberosActionDataFile#KEYTAB_FILE_PATH})
     * <p/>
     * <pre>
     *   data_directory
     *   |- host1
     *   |  |- 16a054404c8826cd604a27ac970e8cc4b9c7a3fa   (keytab file)
     *   |  |- ...                                        (keytab files)
     *   |  |- a3c09cae73406912e8c55296d1c85b674d24f576   (keytab file)
     *   |- host2
     *   |  |- ...
     * </pre>
     *
     * @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 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, Object> requestSharedDataContext)
            throws AmbariException {
        CommandReport commandReport = null;

        if (identityRecord != null) {
            String message;

            if (operationHandler == null) {
                message = String.format("Failed to create keytab file for %s, missing KerberosOperationHandler",
                        evaluatedPrincipal);
                actionLog.writeStdErr(message);
                LOG.error(message);
                commandReport = createCommandReport(1, HostRoleStatus.FAILED, "{}", actionLog.getStdOut(),
                        actionLog.getStdErr());
            } else {
                Map<String, String> principalPasswordMap = getPrincipalPasswordMap(requestSharedDataContext);
                Map<String, Integer> principalKeyNumberMap = getPrincipalKeyNumberMap(requestSharedDataContext);

                String host = identityRecord.get(HOSTNAME);
                String keytabFilePath = identityRecord.get(KEYTAB_FILE_PATH);

                if ((host != null) && !host.isEmpty() && (keytabFilePath != null) && !keytabFilePath.isEmpty()) {
                    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 {
                                        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.error(message, e);
                                        commandReport = createCommandReport(1, HostRoleStatus.FAILED, "{}",
                                                actionLog.getStdOut(), actionLog.getStdErr());
                                    }
                                }

                                if (keytab != null) {
                                    try {
                                        if (operationHandler.createKeytabFile(keytab, destinationKeytabFile)) {
                                            ensureAmbariOnlyAccess(destinationKeytabFile);

                                            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, 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));
                    }
                }
            }
        }

        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()));
                }
            }
        }
    }
}