AMBARI-22293. Improve KDC integration (rlevas)
Project: http://git-wip-us.apache.org/repos/asf/ambari/repo Commit: http://git-wip-us.apache.org/repos/asf/ambari/commit/f844e5f3 Tree: http://git-wip-us.apache.org/repos/asf/ambari/tree/f844e5f3 Diff: http://git-wip-us.apache.org/repos/asf/ambari/diff/f844e5f3 Branch: refs/heads/branch-feature-AMBARI-21674 Commit: f844e5f3f952b57d790a238f0bafaf7d5ba1ddca Parents: a7f0f45 Author: Robert Levas <[email protected]> Authored: Thu Nov 2 12:37:36 2017 -0400 Committer: Robert Levas <[email protected]> Committed: Thu Nov 2 12:37:36 2017 -0400 ---------------------------------------------------------------------- .../docs/security/kerberos/kerberos_service.md | 22 +- .../kerberos/ADKerberosOperationHandler.java | 22 +- .../kerberos/CreatePrincipalsServerAction.java | 4 +- .../kerberos/DestroyPrincipalsServerAction.java | 3 +- .../kerberos/IPAKerberosOperationHandler.java | 1067 +++--------------- .../kerberos/KDCKerberosOperationHandler.java | 391 +++++++ .../kerberos/KerberosOperationHandler.java | 64 +- .../kerberos/MITKerberosOperationHandler.java | 406 ++----- .../server/upgrade/UpgradeCatalog300.java | 30 +- .../1.10.3-10/configuration/kerberos-env.xml | 36 +- .../1.10.3-30/configuration/kerberos-env.xml | 36 +- .../KERBEROS/configuration/kerberos-env.xml | 36 +- .../server/controller/KerberosHelperTest.java | 6 +- .../ADKerberosOperationHandlerTest.java | 261 +++-- .../IPAKerberosOperationHandlerTest.java | 147 ++- .../KDCKerberosOperationHandlerTest.java | 152 +++ .../kerberos/KerberosOperationHandlerTest.java | 152 ++- .../kerberos/KerberosServerActionTest.java | 133 ++- .../MITKerberosOperationHandlerTest.java | 633 +++-------- .../server/upgrade/UpgradeCatalog300Test.java | 152 ++- .../2.5/configs/ranger-admin-secured.json | 2 - .../stacks/2.5/configs/ranger-kms-secured.json | 2 - .../2.6/configs/ranger-admin-secured.json | 2 - .../PreconfigureActionTest_cluster_config.json | 4 +- .../main/admin/kerberos/step2_controller.js | 2 +- 25 files changed, 1623 insertions(+), 2142 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/ambari/blob/f844e5f3/ambari-server/docs/security/kerberos/kerberos_service.md ---------------------------------------------------------------------- diff --git a/ambari-server/docs/security/kerberos/kerberos_service.md b/ambari-server/docs/security/kerberos/kerberos_service.md index 65e312b..c9cbd49 100644 --- a/ambari-server/docs/security/kerberos/kerberos_service.md +++ b/ambari-server/docs/security/kerberos/kerberos_service.md @@ -231,32 +231,12 @@ _Example:_ `-requires_preauth max_renew_life=7d` This property is optional and only used if the `kdc_type` is `mit-kdc` -##### group +##### ipa_user_group The group in IPA user principals should be member of This property is mandatory and only used if the `kdc_type` is `ipa` -##### set_password_expiry - -Indicates whether Ambari should set the password expiry for the principals it creates. By default -IPA does not allow this. It requires write permission of the admin principal to the krbPasswordExpiry -attribute. If set IPA principal password expiry is not true it is assumed that a suitable password -policy is in place for the IPA Group principals are added to. - -_Possible values:_ `true`, `false` - -_Default value:_ `false` - -This property is mandatory and only used if the `kdc_type` is `ipa` - -##### password_chat_timeout - -Indicates the timeout in seconds that Ambari should wait for a response during a password chat. This is -because it can take some time due to lookups before a response is there. - -This property is mandatory and only used if the `kdc_type` is `ipa` - <a name="krb5-conf"></a> #### krb5-conf http://git-wip-us.apache.org/repos/asf/ambari/blob/f844e5f3/ambari-server/src/main/java/org/apache/ambari/server/serveraction/kerberos/ADKerberosOperationHandler.java ---------------------------------------------------------------------- diff --git a/ambari-server/src/main/java/org/apache/ambari/server/serveraction/kerberos/ADKerberosOperationHandler.java b/ambari-server/src/main/java/org/apache/ambari/server/serveraction/kerberos/ADKerberosOperationHandler.java index f7d6060..d7b91b0 100644 --- a/ambari-server/src/main/java/org/apache/ambari/server/serveraction/kerberos/ADKerberosOperationHandler.java +++ b/ambari-server/src/main/java/org/apache/ambari/server/serveraction/kerberos/ADKerberosOperationHandler.java @@ -175,9 +175,7 @@ public class ADKerberosOperationHandler extends KerberosOperationHandler { throw new KerberosLDAPContainerException("principalContainerDn is not a valid LDAP name", e); } - setAdministratorCredential(administratorCredential); - setDefaultRealm(realm); - setKeyEncryptionTypes(translateEncryptionTypes(kerberosConfiguration.get(KERBEROS_ENV_ENCRYPTION_TYPES), "\\s+")); + super.open(administratorCredential, realm, kerberosConfiguration); this.ldapContext = createLdapContext(); this.searchControls = createSearchControls(); @@ -215,11 +213,12 @@ public class ADKerberosOperationHandler extends KerberosOperationHandler { * The implementation is specific to a particular type of KDC. * * @param principal a String containing the principal to test + * @param service a boolean value indicating whether the principal is for a service or not * @return true if the principal exists; false otherwise * @throws KerberosOperationException */ @Override - public boolean principalExists(String principal) throws KerberosOperationException { + public boolean principalExists(String principal, boolean service) throws KerberosOperationException { if (!isOpen()) { throw new KerberosOperationException("This operation handler has not been opened"); } @@ -260,7 +259,7 @@ public class ADKerberosOperationHandler extends KerberosOperationHandler { if (password == null) { throw new KerberosOperationException("principal password is null"); } - if (principalExists(principal)) { + if (principalExists(principal, service)) { throw new KerberosPrincipalAlreadyExistsException(principal); } @@ -347,12 +346,13 @@ public class ADKerberosOperationHandler extends KerberosOperationHandler { * * @param principal a String containing the principal to update * @param password a String containing the password to set + * @param service a boolean value indicating whether the principal is for a service or not * @return an Integer declaring the new key number * @throws KerberosPrincipalDoesNotExistException if the principal does not exist * @throws KerberosOperationException */ @Override - public Integer setPrincipalPassword(String principal, String password) throws KerberosOperationException { + public Integer setPrincipalPassword(String principal, String password, boolean service) throws KerberosOperationException { if (!isOpen()) { throw new KerberosOperationException("This operation handler has not been opened"); } @@ -362,7 +362,7 @@ public class ADKerberosOperationHandler extends KerberosOperationHandler { if (password == null) { throw new KerberosOperationException("principal password is null"); } - if(!principalExists(principal)) { + if (!principalExists(principal, service)) { throw new KerberosPrincipalDoesNotExistException(principal); } @@ -396,11 +396,12 @@ public class ADKerberosOperationHandler extends KerberosOperationHandler { * The implementation is specific to a particular type of KDC. * * @param principal a String containing the principal to remove + * @param service a boolean value indicating whether the principal is for a service or not * @return true if the principal was successfully removed; otherwise false * @throws KerberosOperationException */ @Override - public boolean removePrincipal(String principal) throws KerberosOperationException { + public boolean removePrincipal(String principal, boolean service) throws KerberosOperationException { if (!isOpen()) { throw new KerberosOperationException("This operation handler has not been opened"); } @@ -469,10 +470,9 @@ public class ADKerberosOperationHandler extends KerberosOperationHandler { String message = String.format("Failed to communicate with the Active Directory at %s: %s", ldapUrl, e.getMessage()); LOG.warn(message, e); - if(rootCause instanceof SSLHandshakeException) { + if (rootCause instanceof SSLHandshakeException) { throw new KerberosKDCSSLConnectionException(message, e); - } - else { + } else { throw new KerberosKDCConnectionException(message, e); } } catch (AuthenticationException e) { http://git-wip-us.apache.org/repos/asf/ambari/blob/f844e5f3/ambari-server/src/main/java/org/apache/ambari/server/serveraction/kerberos/CreatePrincipalsServerAction.java ---------------------------------------------------------------------- diff --git a/ambari-server/src/main/java/org/apache/ambari/server/serveraction/kerberos/CreatePrincipalsServerAction.java b/ambari-server/src/main/java/org/apache/ambari/server/serveraction/kerberos/CreatePrincipalsServerAction.java index 08e03bd..59d5327 100644 --- a/ambari-server/src/main/java/org/apache/ambari/server/serveraction/kerberos/CreatePrincipalsServerAction.java +++ b/ambari-server/src/main/java/org/apache/ambari/server/serveraction/kerberos/CreatePrincipalsServerAction.java @@ -253,7 +253,7 @@ public class CreatePrincipalsServerAction extends KerberosServerAction { if (regenerateKeytabs) { try { - keyNumber = kerberosOperationHandler.setPrincipalPassword(principal, password); + keyNumber = kerberosOperationHandler.setPrincipalPassword(principal, password, isServicePrincipal); created = false; } catch (KerberosPrincipalDoesNotExistException e) { message = String.format("Principal, %s, does not exist, creating new principal", principal); @@ -276,7 +276,7 @@ public class CreatePrincipalsServerAction extends KerberosServerAction { actionLog.writeStdOut(message); } - keyNumber = kerberosOperationHandler.setPrincipalPassword(principal, password); + keyNumber = kerberosOperationHandler.setPrincipalPassword(principal, password, isServicePrincipal); created = false; } } http://git-wip-us.apache.org/repos/asf/ambari/blob/f844e5f3/ambari-server/src/main/java/org/apache/ambari/server/serveraction/kerberos/DestroyPrincipalsServerAction.java ---------------------------------------------------------------------- diff --git a/ambari-server/src/main/java/org/apache/ambari/server/serveraction/kerberos/DestroyPrincipalsServerAction.java b/ambari-server/src/main/java/org/apache/ambari/server/serveraction/kerberos/DestroyPrincipalsServerAction.java index 2b3a0ca..4c80bd4 100644 --- a/ambari-server/src/main/java/org/apache/ambari/server/serveraction/kerberos/DestroyPrincipalsServerAction.java +++ b/ambari-server/src/main/java/org/apache/ambari/server/serveraction/kerberos/DestroyPrincipalsServerAction.java @@ -114,7 +114,8 @@ public class DestroyPrincipalsServerAction extends KerberosServerAction { try { try { - operationHandler.removePrincipal(evaluatedPrincipal); + boolean servicePrincipal = "service".equalsIgnoreCase(identityRecord.get(KerberosIdentityDataFileReader.PRINCIPAL_TYPE)); + operationHandler.removePrincipal(evaluatedPrincipal, servicePrincipal); } catch (KerberosOperationException e) { message = String.format("Failed to remove identity for %s from the KDC - %s", evaluatedPrincipal, e.getMessage()); LOG.warn(message); http://git-wip-us.apache.org/repos/asf/ambari/blob/f844e5f3/ambari-server/src/main/java/org/apache/ambari/server/serveraction/kerberos/IPAKerberosOperationHandler.java ---------------------------------------------------------------------- diff --git a/ambari-server/src/main/java/org/apache/ambari/server/serveraction/kerberos/IPAKerberosOperationHandler.java b/ambari-server/src/main/java/org/apache/ambari/server/serveraction/kerberos/IPAKerberosOperationHandler.java index 9a6a07e..c411237 100644 --- a/ambari-server/src/main/java/org/apache/ambari/server/serveraction/kerberos/IPAKerberosOperationHandler.java +++ b/ambari-server/src/main/java/org/apache/ambari/server/serveraction/kerberos/IPAKerberosOperationHandler.java @@ -18,32 +18,14 @@ package org.apache.ambari.server.serveraction.kerberos; -import java.io.BufferedReader; -import java.io.File; -import java.io.IOException; -import java.io.InputStreamReader; -import java.io.OutputStreamWriter; -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; +import java.util.Set; import org.apache.ambari.server.security.credential.PrincipalKeyCredential; -import org.apache.ambari.server.utils.Closeables; import org.apache.ambari.server.utils.ShellCommandUtil; +import org.apache.commons.collections.CollectionUtils; import org.apache.commons.lang.StringUtils; -import org.apache.directory.server.kerberos.shared.keytab.Keytab; +import org.apache.directory.shared.kerberos.codec.types.EncryptionType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -54,12 +36,9 @@ import org.slf4j.LoggerFactory; * It is assumed that the IPA admin tools are installed and that the ipa shell command is * available */ -public class IPAKerberosOperationHandler extends KerberosOperationHandler { +public class IPAKerberosOperationHandler extends KDCKerberosOperationHandler { 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 @@ -67,27 +46,6 @@ public class IPAKerberosOperationHandler extends KerberosOperationHandler { 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; @@ -98,31 +56,6 @@ public class IPAKerberosOperationHandler extends KerberosOperationHandler { 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. @@ -139,84 +72,27 @@ public class IPAKerberosOperationHandler extends KerberosOperationHandler { * @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); + public void open(PrincipalKeyCredential administratorCredentials, String realm, Map<String, String> kerberosConfiguration) + throws KerberosOperationException { 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); + userPrincipalGroup = kerberosConfiguration.get(KERBEROS_ENV_USER_PRINCIPAL_GROUP); } // 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; - } + super.open(administratorCredentials, realm, kerberosConfiguration); } @Override public void close() throws KerberosOperationException { - if (isOpen()) { - credentialsContext.delete(); - } - - // There is nothing to do here. - setOpen(false); - + userPrincipalGroup = null; executableIpa = null; - executableKvno = null; executableIpaGetKeytab = null; - executableKinit = null; - credentialsContext = null; - cachedKeytabs = null; + + super.close(); } /** @@ -226,190 +102,121 @@ public class IPAKerberosOperationHandler extends KerberosOperationHandler { * the result from STDOUT to determine if the presence of the specified principal. * * @param principal a String containing the principal to test + * @param service a boolean value indicating whether the principal is for a service or not * @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"); + public boolean principalExists(String principal, boolean service) + throws KerberosOperationException { 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"); + if (!StringUtils.isEmpty(principal)) { + DeconstructedPrincipal deconstructedPrincipal = createDeconstructPrincipal(principal); + String principalName = deconstructedPrincipal.getPrincipalName(); - // 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; + String[] ipaCommand = new String[]{ + (service) ? "service-show" : "user-show", + principalName + }; + + ShellCommandUtil.Result result = invokeIpa(ipaCommand); + if (result.isSuccessful()) { + return true; } } 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. + * Creates a new principal in a previously configured KDC. + * <p> + * This implementation uses the ipa shell to create either a user or service account. No password + * will be set for either account type. The password (or key) will be automatically generated by + * the IPA server when exporting the keytab entry. Upon success, this method will always return + * <code>0</code> as the key number since the value is not generated until the keytab entry is + * exported. * - * @param principal a String containing the principal add - * @param password a String containing the password to use when creating the principal + * @param principal a String containing the principal to add + * @param password a String containing the password to use when creating the principal (ignored) * @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 + * @return an Integer declaring the generated key number (always 0) + * @throws KerberosOperationException */ @Override public Integer createPrincipal(String principal, String password, boolean service) - throws KerberosOperationException { + throws KerberosOperationException { if (!isOpen()) { throw new KerberosOperationException("This operation handler has not been opened"); } - if ((principal == null) || principal.isEmpty()) { + if (StringUtils.isEmpty(principal)) { 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()))); + DeconstructedPrincipal deconstructedPrincipal = createDeconstructPrincipal(principal); + String normalizedPrincipal = deconstructedPrincipal.getNormalizedPrincipal(); - if (result.isSuccessful()) { - return getKeyNumber(principal); - } + String[] ipaCommand; + if (service) { + ipaCommand = new String[]{ + "service-add", + normalizedPrincipal + }; + } else { + String principalName = deconstructedPrincipal.getPrincipalName(); - throw new KerberosOperationException(String.format("Unknown error while creating principal for %s\n" + - "STDOUT: %s\n" + - "STDERR: %s\n", - principal, result.getStdout(), result.getStderr())); + if (!principalName.equals(principal.toLowerCase())) { + LOG.warn("{} 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.", principalName); } - } - } - /** - * 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"); + ipaCommand = new String[]{ + "user-add", + deconstructedPrincipal.getPrimary(), + "--principal", + principalName, + "--first", + deconstructedPrincipal.getPrimary(), + "--last", + deconstructedPrincipal.getPrimary() + }; } - 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); + ShellCommandUtil.Result result = invokeIpa(ipaCommand); + if (!result.isSuccessful()) { + String message = String.format("Failed to create principal for %s\n%s\nSTDOUT: %s\nSTDERR: %s", + normalizedPrincipal, StringUtils.join(ipaCommand, " "), result.getStdout(), result.getStderr()); + LOG.error(message); - // Create the ipa query: user-mod <user> --setattr userPassword=<password> - invokeIpa(String.format("user-mod %s --setattr userPassword=%s", deconstructedPrincipal.getPrimary(), password)); + String stdErr = result.getStderr(); - 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"); - } + if ((stdErr != null) && + ((service && stdErr.contains(String.format("service with name \"%s\" already exists", normalizedPrincipal))) || + (!service && stdErr.contains(String.format("user with name \"%s\" already exists", deconstructedPrincipal.getPrimary()))))) { + throw new KerberosPrincipalAlreadyExistsException(principal); } else { - updatePassword(deconstructedPrincipal.getPrimary(), password); + throw new KerberosOperationException(String.format("Failed to create principal for %s\nSTDOUT: %s\nSTDERR: %s", + normalizedPrincipal, result.getStdout(), result.getStderr())); } - } 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 {} without password/keytab. Setting one", principal); - createKeytab(principal, password, 0); + } + + if ((!service) && !StringUtils.isEmpty(userPrincipalGroup)) { + result = invokeIpa(new String[]{"group-add-member", userPrincipalGroup, "--users", deconstructedPrincipal.getPrimary()}); + if (!result.isSuccessful()) { + LOG.warn("Failed to add account for {} to group {}: \nSTDOUT: {}\nSTDERR: {}", + normalizedPrincipal, userPrincipalGroup, result.getStdout(), result.getStderr()); } } - return getKeyNumber(principal); + + // Always return 0 since we do not have a key to get a key number for. + return 0; } /** @@ -418,6 +225,7 @@ public class IPAKerberosOperationHandler extends KerberosOperationHandler { * The implementation is specific to a particular type of KDC. * * @param principal a String containing the principal to remove + * @param service a boolean value indicating whether the principal is for a service or not * @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 @@ -425,696 +233,95 @@ public class IPAKerberosOperationHandler extends KerberosOperationHandler { * @throws KerberosOperationException if an unexpected error occurred */ @Override - public boolean removePrincipal(String principal) throws KerberosOperationException { + public boolean removePrincipal(String principal, 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 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(); + if (StringUtils.isEmpty(principal)) { + throw new KerberosOperationException("Failed to remove principal - no principal specified"); } - } - /** - * 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; - } + DeconstructedPrincipal deconstructedPrincipal = createDeconstructPrincipal(principal); - /** - * 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; - } + String[] ipaCommand = (service) + ? new String[]{"service-del", deconstructedPrincipal.getNormalizedPrincipal()} + : new String[]{"user-del", deconstructedPrincipal.getPrincipalName()}; - /** - * 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; + return invokeIpa(ipaCommand).isSuccessful(); } - /** - * 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; + @Override + protected String[] getKinitCommand(String executableKinit, PrincipalKeyCredential credentials, String credentialsCache) { + return new String[]{ + executableKinit, + "-c", + credentialsCache, + credentials.getPrincipal() + }; } - /** - * 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); + @Override + protected void exportKeytabFile(String principal, String keytabFileDestinationPath, Set<EncryptionType> keyEncryptionTypes) throws KerberosOperationException { + String encryptionTypeSpec = null; + if (!CollectionUtils.isEmpty(keyEncryptionTypes)) { + StringBuilder encryptionTypeSpecBuilder = new StringBuilder(); + for (EncryptionType encryptionType : keyEncryptionTypes) { + if (encryptionTypeSpecBuilder.length() > 0) { + encryptionTypeSpecBuilder.append(','); } - throw new KerberosOperationException("No answer data available from stdin stream. STDERR: " + err); + encryptionTypeSpecBuilder.append(encryptionType.getName()); } - count++; - } - while (stdin.ready()) { - stdin.read(data); - sb.append(data); + encryptionTypeSpec = encryptionTypeSpecBuilder.toString(); } - 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; - - 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(); + String[] createKeytabFileCommand = (StringUtils.isEmpty(encryptionTypeSpec)) + ? new String[]{executableIpaGetKeytab, "-s", getAdminServerHost(), "-p", principal, "-k", keytabFileDestinationPath} + : new String[]{executableIpaGetKeytab, "-s", getAdminServerHost(), "-e", encryptionTypeSpec, "-p", principal, "-k", keytabFileDestinationPath}; - 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(); + ShellCommandUtil.Result result = executeCommand(createKeytabFileCommand); + if (!result.isSuccessful()) { + String message = String.format("Failed to export the keytab file 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); } - } /** * Invokes the ipa shell command with administrative credentials to issue queries * - * @param query a String containing the query to send to the kdamin command + * @param query a String containing the query to send to the ipa 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"); + private ShellCommandUtil.Result invokeIpa(String[] query) throws KerberosOperationException { - ShellCommandUtil.Result result = null; - - if ((query == null) || query.isEmpty()) { + if ((query == null) || (query.length == 0)) { throw new KerberosOperationException("Missing ipa query"); } - PrincipalKeyCredential administratorCredentials = getAdministratorCredential(); - String defaultRealm = getDefaultRealm(); - - List<String> command = new ArrayList<>(); - List<String> kinit = new ArrayList<>(); - 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."); + if (StringUtils.isEmpty(executableIpa)) { + 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("Executing: {}", createCleanCommand(command)); - } + String[] command = new String[query.length + 1]; + command[0] = executableIpa; + System.arraycopy(query, 0, command, 1, query.length); - List<String> fixedCommand = fixCommandList(command); - result = executeCommand(fixedCommand.toArray(new String[fixedCommand.size()])); - - - LOG.debug("Done invokeipa"); - return result; - } + ShellCommandUtil.Result result = executeCommand(command); - /** - * 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; + if (result.isSuccessful()) { + if (LOG.isDebugEnabled()) { + LOG.debug("Executed the following command:\n{}\nSTDOUT: {}\nSTDERR: {}", + StringUtils.join(command, " "), result.getStdout(), result.getStderr()); } - } 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; - - // 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; - 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 | InterruptedException e) { - String message = String.format("Failed to execute the command: %s", e.getLocalizedMessage()); - LOG.error(message, e); - throw new KerberosOperationException(message, e); - } finally { - Closeables.closeSilently(osw); - Closeables.closeSilently(reader); - } - - 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(); - } + LOG.error("Failed to execute the following command:\n{}\nSTDOUT: {}\nSTDERR: {}", + StringUtils.join(command, " "), result.getStdout(), result.getStderr()); } + return result; } - } http://git-wip-us.apache.org/repos/asf/ambari/blob/f844e5f3/ambari-server/src/main/java/org/apache/ambari/server/serveraction/kerberos/KDCKerberosOperationHandler.java ---------------------------------------------------------------------- diff --git a/ambari-server/src/main/java/org/apache/ambari/server/serveraction/kerberos/KDCKerberosOperationHandler.java b/ambari-server/src/main/java/org/apache/ambari/server/serveraction/kerberos/KDCKerberosOperationHandler.java new file mode 100644 index 0000000..e5696cd --- /dev/null +++ b/ambari-server/src/main/java/org/apache/ambari/server/serveraction/kerberos/KDCKerberosOperationHandler.java @@ -0,0 +1,391 @@ +/* + * 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.io.File; +import java.io.IOException; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.Map; +import java.util.Queue; +import java.util.Set; + +import org.apache.ambari.server.AmbariException; +import org.apache.ambari.server.security.credential.PrincipalKeyCredential; +import org.apache.ambari.server.utils.ShellCommandUtil; +import org.apache.commons.collections.MapUtils; +import org.apache.directory.server.kerberos.shared.keytab.Keytab; +import org.apache.directory.shared.kerberos.codec.types.EncryptionType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * KDCKerberosOperationHandler is an implementation of a KerberosOperationHandler providing + * functionality KDC-based Kerberos providers. + * <p> + * This implementation provides kinit functionality and keytab file caching utilities for classes. + */ +abstract class KDCKerberosOperationHandler extends KerberosOperationHandler { + private static Logger LOG = LoggerFactory.getLogger(KDCKerberosOperationHandler.class); + + /** + * The FQDN of the host where KDC administration server is + */ + private String adminServerHost = null; + + /** + * A map of principal names to {@link Keytab} entries to ensure a Keyab file is not created/exported + * for the same principal more than once. + */ + private HashMap<String, Keytab> cachedKeytabs = null; + + /** + * A String containing the resolved path to the kinit executable + */ + private String executableKinit = null; + + /** + * The absolute path to the KDC administrator's Kerberos ticket cache. + * <p> + * This path is created as as temporary file with a randomized name when this {@link KerberosOperationHandler} + * is open. It is destoryed when this {@link KerberosOperationHandler} is closed. + */ + private File credentialsCacheFile = null; + + /** + * A Map of environmet values to send to system command invocations. + * <p> + * This map is to be appened to any map of environment values passed in when + * invoking {@link KerberosOperationHandler#executeCommand(String[], Map, ShellCommandUtil.InteractiveHandler)} + */ + private Map<String, String> environmentMap = null; + + @Override + public void open(PrincipalKeyCredential administratorCredentials, String realm, Map<String, String> kerberosConfiguration) + throws KerberosOperationException { + + super.open(administratorCredentials, realm, kerberosConfiguration); + + if (kerberosConfiguration != null) { + adminServerHost = kerberosConfiguration.get(KERBEROS_ENV_ADMIN_SERVER_HOST); + } + + // Pre-determine the paths to relevant Kerberos executables + executableKinit = getExecutable("kinit"); + + setOpen(init()); + } + + @Override + public void close() throws KerberosOperationException { + + if (credentialsCacheFile != null) { + if (credentialsCacheFile.delete()) { + LOG.debug("Failed to remove the cache file, {}", credentialsCacheFile.getAbsolutePath()); + } + credentialsCacheFile = null; + } + + environmentMap = null; + executableKinit = null; + cachedKeytabs = null; + adminServerHost = null; + + super.close(); + } + + /** + * 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 + * @param service a boolean value indicating whether the principal is for a service or not + * @return an Integer declaring the new key number + * @throws KerberosOperationException if an unexpected error occurred + */ + @Override + public Integer setPrincipalPassword(String principal, String password, boolean service) throws KerberosOperationException { + if (!isOpen()) { + throw new KerberosOperationException("This operation handler has not been opened"); + } + + // It is expected that KerberosPrincipalDoesNotExistException is thrown if the principal does not exist. + // The caller expects so that it can attempt to set the password for a principal without checking + // to see if it exists first. If the principal does not exist and is required, the caller will + // create it. This saves a potentially unnecessary round trip to the KDC and back. + if(!principalExists(principal, service)) { + throw new KerberosPrincipalDoesNotExistException(String.format("Principal does not exist while attempting to set its password: %s", principal)); + } + + // This operation does nothing since a new key will be created when exporting the keytab file... + return 0; + } + + /** + * 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 the created Keytab + * @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); + } + + File keytabFile = null; + + try { + try { + keytabFile = File.createTempFile("ambari_tmp", ".keytab"); + + // Remove the file else the command will fail... + if (!keytabFile.delete()) { + LOG.warn("Failed to remove temporary file to hold keytab. Exporting the keytab file for {} may fail.", principal); + } + } catch (IOException e) { + throw new KerberosOperationException(String.format("Failed to create the temporary file needed to hold the exported keytab file for %s: %s", principal, e.getLocalizedMessage()), e); + } + + + exportKeytabFile(principal, keytabFile.getAbsolutePath(), getKeyEncryptionTypes()); + + Keytab keytab = readKeytabFile(keytabFile); + cachedKeytabs.put(principal, keytab); + return keytab; + } finally { + if ((keytabFile != null) && keytabFile.exists()) { + if (!keytabFile.delete()) { + LOG.debug("Failed to remove the temporary keytab file, {}", keytabFile.getAbsolutePath()); + } + } + } + } + + /** + * Executes a shell command in a credentials context + * <p/> + * See {@link ShellCommandUtil#runCommand(String[])} + * <p> + * This implementation sets the proper environment for the custom <code>KRB5CCNAME </code> value. + * + * @param command an array of String value representing the command and its arguments + * @param envp a map of string, string of environment variables + * @param interactiveHandler a handler to provide responses to queries from the command, + * or null if no queries are expected + * @return a ShellCommandUtil.Result declaring the result of the operation + * @throws KerberosOperationException + */ + @Override + protected ShellCommandUtil.Result executeCommand(String[] command, Map<String, String> envp, ShellCommandUtil.InteractiveHandler interactiveHandler) + throws KerberosOperationException { + + Map<String, String> _envp; + + if (MapUtils.isEmpty(environmentMap)) { + _envp = envp; + } else if (MapUtils.isEmpty(envp)) { + _envp = environmentMap; + } else { + _envp = new HashMap<>(); + _envp.putAll(envp); + _envp.putAll(environmentMap); + } + + return super.executeCommand(command, _envp, interactiveHandler); + } + + String getAdminServerHost() { + return adminServerHost; + } + + String getCredentialCacheFilePath() { + return (credentialsCacheFile == null) ? null : credentialsCacheFile.getAbsolutePath(); + } + + /** + * Return an array of Strings containing the command and the relavant arguments needed authenticate + * with the KDC and create the Kerberos ticket/credential cache. + * + * @param executableKinit the absolute path to the kinit executable + * @param credentials the KDC adminisrator's credentials + * @param credentialsCache the absolute path to the expected location of the Kerberos ticket/credential cache file + * @return an array of Strings containing the command to execute + */ + protected abstract String[] getKinitCommand(String executableKinit, PrincipalKeyCredential credentials, String credentialsCache); + + /** + * Export the requested keytab entries for a given principal into the specified file. + * + * @param principal the principal name + * @param keytabFileDestinationPath the absolute path to the keytab file + * @param keyEncryptionTypes a collection of encrption algorithm types indicating which ketyab entries are requested + * @throws KerberosOperationException + */ + protected abstract void exportKeytabFile(String principal, String keytabFileDestinationPath, Set<EncryptionType> keyEncryptionTypes) throws KerberosOperationException; + + /** + * Initialize the Kerberos ticket cache using the supplied KDC administrator's credentials. + * <p> + * A randomly named temporary file is created to store the Kerberos ticket cache for this {@link KerberosOperationHandler}'s + * session. The file will be removed upon closing when the session is complete. The geneated ticket cache + * filename is set in the environment variable map using the variable name "KRB5CCNAME". This will be passed + * in for all relevant-system commands. + * + * @return + * @throws KerberosOperationException + */ + protected boolean init() throws KerberosOperationException { + if (credentialsCacheFile != null) { + if (!credentialsCacheFile.delete()) { + LOG.debug("Failed to remove the orphaned cache file, {}", credentialsCacheFile.getAbsolutePath()); + } + credentialsCacheFile = null; + } + + try { + credentialsCacheFile = File.createTempFile("ambari_krb_", "cc"); + credentialsCacheFile.deleteOnExit(); + ensureAmbariOnlyAccess(credentialsCacheFile); + } catch (IOException e) { + throw new KerberosOperationException(String.format("Failed to create the temporary file needed to hold the administrator ticket cache: %s", e.getLocalizedMessage()), e); + } + + String credentialsCache = String.format("FILE:%s", credentialsCacheFile.getAbsolutePath()); + + environmentMap = new HashMap<>(); + environmentMap.put("KRB5CCNAME", credentialsCache); + + PrincipalKeyCredential credentials = getAdministratorCredential(); + + ShellCommandUtil.Result result = executeCommand(getKinitCommand(executableKinit, credentials, credentialsCache), + environmentMap, + new InteractivePasswordHandler(String.valueOf(credentials.getKey()), null)); + + if (!result.isSuccessful()) { + String message = String.format("Failed to kinit as the KDC administrator user, %s:\n\tExitCode: %s\n\tSTDOUT: %s\n\tSTDERR: %s", + credentials.getPrincipal(), result.getExitCode(), result.getStdout(), result.getStderr()); + LOG.warn(message); + throw new KerberosAdminAuthenticationException(message); + } + + cachedKeytabs = new HashMap<>(); + + return true; + } + + /** + * Ensures that the owner of the Ambari server process is the only local user account able to + * read and write to the specified file or read, write to, and execute the specified directory. + * + * @param file the file or directory for which to modify access + */ + private void ensureAmbariOnlyAccess(File file) throws AmbariException { + if (file.exists()) { + if (!file.setReadable(false, false) || !file.setReadable(true, true)) { + String message = String.format("Failed to set %s readable only by Ambari", file.getAbsolutePath()); + LOG.warn(message); + throw new AmbariException(message); + } + + if (!file.setWritable(false, false) || !file.setWritable(true, true)) { + String message = String.format("Failed to set %s writable only by Ambari", file.getAbsolutePath()); + LOG.warn(message); + throw new AmbariException(message); + } + + if (file.isDirectory()) { + if (!file.setExecutable(false, false) || !file.setExecutable(true, true)) { + String message = String.format("Failed to set %s executable by Ambari", file.getAbsolutePath()); + LOG.warn(message); + throw new AmbariException(message); + } + } else { + if (!file.setExecutable(false, false)) { + String message = String.format("Failed to set %s not executable", file.getAbsolutePath()); + LOG.warn(message); + throw new AmbariException(message); + } + } + } + } + + /** + * InteractivePasswordHandler is a {@link 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) + */ + 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); + } + } +} http://git-wip-us.apache.org/repos/asf/ambari/blob/f844e5f3/ambari-server/src/main/java/org/apache/ambari/server/serveraction/kerberos/KerberosOperationHandler.java ---------------------------------------------------------------------- diff --git a/ambari-server/src/main/java/org/apache/ambari/server/serveraction/kerberos/KerberosOperationHandler.java b/ambari-server/src/main/java/org/apache/ambari/server/serveraction/kerberos/KerberosOperationHandler.java index 8749f81..948fd60 100644 --- a/ambari-server/src/main/java/org/apache/ambari/server/serveraction/kerberos/KerberosOperationHandler.java +++ b/ambari-server/src/main/java/org/apache/ambari/server/serveraction/kerberos/KerberosOperationHandler.java @@ -67,22 +67,7 @@ public abstract class KerberosOperationHandler { /** * Kerberos-env configuration property name: group */ - public final static String KERBEROS_ENV_USER_PRINCIPAL_GROUP = "group"; - - /** - * Kerberos-env configuration property name: password_chat_timeout - */ - public final static String KERBEROS_ENV_PASSWORD_CHAT_TIMEOUT = "password_chat_timeout"; - - /** - * Default timeout for password chat - */ - public final static int DEFAULT_PASSWORD_CHAT_TIMEOUT = 5; - - /** - * Kerberos-env configuration property name: set_password_expiry - */ - public final static String KERBEROS_ENV_SET_PASSWORD_EXPIRY = "set_password_expiry"; + public final static String KERBEROS_ENV_USER_PRINCIPAL_GROUP = "ipa_user_group"; /** * Kerberos-env configuration property name: ad_create_attributes_template @@ -232,16 +217,26 @@ public abstract class KerberosOperationHandler { * @param defaultRealm 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 */ - public abstract void open(PrincipalKeyCredential administratorCredential, String defaultRealm, Map<String, String> kerberosConfiguration) - throws KerberosOperationException; + public void open(PrincipalKeyCredential administratorCredential, String defaultRealm, Map<String, String> kerberosConfiguration) + throws KerberosOperationException { + + setAdministratorCredential(administratorCredential); + setDefaultRealm(defaultRealm); + + if (kerberosConfiguration != null) { + setKeyEncryptionTypes(translateEncryptionTypes(kerberosConfiguration.get(KERBEROS_ENV_ENCRYPTION_TYPES), "\\s+")); + setExecutableSearchPaths(kerberosConfiguration.get(KERBEROS_ENV_EXECUTABLE_SEARCH_PATHS)); + } + } /** * Closes and cleans up any resources used by this KerberosOperationHandler * <p/> * It is expected that this KerberosOperationHandler will not be used after this call. */ - public abstract void close() - throws KerberosOperationException; + public void close() throws KerberosOperationException { + setOpen(false); + } /** * Test to see if the specified principal exists in a previously configured KDC @@ -249,10 +244,11 @@ public abstract class KerberosOperationHandler { * The implementation is specific to a particular type of KDC. * * @param principal a String containing the principal to test + * @param service a boolean value indicating whether the principal is for a service or not * @return true if the principal exists; false otherwise * @throws KerberosOperationException */ - public abstract boolean principalExists(String principal) + public abstract boolean principalExists(String principal, boolean service) throws KerberosOperationException; /** @@ -277,11 +273,12 @@ public abstract class KerberosOperationHandler { * * @param principal a String containing the principal to update * @param password a String containing the password to set + * @param service a boolean value indicating whether the principal is for a service or not * @return an Integer declaring the new key number * @throws KerberosOperationException * @throws KerberosPrincipalDoesNotExistException if the principal does not exist */ - public abstract Integer setPrincipalPassword(String principal, String password) + public abstract Integer setPrincipalPassword(String principal, String password, boolean service) throws KerberosOperationException; /** @@ -290,10 +287,11 @@ public abstract class KerberosOperationHandler { * The implementation is specific to a particular type of KDC. * * @param principal a String containing the principal to remove + * @param service a boolean value indicating whether the principal is for a service or not * @return true if the principal was successfully removed; otherwise false * @throws KerberosOperationException */ - public abstract boolean removePrincipal(String principal) + public abstract boolean removePrincipal(String principal, boolean service) throws KerberosOperationException; /** @@ -313,7 +311,7 @@ public abstract class KerberosOperationHandler { if (credential == null) { throw new KerberosOperationException("Missing KDC administrator credential"); } else { - return principalExists(credential.getPrincipal()); + return principalExists(credential.getPrincipal(), false); } } @@ -568,11 +566,11 @@ public abstract class KerberosOperationHandler { * @param keyEncryptionTypes a Set of EncryptionKey values or null to indicate the default set */ public void setKeyEncryptionTypes(Set<EncryptionType> keyEncryptionTypes) { - this.keyEncryptionTypes = new HashSet<>( - (keyEncryptionTypes == null) - ? DEFAULT_CIPHERS - : keyEncryptionTypes - ); + this.keyEncryptionTypes = Collections.unmodifiableSet(new HashSet<>( + (keyEncryptionTypes == null) + ? DEFAULT_CIPHERS + : keyEncryptionTypes + )); } @@ -713,8 +711,8 @@ public abstract class KerberosOperationHandler { * <p/> * See {@link org.apache.ambari.server.utils.ShellCommandUtil#runCommand(String[], Map<String,String>)} * - * @param command an array of String value representing the command and its arguments - * @param envp a map of string, string of environment variables + * @param command an array of String value representing the command and its arguments + * @param envp a map of string, string of environment variables * @param interactiveHandler a handler to provide responses to queries from the command, * or null if no queries are expected * @return a ShellCommandUtil.Result declaring the result of the operation @@ -751,7 +749,7 @@ public abstract class KerberosOperationHandler { * @see #executeCommand(String[], Map, ShellCommandUtil.InteractiveHandler) */ protected ShellCommandUtil.Result executeCommand(String[] command) - throws KerberosOperationException { + throws KerberosOperationException { return executeCommand(command, null); } @@ -760,7 +758,7 @@ public abstract class KerberosOperationHandler { * <p/> * See {@link org.apache.ambari.server.utils.ShellCommandUtil#runCommand(String[])} * - * @param command an array of String value representing the command and its arguments + * @param command an array of String value representing the command and its arguments * @param interactiveHandler a handler to provide responses to queries from the command, * or null if no queries are expected * @return a ShellCommandUtil.Result declaring the result of the operation
