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.io.File;
  import java.util.ArrayList;
  import java.util.Collection;
  import java.util.HashSet;
  import java.util.Iterator;
  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.audit.event.kerberos.DestroyPrincipalKerberosAuditEvent;
  import org.apache.ambari.server.controller.KerberosHelper;
  import org.apache.ambari.server.orm.dao.KerberosKeytabDAO;
  import org.apache.ambari.server.orm.dao.KerberosKeytabPrincipalDAO;
  import org.apache.ambari.server.orm.dao.KerberosPrincipalDAO;
  import org.apache.ambari.server.orm.entities.KerberosKeytabEntity;
  import org.apache.ambari.server.orm.entities.KerberosKeytabPrincipalEntity;
  import org.apache.ambari.server.orm.entities.KerberosKeytabServiceMappingEntity;
  import org.apache.ambari.server.orm.entities.KerberosPrincipalEntity;
  import org.apache.ambari.server.security.credential.PrincipalKeyCredential;
  import org.apache.ambari.server.serveraction.kerberos.stageutils.ResolvedKerberosPrincipal;
  import org.apache.commons.collections4.CollectionUtils;
  import org.apache.commons.collections4.MapUtils;
  import org.slf4j.Logger;
  import org.slf4j.LoggerFactory;
  
  import com.google.inject.Inject;
  
  /**
   * DestroyPrincipalsServerAction is a ServerAction implementation that destroys principals as instructed.
   * <p/>
   * This class mainly relies on the KerberosServerAction to iterate through metadata identifying
   * the Kerberos principals that need to be removed from the relevant KDC. For each identity in the
   * metadata, this implementation's
   * {@link KerberosServerAction#processIdentity(ResolvedKerberosPrincipal, KerberosOperationHandler, Map, boolean, Map)}
   * is invoked attempting the removal of the relevant principal.
   */
  public class DestroyPrincipalsServerAction extends KerberosServerAction {
    private final static Logger LOG = LoggerFactory.getLogger(DestroyPrincipalsServerAction.class);
  
    /**
     * The KerberosOperationHandlerFactory to use to obtain KerberosOperationHandler instances
     * <p/>
     * This is needed to help with test cases to mock a KerberosOperationHandler
     */
    @Inject
    private KerberosOperationHandlerFactory kerberosOperationHandlerFactory;
  
    @Inject
    private KerberosHelper kerberosHelper;
  
    @Inject
    private KerberosPrincipalDAO kerberosPrincipalDAO;
  
    @Inject
    private KerberosKeytabPrincipalDAO kerberosKeytabPrincipalDAO;
  
    @Inject
    private KerberosKeytabDAO kerberosKeytabDAO;
  
    /**
     * A set of visited principal names used to prevent unnecessary processing on already processed
     * principal names
     */
    private Set<String> seenPrincipals = new HashSet<>();
  
    /**
     * Called to execute this action.  Upon invocation, calls
     * {@link KerberosServerAction#processIdentities(java.util.Map)}
     * to iterate through the Kerberos identity metadata and call
     * {@link org.apache.ambari.server.serveraction.kerberos.DestroyPrincipalsServerAction#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 org.apache.ambari.server.AmbariException
     * @throws InterruptedException
     */
    @Override
    public CommandReport execute(ConcurrentMap<String, Object> requestSharedDataContext) throws
        AmbariException, InterruptedException {
  
      Map<String, String> commandParameters = getCommandParameters();
      KDCType kdcType = getKDCType(commandParameters);
      PrincipalKeyCredential administratorCredential = kerberosHelper.getKDCAdministratorCredentials(getClusterName());
      String defaultRealm = getDefaultRealm(commandParameters);
  
      KerberosOperationHandler operationHandler = kerberosOperationHandlerFactory.getKerberosOperationHandler(kdcType);
      Map<String, String> kerberosConfiguration = getConfigurationProperties("kerberos-env");
  
      try {
        operationHandler.open(administratorCredential, defaultRealm, kerberosConfiguration);
      } catch (KerberosOperationException e) {
        String message = String.format("Failed to process the identities, could not properly open the KDC operation handler: %s",
            e.getMessage());
        actionLog.writeStdErr(message);
        LOG.error(message);
        throw new AmbariException(message, e);
      }
  
      actionLog.writeStdOut("Cleaning up Kerberos identities.");
  
      Map<String, ? extends Collection<String>> serviceComponentFilter = getServiceComponentFilter();
      Set<String> hostFilter = getHostFilter();
      Collection<String> principalNameFilter = getIdentityFilter();
  
      List<KerberosKeytabPrincipalEntity> kerberosKeytabPrincipalEntities;
  
      if (MapUtils.isEmpty(serviceComponentFilter) && CollectionUtils.isEmpty(hostFilter) && CollectionUtils.isEmpty(principalNameFilter)) {
        // Clean up all... this is probably a disable Kerberos operation
        kerberosKeytabPrincipalEntities = kerberosKeytabPrincipalDAO.findAll();
      } else {
        // Build the search filters
        ArrayList<KerberosKeytabPrincipalDAO.KerberosKeytabPrincipalFilter> filters = new ArrayList<>();
  
        if (MapUtils.isEmpty(serviceComponentFilter)) {
          filters.add(KerberosKeytabPrincipalDAO.KerberosKeytabPrincipalFilter.createFilter(
              null,
              null,
              hostFilter,
              principalNameFilter));
        } else {
          for (Map.Entry<String, ? extends Collection<String>> entry : serviceComponentFilter.entrySet()) {
            filters.add(KerberosKeytabPrincipalDAO.KerberosKeytabPrincipalFilter.createFilter(
                entry.getKey(),
                entry.getValue(),
                hostFilter,
                principalNameFilter));
          }
        }
  
        // Get only the entries we care about...
        kerberosKeytabPrincipalEntities = kerberosKeytabPrincipalDAO.findByFilters(filters);
      }
  
      if (kerberosKeytabPrincipalEntities != null) {
        try {
          Set<Long> visitedKKPID = new HashSet<>();
  
          for (KerberosKeytabPrincipalEntity kerberosKeytabPrincipalEntity : kerberosKeytabPrincipalEntities) {
            // Do not re-process duplicate entries
            if (!visitedKKPID.contains(kerberosKeytabPrincipalEntity.getKkpId())) {
  
              visitedKKPID.add(kerberosKeytabPrincipalEntity.getKkpId());
  
              KerberosKeytabEntity kerberosKeytabEntity = kerberosKeytabPrincipalEntity.getKerberosKeytabEntity();
              KerberosPrincipalEntity kerberosPrincipalEntity = kerberosKeytabPrincipalEntity.getKerberosPrincipalEntity();
  
              if (serviceComponentFilter == null) {
                // All service and components "match" in this case... thus all mapping records are to be
                // removed.  The KerberosKeytabServiceMappingEntity has already been selected to be removed
                // based on the host and identity filters.
                kerberosKeytabPrincipalEntity.setServiceMapping(null);
              } else {
                // It is possible that this KerberosKeytabPrincipalEntity needs to stick around since other
                // services and components rely on it.  So remove only the relevant service mapping records
                List<KerberosKeytabServiceMappingEntity> serviceMapping = kerberosKeytabPrincipalEntity.getServiceMapping();
  
                if (CollectionUtils.isNotEmpty(serviceMapping)) {
                  // Prune off the relevant service mappings...
                  Iterator<KerberosKeytabServiceMappingEntity> iterator = serviceMapping.iterator();
                  while (iterator.hasNext()) {
                    KerberosKeytabServiceMappingEntity entity = iterator.next();
  
                    if (serviceComponentFilter.containsKey(entity.getServiceName())) {
                      Collection<String> components = serviceComponentFilter.get(entity.getServiceName());
  
                      if ((CollectionUtils.isEmpty(components)) || components.contains(entity.getComponentName())) {
                        iterator.remove();
                      }
                    }
                  }
  
                  kerberosKeytabPrincipalEntity.setServiceMapping(serviceMapping);
                }
              }
  
              // Apply changes indicated above...
              kerberosKeytabPrincipalEntity = kerberosKeytabPrincipalDAO.merge(kerberosKeytabPrincipalEntity);
  
              // If there are no services or components relying on this KerberosKeytabPrincipalEntity, it
              // should be removed...
              if (CollectionUtils.isEmpty(kerberosKeytabPrincipalEntity.getServiceMapping())) {
                kerberosKeytabPrincipalDAO.remove(kerberosKeytabPrincipalEntity);
  
                if (LOG.isDebugEnabled()) {
                  LOG.debug("Cleaning up keytab/principal entry: {}:{}:{}:{}",
                      kerberosKeytabPrincipalEntity.getKkpId(), kerberosKeytabEntity.getKeytabPath(), kerberosPrincipalEntity.getPrincipalName(), kerberosKeytabPrincipalEntity.getHostName());
                } else {
                  LOG.info("Cleaning up keytab/principal entry: {}:{}:{}",
                      kerberosKeytabEntity.getKeytabPath(), kerberosPrincipalEntity.getPrincipalName(), kerberosKeytabPrincipalEntity.getHostName());
                }
  
                // Remove the KerberosKeytabPrincipalEntity reference from the relevant KerberosKeytabEntity
                kerberosKeytabEntity.getKerberosKeytabPrincipalEntities().remove(kerberosKeytabPrincipalEntity);
                kerberosKeytabEntity = kerberosKeytabDAO.merge(kerberosKeytabEntity);
  
                // Remove the KerberosKeytabPrincipalEntity reference from the relevant KerberosPrincipalEntity
                kerberosPrincipalEntity.getKerberosKeytabPrincipalEntities().remove(kerberosKeytabPrincipalEntity);
                kerberosPrincipalEntity = kerberosPrincipalDAO.merge(kerberosPrincipalEntity);
              }
  
              // If there are no more KerberosKeytabPrincipalEntity items that reference this, the keytab
              // file is no longer needed.
              if (kerberosKeytabDAO.removeIfNotReferenced(kerberosKeytabEntity)) {
                String message = String.format("Cleaning up keytab entry: %s", kerberosKeytabEntity.getKeytabPath());
                LOG.info(message);
                actionLog.writeStdOut(message);
              }
  
              // If there are no more KerberosKeytabPrincipalEntity items that reference this, the principal
              // is no longer needed.
              if (kerberosPrincipalDAO.removeIfNotReferenced(kerberosPrincipalEntity)) {
                String message = String.format("Cleaning up principal entry: %s", kerberosPrincipalEntity.getPrincipalName());
                LOG.info(message);
                actionLog.writeStdOut(message);
  
                destroyIdentity(operationHandler, kerberosPrincipalEntity);
              }
            }
          }
        } finally {
          // The KerberosOperationHandler needs to be closed, if it fails to close ignore the
          // exception since there is little we can or care to do about it now.
          try {
            operationHandler.close();
          } catch (KerberosOperationException e) {
            // Ignore this...
          }
        }
      }
  
      return createCommandReport(0, HostRoleStatus.COMPLETED, "{}", actionLog.getStdOut(), actionLog.getStdErr());
    }
  
    @Override
    protected boolean pruneServiceFilter() {
      return false;
    }
  
    /**
     * For each identity, remove the principal from the configured KDC.
     *
     * @param resolvedPrincipal        a ResolvedKerberosPrincipal object to process
     * @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 includedInFilter         a Boolean value indicating whather the principal is included in
     *                                 the current filter or not
     * @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 org.apache.ambari.server.AmbariException if an error occurs while processing the identity record
     */
    @Override
    protected CommandReport processIdentity(ResolvedKerberosPrincipal resolvedPrincipal,
                                            KerberosOperationHandler operationHandler,
                                            Map<String, String> kerberosConfiguration,
                                            boolean includedInFilter,
                                            Map<String, Object> requestSharedDataContext)
        throws AmbariException {
      throw new UnsupportedOperationException();
    }
  
    private void destroyIdentity(KerberosOperationHandler operationHandler, KerberosPrincipalEntity kerberosPrincipalEntity) {
      String principalName = kerberosPrincipalEntity.getPrincipalName();
      String message = String.format("Destroying identity, %s", principalName);
      LOG.info(message);
      actionLog.writeStdOut(message);
      DestroyPrincipalKerberosAuditEvent.DestroyPrincipalKerberosAuditEventBuilder auditEventBuilder = DestroyPrincipalKerberosAuditEvent.builder()
          .withTimestamp(System.currentTimeMillis())
          .withRequestId(getHostRoleCommand().getRequestId())
          .withTaskId(getHostRoleCommand().getTaskId())
          .withPrincipal(principalName);
  
      try {
        try {
          operationHandler.removePrincipal(principalName, kerberosPrincipalEntity.isService());
        } catch (KerberosOperationException e) {
          message = String.format("Failed to remove identity for %s from the KDC - %s", principalName, e.getMessage());
          LOG.warn(message, e);
          actionLog.writeStdErr(message);
          auditEventBuilder.withReasonOfFailure(message);
        }
  
        try {
          KerberosPrincipalEntity principalEntity = kerberosPrincipalDAO.find(principalName);
  
          if (principalEntity != null) {
            String cachedKeytabPath = principalEntity.getCachedKeytabPath();
  
            // If a cached  keytabs file exists for this principal, delete it.
            if (cachedKeytabPath != null) {
              if (!new File(cachedKeytabPath).delete()) {
                LOG.debug("Failed to remove cached keytab for {}", principalName);
              }
            }
          }
        } catch (Throwable t) {
          message = String.format("Failed to remove identity for %s from the Ambari database - %s", principalName, t.getMessage());
          LOG.warn(message, t);
          actionLog.writeStdErr(message);
          auditEventBuilder.withReasonOfFailure(message);
        }
      } finally {
        auditLog(auditEventBuilder.build());
      }
    }
  }