blob: 537a3346f107c6c833cdde8e967aeb34c9fded2a [file] [log] [blame]
/*
* 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.StringUtils;
import org.apache.directory.server.kerberos.shared.keytab.Keytab;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.text.NumberFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;
import java.util.TimeZone;
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* IPAKerberosOperationHandler is an implementation of a KerberosOperationHandler providing
* functionality specifically for IPA managed KDC. See http://www.freeipa.org
* <p/>
* It is assumed that the IPA admin tools are installed and that the ipa shell command is
* available
*/
public class IPAKerberosOperationHandler extends KerberosOperationHandler {
private final static Logger LOG = LoggerFactory.getLogger(IPAKerberosOperationHandler.class);
private String adminServerHost = null;
private HashMap<String, Keytab> cachedKeytabs = null;
/**
* This is where user principals are members of. Important as the password should not expire
* and thus a separate password policy should apply to this group
*/
private String userPrincipalGroup = null;
/**
* The format used for krbPasswordExpiry
*/
private final SimpleDateFormat expiryFormat = new SimpleDateFormat("yyyyMMddHHmmss.SSS'Z'");
/**
* Time zone for krbPasswordExpiry
*/
private static final TimeZone UTC = TimeZone.getTimeZone("UTC");
/**
* Years to add for password expiry
*/
private static final int PASSWORD_EXPIRY_YEAR = 30;
/**
* A regular expression pattern to use to parse the key number from the text captured from the
* kvno command
*/
private final static Pattern PATTERN_GET_KEY_NUMBER = Pattern.compile("^.*?: kvno = (\\d+).*$", Pattern.DOTALL);
/**
* A String containing the resolved path to the ipa executable
*/
private String executableIpaGetKeytab = null;
/**
* A String containing the resolved path to the ipa executable
*/
private String executableIpa = null;
/**
* A String containing the resolved path to the kinit executable
*/
private String executableKinit = null;
/**
* A String containing the resolved path to the ipa-getkeytab executable
*/
private String executableKvno = null;
/**
* A boolean indicating if password expiry should be set
*/
private boolean usePasswordExpiry = false;
/**
* An int indicating the time out in seconds for the password chat;
*/
private int timeout = DEFAULT_PASSWORD_CHAT_TIMEOUT;
/**
* Credentials context stores a handler to the ccache so it can be reused and removed on request
*/
private CredentialsContext credentialsContext;
/**
* 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 KerberosCredential containing the administrative credentials
* for the relevant IPA 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) {
// todo: ignore if ipa managed krb5.conf?
setKeyEncryptionTypes(translateEncryptionTypes(kerberosConfiguration.get(KERBEROS_ENV_ENCRYPTION_TYPES), "\\s+"));
setExecutableSearchPaths(kerberosConfiguration.get(KERBEROS_ENV_EXECUTABLE_SEARCH_PATHS));
setUserPrincipalGroup(kerberosConfiguration.get(KERBEROS_ENV_USER_PRINCIPAL_GROUP));
setAdminServerHost(kerberosConfiguration.get(KERBEROS_ENV_ADMIN_SERVER_HOST));
setUsePasswordExpiry(kerberosConfiguration.get(KERBEROS_ENV_SET_PASSWORD_EXPIRY));
setTimeout(kerberosConfiguration.get(KERBEROS_ENV_PASSWORD_CHAT_TIMEOUT));
} else {
setKeyEncryptionTypes(null);
setAdminServerHost(null);
setExecutableSearchPaths((String) null);
setUserPrincipalGroup(null);
setUsePasswordExpiry(null);
setTimeout(null);
}
// Pre-determine the paths to relevant Kerberos executables
executableIpa = getExecutable("ipa");
executableKvno = getExecutable("kvno");
executableKinit = getExecutable("kinit");
executableIpaGetKeytab = getExecutable("ipa-getkeytab");
credentialsContext = new CredentialsContext(administratorCredentials);
cachedKeytabs = new HashMap<>();
expiryFormat.setTimeZone(UTC);
setOpen(true);
}
private void setUsePasswordExpiry(String usePasswordExpiry) {
if (usePasswordExpiry == null) {
this.usePasswordExpiry = false;
return;
}
if (usePasswordExpiry.equalsIgnoreCase("true")) {
this.usePasswordExpiry = true;
} else {
this.usePasswordExpiry = false;
}
}
private void setTimeout(String timeout) {
if (timeout == null || timeout.isEmpty()) {
this.timeout = DEFAULT_PASSWORD_CHAT_TIMEOUT;
return;
}
try {
this.timeout = Integer.parseInt(timeout);
} catch (NumberFormatException e) {
this.timeout = DEFAULT_PASSWORD_CHAT_TIMEOUT;
}
}
@Override
public void close() throws KerberosOperationException {
if (isOpen()) {
credentialsContext.delete();
}
// There is nothing to do here.
setOpen(false);
executableIpa = null;
executableKvno = null;
executableIpaGetKeytab = null;
executableKinit = null;
credentialsContext = null;
cachedKeytabs = null;
}
/**
* Test to see if the specified principal exists in a previously configured IPA KDC
* <p/>
* This implementation creates a query to send to the ipa 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 KerberosOperationException if an unexpected error occurred
*/
@Override
public boolean principalExists(String principal)
throws KerberosOperationException {
LOG.debug("Entering principal exists");
if (!isOpen()) {
throw new KerberosOperationException("This operation handler has not been opened");
}
if (principal == null) {
return false;
} else if (isServicePrincipal(principal)) {
return true;
} else {
// TODO: fix exception check to only check for relevant exceptions
try {
DeconstructedPrincipal deconstructedPrincipal = createDeconstructPrincipal(principal);
LOG.debug("Running IPA command user-show");
// Create the ipa query to execute:
ShellCommandUtil.Result result = invokeIpa(String.format("user-show %s", deconstructedPrincipal.getPrincipalName()));
if (result.isSuccessful()) {
return true;
}
} catch (KerberosOperationException e) {
LOG.error("Cannot invoke IPA: " + e);
throw e;
}
}
return false;
}
/**
* Creates a new principal in a previously configured IPA Realm
* <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
*/
@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 ((principal == null) || principal.isEmpty()) {
throw new KerberosOperationException("Failed to create new principal - no principal specified");
} else if (((password == null) || password.isEmpty()) && service) {
throw new KerberosOperationException("Failed to create new user principal - no password specified");
} else {
DeconstructedPrincipal deconstructedPrincipal = createDeconstructPrincipal(principal);
if (service) {
// Create the ipa query: service-add --ok-as-delegate <principal>
ShellCommandUtil.Result result = invokeIpa(String.format("service-add %s", principal));
if (result.isSuccessful()) {
// IPA does not generate encryption types when no keytab has been generated
// So getKeyNumber(principal) cannot be used.
// createKeytabCredentials(principal, password);
// return getKeyNumber(principal);
return 0;
} else {
LOG.error("Failed to execute ipa query: service-add --ok-as-delegate=TRUE {}\nSTDOUT: {}\nSTDERR: {}",
principal, result.getStdout(), result.getStderr());
throw new KerberosOperationException(String.format("Failed to create service principal for %s\nSTDOUT: %s\nSTDERR: %s",
principal, result.getStdout(), result.getStderr()));
}
} else {
if (!StringUtils.isAllLowerCase(deconstructedPrincipal.getPrincipalName())) {
LOG.warn(deconstructedPrincipal.getPrincipalName() + " is not in lowercase. FreeIPA does not recognize user " +
"principals that are not entirely in lowercase. This can lead to issues with kinit and keytabs. Make " +
"sure users are in lowercase ");
}
// Create the ipa query: user-add <username> --principal=<principal_name> --first <primary> --last <primary>
// set-attr userPassword="<password>"
// first and last are required for IPA so we make it equal to the primary
// the --principal arguments makes sure that Kerberos keys are available for use in getKeyNumber
ShellCommandUtil.Result result = invokeIpa(String.format("user-add %s --principal=%s --first %s --last %s --setattr userPassword=%s",
deconstructedPrincipal.getPrimary(), deconstructedPrincipal.getPrincipalName(),
deconstructedPrincipal.getPrimary(), deconstructedPrincipal.getPrimary(), password));
if (!result.isSuccessful()) {
throw new KerberosOperationException(String.format("Failed to create user principal for %s\nSTDOUT: %s\nSTDERR: %s",
principal, result.getStdout(), result.getStderr()));
}
if (getUserPrincipalGroup() != null && !getUserPrincipalGroup().isEmpty()) {
result = invokeIpa(String.format("group-add-member %s --users=%s",
getUserPrincipalGroup(), deconstructedPrincipal.getPrimary()));
if (!result.isSuccessful()) {
throw new KerberosOperationException(String.format("Failed to create user principal for %s\nSTDOUT: %s\nSTDERR: %s",
principal, result.getStdout(), result.getStderr()));
}
}
if (!usePasswordExpiry) {
updatePassword(deconstructedPrincipal.getPrimary(), password);
return getKeyNumber(principal);
}
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.YEAR, PASSWORD_EXPIRY_YEAR);
result = invokeIpa(String.format("user-mod %s --setattr krbPasswordExpiration=%s",
deconstructedPrincipal.getPrimary(), expiryFormat.format(calendar.getTime())));
if (result.isSuccessful()) {
return getKeyNumber(principal);
}
throw new KerberosOperationException(String.format("Unknown error while creating principal for %s\n" +
"STDOUT: %s\n" +
"STDERR: %s\n",
principal, result.getStdout(), result.getStderr()));
}
}
}
/**
* Updates the password for an existing user principal in a previously configured IPA KDC
* <p/>
* This implementation creates a query to send to the ipa 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 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 ((principal == null) || principal.isEmpty()) {
throw new KerberosOperationException("Failed to set password - no principal specified");
} else if ((password == null) || password.isEmpty()) {
throw new KerberosOperationException("Failed to set password - no password specified");
} else if (!isServicePrincipal(principal)) {
DeconstructedPrincipal deconstructedPrincipal = createDeconstructPrincipal(principal);
if (usePasswordExpiry) {
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.YEAR, PASSWORD_EXPIRY_YEAR);
// Create the ipa query: user-mod <user> --setattr userPassword=<password>
invokeIpa(String.format("user-mod %s --setattr userPassword=%s", deconstructedPrincipal.getPrimary(), password));
List<String> command = new ArrayList<>();
command.add(executableIpa);
command.add("user-mod");
command.add(deconstructedPrincipal.getPrimary());
command.add("--setattr");
command.add(String.format("krbPasswordExpiration=%s", expiryFormat.format(calendar.getTime())));
ShellCommandUtil.Result result = executeCommand(command.toArray(new String[command.size()]));
if (!result.isSuccessful()) {
throw new KerberosOperationException("Failed to set password expiry");
}
} else {
updatePassword(deconstructedPrincipal.getPrimary(), password);
}
} else {
ShellCommandUtil.Result result = invokeIpa(String.format("service-show %s", principal));
// ignore the keytab but set the password for this principal
if (result.isSuccessful() && result.getStdout().contains("Keytab: False")) {
LOG.debug("Found service principal " + principal + " without password/keytab. Setting one");
createKeytab(principal, password, 0);
}
}
return getKeyNumber(principal);
}
/**
* 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 ((principal == null) || principal.isEmpty()) {
throw new KerberosOperationException("Failed to remove new principal - no principal specified");
} else {
ShellCommandUtil.Result result = null;
if (isServicePrincipal(principal)) {
result = invokeIpa(String.format("service-del %s", principal));
} else {
DeconstructedPrincipal deconstructedPrincipal = createDeconstructPrincipal(principal);
result = invokeIpa(String.format("user-del %s", deconstructedPrincipal.getPrincipalName()));
}
return result.isSuccessful();
}
}
/**
* Sets the name of the group where user principals should be members of
*
* @param userPrincipalGroup the name of the group
*/
public void setUserPrincipalGroup(String userPrincipalGroup) {
this.userPrincipalGroup = userPrincipalGroup;
}
/**
* Gets the name of the group where user principals should be members of
*
* @return name of the group where user principals should be members of
*/
public String getUserPrincipalGroup() {
return this.userPrincipalGroup;
}
/**
* Sets the KDC administrator server host address
*
* @param adminServerHost the ip address or FQDN of the IPA administrator server
*/
public void setAdminServerHost(String adminServerHost) {
this.adminServerHost = adminServerHost;
}
/**
* Gets the IP address or FQDN of the IPA administrator server
*
* @return the IP address or FQDN of the IPA administrator server
*/
public String getAdminServerHost() {
return this.adminServerHost;
}
/**
* Reads data from a stream without blocking and when available. Allows some time for the
* stream to become ready.
*
* @param stdin the stdin BufferedReader to read from
* @param stderr the stderr BufferedReader in case something goes wrong
* @return a String with available data
* @throws KerberosOperationException if a timeout happens
* @throws IOException when somethings goes wrong with the underlying stream
* @throws InterruptedException if the thread is interrupted
*/
private String readData(BufferedReader stdin, BufferedReader stderr) throws KerberosOperationException, IOException, InterruptedException {
char[] data = new char[1024];
StringBuilder sb = new StringBuilder();
int count = 0;
while (!stdin.ready()) {
Thread.sleep(1000L);
if (count >= timeout) {
char[] err_data = new char[1024];
StringBuilder err = new StringBuilder();
while (stderr.ready()) {
stderr.read(err_data);
err.append(err_data);
}
throw new KerberosOperationException("No answer data available from stdin stream. STDERR: " + err.toString());
}
count++;
}
while (stdin.ready()) {
stdin.read(data);
sb.append(data);
}
return sb.toString();
}
/**
* Updates a password for a (user) principal. This is done by first setting a random password and
* then invoking kInit to directly set the password. This is done to circumvent issues with expired
* password in IPA, as IPA needs passwords set by the admin to be set again by the user. Note that
* this resets the current principal to the principal specified here. To invoke further administrative
* commands a new kInit to admin is required.
*
* @param principal The principal user name that needs to be updated
* @param password The new password
* @throws KerberosOperationException if something is not as expected
*/
private void updatePassword(String principal, String password) throws KerberosOperationException {
BufferedReader reader = null;
BufferedReader stderr = null;
OutputStreamWriter out = null;
LOG.debug("Updating password for: " + principal);
UUID uuid = UUID.randomUUID();
String fileName = System.getProperty("java.io.tmpdir") +
File.pathSeparator +
"krb5cc_" + uuid.toString();
try {
ShellCommandUtil.Result result = invokeIpa(String.format("user-mod %s --random", principal));
if (!result.isSuccessful()) {
throw new KerberosOperationException(result.getStderr());
}
Pattern pattern = Pattern.compile("password: (.*)");
Matcher matcher = pattern.matcher(result.getStdout());
if (!matcher.find()) {
throw new KerberosOperationException("Unexpected response from ipa: " + result.getStdout());
}
String old_password = matcher.group(1);
String credentialsCache = String.format("FILE:%s", fileName);
Process process = Runtime.getRuntime().exec(new String[]{executableKinit, "-c", credentialsCache, principal});
reader = new BufferedReader(new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8));
stderr = new BufferedReader(new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8));
out = new OutputStreamWriter(process.getOutputStream());
String data = readData(reader, stderr);
if (!data.startsWith("Password")) {
process.destroy();
throw new KerberosOperationException("Unexpected response from kinit while trying to password for "
+ principal + " got: " + data);
}
LOG.debug("Sending old password");
out.write(old_password);
out.write('\n');
out.flush();
data = readData(reader, stderr);
if (!data.contains("Enter")) {
process.destroy();
throw new KerberosOperationException("Unexpected response from kinit while trying to password for "
+ principal + " got: " + data);
}
LOG.debug("Sending new password");
out.write(password);
out.write('\n');
out.flush();
data = readData(reader, stderr);
if (!data.contains("again")) {
process.destroy();
throw new KerberosOperationException("Unexpected response from kinit while trying to password for "
+ principal + " got: " + data);
}
LOG.debug("Sending new password again");
out.write(password);
out.write('\n');
out.flush();
process.waitFor();
} catch (IOException e) {
LOG.error("Cannot read stream: " + e);
throw new KerberosOperationException(e.getMessage());
} catch (InterruptedException e) {
LOG.error("Process interrupted: " + e);
throw new KerberosOperationException(e.getMessage());
} finally {
try {
if (out != null)
out.close();
} catch (IOException e) {
LOG.warn("Cannot close out stream: " + e);
}
try {
if (reader != null)
reader.close();
} catch (IOException e) {
LOG.warn("Cannot close stdin stream: " + e);
}
try {
if (stderr != null)
stderr.close();
} catch (IOException e) {
LOG.warn("Cannot close stderr stream: " + e);
}
File ccache = new File(fileName);
ccache.delete();
}
}
/**
* Invokes the ipa shell command with administrative credentials 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 KerberosOperationException if an unexpected error occurred
*/
protected ShellCommandUtil.Result invokeIpa(String query)
throws KerberosOperationException {
LOG.debug("Entering invokeipa");
ShellCommandUtil.Result result = null;
if ((query == null) || query.isEmpty()) {
throw new KerberosOperationException("Missing ipa query");
}
PrincipalKeyCredential administratorCredentials = getAdministratorCredential();
String defaultRealm = getDefaultRealm();
List<String> command = new ArrayList<String>();
List<String> kinit = new ArrayList<String>();
String adminPrincipal = (administratorCredentials == null)
? null
: administratorCredentials.getPrincipal();
if ((adminPrincipal == null) || adminPrincipal.isEmpty()) {
throw new KerberosOperationException("No admin principal for ipa available - " +
"this KerberosOperationHandler may not have been opened.");
}
if ((executableIpa == null) || executableIpa.isEmpty()) {
throw new KerberosOperationException("No path for ipa is available - " +
"this KerberosOperationHandler may not have been opened.");
}
// Set the ipa interface to be ipa
command.add(executableIpa);
command.add(query);
if (LOG.isDebugEnabled()) {
LOG.debug(String.format("Executing: %s", createCleanCommand(command)));
}
List<String> fixedCommand = fixCommandList(command);
result = executeCommand(fixedCommand.toArray(new String[fixedCommand.size()]));
LOG.debug("Done invokeipa");
return result;
}
/**
* Executes a shell command in a credentials context
* <p/>
* See {@link org.apache.ambari.server.utils.ShellCommandUtil#runCommand(String[])}
*
* @param command an array of String value representing the command and its arguments
* @return a ShellCommandUtil.Result declaring the result of the operation
* @throws KerberosOperationException
*/
@Override
protected ShellCommandUtil.Result executeCommand(String[] command)
throws KerberosOperationException {
return credentialsContext.executeCommand(command);
}
/**
* Rebuilds the command line to make sure space are converted to arguments
*
* @param command a List of items making up the command
* @return the fixed command
*/
private List<String> fixCommandList(List<String> command) {
List<String> fixedCommandList = new ArrayList<>();
Iterator<String> iterator = command.iterator();
if (iterator.hasNext()) {
fixedCommandList.add(iterator.next());
}
while (iterator.hasNext()) {
String part = iterator.next();
// split arguments
if (part.contains(" ")) {
StringTokenizer st = new StringTokenizer(part, " ");
while (st.hasMoreElements()) {
fixedCommandList.add(st.nextToken());
}
} else {
fixedCommandList.add(part);
}
}
return fixedCommandList;
}
/**
* Build the ipa 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(' ');
cleanedCommand.append(part);
if ("--setattr".equals(part)) {
// Skip the password and use "********" instead
String arg= null;
if (iterator.hasNext()) {
arg = iterator.next();
if (arg.contains("userPassword")) {
cleanedCommand.append("userPassword=******");
} else {
cleanedCommand.append(arg);
}
}
}
}
return cleanedCommand.toString();
}
/**
* Determine is a principal is a service principal
*
* @param principal
* @return true if the principal is a (existing) service principal
* @throws KerberosOperationException
*/
private boolean isServicePrincipal(String principal)
throws KerberosOperationException {
if ((principal == null) || principal.isEmpty()) {
throw new KerberosOperationException("Failed to determine principal type- no principal specified");
} else if (!principal.contains("/")) {
return false;
}
try {
ShellCommandUtil.Result result = invokeIpa(String.format("service-show %s", principal));
// TODO: unfortunately we can be in limbo if the "Keytab: False" is present
if (result.isSuccessful()) {
return true;
}
} catch (KerberosOperationException e) {
LOG.warn("Exception while invoking ipa service-show: " + e);
return false;
}
return false;
}
/**
* 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 ((principal == null) || principal.isEmpty()) {
throw new KerberosOperationException("Failed to get key number for principal - no principal specified");
} else {
// Create the kvno query: <principal>
List<String> command = new ArrayList<>();
command.add(executableKvno);
command.add(principal);
ShellCommandUtil.Result result = executeCommand(command.toArray(new String[command.size()]));
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);
}
}
}
/**
* Creates a key tab by using the ipa commandline utilities.
*
* @param principal a String containing the principal to test
* @param password a String containing the password to use when creating the principal
* @return
* @throws KerberosOperationException
*/
/*private Keytab createKeytabCredentials(String principal, String password)
throws KerberosOperationException {
if ((principal == null) || principal.isEmpty()) {
throw new KerberosOperationException("Failed to create keytab file, missing principal");
}
BufferedReader reader = null;
BufferedReader stderr = null;
OutputStreamWriter out = null;
UUID uuid = UUID.randomUUID();
String fileName = System.getProperty("java.io.tmpdir") +
File.pathSeparator +
"ambari." + uuid.toString();
try {
// TODO: add ciphers
Process p = credentialsContext.exec(new String[]{executableIpaGetKeytab, "-s",
getAdminServerHost(), "-p", principal, "-k", fileName, "-P"});
reader = new BufferedReader(new InputStreamReader(p.getInputStream(), StandardCharsets.UTF_8));
stderr = new BufferedReader(new InputStreamReader(p.getErrorStream(), StandardCharsets.UTF_8));
out = new OutputStreamWriter(p.getOutputStream());
String data = readData(reader, stderr);
if (!data.startsWith("New")) {
p.destroy();
throw new KerberosOperationException("Unexpected response from ipa-getkeytab while trying to password for "
+ principal + " got: " + data);
}
LOG.debug("Sending password");
out.write(password);
out.write('\n');
out.flush();
data = readData(reader, stderr);
if (!data.contains("Verify")) {
p.destroy();
throw new KerberosOperationException("Unexpected response from ipa-getkeytab while trying to password for "
+ principal + " got: " + data);
}
LOG.debug("Sending new password");
out.write(password);
out.write('\n');
out.flush();
p.waitFor();
} catch (IOException e) {
LOG.error("Cannot read stream: " + e);
throw new KerberosOperationException(e.getMessage());
} catch (InterruptedException e) {
LOG.error("Process interrupted: " + e);
throw new KerberosOperationException(e.getMessage());
} finally {
try {
if (out != null)
out.close();
} catch (IOException e) {
LOG.warn("Cannot close out stream: " + e);
}
try {
if (reader != null)
reader.close();
} catch (IOException e) {
LOG.warn("Cannot close stdin stream: " + e);
}
try {
if (stderr != null)
stderr.close();
} catch (IOException e) {
LOG.warn("Cannot close stderr stream: " + e);
}
}
File keytabFile = new File(fileName);
Keytab keytab = readKeytabFile(keytabFile);
keytabFile.delete();
return keytab;
}*/
/**
* Creates a key tab by using the ipa commandline utilities. It ignores key number and password
* as this will be handled by IPA
*
* @param principal a String containing the principal to test
* @param password (IGNORED) a String containing the password to use when creating the principal
* @param keyNumber (IGNORED) a Integer indicating the key number for the keytab entries
* @return
* @throws KerberosOperationException
*/
@Override
protected Keytab createKeytab(String principal, String password, Integer keyNumber)
throws KerberosOperationException {
if ((principal == null) || principal.isEmpty()) {
throw new KerberosOperationException("Failed to create keytab file, missing principal");
}
// use cache if available
if (cachedKeytabs.containsKey(principal)) {
return cachedKeytabs.get(principal);
}
UUID uuid = UUID.randomUUID();
String fileName = System.getProperty("java.io.tmpdir") +
File.pathSeparator +
"ambari." + uuid.toString();
// TODO: add ciphers
List<String> command = new ArrayList<>();
command.add(executableIpaGetKeytab);
command.add("-s");
command.add(getAdminServerHost());
command.add("-p");
command.add(principal);
command.add("-k");
command.add(fileName);
// TODO: is it really required to set the password?
ShellCommandUtil.Result result = executeCommand(command.toArray(new String[command.size()]));
if (!result.isSuccessful()) {
String message = String.format("Failed to get key number for %s:\n\tExitCode: %s\n\tSTDOUT: %s\n\tSTDERR: %s",
principal, result.getExitCode(), result.getStdout(), result.getStderr());
LOG.warn(message);
throw new KerberosOperationException(message);
}
File keytabFile = new File(fileName);
Keytab keytab = readKeytabFile(keytabFile);
keytabFile.delete();
cachedKeytabs.put(principal, keytab);
return keytab;
}
/**
* Credentials context executes commands wrapped with kerberos credentials
*/
class CredentialsContext {
private PrincipalKeyCredential credentials;
Map<String, String> env = new HashMap();
private String fileName;
private List<Process> processes = new ArrayList<>();
public CredentialsContext(PrincipalKeyCredential credentials) throws KerberosOperationException {
this.credentials = credentials;
UUID uuid = UUID.randomUUID();
fileName = System.getProperty("java.io.tmpdir") +
File.pathSeparator +
"krb5cc_" + uuid.toString();
env.put("KRB5CCNAME", String.format("FILE:%s", fileName));
init(credentials, fileName);
}
protected ShellCommandUtil.Result executeCommand(String[] command)
throws KerberosOperationException {
if ((command == null) || (command.length == 0)) {
return null;
} else {
try {
return ShellCommandUtil.runCommand(command, env);
} catch (IOException e) {
String message = String.format("Failed to execute the command: %s", e.getLocalizedMessage());
LOG.error(message, e);
throw new KerberosOperationException(message, e);
} catch (InterruptedException e) {
String message = String.format("Failed to wait for the command to complete: %s", e.getLocalizedMessage());
LOG.error(message, e);
throw new KerberosOperationException(message, e);
}
}
}
/**
* Does a kinit to obtain a ticket for the specified principal and stores it in the specified cache
*
* @param credentials Credentials to be used to obtain the ticket
* @param fileName Filename where to store the credentials
* @throws KerberosOperationException In case the ticket cannot be obtained
*/
private void init(PrincipalKeyCredential credentials, String fileName) throws KerberosOperationException {
Process process;
BufferedReader reader = null;
OutputStreamWriter osw = null;
LOG.debug("Entering doKinit");
try {
String credentialsCache = String.format("FILE:%s", fileName);
LOG.debug("start subprocess " + executableKinit + " " + credentials.getPrincipal());
process = Runtime.getRuntime().exec(new String[]{executableKinit, "-c", credentialsCache, credentials.getPrincipal()});
reader = new BufferedReader(new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8));
osw = new OutputStreamWriter(process.getOutputStream());
char[] data = new char[1024];
StringBuilder sb = new StringBuilder();
int count = 0;
while (!reader.ready()) {
Thread.sleep(1000L);
if (count >= 5) {
process.destroy();
throw new KerberosOperationException("No answer from kinit");
}
count++;
}
while (reader.ready()) {
reader.read(data);
sb.append(data);
}
String line = sb.toString();
LOG.debug("Reading a line: " + line);
if (!line.startsWith("Password")) {
throw new KerberosOperationException("Unexpected response from kinit while trying to get ticket for "
+ credentials.getPrincipal() + " got: " + line);
}
osw.write(credentials.getKey());
osw.write('\n');
osw.close();
process.waitFor();
LOG.debug("done subprocess");
} catch (IOException e) {
String message = String.format("Failed to execute the command: %s", e.getLocalizedMessage());
LOG.error(message, e);
throw new KerberosOperationException(message, e);
} catch (InterruptedException e) {
String message = String.format("Failed to execute the command: %s", e.getLocalizedMessage());
LOG.error(message, e);
throw new KerberosOperationException(message, e);
} finally {
if (osw != null) {
try {
osw.close();
} catch (IOException e) {
}
}
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
}
}
}
if (process.exitValue() != 0) {
throw new KerberosOperationException("kinit failed for " + credentials.getPrincipal() + ". Wrong password?");
}
}
public Process exec(String[] args) throws IOException {
Process process = Runtime.getRuntime().exec(args);
processes.add(process);
return process;
}
public void delete() {
File ccache = new File(fileName);
ccache.delete();
for (Process p : processes) {
p.destroy();
}
}
}
}