/*
 * 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 com.google.common.base.Optional;
import com.google.inject.Inject;
import org.apache.ambari.server.AmbariException;
import org.apache.ambari.server.actionmanager.HostRoleStatus;
import org.apache.ambari.server.agent.CommandReport;
import org.apache.ambari.server.audit.event.kerberos.CreatePrincipalKerberosAuditEvent;
import org.apache.ambari.server.orm.dao.KerberosPrincipalDAO;
import org.apache.ambari.server.orm.dao.KerberosPrincipalHostDAO;
import org.apache.ambari.server.orm.entities.KerberosPrincipalEntity;
import org.apache.ambari.server.security.SecurePasswordHelper;
import org.apache.ambari.server.serveraction.ActionLog;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentMap;

/**
 * CreatePrincipalsServerAction is a ServerAction implementation that creates principals as instructed.
 * <p/>
 * This class mainly relies on the KerberosServerAction to iterate through metadata identifying
 * the Kerberos principals that need to be created. For each identity in the metadata, this implementation's
 * {@link KerberosServerAction#processIdentity(Map, String, KerberosOperationHandler, Map, Map)}
 * is invoked attempting the creation of the relevant principal.
 */
public class CreatePrincipalsServerAction extends KerberosServerAction {
  private final static Logger LOG = LoggerFactory.getLogger(CreatePrincipalsServerAction.class);

  /**
   * KerberosPrincipalDAO used to set and get Kerberos principal details
   */
  @Inject
  private KerberosPrincipalDAO kerberosPrincipalDAO;

  /**
   * KerberosPrincipalHostDAO used to get Kerberos principal details
   */
  @Inject
  private KerberosPrincipalHostDAO kerberosPrincipalHostDAO;

  /**
   * SecurePasswordHelper used to generate secure passwords for newly created principals
   */
  @Inject
  private SecurePasswordHelper securePasswordHelper;

  /**
   * A set of visited principal names used to prevent unnecessary processing on already processed
   * principal names
   */
  private Set<String> seenPrincipals = new HashSet<String>();

  /**
   * Called to execute this action.  Upon invocation, calls
   * {@link org.apache.ambari.server.serveraction.kerberos.KerberosServerAction#processIdentities(java.util.Map)}
   * to iterate through the Kerberos identity metadata and call
   * {@link org.apache.ambari.server.serveraction.kerberos.CreatePrincipalsServerAction#processIdentities(java.util.Map)}
   * for each identity to process.
   *
   * @param requestSharedDataContext a Map to be used a shared data among all ServerActions related
   *                                 to a given request
   * @return a CommandReport indicating the result of this action
   * @throws AmbariException
   * @throws InterruptedException
   */
  @Override
  public CommandReport execute(ConcurrentMap<String, Object> requestSharedDataContext) throws
      AmbariException, InterruptedException {
    return processIdentities(requestSharedDataContext);
  }


  /**
   * For each identity, generate a unique password create a new or update an existing principal in
   * an assume to be configured KDC.
   * <p/>
   * If a password has not been previously created the current evaluatedPrincipal, create a "secure"
   * password using {@link SecurePasswordHelper#createSecurePassword()}.  Then if the principal
   * does not exist in the KDC, create it using the generated password; else if it does exist update
   * its password.  Finally store the generated password in the shared principal-to-password map and
   * store the new key numbers in the shared principal-to-key_number map so that subsequent process
   * may use the data if necessary.
   *
   * @param identityRecord           a Map containing the data for the current identity record
   * @param evaluatedPrincipal       a String indicating the relevant principal
   * @param operationHandler         a KerberosOperationHandler used to perform Kerberos-related
   *                                 tasks for specific Kerberos implementations
   *                                 (MIT, Active Directory, etc...)
   * @param kerberosConfiguration    a Map of configuration properties from kerberos-env
   * @param requestSharedDataContext a Map to be used a shared data among all ServerActions related
   *                                 to a given request  @return a CommandReport, indicating an error
   *                                 condition; or null, indicating a success condition
   * @throws AmbariException if an error occurs while processing the identity record
   */
  @Override
  protected CommandReport processIdentity(Map<String, String> identityRecord, String evaluatedPrincipal,
                                          KerberosOperationHandler operationHandler,
                                          Map<String, String> kerberosConfiguration,
                                          Map<String, Object> requestSharedDataContext)
      throws AmbariException {
    CommandReport commandReport = null;

    //  Only process this principal name if we haven't already processed it
    if (!seenPrincipals.contains(evaluatedPrincipal)) {
      seenPrincipals.add(evaluatedPrincipal);

      boolean processPrincipal;
      boolean regenerateKeytabs = "true".equalsIgnoreCase(getCommandParameterValue(getCommandParameters(), REGENERATE_ALL));

      if (regenerateKeytabs) {
        processPrincipal = true;
      } else {
        KerberosPrincipalEntity kerberosPrincipalEntity = kerberosPrincipalDAO.find(evaluatedPrincipal);

        if (kerberosPrincipalEntity == null) {
          // This principal has not been processed before, process it.
          processPrincipal = true;
        } else if (!StringUtils.isEmpty(kerberosPrincipalEntity.getCachedKeytabPath())) {
          // This principal has been processed and a keytab file has been cached for it... do not process it.
          processPrincipal = false;
        } else if (kerberosPrincipalHostDAO.exists(evaluatedPrincipal)) {
          // This principal has been processed and a keytab file has been distributed... do not process it.
          processPrincipal = false;
        } else {
          // This principal has been processed but a keytab file for it has been distributed... process it.
          processPrincipal = true;
        }
      }

      if (processPrincipal) {
        Map<String, String> principalPasswordMap = getPrincipalPasswordMap(requestSharedDataContext);

        String password = principalPasswordMap.get(evaluatedPrincipal);

        if (password == null) {
          boolean servicePrincipal = "service".equalsIgnoreCase(identityRecord.get(KerberosIdentityDataFileReader.PRINCIPAL_TYPE));
          CreatePrincipalResult result = createPrincipal(evaluatedPrincipal, servicePrincipal, kerberosConfiguration, operationHandler, regenerateKeytabs, actionLog);

          if (result == null) {
            commandReport = createCommandReport(1, HostRoleStatus.FAILED, "{}", actionLog.getStdOut(), actionLog.getStdErr());
          } else {
            Map<String, Integer> principalKeyNumberMap = getPrincipalKeyNumberMap(requestSharedDataContext);

            principalPasswordMap.put(evaluatedPrincipal, result.getPassword());
            principalKeyNumberMap.put(evaluatedPrincipal, result.getKeyNumber());
          }
        }
      }
    }

    return commandReport;
  }

  /**
   * Creates a principal in the relevant KDC
   *
   * @param principal                the principal name to create
   * @param isServicePrincipal       true if the principal is a service principal; false if the
   *                                 principal is a user principal
   * @param kerberosConfiguration    the kerberos-env configuration properties
   * @param kerberosOperationHandler the KerberosOperationHandler for the relevant KDC
   * @param regenerateKeytabs        true if this was triggered in response to regenerating keytab files; false otherwise
   * @param actionLog                the logger (may be null if no logging is desired)  @return a CreatePrincipalResult containing the generated password and key number value
   */
  public CreatePrincipalResult createPrincipal(String principal, boolean isServicePrincipal,
                                               Map<String, String> kerberosConfiguration,
                                               KerberosOperationHandler kerberosOperationHandler,
                                               boolean regenerateKeytabs, ActionLog actionLog) {

    // in case this is called directly from TopologyManager there's no HostRoleCommand
    CreatePrincipalKerberosAuditEvent.CreatePrincipalKerberosAuditEventBuilder auditEventBuilder = CreatePrincipalKerberosAuditEvent.builder()
        .withTimestamp(System.currentTimeMillis())
        .withRequestId(getHostRoleCommand() != null ? getHostRoleCommand().getRequestId() : -1)
        .withTaskId(getHostRoleCommand() != null ? getHostRoleCommand().getTaskId() : -1)
        .withPrincipal(principal);
    CreatePrincipalResult result = null;
    String message = null;

    try {
      message = String.format("Processing principal, %s", principal);
      LOG.info(message);
      if (actionLog != null) {
        actionLog.writeStdOut(message);
      }

      Integer length;
      Integer minLowercaseLetters;
      Integer minUppercaseLetters;
      Integer minDigits;
      Integer minPunctuation;
      Integer minWhitespace;

      if (kerberosConfiguration == null) {
        length = null;
        minLowercaseLetters = null;
        minUppercaseLetters = null;
        minDigits = null;
        minPunctuation = null;
        minWhitespace = null;
      } else {
        length = toInt(kerberosConfiguration.get("password_length"));
        minLowercaseLetters = toInt(kerberosConfiguration.get("password_min_lowercase_letters"));
        minUppercaseLetters = toInt(kerberosConfiguration.get("password_min_uppercase_letters"));
        minDigits = toInt(kerberosConfiguration.get("password_min_digits"));
        minPunctuation = toInt(kerberosConfiguration.get("password_min_punctuation"));
        minWhitespace = toInt(kerberosConfiguration.get("password_min_whitespace"));
      }

      String password = securePasswordHelper.createSecurePassword(length, minLowercaseLetters, minUppercaseLetters, minDigits, minPunctuation, minWhitespace);

      try {
        /**
         * true indicates a new principal was created, false indicates an existing principal was updated
         */
        boolean created;
        Integer keyNumber;

        if (regenerateKeytabs) {
          try {
            keyNumber = kerberosOperationHandler.setPrincipalPassword(principal, password);
            created = false;
          } catch (KerberosPrincipalDoesNotExistException e) {
            message = String.format("Principal, %s, does not exist, creating new principal", principal);
            LOG.warn(message);
            if (actionLog != null) {
              actionLog.writeStdOut(message);
            }

            keyNumber = kerberosOperationHandler.createPrincipal(principal, password, isServicePrincipal);
            created = true;
          }
        } else {
          try {
            keyNumber = kerberosOperationHandler.createPrincipal(principal, password, isServicePrincipal);
            created = true;
          } catch (KerberosPrincipalAlreadyExistsException e) {
            message = String.format("Principal, %s, already exists, setting new password", principal);
            LOG.warn(message);
            if (actionLog != null) {
              actionLog.writeStdOut(message);
            }

            keyNumber = kerberosOperationHandler.setPrincipalPassword(principal, password);
            created = false;
          }
        }

        if (keyNumber != null) {
          result = new CreatePrincipalResult(principal, password, keyNumber);

          if (created) {
            message = String.format("Successfully created new principal, %s", principal);
          } else {
            message = String.format("Successfully set password for %s", principal);
          }
          LOG.debug(message);
        } else {
          if (created) {
            message = String.format("Failed to create principal, %s - unknown reason", principal);
          } else {
            message = String.format("Failed to set password for %s - unknown reason", principal);
          }
          LOG.error(message);
          if (actionLog != null) {
            actionLog.writeStdErr(message);
          }
        }

        if (!kerberosPrincipalDAO.exists(principal)) {
          kerberosPrincipalDAO.create(principal, isServicePrincipal);
        }

      } catch (KerberosOperationException e) {
        message = String.format("Failed to create principal, %s - %s", principal, e.getMessage());
        LOG.error(message, e);
        if (actionLog != null) {
          actionLog.writeStdErr(message);
        }
      }
    } finally {
      if (result == null) {
        auditEventBuilder.withReasonOfFailure(message == null ? "Unknown error" : message);
      }
      auditLog(auditEventBuilder.build());
    }

    return result;
  }

  /**
   * Translates a String containing an integer value to an Integer.
   * <p/>
   * If the string is null, empty or not a number, returns null; otherwise returns an Integer value
   * representing the integer value of the string.
   *
   * @param string the string to parse
   * @return an Integer or null
   */
  private static Integer toInt(String string) {
    if ((string == null) || string.isEmpty()) {
      return null;
    } else {
      try {
        return Integer.parseInt(string);
      } catch (NumberFormatException e) {
        return null;
      }
    }
  }

  /**
   * CreatePrincipalResult holds values created as a result of creating a principal in a KDC.
   */
  public static class CreatePrincipalResult {
    final private String principal;
    final private String password;
    final private Integer keyNumber;

    /**
     * Constructor
     *
     * @param principal a principal name
     * @param password  a password
     * @param keyNumber a key number
     */
    public CreatePrincipalResult(String principal, String password, Integer keyNumber) {
      this.principal = principal;
      this.password = password;
      this.keyNumber = keyNumber;
    }

    /**
     * Gets the principal name
     *
     * @return the principal name
     */
    public String getPrincipal() {
      return principal;
    }

    /**
     * Gets the principal's password
     *
     * @return the principal's passwrod
     */
    public String getPassword() {
      return password;
    }

    /**
     * Gets the password's key number
     *
     * @return the password's key number
     */
    public Integer getKeyNumber() {
      return keyNumber;
    }
  }
}
