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.customactions;
  
  import java.io.File;
  import java.util.ArrayList;
  import java.util.EnumSet;
  import java.util.HashMap;
  import java.util.HashSet;
  import java.util.List;
  import java.util.Map;
  import java.util.Set;
  
  import javax.xml.bind.JAXBContext;
  import javax.xml.bind.JAXBException;
  import javax.xml.bind.UnmarshalException;
  import javax.xml.bind.Unmarshaller;
  
  import org.apache.ambari.server.AmbariException;
  import org.apache.ambari.server.actionmanager.ActionType;
  import org.apache.ambari.server.actionmanager.TargetHostType;
  import org.apache.ambari.server.security.authorization.RoleAuthorization;
  import org.apache.ambari.server.stack.StackDirectory;
  import org.apache.commons.lang.StringUtils;
  import org.slf4j.Logger;
  import org.slf4j.LoggerFactory;
  
  /**
   * Manages Action definitions read from XML files
   */
  public class ActionDefinitionManager {
    public static final Integer MIN_TIMEOUT = 60;
    private final static Logger LOG = LoggerFactory
        .getLogger(ActionDefinitionManager.class);
    private static final Map<Class<?>, JAXBContext> _jaxbContexts =
      new HashMap<>();
    private static final Integer MAX_TIMEOUT = Integer.MAX_VALUE-1;
  
    static {
      try {
        JAXBContext ctx = JAXBContext.newInstance(ActionDefinitionXml.class);
        _jaxbContexts.put(ActionDefinitionXml.class, ctx);
      } catch (JAXBException e) {
        throw new RuntimeException(e);
      }
    }
  
    private final Map<String, ActionDefinition> actionDefinitionMap = new HashMap<>();
  
    public ActionDefinitionManager() {
    }
  
    public static <T> T unmarshal(Class<T> clz, File file) throws JAXBException {
      Unmarshaller u = _jaxbContexts.get(clz).createUnmarshaller();
  
      return clz.cast(u.unmarshal(file));
    }
  
    private <E extends Enum<E>> E safeValueOf(Class<E> enumm, String s, StringBuilder reason) {
      if (s == null || s.length() == 0) {
        return null;
      }
  
      try {
        return Enum.valueOf(enumm, s);
      } catch (IllegalArgumentException iaex) {
        reason.append("Invalid value provided for ").append(enumm.getName());
        return null;
      }
    }
  
    public void readCustomActionDefinitions(File customActionDefinitionRoot) throws JAXBException, AmbariException {
      if (customActionDefinitionRoot == null
          || !customActionDefinitionRoot.exists()
          || !customActionDefinitionRoot.canRead()) {
        LOG.warn("Cannot read custom action definitions. " +
            (customActionDefinitionRoot == null ? "" : ("Check path " + customActionDefinitionRoot.getAbsolutePath())));
      }
  
      File[] customActionDefinitionFiles
          = customActionDefinitionRoot.listFiles(StackDirectory.FILENAME_FILTER);
  
      if (customActionDefinitionFiles != null) {
        for (File definitionFile : customActionDefinitionFiles) {
          ActionDefinitionXml adx = null;
          try {
            adx = unmarshal(ActionDefinitionXml.class, definitionFile);
          } catch (UnmarshalException uex) {
            LOG.warn("Encountered badly formed action definition file - " + definitionFile.getAbsolutePath());
            continue;
          }
          for (ActionDefinitionSpec ad : adx.actionDefinitions()) {
            LOG.debug("Read action definition = {}", ad);
            StringBuilder errorReason =
                new StringBuilder("Error while parsing action definition. ").append(ad).append(" --- ");
  
            TargetHostType targetType = safeValueOf(TargetHostType.class, ad.getTargetType(), errorReason);
            ActionType actionType = safeValueOf(ActionType.class, ad.getActionType(), errorReason);
  
            Integer defaultTimeout = MIN_TIMEOUT;
            if (ad.getDefaultTimeout() != null && !ad.getDefaultTimeout().isEmpty()) {
              defaultTimeout = Integer.parseInt(ad.getDefaultTimeout());
            }
  
            if (isValidActionDefinition(ad, actionType, defaultTimeout, errorReason)) {
              String actionName = ad.getActionName();
              if (actionDefinitionMap.containsKey(actionName)) {
                LOG.warn("Ignoring action definition as a different definition by that name already exists. "
                    + ad);
                continue;
              }
  
              actionDefinitionMap.put(ad.getActionName(), new ActionDefinition(ad.getActionName(), actionType,
                  ad.getInputs(), ad.getTargetService(), ad.getTargetComponent(), ad.getDescription(), targetType, defaultTimeout,
                  translatePermissions(ad.getPermissions())));
              LOG.info("Added custom action definition for " + ad.getActionName());
            } else {
              LOG.warn(errorReason.toString());
            }
          }
        }
      }
    }
  
    private boolean isValidActionDefinition(ActionDefinitionSpec ad, ActionType actionType,
                                            Integer defaultTimeout, StringBuilder reason) {
      if (isValidActionName(ad.getActionName(), reason)) {
  
        if (defaultTimeout < MIN_TIMEOUT || defaultTimeout > MAX_TIMEOUT) {
          reason.append("Default timeout should be between " + MIN_TIMEOUT + " and " + MAX_TIMEOUT);
          return false;
        }
  
        if (actionType == null || actionType == ActionType.SYSTEM_DISABLED) {
          reason.append("Action type cannot be ").append(actionType);
          return false;
        }
  
        if (ad.getDescription() == null || ad.getDescription().isEmpty()) {
          reason.append("Action description cannot be empty");
          return false;
        }
  
        if (ad.getTargetService() == null || ad.getTargetService().isEmpty()) {
          if (ad.getTargetComponent() != null && !ad.getTargetComponent().isEmpty()) {
            reason.append("Target component cannot be specified unless target service is specified");
            return false;
          }
        }
  
        if (ad.getInputs() != null && !ad.getInputs().isEmpty()) {
          String[] parameters = ad.getInputs().split(",");
          for (String parameter : parameters) {
            if (parameter.trim().isEmpty()) {
              reason.append("Empty parameter cannot be specified as an input parameter");
            }
          }
        }
      } else {
        return false;
      }
  
      return true;
    }
  
    public List<ActionDefinition> getAllActionDefinition() {
      return new ArrayList<>(actionDefinitionMap.values());
    }
  
    public ActionDefinition getActionDefinition(String name) {
      return actionDefinitionMap.get(name);
    }
  
    public void addActionDefinition(ActionDefinition ad) throws AmbariException {
      if (!actionDefinitionMap.containsKey(ad.getActionName())) {
        actionDefinitionMap.put(ad.getActionName(), ad);
      } else {
        throw new AmbariException("Action definition by name " + ad.getActionName() + " already exists.");
      }
    }
  
    private boolean isValidActionName(String actionName, StringBuilder reason) {
      if (actionName == null || actionName.isEmpty()) {
        reason.append("Action name cannot be empty");
        return false;
      }
      String trimmedName = actionName.replaceAll("\\s+", "");
      if (actionName.length() > trimmedName.length()) {
        reason.append("Action name cannot contain white spaces");
        return false;
      }
      return true;
    }
  
    /**
     * Given a comma-delimited list of permission names, translates into a {@link Set} of
     * {@link RoleAuthorization}s.
     * <p>
     * <code>null</code> is returned if the permission string is null or empty, or if none of the
     * permissions in the string translate to a {@link RoleAuthorization}.  Permissions that do not
     * translate to a {@link RoleAuthorization} will yield a {@link IllegalArgumentException}.
     *
     * @param permissions a comma-delimited string of permission names
     * @return a set of {@link RoleAuthorization}s; or null if no permissions are specified
     */
    private Set<RoleAuthorization> translatePermissions(String permissions) {
      if (StringUtils.isEmpty(permissions)) {
        return null;
      } else {
        Set<RoleAuthorization> authorizations = new HashSet<>();
        String[] parts = permissions.split(",");
  
        for (String permission : parts) {
          RoleAuthorization authorization = RoleAuthorization.translate(permission);
          if (authorization != null) {
            authorizations.add(authorization);
          }
        }
  
        return (authorizations.isEmpty()) ? null : EnumSet.copyOf(authorizations);
      }
    }
  }
  