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.HashSet;
  import java.util.Map;
  import java.util.Set;
  import java.util.concurrent.ConcurrentMap;
  import java.util.concurrent.locks.Lock;
  
  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.agent.stomp.TopologyHolder;
  import org.apache.ambari.server.events.TopologyUpdateEvent;
  import org.apache.ambari.server.serveraction.kerberos.stageutils.ResolvedKerberosPrincipal;
  import org.apache.ambari.server.utils.ShellCommandUtil;
  import org.apache.ambari.server.utils.StageUtils;
  import org.apache.commons.lang3.StringUtils;
  import org.slf4j.Logger;
  import org.slf4j.LoggerFactory;
  
  import com.google.common.util.concurrent.Striped;
  import com.google.inject.Inject;
  
  @SuppressWarnings("UnstableApiUsage")
  public class FinalizeKerberosServerAction extends KerberosServerAction {
    private final static Logger LOG = LoggerFactory.getLogger(FinalizeKerberosServerAction.class);
  
    private final TopologyHolder topologyHolder;
  
    /**
     * Used to prevent multiple threads from working with the same keytab.
     */
    private Striped<Lock> m_locksByKeytab = Striped.lazyWeakLock(25);
  
    @Inject
    public FinalizeKerberosServerAction(TopologyHolder topologyHolder) {
      this.topologyHolder = topologyHolder;
    }
  
    /**
     * Processes an identity as necessary.
     * <p/>
     * This implementation ensures that keytab files for the Ambari identities have the correct
     * permissions.  This is important in the event a secure cluster was created via Blueprints since
     * some user accounts and groups may not have been available (at the OS level) when the keytab files
     * were created.
     *
     * @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 null, always
     * @throws AmbariException
     */
    @Override
    protected CommandReport processIdentity(ResolvedKerberosPrincipal resolvedPrincipal,
                                            KerberosOperationHandler operationHandler,
                                            Map<String, String> kerberosConfiguration,
                                            boolean includedInFilter,
                                            Map<String, Object> requestSharedDataContext)
        throws AmbariException {
  
      if (resolvedPrincipal != null) {
        // If the record's HOSTNAME value is "ambari-server", rather than an actual hostname it will
        // not match the Ambari server's host name. This will occur if the there is no agent installed
        // on the Ambari server host.  This is ok, since any keytab files installed on the Ambari server
        // host will already have the permissions set so that only the Ambari server can read it.
        // There is no need to update the permissions for those keytab files so that installed services
        // can access them since no services will be installed on the host.
        if (StageUtils.getHostName().equals(resolvedPrincipal.getHostName())) {
  
          // If the principal name exists in one of the shared data maps, it has been processed by the
          // current "Enable Kerberos" or "Add component" workflow and therefore should already have
          // the correct permissions assigned. The relevant keytab files can be skipped.
          Map<String, String> principalPasswordMap = getPrincipalPasswordMap(requestSharedDataContext);
          if ((principalPasswordMap == null) || !principalPasswordMap.containsKey(resolvedPrincipal.getPrincipal())) {
  
            String keytabFilePath = resolvedPrincipal.getKeytabPath();
  
            if (StringUtils.isEmpty(keytabFilePath)) {
              return null;
            }
  
            Lock lock = m_locksByKeytab.get(keytabFilePath);
            lock.lock();
            try {
              Set<String> visited = (Set<String>) requestSharedDataContext.get(this.getClass().getName() + "_visited");
  
              if (!visited.contains(keytabFilePath)) {
                String ownerName = resolvedPrincipal.getResolvedKerberosKeytab().getOwnerName();
                String ownerAccess = resolvedPrincipal.getResolvedKerberosKeytab().getOwnerAccess();
                boolean ownerWritable = "w".equalsIgnoreCase(ownerAccess) || "rw".equalsIgnoreCase(ownerAccess);
                boolean ownerReadable = "r".equalsIgnoreCase(ownerAccess) || "rw".equalsIgnoreCase(ownerAccess);
                String groupName = resolvedPrincipal.getResolvedKerberosKeytab().getGroupName();
                String groupAccess = resolvedPrincipal.getResolvedKerberosKeytab().getGroupAccess();
                boolean groupWritable = "w".equalsIgnoreCase(groupAccess) || "rw".equalsIgnoreCase(groupAccess);
                boolean groupReadable = "r".equalsIgnoreCase(groupAccess) || "rw".equalsIgnoreCase(groupAccess);
  
                ShellCommandUtil.Result result;
                String message;
  
                result = ShellCommandUtil.setFileOwner(keytabFilePath, ownerName);
                if (result.isSuccessful()) {
                  message = String.format("Updated the owner of the keytab file at %s to %s",
                      keytabFilePath, ownerName);
                  LOG.info(message);
                  actionLog.writeStdOut(message);
                } else {
                  message = String.format("Failed to update the owner of the keytab file at %s to %s: %s",
                      keytabFilePath, ownerName, result.getStderr());
                  LOG.error(message);
                  actionLog.writeStdOut(message);
                  actionLog.writeStdErr(message);
                }
  
                result = ShellCommandUtil.setFileGroup(keytabFilePath, groupName);
                if (result.isSuccessful()) {
                  message = String.format("Updated the group of the keytab file at %s to %s",
                      keytabFilePath, groupName);
                  LOG.info(message);
                  actionLog.writeStdOut(message);
                } else {
                  message = String.format("Failed to update the group of the keytab file at %s to %s: %s",
                      keytabFilePath, groupName, result.getStderr());
                  LOG.error(message);
                  actionLog.writeStdOut(message);
                  actionLog.writeStdErr(message);
                }
  
                result = ShellCommandUtil.setFileMode(keytabFilePath,
                    ownerReadable, ownerWritable, false,
                    groupReadable, groupWritable, false,
                    false, false, false);
                if (result.isSuccessful()) {
                  message = String.format("Updated the access mode of the keytab file at %s to owner:'%s' and group:'%s'",
                      keytabFilePath, ownerAccess, groupAccess);
                  LOG.info(message);
                  actionLog.writeStdOut(message);
                } else {
                  message = String.format("Failed to update the access mode of the keytab file at %s to owner:'%s' and group:'%s': %s",
                      keytabFilePath, ownerAccess, groupAccess, result.getStderr());
                  LOG.error(message);
                  actionLog.writeStdOut(message);
                  actionLog.writeStdErr(message);
                }
  
                visited.add(keytabFilePath);
              }
            } finally {
              lock.unlock();
            }
          }
        }
      }
  
      return null;
    }
  
    /**
     * @param requestSharedDataContext a Map to be used a shared data among all ServerActions related
     *                                 to a given request
     * @return
     * @throws AmbariException
     * @throws InterruptedException
     */
    @Override
    public CommandReport execute(ConcurrentMap<String, Object> requestSharedDataContext) throws AmbariException, InterruptedException {
      String dataDirectoryPath = getCommandParameterValue(DATA_DIRECTORY);
      if(getKDCType(getCommandParameters()) != KDCType.NONE) {
        // Ensure the keytab files for the Ambari identities have the correct permissions
        // This is important in the event a secure cluster was created via Blueprints since some
        // user accounts and group may not have been created when the keytab files were created.
        requestSharedDataContext.put(this.getClass().getName() + "_visited", new HashSet<String>());
        processIdentities(requestSharedDataContext);
        requestSharedDataContext.remove(this.getClass().getName() + "_visited");
      }
      deleteDataDirectory(dataDirectoryPath);
  
      return sendTopologyUpdateEvent();
    }
  
    private CommandReport sendTopologyUpdateEvent() throws AmbariException, InterruptedException {
      CommandReport commandReport = null;
      try {
        final TopologyUpdateEvent updateEvent = topologyHolder.getCurrentData();
        topologyHolder.updateData(updateEvent);
      } catch (Exception e) {
        String message = "Could not send topology update event when enabling kerberos";
        actionLog.writeStdErr(message);
        LOG.error(message, e);
        commandReport = createCommandReport(1, HostRoleStatus.FAILED, "{}", actionLog.getStdOut(), actionLog.getStdErr());
      }
      return commandReport == null ? createCompletedCommandReport() : commandReport;
    }
  
  }
  