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

Java tutorial

Introduction

Here is the source code for org.apache.ambari.server.serveraction.kerberos.MITKerberosOperationHandler.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 org.apache.ambari.server.security.credential.PrincipalKeyCredential;
import org.apache.ambari.server.utils.ShellCommandUtil;
import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.text.NumberFormat;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * MITKerberosOperationHandler is an implementation of a KerberosOperationHandler providing
 * functionality specifically for an MIT KDC. See http://web.mit.edu/kerberos.
 * <p/>
 * It is assumed that a MIT Kerberos client is installed and that the kdamin shell command is
 * available
 */
public class MITKerberosOperationHandler extends KerberosOperationHandler {

    /**
     * A regular expression pattern to use to parse the key number from the text captured from the
     * get_principal kadmin command
     */
    private final static Pattern PATTERN_GET_KEY_NUMBER = Pattern.compile("^.*?Key: vno (\\d+).*$", Pattern.DOTALL);

    private final static Logger LOG = LoggerFactory.getLogger(MITKerberosOperationHandler.class);

    /**
     * A String containing user-specified attributes used when creating principals
     */
    private String createAttributes = null;

    private String adminServerHost = null;

    /**
     * A String containing the resolved path to the kdamin executable
     */
    private String executableKadmin = null;

    /**
     * A String containing the resolved path to the kdamin.local executable
     */
    private String executableKadminLocal = null;

    /**
     * Prepares and creates resources to be used by this KerberosOperationHandler
     * <p/>
     * It is expected that this KerberosOperationHandler will not be used before this call.
     * <p/>
     * The kerberosConfiguration Map is not being used.
     *
     * @param administratorCredentials a PrincipalKeyCredential containing the administrative credential
     *                                 for the relevant KDC
     * @param realm                    a String declaring the default Kerberos realm (or domain)
     * @param kerberosConfiguration    a Map of key/value pairs containing data from the kerberos-env configuration set
     * @throws KerberosKDCConnectionException       if a connection to the KDC cannot be made
     * @throws KerberosAdminAuthenticationException if the administrator credentials fail to authenticate
     * @throws KerberosRealmException               if the realm does not map to a KDC
     * @throws KerberosOperationException           if an unexpected error occurred
     */
    @Override
    public void open(PrincipalKeyCredential administratorCredentials, String realm,
            Map<String, String> kerberosConfiguration) throws KerberosOperationException {

        setAdministratorCredential(administratorCredentials);
        setDefaultRealm(realm);

        if (kerberosConfiguration != null) {
            setKeyEncryptionTypes(
                    translateEncryptionTypes(kerberosConfiguration.get(KERBEROS_ENV_ENCRYPTION_TYPES), "\\s+"));
            setAdminServerHost(kerberosConfiguration.get(KERBEROS_ENV_ADMIN_SERVER_HOST));
            setExecutableSearchPaths(kerberosConfiguration.get(KERBEROS_ENV_EXECUTABLE_SEARCH_PATHS));
            setCreateAttributes(kerberosConfiguration.get(KERBEROS_ENV_KDC_CREATE_ATTRIBUTES));
        } else {
            setKeyEncryptionTypes(null);
            setAdminServerHost(null);
            setExecutableSearchPaths((String) null);
            setCreateAttributes(null);
        }

        // Pre-determine the paths to relevant Kerberos executables
        executableKadmin = getExecutable("kadmin");
        executableKadminLocal = getExecutable("kadmin.local");

        setOpen(true);
    }

    @Override
    public void close() throws KerberosOperationException {
        // There is nothing to do here.
        setOpen(false);

        executableKadmin = null;
        executableKadminLocal = null;
    }

    /**
     * Test to see if the specified principal exists in a previously configured MIT KDC
     * <p/>
     * This implementation creates a query to send to the kadmin shell command and then interrogates
     * the result from STDOUT to determine if the presence of the specified principal.
     *
     * @param principal a String containing the principal to test
     * @return true if the principal exists; false otherwise
     * @throws KerberosKDCConnectionException       if a connection to the KDC cannot be made
     * @throws KerberosAdminAuthenticationException if the administrator credentials fail to authenticate
     * @throws KerberosRealmException               if the realm does not map to a KDC
     * @throws KerberosOperationException           if an unexpected error occurred
     */
    @Override
    public boolean principalExists(String principal) throws KerberosOperationException {

        if (!isOpen()) {
            throw new KerberosOperationException("This operation handler has not been opened");
        }

        if (principal == null) {
            return false;
        } else {
            // Create the KAdmin query to execute:
            ShellCommandUtil.Result result = invokeKAdmin(String.format("get_principal %s", principal));

            // If there is data from STDOUT, see if the following string exists:
            //    Principal: <principal>
            String stdOut = result.getStdout();
            return (stdOut != null) && stdOut.contains(String.format("Principal: %s", principal));
        }
    }

    /**
     * Creates a new principal in a previously configured MIT KDC
     * <p/>
     * This implementation creates a query to send to the kadmin shell command and then interrogates
     * the result from STDOUT to determine if the operation executed successfully.
     *
     * @param principal a String containing the principal add
     * @param password  a String containing the password to use when creating the principal
     * @param service   a boolean value indicating whether the principal is to be created as a service principal or not
     * @return an Integer declaring the generated key number
     * @throws KerberosKDCConnectionException          if a connection to the KDC cannot be made
     * @throws KerberosAdminAuthenticationException    if the administrator credentials fail to authenticate
     * @throws KerberosRealmException                  if the realm does not map to a KDC
     * @throws KerberosPrincipalAlreadyExistsException if the principal already exists
     * @throws KerberosOperationException              if an unexpected error occurred
     */
    @Override
    public Integer createPrincipal(String principal, String password, boolean service)
            throws KerberosOperationException {

        if (!isOpen()) {
            throw new KerberosOperationException("This operation handler has not been opened");
        }

        if (StringUtils.isEmpty(principal)) {
            throw new KerberosOperationException("Failed to create new principal - no principal specified");
        } else if (StringUtils.isEmpty(password)) {
            throw new KerberosOperationException("Failed to create new principal - no password specified");
        } else {
            String createAttributes = getCreateAttributes();
            // Create the kdamin query:  add_principal <-randkey|-pw <password>> [<options>] <principal>
            ShellCommandUtil.Result result = invokeKAdmin(String.format("add_principal -pw \"%s\" %s %s", password,
                    (createAttributes == null) ? "" : createAttributes, principal));

            // If there is data from STDOUT, see if the following string exists:
            //    Principal "<principal>" created
            String stdOut = result.getStdout();
            String stdErr = result.getStderr();
            if ((stdOut != null) && stdOut.contains(String.format("Principal \"%s\" created", principal))) {
                return getKeyNumber(principal);
            } else if ((stdErr != null) && stdErr.contains(
                    String.format("Principal or policy already exists while creating \"%s\"", principal))) {
                throw new KerberosPrincipalAlreadyExistsException(principal);
            } else {
                LOG.error(
                        "Failed to execute kadmin query: add_principal -pw \"********\" {} {}\nSTDOUT: {}\nSTDERR: {}",
                        (createAttributes == null) ? "" : createAttributes, principal, stdOut, result.getStderr());
                throw new KerberosOperationException(
                        String.format("Failed to create service principal for %s\nSTDOUT: %s\nSTDERR: %s",
                                principal, stdOut, result.getStderr()));
            }
        }
    }

    /**
     * Updates the password for an existing principal in a previously configured MIT KDC
     * <p/>
     * This implementation creates a query to send to the kadmin shell command and then interrogates
     * the exit code to determine if the operation executed successfully.
     *
     * @param principal a String containing the principal to update
     * @param password  a String containing the password to set
     * @return an Integer declaring the new key number
     * @throws KerberosKDCConnectionException         if a connection to the KDC cannot be made
     * @throws KerberosAdminAuthenticationException   if the administrator credentials fail to authenticate
     * @throws KerberosRealmException                 if the realm does not map to a KDC
     * @throws KerberosPrincipalDoesNotExistException if the principal does not exist
     * @throws KerberosOperationException             if an unexpected error occurred
     */
    @Override
    public Integer setPrincipalPassword(String principal, String password) throws KerberosOperationException {
        if (!isOpen()) {
            throw new KerberosOperationException("This operation handler has not been opened");
        }

        if (StringUtils.isEmpty(principal)) {
            throw new KerberosOperationException("Failed to set password - no principal specified");
        } else if (StringUtils.isEmpty(password)) {
            throw new KerberosOperationException("Failed to set password - no password specified");
        } else {
            // Create the kdamin query:  change_password <-randkey|-pw <password>> <principal>
            ShellCommandUtil.Result result = invokeKAdmin(
                    String.format("change_password -pw \"%s\" %s", password, principal));

            String stdOut = result.getStdout();
            String stdErr = result.getStderr();
            if ((stdOut != null) && stdOut.contains(String.format("Password for \"%s\" changed", principal))) {
                return getKeyNumber(principal);
            } else if ((stdErr != null) && stdErr.contains("Principal does not exist")) {
                throw new KerberosPrincipalDoesNotExistException(principal);
            } else {
                LOG.error(
                        "Failed to execute kadmin query: change_password -pw \"********\" {} \nSTDOUT: {}\nSTDERR: {}",
                        principal, stdOut, result.getStderr());
                throw new KerberosOperationException(
                        String.format("Failed to update password for %s\nSTDOUT: %s\nSTDERR: %s", principal, stdOut,
                                result.getStderr()));
            }
        }
    }

    /**
     * Removes an existing principal in a previously configured KDC
     * <p/>
     * The implementation is specific to a particular type of KDC.
     *
     * @param principal a String containing the principal to remove
     * @return true if the principal was successfully removed; otherwise false
     * @throws KerberosKDCConnectionException       if a connection to the KDC cannot be made
     * @throws KerberosAdminAuthenticationException if the administrator credentials fail to authenticate
     * @throws KerberosRealmException               if the realm does not map to a KDC
     * @throws KerberosOperationException           if an unexpected error occurred
     */
    @Override
    public boolean removePrincipal(String principal) throws KerberosOperationException {
        if (!isOpen()) {
            throw new KerberosOperationException("This operation handler has not been opened");
        }

        if (StringUtils.isEmpty(principal)) {
            throw new KerberosOperationException("Failed to remove new principal - no principal specified");
        } else {
            ShellCommandUtil.Result result = invokeKAdmin(String.format("delete_principal -force %s", principal));

            // If there is data from STDOUT, see if the following string exists:
            //    Principal "<principal>" created
            String stdOut = result.getStdout();
            return (stdOut != null) && !stdOut.contains("Principal does not exist");
        }
    }

    /**
     * Sets the KDC administrator server host address
     *
     * @param adminServerHost the ip address or FQDN of the KDC administrator server
     */
    public void setAdminServerHost(String adminServerHost) {
        this.adminServerHost = adminServerHost;
    }

    /**
     * Gets the IP address or FQDN of the KDC administrator server
     *
     * @return the IP address or FQDN of the KDC administrator server
     */
    public String getAdminServerHost() {
        return this.adminServerHost;
    }

    /**
     * Sets the (additional) principal creation attributes
     *
     * @param createAttributes the additional principal creations attributes
     */
    public void setCreateAttributes(String createAttributes) {
        this.createAttributes = createAttributes;
    }

    /**
     * Gets the (additional) principal creation attributes
     *
     * @return the additional principal creations attributes or null
     */
    public String getCreateAttributes() {
        return createAttributes;
    }

    /**
     * Retrieves the current key number assigned to the identity identified by the specified principal
     *
     * @param principal a String declaring the principal to look up
     * @return an Integer declaring the current key number
     * @throws KerberosKDCConnectionException       if a connection to the KDC cannot be made
     * @throws KerberosAdminAuthenticationException if the administrator credentials fail to authenticate
     * @throws KerberosRealmException               if the realm does not map to a KDC
     * @throws KerberosOperationException           if an unexpected error occurred
     */
    private Integer getKeyNumber(String principal) throws KerberosOperationException {
        if (!isOpen()) {
            throw new KerberosOperationException("This operation handler has not been opened");
        }

        if (StringUtils.isEmpty(principal)) {
            throw new KerberosOperationException(
                    "Failed to get key number for principal  - no principal specified");
        } else {
            // Create the kdamin query:  get_principal <principal>
            ShellCommandUtil.Result result = invokeKAdmin(String.format("get_principal %s", principal));

            String stdOut = result.getStdout();
            if (stdOut == null) {
                String message = String.format(
                        "Failed to get key number for %s:\n\tExitCode: %s\n\tSTDOUT: NULL\n\tSTDERR: %s", principal,
                        result.getExitCode(), result.getStderr());
                LOG.warn(message);
                throw new KerberosOperationException(message);
            }

            Matcher matcher = PATTERN_GET_KEY_NUMBER.matcher(stdOut);
            if (matcher.matches()) {
                NumberFormat numberFormat = NumberFormat.getIntegerInstance();
                String keyNumber = matcher.group(1);

                numberFormat.setGroupingUsed(false);
                try {
                    Number number = numberFormat.parse(keyNumber);
                    return (number == null) ? 0 : number.intValue();
                } catch (ParseException e) {
                    String message = String.format(
                            "Failed to get key number for %s - invalid key number value (%s):\n\tExitCode: %s\n\tSTDOUT: NULL\n\tSTDERR: %s",
                            principal, keyNumber, result.getExitCode(), result.getStderr());
                    LOG.warn(message);
                    throw new KerberosOperationException(message);
                }
            } else {
                String message = String.format(
                        "Failed to get key number for %s - unexpected STDOUT data:\n\tExitCode: %s\n\tSTDOUT: NULL\n\tSTDERR: %s",
                        principal, result.getExitCode(), result.getStderr());
                LOG.warn(message);
                throw new KerberosOperationException(message);
            }
        }
    }

    /**
     * Invokes the kadmin shell command to issue queries
     *
     * @param query a String containing the query to send to the kdamin command
     * @return a ShellCommandUtil.Result containing the result of the operation
     * @throws KerberosKDCConnectionException       if a connection to the KDC cannot be made
     * @throws KerberosAdminAuthenticationException if the administrator credentials fail to authenticate
     * @throws KerberosRealmException               if the realm does not map to a KDC
     * @throws KerberosOperationException           if an unexpected error occurred
     */
    protected ShellCommandUtil.Result invokeKAdmin(String query) throws KerberosOperationException {
        if (StringUtils.isEmpty(query)) {
            throw new KerberosOperationException("Missing kadmin query");
        }

        ShellCommandUtil.Result result;
        PrincipalKeyCredential administratorCredential = getAdministratorCredential();
        String defaultRealm = getDefaultRealm();

        List<String> command = new ArrayList<String>();

        String adminPrincipal = (administratorCredential == null) ? null : administratorCredential.getPrincipal();

        if (StringUtils.isEmpty(adminPrincipal)) {
            // Set the kdamin interface to be kadmin.local
            if (StringUtils.isEmpty(executableKadminLocal)) {
                throw new KerberosOperationException(
                        "No path for kadmin.local is available - this KerberosOperationHandler may not have been opened.");
            }

            command.add(executableKadminLocal);
        } else {
            if (StringUtils.isEmpty(executableKadmin)) {
                throw new KerberosOperationException(
                        "No path for kadmin is available - this KerberosOperationHandler may not have been opened.");
            }
            char[] adminPassword = administratorCredential.getKey();

            // Set the kdamin interface to be kadmin
            command.add(executableKadmin);

            // Add explicit KDC admin host, if available
            if (getAdminServerHost() != null) {
                command.add("-s");
                command.add(getAdminServerHost());
            }

            // Add the administrative principal
            command.add("-p");
            command.add(adminPrincipal);

            if (!ArrayUtils.isEmpty(adminPassword)) {
                // Add password for administrative principal
                command.add("-w");
                command.add(String.valueOf(adminPassword));
            }
        }

        if (!StringUtils.isEmpty(defaultRealm)) {
            // Add default realm clause
            command.add("-r");
            command.add(defaultRealm);
        }

        // Add kadmin query
        command.add("-q");
        command.add(query);

        if (LOG.isDebugEnabled()) {
            LOG.debug(String.format("Executing: %s", createCleanCommand(command)));
        }

        result = executeCommand(command.toArray(new String[command.size()]));

        if (!result.isSuccessful()) {
            String message = String.format(
                    "Failed to execute kadmin:\n\tCommand: %s\n\tExitCode: %s\n\tSTDOUT: %s\n\tSTDERR: %s",
                    createCleanCommand(command), result.getExitCode(), result.getStdout(), result.getStderr());
            LOG.warn(message);

            // Test STDERR to see of any "expected" error conditions were encountered...
            String stdErr = result.getStderr();
            // Did admin credentials fail?
            if (stdErr.contains("Client not found in Kerberos database")) {
                throw new KerberosAdminAuthenticationException(stdErr);
            } else if (stdErr.contains("Incorrect password while initializing")) {
                throw new KerberosAdminAuthenticationException(stdErr);
            }
            // Did we fail to connect to the KDC?
            else if (stdErr.contains("Cannot contact any KDC")) {
                throw new KerberosKDCConnectionException(stdErr);
            } else if (stdErr.contains(
                    "Cannot resolve network address for admin server in requested realm while initializing kadmin interface")) {
                throw new KerberosKDCConnectionException(stdErr);
            }
            // Was the realm invalid?
            else if (stdErr.contains("Missing parameters in krb5.conf required for kadmin client")) {
                throw new KerberosRealmException(stdErr);
            } else if (stdErr.contains("Cannot find KDC for requested realm while initializing kadmin interface")) {
                throw new KerberosRealmException(stdErr);
            } else {
                throw new KerberosOperationException("Unexpected error condition executing the kadmin command");
            }
        }

        return result;
    }

    /**
     * Build the kadmin command string, replacing administrator password with "********"
     *
     * @param command a List of items making up the command
     * @return the cleaned command string
     */
    private String createCleanCommand(List<String> command) {
        StringBuilder cleanedCommand = new StringBuilder();
        Iterator<String> iterator = command.iterator();

        if (iterator.hasNext()) {
            cleanedCommand.append(iterator.next());
        }

        while (iterator.hasNext()) {
            String part = iterator.next();

            cleanedCommand.append(' ');

            if (part.contains(" ")) {
                cleanedCommand.append('"');
                cleanedCommand.append(part);
                cleanedCommand.append('"');
            } else {
                cleanedCommand.append(part);
            }

            if ("-w".equals(part)) {
                // Skip the password and use "********" instead
                if (iterator.hasNext()) {
                    iterator.next();
                }
                cleanedCommand.append(" ********");
            }
        }

        return cleanedCommand.toString();
    }
}