/*
 * JBoss, Home of Professional Open Source.
 * Copyright (c) 2014, Red Hat, Inc., and individual contributors
 * as indicated by the @author tags. See the copyright.txt file in the
 * distribution for a full listing of individual contributors.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package org.jboss.as.test.integration.security.vault;

import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.CORE_SERVICE;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.RESULT;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.VAULT;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.VAULT_OPTIONS;
import static org.jboss.as.test.shared.integration.ejb.security.PermissionUtils.createPermissionsXmlAsset;
import static org.junit.Assert.fail;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.net.URI;
import java.net.URL;
import java.nio.file.Files;
import java.util.Iterator;
import java.util.Set;

import org.jboss.arquillian.container.test.api.Deployment;
import org.jboss.arquillian.container.test.api.RunAsClient;
import org.jboss.arquillian.junit.Arquillian;
import org.jboss.arquillian.test.api.ArquillianResource;
import org.jboss.as.arquillian.api.ServerSetup;
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.client.OperationBuilder;
import org.jboss.as.controller.operations.common.Util;
import org.jboss.as.security.vault.VaultSession;
import org.jboss.as.test.integration.security.common.Utils;
import org.jboss.as.test.integration.security.common.VaultHandler;
import org.jboss.as.test.integration.security.loginmodules.ExternalPasswordProvider;
import org.jboss.dmr.ModelNode;
import org.jboss.logging.Logger;
import org.jboss.security.util.StringPropertyReplacer;
import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.spec.WebArchive;
import org.junit.Assert;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;

/**
 * Tests for Security Vault external passwords, obtained from external command
 * or class. Each test case initialize a test vault with different password
 * type.
 *
 * @author Filip Bogyai
 */
@RunWith(Arquillian.class)
@RunAsClient
@ServerSetup({ExternalPasswordModuleSetupTask.class,
        ExternalPasswordCommandsTestCase.ExternalVaultPasswordSetup.class})
public class ExternalPasswordCommandsTestCase {

    private static Logger LOGGER = Logger.getLogger(ExternalPasswordCommandsTestCase.class);

    @ArquillianResource
    private ManagementClient managementClient;

    @ArquillianResource
    private URL url;

    public static final String ATTRIBUTE_NAME = "someAttributeName";
    public static final String VAULT_BLOCK = "someVaultBlockaj";
    public static final String VAULT_ATTRIBUTE = "secretValue";
    public static final String VAULTED_PROPERTY = "VAULT::" + VAULT_BLOCK + "::" + ATTRIBUTE_NAME + "::1";

    public static String KEYSTORE_URL = "vault.keystore";
    public static final String VAULT_PASSWORD = "VaultPassword";
    public static final String VAULT_ALIAS = "vault";
    public static String RESOURCE_LOCATION = "";
    public static final String VAULT_DAT_FILE = RESOURCE_LOCATION + "VAULT.dat";
    public static final String SALT = "87654321";
    public static final int ITER_COUNT = 47;

    private static final ExternalPasswordProvider passwordProvider = new ExternalPasswordProvider(System.getProperty("java.io.tmpdir")
            + File.separator + "tmp.counter");
    public static final PathAddress VAULT_PATH = PathAddress.pathAddress().append(CORE_SERVICE, VAULT);

    /**
     * Test deployment used for checking if vaulted password can be loaded from
     * vault configured in server config.
     */
    @Deployment(name = "vault")
    public static WebArchive appDeploymentCahce() {
        WebArchive war = ShrinkWrap.create(WebArchive.class, "vault" + ".war");
        war.addClass(CheckVaultedPassServlet.class);
        war.addAsManifestResource(Utils.getJBossDeploymentStructure("org.picketbox"), "jboss-deployment-structure.xml");
        war.addAsManifestResource(createPermissionsXmlAsset(new RuntimePermission("org.jboss.security.vault.SecurityVaultFactory.get")), "permissions.xml");
        return war;
    }

    /**
     * Testing {EXT} option password as a exact command to execute. The exact
     * command is a string delimited by ' '.
     */
    @Test
    public void testExtPassword() throws Exception {

        createVault(buildCommand("EXT", " "));

        Assert.assertEquals(VAULT_ATTRIBUTE, getVaultedPassword());

        removeVault();
    }

    /**
     * Testing {CMD} option password as a general command to execute. The
     * general command is a string delimited by ',' where the first part is the
     * actual command and further parts represents its parameters.
     */
    @Test
    public void testCmdPassword() throws Exception {

        createVault(buildCommand("CMD", ","));

        Assert.assertEquals(VAULT_ATTRIBUTE, getVaultedPassword());

        removeVault();
    }

    /**
     * Testing {CLASS[@module_name]}classname' option of password where the
     * the class @ExternalPassword is loaded from custom module and toCharArray()
     * method return password.
     */
    @Test
    @Ignore("BZ-1146006")    // fix for this should be in next PicketBox update
    public void testCustomModuleClassPassword() throws Exception {

        createVault("{CLASS@" + ExternalPasswordModuleSetupTask.getModuleName() + "}" + ExternalPassword.class.getName());

        Assert.assertEquals(VAULT_ATTRIBUTE, getVaultedPassword());

        removeVault();

    }

    /**
     * Testing {CLASS[@module_name]}classname[:ctorargs]' option of password
     * where the the class @ExternalPassword is loaded from custom module and
     * toCharArray() method return password. The class constructor takes two
     * arguments, which will be used to construct the password.
     */
    @Test
    @Ignore("BZ-1146006")    // fix for this should be in next PicketBox update
    public void testCustomModuleClassWithArguments() throws Exception {

        createVault("{CLASS@" + ExternalPasswordModuleSetupTask.getModuleName() + "}"
                + ExternalPassword.class.getName() + ":Vault,Password");

        Assert.assertEquals(VAULT_ATTRIBUTE, getVaultedPassword());

        removeVault();

    }


    /**
     * TestingTesting {CLASS[@module_name]}classname[:ctorargs]' option password
     * where the '[:ctorargs]' is an optional string delimited by the ':' from
     * the classname that will be passed to the class constructor. The class
     *
     * @TmpFilePassword constructor takes one argument with file, in which the password
     * is stored password.
     */
    @Test
    @Ignore("BZ-1146006")    // fix for this should be in next PicketBox update
    public void testPicketboxClassPassword() throws Exception {

        File tmpPassword = new File(System.getProperty("java.io.tmpdir"), "tmp.password");
        FileWriter writer = new FileWriter(tmpPassword);
        writer.write(VAULT_PASSWORD);
        writer.close();

        String passwordCmd = "{CLASS@org.picketbox}org.jboss.security.plugins.TmpFilePassword:${java.io.tmpdir}/tmp.password";
        passwordCmd = StringPropertyReplacer.replaceProperties(passwordCmd);
        createVault(passwordCmd);

        Assert.assertEquals(VAULT_ATTRIBUTE, getVaultedPassword());

        removeVault();
        tmpPassword.delete();
    }

    /**
     * Testing command which return wrong password. The vault configuration
     * should fail to create, because of wrong password.
     */
    @Test
    public void testWrongPassword() throws Exception {

        try {
            createVault("{CLASS@" + ExternalPasswordModuleSetupTask.getModuleName() + "}"
                    + ExternalPassword.class.getName() + ":Wrong,Password");
            fail();
        } catch (Exception ex) {
            // OK
        }
    }

    public String getVaultedPassword() throws Exception {

        URI vaultCheckServlet = new URI(url.toExternalForm() + CheckVaultedPassServlet.SERVLET_PATH + "?"
                + CheckVaultedPassServlet.VAULTED_PASS + "=" + VAULTED_PROPERTY);

        return Utils.makeCall(vaultCheckServlet, 200);
    }

    public void createVault(String vaultPassword) throws Exception {

        ModelNode op = new ModelNode();

        op = Util.createAddOperation(VAULT_PATH);
        ModelNode vaultOption = op.get(VAULT_OPTIONS);
        vaultOption.get("KEYSTORE_URL").set(KEYSTORE_URL);
        vaultOption.get("KEYSTORE_PASSWORD").set(vaultPassword);
        vaultOption.get("KEYSTORE_ALIAS").set(VAULT_ALIAS);
        vaultOption.get("SALT").set(SALT);
        vaultOption.get("ITERATION_COUNT").set(Integer.toString(ITER_COUNT));
        vaultOption.get("ENC_FILE_DIR").set(RESOURCE_LOCATION);
        Utils.applyUpdate(op, managementClient.getControllerClient());

        LOGGER.debug("Vault created in server configuration");

    }

    public void removeVault() throws Exception {
        ModelNode op = Util.createRemoveOperation(VAULT_PATH);
        Utils.applyUpdate(op, managementClient.getControllerClient());
    }

    /**
     * Creates runnable command for vault to retrieve password from external
     * source
     *
     * @param option    as command type EXT or CMD
     * @param delimiter to split argument from command
     * @return command to run a class which will return password
     */
    public String buildCommand(String option, String delimiter) {
        // First check for java.exe or java as the binary
        File java = new File(System.getProperty("java.home"), "/bin/java");
        File javaExe = new File(System.getProperty("java.home"), "/bin/java.exe");
        String jre;
        if (java.exists()) { jre = java.getAbsolutePath(); } else { jre = javaExe.getAbsolutePath(); }
        // Build the command to run this jre
        String cmd = jre + delimiter + "-cp" + delimiter
                + ExternalPasswordProvider.class.getProtectionDomain().getCodeSource().getLocation().getPath() + delimiter
                + "org.jboss.as.test.integration.security.loginmodules.ExternalPasswordProvider" + delimiter
                + passwordProvider.getCounterFile() + delimiter + VAULT_PASSWORD;

        return "{" + option + "}" + cmd;
    }

    static class ExternalVaultPasswordSetup implements ServerSetupTask {

        private ModelNode originalVault;
        private VaultSession nonInteractiveSession;
        private VaultHandler vaultHandler;

        @Override
        public void setup(ManagementClient managementClient, String containerId) throws Exception {

            passwordProvider.resetFileCounter();

            ModelNode op = new ModelNode();

            // save original vault setting
            LOGGER.trace("Saving original vault setting");
            op = Util.getReadAttributeOperation(VAULT_PATH, VAULT_OPTIONS);
            originalVault = (managementClient.getControllerClient().execute(new OperationBuilder(op).build())).get(RESULT);

            // remove original vault
            if (originalVault.get("KEYSTORE_URL") != null && originalVault.hasDefined("KEYSTORE_URL")) {
                op = Util.createRemoveOperation(VAULT_PATH);
                Utils.applyUpdate(op, managementClient.getControllerClient());
            }

            // create new vault
            LOGGER.trace("Creating new vault");
            clean();
            vaultHandler = new VaultHandler(KEYSTORE_URL, VAULT_PASSWORD, null, RESOURCE_LOCATION, 128, VAULT_ALIAS, SALT, ITER_COUNT);

            KEYSTORE_URL = vaultHandler.getKeyStore();
            RESOURCE_LOCATION = vaultHandler.getEncodedVaultFileDirectory();

            nonInteractiveSession = new VaultSession(KEYSTORE_URL, VAULT_PASSWORD, RESOURCE_LOCATION, SALT, ITER_COUNT);
            nonInteractiveSession.startVaultSession(VAULT_ALIAS);

            // create security attributes
            LOGGER.trace("Inserting attribute " + VAULT_ATTRIBUTE + " to vault");
            nonInteractiveSession.addSecuredAttribute(VAULT_BLOCK, ATTRIBUTE_NAME, VAULT_ATTRIBUTE.toCharArray());

        }

        @Override
        public void tearDown(ManagementClient managementClient, String containerId) throws Exception {

            ModelNode op;

            // set original vault
            if (originalVault.get("KEYSTORE_URL") != null && originalVault.hasDefined("KEYSTORE_URL")) {
                Set<String> originalVaultParam = originalVault.keys();
                Iterator<String> it = originalVaultParam.iterator();
                op = Util.createAddOperation(VAULT_PATH);
                ModelNode vaultOption = op.get(VAULT_OPTIONS);
                while (it.hasNext()) {
                    String param = it.next();
                    vaultOption.get(param).set(originalVault.get(param));
                }
                Utils.applyUpdate(op, managementClient.getControllerClient());
            }

            // remove vault files
            vaultHandler.cleanUp();
            // remove password helper file
            passwordProvider.cleanup();
        }

        private void clean() throws IOException{

            File datFile = new File(VAULT_DAT_FILE);
            if (datFile.exists()) {
                Files.delete(datFile.toPath());
            }
        }
    }

}
