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.Collection;
  import java.util.HashMap;
  import java.util.HashSet;
  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.actionmanager.HostRoleStatus;
  import org.apache.ambari.server.agent.CommandReport;
  import org.apache.ambari.server.controller.KerberosHelper;
  import org.apache.ambari.server.controller.RootComponent;
  import org.apache.ambari.server.controller.RootService;
  import org.apache.ambari.server.controller.UpdateConfigurationPolicy;
  import org.apache.ambari.server.state.Cluster;
  import org.apache.ambari.server.state.ServiceComponentHost;
  import org.apache.ambari.server.state.kerberos.KerberosComponentDescriptor;
  import org.apache.ambari.server.state.kerberos.KerberosDescriptor;
  import org.apache.ambari.server.state.kerberos.KerberosIdentityDescriptor;
  import org.apache.ambari.server.state.kerberos.KerberosServiceDescriptor;
  import org.apache.commons.collections4.CollectionUtils;
  import org.apache.commons.lang.StringUtils;
  import org.slf4j.Logger;
  import org.slf4j.LoggerFactory;
  
  /**
   * PrepareKerberosIdentitiesServerAction is a ServerAction implementation that prepares metadata needed
   * to process Kerberos identities (principals and keytabs files).
   */
  public class PrepareKerberosIdentitiesServerAction extends AbstractPrepareKerberosServerAction {
    private final static Logger LOG = LoggerFactory.getLogger(PrepareKerberosIdentitiesServerAction.class);
  
    /**
     * Called to execute this action.  Upon invocation, calls
     * {@link KerberosServerAction#processIdentities(Map)}
     * to iterate through the Kerberos identity metadata and call
     * {@link PrepareKerberosIdentitiesServerAction#processIdentities(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 {
  
      Cluster cluster = getCluster();
  
      if (cluster == null) {
        throw new AmbariException("Missing cluster object");
      }
  
      KerberosHelper kerberosHelper = getKerberosHelper();
  
      KerberosDescriptor kerberosDescriptor = getKerberosDescriptor(cluster, false);
      Map<String, String> commandParameters = getCommandParameters();
      OperationType operationType = getOperationType(getCommandParameters());
  
      Map<String, ? extends Collection<String>> serviceComponentFilter = getServiceComponentFilter();
      Collection<String> hostFilter = getHostFilter();
      Collection<String> identityFilter = getIdentityFilter();
      // If the operationType is default, use the getServiceComponentHostsToProcess method to determine
      // which ServiceComponentHosts to process based on the filters.  However if we are regenerating
      // keytabs for a specific set of components, build the identity filter below so we can
      // customized what needs to be done.
      List<ServiceComponentHost> schToProcess = kerberosHelper.getServiceComponentHostsToProcess(cluster, kerberosDescriptor,
          (operationType == OperationType.DEFAULT) ? serviceComponentFilter : null, hostFilter);
  
      String dataDirectory = getCommandParameterValue(commandParameters, DATA_DIRECTORY);
      Map<String, Map<String, String>> kerberosConfigurations = new HashMap<>();
  
      int schCount = schToProcess.size();
      if (schCount == 0) {
        actionLog.writeStdOut("There are no components to process");
      } else if (schCount == 1) {
        actionLog.writeStdOut(String.format("Processing %d component", schCount));
      } else {
        actionLog.writeStdOut(String.format("Processing %d components", schCount));
      }
  
      Set<String> services = cluster.getServices().keySet();
      Map<String, Set<String>> propertiesToRemove = new HashMap<>();
      Map<String, Set<String>> propertiesToIgnore = new HashMap<>();
      boolean includeAmbariIdentity = "true".equalsIgnoreCase(getCommandParameterValue(commandParameters, KerberosServerAction.INCLUDE_AMBARI_IDENTITY));
  
      // If we are including the Ambari identity; then ensure that if a host filter is set, do not the Ambari service identity.
      includeAmbariIdentity &= (hostFilter == null);
  
      if (serviceComponentFilter != null) {
        // If we are including the Ambari identity; then ensure that if a service/component filter is set,
        // it contains the AMBARI/AMBARI_SERVER component; else do not include the Ambari service identity.
        includeAmbariIdentity &= (serviceComponentFilter.get(RootService.AMBARI.name()) != null)
            && serviceComponentFilter.get(RootService.AMBARI.name()).contains(RootComponent.AMBARI_SERVER.name());
  
        if ((operationType != OperationType.DEFAULT)) {
          // Update the identity filter, if necessary
          identityFilter = updateIdentityFilter(kerberosDescriptor, identityFilter, serviceComponentFilter);
        }
      }
  
      // Calculate the current host-specific configurations. These will be used to replace
      // variables within the Kerberos descriptor data
      Map<String, Map<String, String>> configurations = kerberosHelper.calculateConfigurations(cluster, null, kerberosDescriptor, false, false, null);
  
      processServiceComponentHosts(cluster, kerberosDescriptor, schToProcess, identityFilter, dataDirectory,
          configurations, kerberosConfigurations, includeAmbariIdentity, propertiesToIgnore);
  
      UpdateConfigurationPolicy updateConfigurationPolicy = getUpdateConfigurationPolicy(commandParameters);
  
      if (updateConfigurationPolicy != UpdateConfigurationPolicy.NONE) {
        if (updateConfigurationPolicy.invokeStackAdvisor()) {
          kerberosHelper.applyStackAdvisorUpdates(cluster, services, configurations, kerberosConfigurations,
              propertiesToIgnore, propertiesToRemove, true);
        }
  
        // TODO: Determine if we need to do this again since it is done a few lines above.
        Map<String, Map<String, String>> calculatedConfigurations = kerberosHelper.calculateConfigurations(cluster, null, kerberosDescriptor, false, false, null);
  
        if (updateConfigurationPolicy.applyIdentityChanges()) {
          processAuthToLocalRules(cluster, calculatedConfigurations, kerberosDescriptor, schToProcess, kerberosConfigurations, getDefaultRealm(commandParameters), false);
        }
  
        processConfigurationChanges(dataDirectory, kerberosConfigurations, propertiesToRemove, kerberosDescriptor, updateConfigurationPolicy);
      }
  
      return createCommandReport(0, HostRoleStatus.COMPLETED, "{}", actionLog.getStdOut(), actionLog.getStdErr());
    }
  
    /**
     * Calls {@link KerberosHelper#getKerberosDescriptor(Cluster, boolean)}
     *
     * @param cluster                 cluster instance
     * @param includePreconfigureData <code>true</code> to include the preconfigure data; <code>false</code> otherwise
     * @return the kerberos descriptor associated with the specified cluster
     * @throws AmbariException if unable to obtain the descriptor
     * @see KerberosHelper#getKerberosDescriptor(Cluster, boolean)
     */
    protected KerberosDescriptor getKerberosDescriptor(Cluster cluster, boolean includePreconfigureData)
        throws AmbariException {
      return getKerberosHelper().getKerberosDescriptor(cluster, includePreconfigureData);
    }
  
    /**
     * Conditionally calls {@link KerberosHelper#setAuthToLocalRules(Cluster, KerberosDescriptor, String, Map, Map, Map, boolean)}
     * if there are ServiceComponentHosts to process
     *
     * @param cluster                  the cluster
     * @param calculatedConfiguration  the configurations for the current cluster, used for replacements
     * @param kerberosDescriptor       the current Kerberos descriptor
     * @param schToProcess             a list of ServiceComponentHosts to process
     * @param kerberosConfigurations   the Kerberos-specific configuration map
     * @param defaultRealm             the default realm
     * @param includePreconfiguredData true to include services flagged to be pre-configured; false otherwise
     * @throws AmbariException
     * @see KerberosHelper#setAuthToLocalRules(Cluster, KerberosDescriptor, String, Map, Map, Map, boolean)
     */
    void processAuthToLocalRules(Cluster cluster, Map<String, Map<String, String>> calculatedConfiguration,
                                 KerberosDescriptor kerberosDescriptor,
                                 List<ServiceComponentHost> schToProcess,
                                 Map<String, Map<String, String>> kerberosConfigurations,
                                 String defaultRealm, boolean includePreconfiguredData)
        throws AmbariException {
      if (!schToProcess.isEmpty()) {
        actionLog.writeStdOut("Creating auth-to-local rules");
  
        Map<String, Set<String>> services = new HashMap<>();
        for (ServiceComponentHost sch : schToProcess) {
          Set<String> components = services.get(sch.getServiceName());
          if (components == null) {
            components = new HashSet<>();
            services.put(sch.getServiceName(), components);
          }
  
          components.add(sch.getServiceComponentName());
        }
  
        KerberosHelper kerberosHelper = getKerberosHelper();
        kerberosHelper.setAuthToLocalRules(cluster, kerberosDescriptor, defaultRealm, services,
            calculatedConfiguration, kerberosConfigurations, includePreconfiguredData);
      }
    }
  
    /**
     * Iterate through the identities in the Kerberos descriptor to find the relevant identities to
     * add to the identity filter.
     * <p>
     * The set of identities to include in the filter are determined by whether they are explicit
     * identities set in a component or service in the supplied service/component filter.
     *
     * @param kerberosDescriptor     the Kerberos descriptor
     * @param identityFilter         the existing identity filter
     * @param serviceComponentFilter the service/component filter
     * @return a new collection of paths (including any existing paths) to act as the updated identity filter
     */
    private Collection<String> updateIdentityFilter(KerberosDescriptor kerberosDescriptor,
                                                    Collection<String> identityFilter,
                                                    Map<String, ? extends Collection<String>> serviceComponentFilter) {
  
      Set<String> updatedFilter = (identityFilter == null) ? new HashSet<>() : new HashSet<>(identityFilter);
  
      Map<String, KerberosServiceDescriptor> serviceDescriptors = kerberosDescriptor.getServices();
  
      if (serviceDescriptors != null) {
        for (KerberosServiceDescriptor serviceDescriptor : serviceDescriptors.values()) {
          String serviceName = serviceDescriptor.getName();
  
          if (serviceComponentFilter.containsKey("*") || serviceComponentFilter.containsKey(serviceName)) {
            Collection<String> componentFilter = serviceComponentFilter.get(serviceName);
            boolean anyComponent = ((componentFilter == null) || componentFilter.contains("*"));
  
            // Only include the service-wide identities if the component filter is null contains "*", which indicates
            // that all component for the given service are to be processed.
            if (anyComponent) {
              addIdentitiesToFilter(serviceDescriptor.getIdentities(), updatedFilter, true);
            }
  
            Map<String, KerberosComponentDescriptor> componentDescriptors = serviceDescriptor.getComponents();
            if (componentDescriptors != null) {
              for (KerberosComponentDescriptor componentDescriptor : componentDescriptors.values()) {
                String componentName = componentDescriptor.getName();
                if (anyComponent || (componentFilter.contains(componentName))) {
                  addIdentitiesToFilter(componentDescriptor.getIdentities(), updatedFilter, true);
                }
              }
            }
          }
        }
      }
  
      return updatedFilter;
    }
  
    /**
     * Add the path of each identity in the collection of identities to the supplied identity filter
     * if that identity is not a reference to another identity or if references are allowed.
     *
     * @param identityDescriptors the collection of identity descriptors to process
     * @param identityFilter      the identity filter to modify
     * @param skipReferences
     */
    private void addIdentitiesToFilter(List<KerberosIdentityDescriptor> identityDescriptors,
                                       Collection<String> identityFilter, boolean skipReferences) {
      if (!CollectionUtils.isEmpty(identityDescriptors)) {
        for (KerberosIdentityDescriptor identityDescriptor : identityDescriptors) {
          if (!skipReferences || !identityDescriptor.isReference()) {
            String identityPath = identityDescriptor.getPath();
  
            if (!StringUtils.isEmpty(identityPath)) {
              identityFilter.add(identityPath);
  
              // Find and add the references TO this identity to ensure the new/updated keytab file is
              // sent to the appropriate host(s)
              addIdentitiesToFilter(identityDescriptor.findReferences(), identityFilter, false);
            }
          }
        }
      }
    }
  }
  
  