/*
* 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.text.NumberFormat;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.ambari.server.configuration.Configuration;
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 com.google.inject.Inject;
/**
* 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 {
@Inject
private Configuration configuration;
/**
* 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), null);
// 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 %s %s",
(createAttributes == null) ? "" : createAttributes, principal), password);
// 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 %s", principal), password);
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), null);
// 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), null);
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
* @param userPassword a String containing the user's password to set or update if necessary,
* null if not needed
* @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, String userPassword)
throws KerberosOperationException {
if (StringUtils.isEmpty(query)) {
throw new KerberosOperationException("Missing kadmin query");
}
ShellCommandUtil.Result result = null;
PrincipalKeyCredential administratorCredential = getAdministratorCredential();
String defaultRealm = getDefaultRealm();
List<String> command = new ArrayList<>();
String adminPrincipal = (administratorCredential == null)
? null
: administratorCredential.getPrincipal();
ShellCommandUtil.InteractiveHandler interactiveHandler = null;
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);
if (userPassword != null) {
interactiveHandler = new InteractivePasswordHandler(null, userPassword);
}
} 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 (!StringUtils.isEmpty(getAdminServerHost())) {
command.add("-s");
command.add(getAdminServerHost());
}
// Add the administrative principal
command.add("-p");
command.add(adminPrincipal);
if (!ArrayUtils.isEmpty(adminPassword)) {
interactiveHandler = new InteractivePasswordHandler(String.valueOf(adminPassword), userPassword);
} else if (userPassword != null) {
interactiveHandler = new InteractivePasswordHandler(null, userPassword);
}
}
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", command));
}
int retryCount = configuration.getKerberosOperationRetries();
int tries = 0;
while (tries <= retryCount) {
try {
result = executeCommand(command.toArray(new String[command.size()]), null, interactiveHandler);
} catch (KerberosOperationException exception) {
if (tries == retryCount) {
throw exception;
}
}
if (result != null && result.isSuccessful()) {
break; // break on successful result
}
tries++;
try {
Thread.sleep(1000 * configuration.getKerberosOperationRetryTimeout());
} catch (InterruptedException ignored) {
}
String message = String.format("Retrying to execute kadmin after a wait of %d seconds :\n\tCommand: %s",
configuration.getKerberosOperationRetryTimeout(),
command);
LOG.warn(message);
}
if (!result.isSuccessful()) {
String message = String.format("Failed to execute kadmin:\n\tCommand: %s\n\tExitCode: %s\n\tSTDOUT: %s\n\tSTDERR: %s",
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(String.format("Unexpected error condition executing the kadmin command. STDERR: %s", stdErr));
}
}
return result;
}
/**
* InteractivePasswordHandler is a {@link org.apache.ambari.server.utils.ShellCommandUtil.InteractiveHandler}
* implementation that answers queries from kadmin or kdamin.local command for the admin and/or user
* passwords.
*/
protected static class InteractivePasswordHandler implements ShellCommandUtil.InteractiveHandler {
/**
* The queue of responses to return
*/
private LinkedList<String> responses;
private Queue<String> currentResponses;
/**
* Constructor.
*
* @param adminPassword the KDC administrator's password (optional)
* @param userPassword the user's password (optional)
*/
public InteractivePasswordHandler(String adminPassword, String userPassword) {
responses = new LinkedList<>();
if (adminPassword != null) {
responses.offer(adminPassword);
}
if (userPassword != null) {
responses.offer(userPassword);
responses.offer(userPassword); // Add a 2nd time for the password "confirmation" request
}
currentResponses = new LinkedList<>(responses);
}
@Override
public boolean done() {
return currentResponses.size() == 0;
}
@Override
public String getResponse(String query) {
return currentResponses.poll();
}
@Override
public void start() {
currentResponses = new LinkedList<>(responses);
}
}
}