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.io.IOException;
  import java.nio.charset.Charset;
  import java.util.Map;
  import java.util.concurrent.ConcurrentMap;
  import java.util.concurrent.locks.Lock;
  
  import org.apache.ambari.server.AmbariException;
  import org.apache.ambari.server.agent.CommandReport;
  import org.apache.ambari.server.controller.RootService;
  import org.apache.ambari.server.controller.utilities.KerberosChecker;
  import org.apache.ambari.server.orm.dao.HostDAO;
  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.HostEntity;
  import org.apache.ambari.server.orm.entities.KerberosKeytabEntity;
  import org.apache.ambari.server.orm.entities.KerberosKeytabPrincipalEntity;
  import org.apache.ambari.server.orm.entities.KerberosPrincipalEntity;
  import org.apache.ambari.server.serveraction.ActionLog;
  import org.apache.ambari.server.serveraction.kerberos.stageutils.ResolvedKerberosKeytab;
  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.codec.digest.DigestUtils;
  import org.apache.commons.io.FileUtils;
  import org.slf4j.Logger;
  import org.slf4j.LoggerFactory;
  
  import com.google.common.util.concurrent.Striped;
  import com.google.inject.Inject;
  
  /**
   * ConfigureAmbariIdentitiesServerAction is a ServerAction implementation that creates keytab files as
   * instructed.
   * <p/>
   * This class mainly relies on the KerberosServerAction to iterate through metadata identifying
   * the Kerberos keytab files that need to be created. For each identity in the metadata, this
   * implementation's
   * {@link KerberosServerAction#processIdentity(ResolvedKerberosPrincipal, KerberosOperationHandler, Map, boolean, Map)}
   * is invoked attempting the creation of the relevant keytab file.
   */
  public class ConfigureAmbariIdentitiesServerAction extends KerberosServerAction {
  
  
    private static final String KEYTAB_PATTERN = "keyTab=\"(.+)?\"";
    private static final String PRINCIPAL_PATTERN = "principal=\"(.+)?\"";
  
    private final static Logger LOG = LoggerFactory.getLogger(ConfigureAmbariIdentitiesServerAction.class);
  
    @Inject
    private KerberosKeytabDAO kerberosKeytabDAO;
  
    @Inject
    private KerberosKeytabPrincipalDAO kerberosKeytabPrincipalDAO;
  
    @Inject
    private KerberosPrincipalDAO kerberosPrincipalDAO;
  
    @Inject
    private HostDAO hostDAO;
  
    /**
     * Used to prevent multiple threads from working with the same keytab.
     */
    private Striped<Lock> m_locksByKeytab = Striped.lazyWeakLock(25);  
  
    /**
     * Called to execute this action.  Upon invocation, calls
     * {@link KerberosServerAction#processIdentities(Map)} )}
     * to iterate through the Kerberos identity metadata and call
     * {@link ConfigureAmbariIdentitiesServerAction#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 {
      return processIdentities(requestSharedDataContext);
    }
  
  
    /**
     * Creates keytab file for ambari-server identity.
     * <p/>
     * It is expected that the {@link CreatePrincipalsServerAction}
     * (or similar) and {@link CreateKeytabFilesServerAction} has executed before this action.
     *
     * @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 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 {
      CommandReport commandReport = null;
  
      if (includedInFilter && resolvedPrincipal != null && StageUtils.getHostName().equals(resolvedPrincipal.getHostName())) {
        final String hostName = resolvedPrincipal.getHostName();
        final String dataDirectory = getDataDirectoryPath();
        for (Map.Entry<String, String> serviceMappingEntry : resolvedPrincipal.getServiceMapping().entries()) {
          // TODO check if changes needed for multiple principals in one keytab
          if (RootService.AMBARI.name().equals(serviceMappingEntry.getKey())) {
            ResolvedKerberosKeytab keytab = resolvedPrincipal.getResolvedKerberosKeytab();
            String destKeytabFilePath = resolvedPrincipal.getResolvedKerberosKeytab().getFile();
            File hostDirectory = new File(dataDirectory, hostName);
            File srcKeytabFile = new File(hostDirectory, DigestUtils.sha256Hex(destKeytabFilePath));
  
            Lock lock = m_locksByKeytab.get(destKeytabFilePath);
            lock.lock();
            try {
              if (srcKeytabFile.exists()) {
                String ownerAccess = keytab.getOwnerAccess();
                String groupAccess = keytab.getGroupAccess();
  
                installAmbariServerIdentity(resolvedPrincipal, srcKeytabFile.getAbsolutePath(), destKeytabFilePath,
                  keytab.getOwnerName(), ownerAccess,
                  keytab.getGroupName(), groupAccess, actionLog);
  
                if (serviceMappingEntry.getValue().contains("AMBARI_SERVER_SELF")) {
                  // Create/update the JAASFile...
                  configureJAAS(resolvedPrincipal.getPrincipal(), destKeytabFilePath, actionLog);
                }
              }
            } finally {
              lock.unlock();
            }
          }
        }
      }
  
      return commandReport;
    }
  
    /**
     * Installs the Ambari Server Kerberos identity by copying its keytab file to the specified location
     * and then creating the Ambari Server JAAS File.
     *
     * @param principal          the ambari server principal name
     * @param srcKeytabFilePath  the source location of the ambari server keytab file
     * @param destKeytabFilePath the destination location of the ambari server keytab file
     * @param ownerName          the username for the owner of the generated keytab file
     * @param ownerAccess        the user file access, "", "r" or "rw"
     * @param groupName          the name of the group for the generated keytab file
     * @param groupAccess        the group file access, "", "r" or "rw"
     * @param actionLog          the logger
     * @return true if success; false otherwise
     * @throws AmbariException
     */
    public boolean installAmbariServerIdentity(ResolvedKerberosPrincipal principal,
                                               String srcKeytabFilePath,
                                               String destKeytabFilePath,
                                               String ownerName, String ownerAccess,
                                               String groupName, String groupAccess,
                                               ActionLog actionLog) throws AmbariException {
  
      try {
        // Copy the keytab file into place (creating the parent directory, if necessary...
        boolean ownerWritable = "w".equalsIgnoreCase(ownerAccess) || "rw".equalsIgnoreCase(ownerAccess);
        boolean ownerReadable = "r".equalsIgnoreCase(ownerAccess) || "rw".equalsIgnoreCase(ownerAccess);
        boolean groupWritable = "w".equalsIgnoreCase(groupAccess) || "rw".equalsIgnoreCase(groupAccess);
        boolean groupReadable = "r".equalsIgnoreCase(groupAccess) || "rw".equalsIgnoreCase(groupAccess);
  
        copyFile(srcKeytabFilePath, destKeytabFilePath);
        setFileACL(destKeytabFilePath,
            ownerName, ownerReadable, ownerWritable,
            groupName, groupReadable, groupWritable);
  
        Long ambariServerHostID = ambariServerHostID();
        HostEntity hostEntity = null;
        if (ambariServerHostID != null) {
          hostEntity = hostDAO.findById(ambariServerHostID);
        }
  
        KerberosKeytabEntity kke = kerberosKeytabDAO.find(destKeytabFilePath);
        if (kke == null) {
          kke = new KerberosKeytabEntity(destKeytabFilePath);
          kke.setOwnerName(ownerName);
          kke.setOwnerAccess(ownerAccess);
          kke.setGroupName(groupName);
          kke.setGroupAccess(groupAccess);
          kerberosKeytabDAO.create(kke);
        }
  
        KerberosPrincipalEntity kpe = kerberosPrincipalDAO.find(principal.getPrincipal());
        if(kpe == null) {
          kpe = new KerberosPrincipalEntity(principal.getPrincipal(), principal.isService(), principal.getCacheFile());
          kerberosPrincipalDAO.create(kpe);
        }
  
        for(Map.Entry<String, String> mapping : principal.getServiceMapping().entries()) {
          String serviceName = mapping.getKey();
          String componentName = mapping.getValue();
          KerberosKeytabPrincipalEntity entity = kerberosKeytabPrincipalDAO.findOrCreate(kke, hostEntity, kpe, null).kkp;
          entity.setDistributed(true);
          entity.putServiceMapping(serviceName, componentName);
          kerberosKeytabPrincipalDAO.merge(entity);
  
          kke.addKerberosKeytabPrincipal(entity);
          kerberosKeytabDAO.merge(kke);
  
          kpe.addKerberosKeytabPrincipal(entity);
          kerberosPrincipalDAO.merge(kpe);
        }
  
        if (actionLog != null) {
          actionLog.writeStdOut(String.format("Created Ambari server keytab file for %s at %s", principal, destKeytabFilePath));
        }
      } catch (InterruptedException | IOException e) {
        throw new AmbariException(e.getLocalizedMessage(), e);
      }
  
      return true;
    }
  
    /**
     * Configure Ambari's JAAS file to reflect the principal name and keytab file for Ambari's Kerberos
     * identity.
     *
     * @param principal      the Ambari server's principal name
     * @param keytabFilePath the absolute path to the Ambari server's keytab file
     * @param actionLog      the logger
     */
    public void configureJAAS(String principal, String keytabFilePath, ActionLog actionLog) {
      String jaasConfPath = getJAASConfFilePath();
      if (jaasConfPath != null) {
        File jaasConfigFile = new File(jaasConfPath);
        try {
          String jaasConfig = FileUtils.readFileToString(jaasConfigFile, Charset.defaultCharset());
          File oldJaasConfigFile = new File(jaasConfPath + ".bak");
          FileUtils.writeStringToFile(oldJaasConfigFile, jaasConfig, Charset.defaultCharset());
          jaasConfig = jaasConfig.replaceFirst(KEYTAB_PATTERN, "keyTab=\"" + keytabFilePath + "\"");
          jaasConfig = jaasConfig.replaceFirst(PRINCIPAL_PATTERN, "principal=\"" + principal + "\"");
          FileUtils.writeStringToFile(jaasConfigFile, jaasConfig, Charset.defaultCharset());
          String message = String.format("JAAS config file %s modified successfully for principal %s.",
              jaasConfigFile.getName(), principal);
          if (actionLog != null) {
            actionLog.writeStdOut(message);
          }
        } catch (IOException e) {
          String message = String.format("Failed to configure JAAS file %s for %s - %s",
              jaasConfigFile, principal, e.getMessage());
          if (actionLog != null) {
            actionLog.writeStdErr(message);
          }
          LOG.error(message, e);
        }
      } else {
        String message = String.format("Failed to configure JAAS, config file should be passed to Ambari server as: " +
            "%s.", KerberosChecker.JAVA_SECURITY_AUTH_LOGIN_CONFIG);
        if (actionLog != null) {
          actionLog.writeStdErr(message);
        }
        LOG.error(message);
      }
    }
  
    /**
     * Copies the specified source file to the specified destination path, creating any needed parent
     * directories.
     * <p>
     * This method is mocked in unit tests to avoid dealing with ShellCommandUtil in a mocked env.
     *
     * @param srcKeytabFilePath  the source location of the ambari server keytab file
     * @param destKeytabFilePath the destination location of the ambari server keytab file
     * @throws IOException
     * @throws InterruptedException
     * @throws AmbariException
     * @see ShellCommandUtil#mkdir(String, boolean);
     * @see ShellCommandUtil#copyFile(String, String, boolean, boolean)
     */
    void copyFile(String srcKeytabFilePath, String destKeytabFilePath)
        throws IOException, InterruptedException {
  
      ShellCommandUtil.Result result;
  
      // Create the parent directory if necessary (using sudo)
      File destKeytabFile = new File(destKeytabFilePath);
      result = ShellCommandUtil.mkdir(destKeytabFile.getParent(), true);
      if (!result.isSuccessful()) {
        throw new AmbariException(result.getStderr());
      }
  
      // Copy the file (using sudo)
      result = ShellCommandUtil.copyFile(srcKeytabFilePath, destKeytabFilePath, true, true);
      if (!result.isSuccessful()) {
        throw new AmbariException(result.getStderr());
      }
    }
  
    /**
     * Sets the access control list for this specified file.
     * <p>
     * The owner and group for the file is set as well as the owner's and group's ability to read and write
     * the file.
     * <p>
     * The result of the operation to set the group for the file is ignored since it is possible that
     * the group does not exist when performing this operation. It is expected this issue will be remedied
     * when the group becomes available.
     * <p>
     * Access for other users is denied and the file is assumed to not be executeable by anyone.
     *
     * @param filePath      the path to the file
     * @param ownerName     the username for the owner of the generated keytab file
     * @param ownerWritable true if the owner should be able to write to this file; otherwise false
     * @param ownerReadable true if the owner should be able to read this file; otherwise false
     * @param groupName     the name of the group for the generated keytab file
     * @param groupWritable true if the group should be able to write to this file; otherwise false
     * @param groupReadable true if the group should be able to read this file; otherwise false
     * @throws AmbariException if an error occurs setting the permissions on the fils
     */
    void setFileACL(String filePath,
                            String ownerName, boolean ownerReadable, boolean ownerWritable,
                            String groupName, boolean groupReadable, boolean groupWritable)
        throws AmbariException {
  
      ShellCommandUtil.Result result;
  
      result = ShellCommandUtil.setFileOwner(filePath, ownerName);
  
      if (result.isSuccessful()) {
        result = ShellCommandUtil.setFileGroup(filePath, groupName);
  
        if (!result.isSuccessful()) {
          // Ignore, but log, this it is possible that the group does not exist when performing this operation
          LOG.warn("Failed to set the group for the file at {} to {}: {}", filePath, groupName, result.getStderr());
        }
  
        result = ShellCommandUtil.setFileMode(filePath,
            ownerReadable, ownerWritable, false,
            groupReadable, groupWritable, false,
            false, false, false);
      }
  
      if (!result.isSuccessful()) {
        throw new AmbariException(result.getStderr());
      }
    }
  
    /**
     * Gets the location of Ambari's JAAS config file.
     * <p>
     * This method is mocked in unit tests to avoid having to alter the System properties in
     * order to locate the test JAAS config file.
     *
     * @return the path to Ambari's JAAS config file
     */
    String getJAASConfFilePath() {
      return System.getProperty(KerberosChecker.JAVA_SECURITY_AUTH_LOGIN_CONFIG);
    }
  }
  