content: /*
   * 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.util.Arrays;
  import java.util.Map;
  import java.util.Set;
  
  import org.apache.ambari.server.security.credential.PrincipalKeyCredential;
  import org.apache.ambari.server.utils.ShellCommandUtil;
  import org.apache.commons.collections4.CollectionUtils;
  import org.apache.commons.lang.StringUtils;
  import org.apache.directory.shared.kerberos.codec.types.EncryptionType;
  import org.slf4j.Logger;
  import org.slf4j.LoggerFactory;
  
  /**
   * 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 KDCKerberosOperationHandler {
    private final static Logger LOG = LoggerFactory.getLogger(IPAKerberosOperationHandler.class);
  
    /**
     * 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;
  
    /**
     * 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;
  
    /**
     * 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 {
  
      if (kerberosConfiguration != null) {
        userPrincipalGroup = kerberosConfiguration.get(KERBEROS_ENV_USER_PRINCIPAL_GROUP);
      }
  
      // Pre-determine the paths to relevant Kerberos executables
      executableIpa = getExecutable("ipa");
      executableIpaGetKeytab = getExecutable("ipa-getkeytab");
  
      super.open(administratorCredentials, realm, kerberosConfiguration);
    }
  
    @Override
    public void close() throws KerberosOperationException {
      userPrincipalGroup = null;
      executableIpa = null;
      executableIpaGetKeytab = null;
  
      super.close();
    }
  
    /**
     * 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
     * @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, boolean service)
        throws KerberosOperationException {
  
      if (!isOpen()) {
        throw new KerberosOperationException("This operation handler has not been opened");
      }
  
      if (!StringUtils.isEmpty(principal)) {
        DeconstructedPrincipal deconstructedPrincipal = createDeconstructPrincipal(principal);
        String principalName = deconstructedPrincipal.getPrincipalName();
  
        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 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 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 (always 0)
     * @throws KerberosOperationException
     */
    @Override
    public Integer createPrincipal(String principal, String password, boolean service)
        throws KerberosOperationException {
  
      if (!isOpen()) {
        throw new KerberosOperationException("This operation handler has not been opened");
      }
  
      if (StringUtils.isEmpty(principal)) {
        throw new KerberosOperationException("Failed to create new principal - no principal specified");
      }
  
      DeconstructedPrincipal deconstructedPrincipal = createDeconstructPrincipal(principal);
      String normalizedPrincipal = deconstructedPrincipal.getNormalizedPrincipal();
  
      String[] ipaCommand;
      if (service) {
        ipaCommand = new String[]{
            "service-add",
            normalizedPrincipal
        };
      } else {
        String principalName = deconstructedPrincipal.getPrincipalName();
  
        if (!principalName.equals(principalName.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);
        }
  
        ipaCommand = new String[]{
            "user-add",
            deconstructedPrincipal.getPrimary(),
            "--principal",
            principalName,
            "--first",
            deconstructedPrincipal.getPrimary(),
            "--last",
            deconstructedPrincipal.getPrimary(),
            "--cn",
            deconstructedPrincipal.getPrimary()
        };
      }
  
      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);
  
        String stdErr = result.getStderr();
  
        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 {
          throw new KerberosOperationException(String.format("Failed to create principal for %s\nSTDOUT: %s\nSTDERR: %s",
              normalizedPrincipal, result.getStdout(), result.getStderr()));
        }
      }
  
      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());
        }
      }
  
      // Always return 0 since we do not have a key to get a key number for.
      return 0;
    }
  
    /**
     * 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
     * @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
     * @throws KerberosRealmException               if the realm does not map to a KDC
     * @throws KerberosOperationException           if an unexpected error occurred
     */
    @Override
    public boolean removePrincipal(String principal, boolean service) throws KerberosOperationException {
      if (!isOpen()) {
        throw new KerberosOperationException("This operation handler has not been opened");
      }
  
      if (StringUtils.isEmpty(principal)) {
        throw new KerberosOperationException("Failed to remove principal - no principal specified");
      }
  
      DeconstructedPrincipal deconstructedPrincipal = createDeconstructPrincipal(principal);
  
      String[] ipaCommand = (service)
          ? new String[]{"service-del", deconstructedPrincipal.getNormalizedPrincipal()}
          : new String[]{"user-del", deconstructedPrincipal.getPrincipalName()};
  
      return invokeIpa(ipaCommand).isSuccessful();
    }
  
    @Override
    protected String[] getKinitCommand(String executableKinit, PrincipalKeyCredential credentials, String credentialsCache, Map<String, String> kerberosConfiguration) throws KerberosOperationException {
      String [] command = new String[]{
          executableKinit,
          "-c",
          credentialsCache,
          credentials.getPrincipal()
      };
      if (Arrays.asList(command).contains(null)){
        throw new KerberosOperationException("Got a null value, can not create 'kinit' command");
      }
      return command;
    }
  
    @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(',');
          }
          encryptionTypeSpecBuilder.append(encryptionType.getName());
        }
  
        encryptionTypeSpec = encryptionTypeSpecBuilder.toString();
      }
  
      String[] createKeytabFileCommand = (StringUtils.isEmpty(encryptionTypeSpec))
          ? new String[]{executableIpaGetKeytab, "-s", getAdminServerHost(true), "-p", principal, "-k", keytabFileDestinationPath}
          : new String[]{executableIpaGetKeytab, "-s", getAdminServerHost(true), "-e", encryptionTypeSpec, "-p", principal, "-k", keytabFileDestinationPath};
  
      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 ipa command
     * @return a ShellCommandUtil.Result containing the result of the operation
     * @throws KerberosOperationException if an unexpected error occurred
     */
    private ShellCommandUtil.Result invokeIpa(String[] query) throws KerberosOperationException {
  
      if ((query == null) || (query.length == 0)) {
        throw new KerberosOperationException("Missing ipa query");
      }
  
      if (StringUtils.isEmpty(executableIpa)) {
        throw new KerberosOperationException("No path for ipa is available - this KerberosOperationHandler may not have been opened.");
      }
  
      String[] command = new String[query.length + 1];
      command[0] = executableIpa;
      System.arraycopy(query, 0, command, 1, query.length);
  
      ShellCommandUtil.Result result = executeCommand(command);
  
      if (result.isSuccessful()) {
        if (LOG.isDebugEnabled()) {
          LOG.debug("Executed the following command:\n{}\nSTDOUT: {}\nSTDERR: {}",
              StringUtils.join(command, " "), result.getStdout(), result.getStderr());
        }
      } else {
        LOG.error("Failed to execute the following command:\n{}\nSTDOUT: {}\nSTDERR: {}",
            StringUtils.join(command, " "), result.getStdout(), result.getStderr());
      }
  
      return result;
    }
  }
  