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