package org.infinispan.test.integration.security.tasks;

import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.ADD;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.ALLOW_RESOURCE_SERVICE_RESTART;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.COMPOSITE;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.OP;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.OPERATION_HEADERS;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.OP_ADDR;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.REMOVE;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.ROLLBACK_ON_RUNTIME_FAILURE;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.STEPS;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.SUBSYSTEM;

import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import org.apache.commons.lang.StringUtils;
import org.jboss.as.arquillian.api.ServerSetupTask;
import org.jboss.as.arquillian.container.ManagementClient;
import org.jboss.as.controller.PathAddress;
import org.jboss.as.controller.descriptions.ModelDescriptionConstants;
import org.jboss.as.controller.operations.common.Util;
import org.jboss.as.test.integration.security.common.Constants;
import org.jboss.as.test.integration.security.common.CoreUtils;
import org.jboss.as.test.integration.security.common.config.AuthnModule;
import org.jboss.as.test.integration.security.common.config.JSSE;
import org.jboss.as.test.integration.security.common.config.JaspiAuthn;
import org.jboss.as.test.integration.security.common.config.LoginModuleStack;
import org.jboss.as.test.integration.security.common.config.SecureStore;
import org.jboss.as.test.integration.security.common.config.SecurityDomain;
import org.jboss.as.test.integration.security.common.config.SecurityModule;
import org.jboss.dmr.ModelNode;
import org.jboss.logging.Logger;

/**
 * {@link ServerSetupTask} instance for security domain setup. It supports JSSE configuration, JASPI authentication
 * configuration and stacks of login-modules (classic authentication), policy-modules and (role-)mapping-modules.
 *
 * @author <a href="mailto:jcacek@redhat.com">Josef Cacek</a>
 * @author <a href="mailto:vchepeli@redhat.com">Vitalii Chepeliuk</a>
 */
public abstract class AbstractSecurityDomainsServerSetupTask implements ServerSetupTask {

   private static final Logger LOGGER = Logger.getLogger(AbstractSecurityDomainsServerSetupTask.class);

   private static final String ROLE = "role"; // The type attribute value of mapping-modules used for role assignment.
   private static final String SUBSYSTEM_SECURITY = "security"; // The SUBSYSTEM_SECURITY

   protected ManagementClient managementClient;

   private SecurityDomain[] securityDomains;

   public void setup(final ManagementClient managementClient, String containerId) throws Exception {
      this.managementClient = managementClient;
      securityDomains = getSecurityDomains();

      if (securityDomains == null || securityDomains.length == 0) {
         LOGGER.warn("Empty security domain configuration.");
         return;
      }

      final List<ModelNode> updates = new LinkedList<ModelNode>();
      for (final SecurityDomain securityDomain : securityDomains) {
         final String securityDomainName = securityDomain.getName();
         if (LOGGER.isInfoEnabled()) {
            LOGGER.info("Adding security domain " + securityDomainName);
         }
         final ModelNode compositeOp = new ModelNode();
         compositeOp.get(OP).set(COMPOSITE);
         compositeOp.get(OP_ADDR).setEmptyList();
         ModelNode steps = compositeOp.get(STEPS);

         PathAddress opAddr = PathAddress.pathAddress()
               .append(SUBSYSTEM, SUBSYSTEM_SECURITY)
               .append(Constants.SECURITY_DOMAIN, securityDomainName);
         ModelNode op = Util.createAddOperation(opAddr);
         if (StringUtils.isNotEmpty(securityDomain.getCacheType())) {
            op.get(Constants.CACHE_TYPE).set(securityDomain.getCacheType());
         }
         steps.add(op);

         //only one can occur - authenticationType or authenticationJaspiType
         final boolean authNodeAdded = createSecurityModelNode(Constants.AUTHENTICATION, Constants.LOGIN_MODULE, Constants.FLAG,
                                                               Constants.REQUIRED, securityDomain.getLoginModules(), securityDomainName, steps);
         if (!authNodeAdded) {
            final List<ModelNode> jaspiAuthnNodes = createJaspiAuthnNodes(securityDomain.getJaspiAuthn(), securityDomain.getName());
            if (jaspiAuthnNodes != null) {
               for (ModelNode node : jaspiAuthnNodes) {
                  steps.add(node);
               }
            }
         }
         createSecurityModelNode(Constants.AUTHORIZATION, Constants.POLICY_MODULE, Constants.FLAG, Constants.REQUIRED, securityDomain.getAuthorizationModules(), securityDomainName, steps);
         createSecurityModelNode(Constants.MAPPING, Constants.MAPPING_MODULE, Constants.TYPE, ROLE, securityDomain.getMappingModules(), securityDomainName, steps);

         final ModelNode jsseNode = createJSSENode(securityDomain.getJsse(), securityDomain.getName());
         if (jsseNode != null) {
            steps.add(jsseNode);
         }
         updates.add(compositeOp);
      }
      CoreUtils.applyUpdates(updates, managementClient.getControllerClient());
   }

   public void tearDown(ManagementClient managementClient, String containerId) throws Exception {
      if (securityDomains == null || securityDomains.length == 0) {
         LOGGER.warn("Empty security domain configuration.");
         return;
      }

      final List<ModelNode> updates = new ArrayList<ModelNode>();
      for (final SecurityDomain securityDomain : securityDomains) {
         final String domainName = securityDomain.getName();
         if (LOGGER.isInfoEnabled()) {
            LOGGER.info("Removing security domain " + domainName);
         }
         final ModelNode op = new ModelNode();
         op.get(OP).set(REMOVE);
         op.get(OP_ADDR).add(SUBSYSTEM, "security");
         op.get(OP_ADDR).add(Constants.SECURITY_DOMAIN, domainName);
         // Don't rollback when the AS detects the war needs the module
         op.get(OPERATION_HEADERS, ROLLBACK_ON_RUNTIME_FAILURE).set(false);
         op.get(OPERATION_HEADERS, ALLOW_RESOURCE_SERVICE_RESTART).set(true);
         updates.add(op);
      }
      CoreUtils.applyUpdates(updates, managementClient.getControllerClient());

      this.managementClient = null;
   }

   /**
    * Creates authenticaton=>jaspi node and its child nodes.
    *
    * @param securityConfigurations
    * @return
    */
   private List<ModelNode> createJaspiAuthnNodes(JaspiAuthn securityConfigurations, String domainName) {
      if (securityConfigurations == null) {
         LOGGER.info("No security configuration for JASPI module.");
         return null;
      }
      if (securityConfigurations.getAuthnModules() == null || securityConfigurations.getAuthnModules().length == 0
            || securityConfigurations.getLoginModuleStacks() == null
            || securityConfigurations.getLoginModuleStacks().length == 0) {
         throw new IllegalArgumentException("Missing mandatory part of JASPI configuration in the security domain.");
      }

      final List<ModelNode> steps = new ArrayList<ModelNode>();

      PathAddress domainAddress = PathAddress.pathAddress()
            .append(SUBSYSTEM, SUBSYSTEM_SECURITY)
            .append(Constants.SECURITY_DOMAIN, domainName);
      PathAddress jaspiAddress = domainAddress.append(Constants.AUTHENTICATION, Constants.JASPI);
      steps.add(Util.createAddOperation(jaspiAddress));

      for (final AuthnModule config : securityConfigurations.getAuthnModules()) {
         LOGGER.info("Adding auth-module: " + config);
         final ModelNode securityModuleNode = Util.createAddOperation(jaspiAddress.append(Constants.AUTH_MODULE, config.getName()));
         steps.add(securityModuleNode);
         securityModuleNode.get(ModelDescriptionConstants.CODE).set(config.getName());
         if (config.getFlag() != null) {
            securityModuleNode.get(Constants.FLAG).set(config.getFlag());
         }
         if (config.getModule() != null) {
            securityModuleNode.get(Constants.MODULE).set(config.getModule());
         }
         if (config.getLoginModuleStackRef() != null) {
            securityModuleNode.get(Constants.LOGIN_MODULE_STACK_REF).set(config.getLoginModuleStackRef());
         }
         Map<String, String> configOptions = config.getOptions();
         if (configOptions == null) {
            LOGGER.info("No module options provided.");
            configOptions = Collections.emptyMap();
         }
         final ModelNode moduleOptionsNode = securityModuleNode.get(Constants.MODULE_OPTIONS);
         for (final Map.Entry<String, String> entry : configOptions.entrySet()) {
            final String optionName = entry.getKey();
            final String optionValue = entry.getValue();
            moduleOptionsNode.add(optionName, optionValue);
            if (LOGGER.isDebugEnabled()) {
               LOGGER.debug("Adding module option [" + optionName + "=" + optionValue + "]");
            }
         }
      }
      //Unable to use securityComponentNode.get(OPERATION_HEADERS).get(ALLOW_RESOURCE_SERVICE_RESTART).set(true) because the login-module-stack is empty


      for (final LoginModuleStack lmStack : securityConfigurations.getLoginModuleStacks()) {
         PathAddress lmStackAddress = jaspiAddress.append(Constants.LOGIN_MODULE_STACK, lmStack.getName());
         steps.add(Util.createAddOperation(lmStackAddress));

         for (final SecurityModule config : lmStack.getLoginModules()) {
            final String code = config.getName();
            final ModelNode securityModuleNode = Util.createAddOperation(lmStackAddress.append(Constants.LOGIN_MODULE, code));

            final String flag = StringUtils.defaultIfEmpty(config.getFlag(), Constants.REQUIRED);
            securityModuleNode.get(ModelDescriptionConstants.CODE).set(code);
            securityModuleNode.get(Constants.FLAG).set(flag);
            if (LOGGER.isInfoEnabled()) {
               LOGGER.info("Adding JASPI login module stack [code=" + code + ", flag=" + flag + "]");
            }
            Map<String, String> configOptions = config.getOptions();
            if (configOptions == null) {
               LOGGER.info("No module options provided.");
               configOptions = Collections.emptyMap();
            }
            final ModelNode moduleOptionsNode = securityModuleNode.get(Constants.MODULE_OPTIONS);
            for (final Map.Entry<String, String> entry : configOptions.entrySet()) {
               final String optionName = entry.getKey();
               final String optionValue = entry.getValue();
               moduleOptionsNode.add(optionName, optionValue);
               if (LOGGER.isDebugEnabled()) {
                  LOGGER.debug("Adding module option [" + optionName + "=" + optionValue + "]");
               }
            }
            securityModuleNode.get(OPERATION_HEADERS).get(ALLOW_RESOURCE_SERVICE_RESTART).set(true);
            steps.add(securityModuleNode);
         }

      }

      return steps;
   }

   /**
    * Creates a {@link ModelNode} with the security component configuration. If the securityConfigurations array is
    * empty or null, then null is returned.
    *
    * @param securityComponent name of security component (e.g. {@link Constants#AUTHORIZATION})
    * @param subnodeName       name of the security component subnode, which holds module configurations (e.g. {@link
    *                          Constants#POLICY_MODULES})
    * @param flagAttributeName name of attribute to which the value of {@link SecurityModule#getFlag()} is set
    * @param flagDefaultValue  default value for flagAttributeName attr.
    * @param securityModules   configurations
    * @return ModelNode instance or null
    */
   private boolean createSecurityModelNode(String securityComponent, String subnodeName, String flagAttributeName,
                                           String flagDefaultValue, final SecurityModule[] securityModules, String domainName, ModelNode operations) {
      if (securityModules == null || securityModules.length == 0) {
         if (LOGGER.isInfoEnabled()) {
            LOGGER.info("No security configuration for " + securityComponent + " module.");
         }
         return false;
      }
      PathAddress address = PathAddress.pathAddress()
            .append(SUBSYSTEM, SUBSYSTEM_SECURITY)
            .append(Constants.SECURITY_DOMAIN, domainName)
            .append(securityComponent, Constants.CLASSIC);
      operations.add(Util.createAddOperation(address));

      for (final SecurityModule config : securityModules) {
         final String code = config.getName();
         final ModelNode securityModuleNode = Util.createAddOperation(address.append(subnodeName, code));

         final String flag = StringUtils.defaultIfEmpty(config.getFlag(), flagDefaultValue);
         securityModuleNode.get(ModelDescriptionConstants.CODE).set(code);
         securityModuleNode.get(flagAttributeName).set(flag);
         Map<String, String> configOptions = config.getOptions();
         if (configOptions == null) {
            LOGGER.info("No module options provided.");
            configOptions = Collections.emptyMap();
         }
         if (LOGGER.isInfoEnabled()) {
            LOGGER.info("Adding " + securityComponent + " module [code=" + code + ", " + flagAttributeName + "=" + flag
                              + ", options = " + configOptions + "]");
         }
         final ModelNode moduleOptionsNode = securityModuleNode.get(Constants.MODULE_OPTIONS);
         for (final Map.Entry<String, String> entry : configOptions.entrySet()) {
            final String optionName = entry.getKey();
            final String optionValue = entry.getValue();
            moduleOptionsNode.add(optionName, optionValue);
         }
         securityModuleNode.get(OPERATION_HEADERS).get(ALLOW_RESOURCE_SERVICE_RESTART).set(true);
         operations.add(securityModuleNode);
      }
      return true;
   }

   private ModelNode createJSSENode(final JSSE jsse, String domainName) {
      if (jsse == null) {
         if (LOGGER.isInfoEnabled()) {
            LOGGER.info("No security configuration for JSSE module.");
         }
         return null;
      }
      final ModelNode securityComponentNode = new ModelNode();
      securityComponentNode.get(OP).set(ADD);
      securityComponentNode.get(OP_ADDR).add(SUBSYSTEM, SUBSYSTEM_SECURITY);
      securityComponentNode.get(OP_ADDR).add(Constants.SECURITY_DOMAIN, domainName);

      securityComponentNode.get(OP_ADDR).add(Constants.JSSE, Constants.CLASSIC);
      addSecureStore(jsse.getTrustStore(), Constants.TRUSTSTORE, securityComponentNode);
      addSecureStore(jsse.getKeyStore(), Constants.KEYSTORE, securityComponentNode);
      securityComponentNode.get(OPERATION_HEADERS).get(ALLOW_RESOURCE_SERVICE_RESTART).set(true);
      return securityComponentNode;
   }

   private void addSecureStore(SecureStore secureStore, String storeName, ModelNode jsseNode) {
      if (secureStore == null) {
         return;
      }
      if (secureStore.getUrl() != null) {
         jsseNode.get(storeName, Constants.URL).set(secureStore.getUrl().toExternalForm());
      }
      if (secureStore.getPassword() != null) {
         jsseNode.get(storeName, Constants.PASSWORD).set(secureStore.getPassword());
      }
      if (secureStore.getType() != null) {
         jsseNode.get(storeName, Constants.TYPE).set(secureStore.getType());
      }
   }

   protected abstract SecurityDomain[] getSecurityDomains() throws Exception;
}
