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.controller;
  
  import static java.util.Collections.singleton;
  import static java.util.stream.Collectors.groupingBy;
  import static java.util.stream.Collectors.mapping;
  import static java.util.stream.Collectors.toSet;
  import static org.apache.ambari.server.controller.KerberosHelperImpl.BASE_LOG_DIR;
  import static org.apache.ambari.server.controller.KerberosHelperImpl.REMOVE_KEYTAB;
  
  import java.io.File;
  import java.lang.reflect.Type;
  import java.util.ArrayList;
  import java.util.Collection;
  import java.util.HashMap;
  import java.util.List;
  import java.util.Map;
  import java.util.Set;
  import java.util.concurrent.ConcurrentMap;
  
  import org.apache.ambari.server.AmbariException;
  import org.apache.ambari.server.Role;
  import org.apache.ambari.server.RoleCommand;
  import org.apache.ambari.server.actionmanager.HostRoleStatus;
  import org.apache.ambari.server.actionmanager.Stage;
  import org.apache.ambari.server.actionmanager.StageFactory;
  import org.apache.ambari.server.agent.CommandReport;
  import org.apache.ambari.server.controller.internal.RequestResourceFilter;
  import org.apache.ambari.server.serveraction.ServerAction;
  import org.apache.ambari.server.serveraction.kerberos.AbstractPrepareKerberosServerAction;
  import org.apache.ambari.server.serveraction.kerberos.Component;
  import org.apache.ambari.server.serveraction.kerberos.DestroyPrincipalsServerAction;
  import org.apache.ambari.server.serveraction.kerberos.KDCType;
  import org.apache.ambari.server.serveraction.kerberos.KerberosOperationHandler;
  import org.apache.ambari.server.serveraction.kerberos.KerberosServerAction;
  import org.apache.ambari.server.serveraction.kerberos.stageutils.ResolvedKerberosPrincipal;
  import org.apache.ambari.server.state.Cluster;
  import org.apache.ambari.server.state.Config;
  import org.apache.ambari.server.state.StackId;
  import org.apache.ambari.server.state.kerberos.KerberosDescriptor;
  import org.apache.ambari.server.state.svccomphost.ServiceComponentHostServerActionEvent;
  import org.apache.ambari.server.utils.StageUtils;
  import org.apache.commons.collections4.CollectionUtils;
  
  import com.google.gson.reflect.TypeToken;
  
  
  /**
   * I delete kerberos identities (principals and keytabs) of a given component.
   */
  class DeleteIdentityHandler {
    public static final String COMPONENT_FILTER = "component_filter";
  
    private final AmbariCustomCommandExecutionHelper customCommandExecutionHelper;
    private final Integer taskTimeout;
    private final StageFactory stageFactory;
    private final AmbariManagementController ambariManagementController;
  
    public DeleteIdentityHandler(AmbariCustomCommandExecutionHelper customCommandExecutionHelper, Integer taskTimeout, StageFactory stageFactory, AmbariManagementController ambariManagementController) {
      this.customCommandExecutionHelper = customCommandExecutionHelper;
      this.taskTimeout = taskTimeout;
      this.stageFactory = stageFactory;
      this.ambariManagementController = ambariManagementController;
    }
  
    /**
     * Creates and adds stages to the given stage container for deleting kerberos identities.
     * The service component that belongs to the identity doesn't need to be installed.
     */
    public void addDeleteIdentityStages(Cluster cluster, OrderedRequestStageContainer stageContainer, CommandParams commandParameters, boolean manageIdentities)
      throws AmbariException
    {
      ServiceComponentHostServerActionEvent event = new ServiceComponentHostServerActionEvent(RootComponent.AMBARI_SERVER.name(), StageUtils.getHostName(), System.currentTimeMillis());
      String hostParamsJson = StageUtils.getGson().toJson(customCommandExecutionHelper.createDefaultHostParams(cluster, cluster.getDesiredStackVersion()));
      if (manageIdentities) {
        addPrepareDeleteIdentity(cluster, hostParamsJson, event, commandParameters, stageContainer);
        addDeleteKeytab(cluster, commandParameters.getAffectedHostNames(), hostParamsJson, commandParameters, stageContainer);
        addDestroyPrincipals(cluster, hostParamsJson, event, commandParameters, stageContainer);
      }
      addFinalize(cluster, hostParamsJson, event, stageContainer, commandParameters);
    }
  
    private void addPrepareDeleteIdentity(Cluster cluster,
                                          String hostParamsJson, ServiceComponentHostServerActionEvent event,
                                          CommandParams commandParameters,
                                          OrderedRequestStageContainer stageContainer)
      throws AmbariException
    {
      Stage stage = createServerActionStage(stageContainer.getLastStageId(),
        cluster,
        stageContainer.getId(),
        "Prepare delete identities",
        "{}",
        hostParamsJson,
        PrepareDeleteIdentityServerAction.class,
        event,
        commandParameters.asMap(),
        "Prepare delete identities",
        taskTimeout);
      stageContainer.addStage(stage);
    }
  
    private void addDestroyPrincipals(Cluster cluster,
                                      String hostParamsJson, ServiceComponentHostServerActionEvent event,
                                      CommandParams commandParameters,
                                      OrderedRequestStageContainer stageContainer)
      throws AmbariException
    {
      Stage stage = createServerActionStage(stageContainer.getLastStageId(),
        cluster,
        stageContainer.getId(),
        "Destroy Principals",
        "{}",
        hostParamsJson,
        DestroyPrincipalsServerAction.class,
        event,
        commandParameters.asMap(),
        "Destroy Principals",
        Math.max(ServerAction.DEFAULT_LONG_RUNNING_TASK_TIMEOUT_SECONDS, taskTimeout));
      stageContainer.addStage(stage);
    }
  
    private void addDeleteKeytab(Cluster cluster,
                                 Set<String> hostFilter,
                                 String hostParamsJson,
                                 CommandParams commandParameters,
                                 OrderedRequestStageContainer stageContainer)
        throws AmbariException {
  
      // Filter out any hosts that have been removed
      Set<String> hostNames = (CollectionUtils.isEmpty(hostFilter))
          ? null
          : hostFilter.stream()
          .filter(hostname -> ambariManagementController.getClusters().hostExists(hostname))
          .collect(toSet());
  
      if(CollectionUtils.isNotEmpty(hostNames)) {
        Stage stage = createNewStage(stageContainer.getLastStageId(),
            cluster,
            stageContainer.getId(),
            "Delete Keytabs",
            commandParameters.asJson(),
            hostParamsJson);
  
        Map<String, String> requestParams = new HashMap<>();
        List<RequestResourceFilter> requestResourceFilters = new ArrayList<>();
        RequestResourceFilter reqResFilter = new RequestResourceFilter("KERBEROS", "KERBEROS_CLIENT", new ArrayList<>(hostNames));
        requestResourceFilters.add(reqResFilter);
  
        ActionExecutionContext actionExecContext = new ActionExecutionContext(
            cluster.getClusterName(),
            REMOVE_KEYTAB,
            requestResourceFilters,
            requestParams);
        customCommandExecutionHelper.addExecutionCommandsToStage(actionExecContext, stage, requestParams, null);
        stageContainer.addStage(stage);
      }
    }
  
    private void addFinalize(Cluster cluster,
                             String hostParamsJson, ServiceComponentHostServerActionEvent event,
                             OrderedRequestStageContainer requestStageContainer,
                             CommandParams commandParameters)
      throws AmbariException
    {
      Stage stage = createServerActionStage(requestStageContainer.getLastStageId(),
        cluster,
        requestStageContainer.getId(),
        "Finalize Operations",
        "{}",
        hostParamsJson,
        DeleteDataDirAction.class,
        event,
        commandParameters.asMap(),
        "Finalize Operations", 300);
      requestStageContainer.addStage(stage);
    }
  
  
    public static class CommandParams {
      private final List<Component> components;
      private final Set<String> identities;
      private final String authName;
      private final File dataDirectory;
      private final String defaultRealm;
      private final KDCType kdcType;
  
      public CommandParams(List<Component> components, Set<String> identities, String authName, File dataDirectory, String defaultRealm, KDCType kdcType) {
        this.components = components;
        this.identities = identities;
        this.authName = authName;
        this.dataDirectory = dataDirectory;
        this.defaultRealm = defaultRealm;
        this.kdcType = kdcType;
      }
  
      public Map<String, String> asMap() {
        Map<String, String> commandParameters = new HashMap<>();
        commandParameters.put(KerberosServerAction.AUTHENTICATED_USER_NAME, authName);
        commandParameters.put(KerberosServerAction.DEFAULT_REALM, defaultRealm);
        commandParameters.put(KerberosServerAction.KDC_TYPE, kdcType.name());
        commandParameters.put(KerberosServerAction.IDENTITY_FILTER, StageUtils.getGson().toJson(identities));
        commandParameters.put(COMPONENT_FILTER, StageUtils.getGson().toJson(components));
        commandParameters.put(KerberosServerAction.SERVICE_COMPONENT_FILTER, StageUtils.getGson().toJson(toServiceComponentFilter(components)));
        commandParameters.put(KerberosServerAction.HOST_FILTER, StageUtils.getGson().toJson(toHostFilter(components)));
        commandParameters.put(KerberosServerAction.DATA_DIRECTORY, dataDirectory.getAbsolutePath());
        return commandParameters;
      }
  
      public Set<String> getAffectedHostNames() {
        return components.stream().map(Component::getHostName).collect(toSet());
      }
  
      public String asJson() {
        return StageUtils.getGson().toJson(asMap());
      }
  
      /**
       * Convert a collection of {@link Component}s to a service/component filter.
       *
       * @param components the collection of relevant {@link Component}s
       * @return a map of service names to component names to include in an operation
       */
      private Map<String, ? extends Collection<String>> toServiceComponentFilter(List<Component> components) {
        if (components == null) {
          return null;
        }
  
        return components.stream().collect(groupingBy(Component::getServiceName, mapping(Component::getServiceComponentName, toSet())));
      }
  
      /**
       * Convert a collection of {@link Component}s to a host filter.
       *
       * @param components the collection of relevant {@link Component}s
       * @return a set of hostnames of hosts to include in an operation
       */
      private Set<String> toHostFilter(List<Component> components) {
        if (components == null) {
          return null;
        }
  
        return components.stream().map(Component::getHostName).collect(toSet());
      }
    }
  
    public static class PrepareDeleteIdentityServerAction extends AbstractPrepareKerberosServerAction {
      @Override
      public CommandReport execute(ConcurrentMap<String, Object> requestSharedDataContext) throws AmbariException, InterruptedException {
        KerberosDescriptor kerberosDescriptor = getKerberosDescriptor();
        processServiceComponents(
          getCluster(),
          kerberosDescriptor,
          componentFilter(),
          getIdentityFilter(),
          dataDirectory(),
          calculateConfig(kerberosDescriptor, serviceNames()),
          new HashMap<>(),
          false,
          new HashMap<>());
        return createCommandReport(0, HostRoleStatus.COMPLETED, "{}", actionLog.getStdOut(), actionLog.getStdErr());
      }
  
      private Set<String> serviceNames() {
        return componentFilter().stream().map(Component::getServiceName).collect(toSet());
      }
  
      private List<Component> componentFilter() {
        Type jsonType = new TypeToken<List<Component>>() {}.getType();
        return StageUtils.getGson().fromJson(getCommandParameterValue(COMPONENT_FILTER), jsonType);
      }
  
      /**
       * Cleaning identities is asynchronous, it can happen that the service and its configuration is already deleted at this point.
       * We're extending the actual config with the properties of the latest deleted configuration of the service.
       * The service configuration is needed because principal names may contain placeholder variables which are replaced based on the service configuration.
       */
      private Map<String, Map<String, String>> calculateConfig(KerberosDescriptor kerberosDescriptor, Set<String> serviceNames) throws AmbariException {
        Map<String, Map<String, String>> actualConfig = getKerberosHelper().calculateConfigurations(getCluster(), null, kerberosDescriptor, false, false, null);
        extendWithDeletedConfigOfService(actualConfig, serviceNames);
        return actualConfig;
      }
  
      private void extendWithDeletedConfigOfService(Map<String, Map<String, String>> configToBeExtended, Set<String> serviceNames) throws AmbariException {
        Set<String> deletedConfigTypes = serviceNames.stream()
          .flatMap(serviceName -> configTypesOfService(serviceName).stream())
          .collect(toSet());
        for (Config deletedConfig : getCluster().getLatestConfigsWithTypes(deletedConfigTypes)) {
          configToBeExtended.put(deletedConfig.getType(), deletedConfig.getProperties());
        }
      }
  
      private Set<String> configTypesOfService(String serviceName) {
        try {
          StackId stackId = getCluster().getCurrentStackVersion();
          StackServiceRequest stackServiceRequest = new StackServiceRequest(stackId.getStackName(), stackId.getStackVersion(), serviceName);
          return AmbariServer.getController().getStackServices(singleton(stackServiceRequest)).stream()
            .findFirst()
            .orElseThrow(() -> new IllegalArgumentException("Could not find stack service " + serviceName))
            .getConfigTypes()
            .keySet();
        } catch (AmbariException e) {
          throw new RuntimeException(e);
        }
      }
  
      private String dataDirectory() {
        return getCommandParameterValue(getCommandParameters(), DATA_DIRECTORY);
      }
  
      private KerberosDescriptor getKerberosDescriptor() throws AmbariException {
        return getKerberosHelper().getKerberosDescriptor(getCluster(), false);
      }
  
      @Override
      protected boolean pruneServiceFilter() {
        // Do not prune off services that have been previously removed.
        return false;
      }
    }
  
    private Stage createNewStage(long id, Cluster cluster, long requestId, String requestContext, String commandParams, String hostParams) {
      Stage stage = stageFactory.createNew(requestId,
        BASE_LOG_DIR + File.pathSeparator + requestId,
        cluster.getClusterName(),
        cluster.getClusterId(),
        requestContext,
        commandParams,
        hostParams);
      stage.setStageId(id);
      return stage;
    }
  
    private Stage createServerActionStage(long id, Cluster cluster, long requestId,
                                         String requestContext,
                                         String commandParams, String hostParams,
                                         Class<? extends ServerAction> actionClass,
                                         ServiceComponentHostServerActionEvent event,
                                         Map<String, String> commandParameters, String commandDetail,
                                         Integer timeout) throws AmbariException {
  
      Stage stage = createNewStage(id, cluster, requestId, requestContext,  commandParams, hostParams);
      stage.addServerActionCommand(actionClass.getName(), null, Role.AMBARI_SERVER_ACTION,
        RoleCommand.EXECUTE, cluster.getClusterName(), event, commandParameters, commandDetail,
        ambariManagementController.findConfigurationTagsWithOverrides(cluster, null, null), timeout,
        false, false);
  
      return stage;
    }
  
    private static class DeleteDataDirAction extends KerberosServerAction {
  
      @Override
      public CommandReport execute(ConcurrentMap<String, Object> requestSharedDataContext) throws AmbariException, InterruptedException {
        deleteDataDirectory(getCommandParameterValue(DATA_DIRECTORY));
        return createCommandReport(0, HostRoleStatus.COMPLETED, "{}", actionLog.getStdOut(), actionLog.getStdErr());
      }
  
      @Override
      protected boolean pruneServiceFilter() {
        // Do not prune off services that have been previously removed.
        return false;
      }
  
      @Override
      protected CommandReport processIdentity(ResolvedKerberosPrincipal resolvedPrincipal, KerberosOperationHandler operationHandler, Map<String, String> kerberosConfiguration, boolean includedInFilter, Map<String, Object> requestSharedDataContext) throws AmbariException {
        return null;
      }
    }
  }
  