/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2013, 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.domain.rbac;

import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.ADD;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.BASE_ROLE;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.HOSTS;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.PROFILE;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.REMOVE;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.SUBSYSTEM;
import static org.jboss.as.test.integration.management.util.ModelUtil.createOpNode;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.management.Attribute;
import javax.management.JMRuntimeException;
import javax.management.MBeanServerConnection;
import javax.management.ObjectName;
import javax.management.remote.JMXServiceURL;
import org.apache.commons.lang.ArrayUtils;
import org.jboss.as.controller.PathAddress;
import org.jboss.as.controller.PathElement;
import org.jboss.as.controller.client.helpers.Operations;
import org.jboss.as.controller.client.helpers.domain.DomainClient;
import org.jboss.as.network.NetworkUtils;
import org.jboss.as.test.integration.domain.suites.FullRbacProviderTestSuite;
import org.jboss.as.test.integration.management.interfaces.JmxManagementInterface;
import org.jboss.as.test.integration.management.rbac.Outcome;
import org.jboss.as.test.integration.management.rbac.RbacAdminCallbackHandler;
import org.jboss.as.test.integration.management.rbac.RbacUtil;
import org.jboss.as.test.integration.management.rbac.UserRolesMappingServerSetupTask;
import org.jboss.as.test.jmx.JMXServiceDeploymentSetupTask;
import org.jboss.dmr.ModelNode;
import org.jboss.sasl.util.UsernamePasswordHashUtil;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;

/**
 * Tests of server group scoped roles using the "rbac" access control provider.
 *
 * @author Brian Stansberry (c) 2013 Red Hat Inc.
 */
public class JmxRBACProviderHostScopedRolesTestCase extends AbstractHostScopedRolesTestCase {
    private static JMXServiceDeploymentSetupTask jmxTask = new JMXServiceDeploymentSetupTask();
    public static final String  OBJECT_NAME = "jboss.test:service=testdeployments";
    private static final String SLAVE_HOST_USER = "SlaveHostSuperUser";
    private static final String SLAVE_HOST_ROLE = "SlaveHostSuperUser";
    private boolean mbeanSensitivity = true;

    @BeforeClass
    public static void setupDomain() throws Exception {
        testSupport = FullRbacProviderTestSuite.createSupport(JmxRBACProviderHostScopedRolesTestCase.class.getSimpleName());
        masterClientConfig = testSupport.getDomainMasterConfiguration();
        DomainClient domainClient = testSupport.getDomainMasterLifecycleUtil().getDomainClient();
        setupRoles(domainClient);
        setNonCoreMbeanSensitivity(domainClient, true);
        HostRolesMappingSetup.INSTANCE.setup(domainClient);
        deployDeployment1(domainClient);
        jmxTask.setup(domainClient, SERVER_GROUP_A);
        List<String> users = new ArrayList<String>(USERS.length);
        for (int i = 0; i < USERS.length; i++) {
            users.add(USERS[i] + "=" + new UsernamePasswordHashUtil().generateHashedHexURP(USERS[i], "ApplicationRealm", RbacAdminCallbackHandler.STD_PASSWORD.toCharArray()));
        }
        users.add(SLAVE_HOST_USER + "=" + new UsernamePasswordHashUtil().generateHashedHexURP(SLAVE_HOST_USER, "ApplicationRealm", RbacAdminCallbackHandler.STD_PASSWORD.toCharArray()));
        createApplicationUsers(masterClientConfig.getJbossHome() + File.separatorChar + "domain"+ File.separatorChar +"configuration", users);
    }

    protected static void setupRoles(DomainClient domainClient) throws IOException {
        AbstractHostScopedRolesTestCase.setupRoles(domainClient);
        ModelNode op = createOpNode(SCOPED_ROLE + SLAVE_HOST_ROLE, ADD);
        op.get(BASE_ROLE).set(RbacUtil.SUPERUSER_USER);
        op.get(HOSTS).add(SLAVE);
        RbacUtil.executeOperation(domainClient, op, Outcome.SUCCESS);
    }

    protected static void tearDownRoles(DomainClient domainClient) throws IOException {
        AbstractHostScopedRolesTestCase.tearDownRoles(domainClient);
        ModelNode op = createOpNode(SCOPED_ROLE + SLAVE_HOST_ROLE, REMOVE);
        RbacUtil.executeOperation(domainClient, op, Outcome.SUCCESS);
    }

    private static void setNonCoreMbeanSensitivity(DomainClient domainClient, boolean sensitivity) throws IOException {
        ModelNode op = Operations.createWriteAttributeOperation(
                PathAddress.pathAddress(PathElement.pathElement(PROFILE, "profile-a"), PathElement.pathElement(SUBSYSTEM, "jmx")).toModelNode(),
                "non-core-mbean-sensitivity", sensitivity);
        ModelNode result = domainClient.execute(op);
        assertTrue(Operations.isSuccessfulOutcome(result));
        op = Operations.createWriteAttributeOperation(
                PathAddress.pathAddress(PathElement.pathElement(PROFILE, "profile-b"), PathElement.pathElement(SUBSYSTEM, "jmx")).toModelNode(),
                "non-core-mbean-sensitivity", sensitivity);
        result = domainClient.execute(op);
        assertTrue(Operations.isSuccessfulOutcome(result));
    }

    @After
    public void activateMBeanSensitivity() throws IOException {
        mbeanSensitivity = true;
        setNonCoreMbeanSensitivity(testSupport.getDomainMasterLifecycleUtil().getDomainClient(), true);
    }

    protected void deactivateMBeanSensitivity() throws IOException {
        mbeanSensitivity = false;
        setNonCoreMbeanSensitivity(testSupport.getDomainMasterLifecycleUtil().getDomainClient(), false);
    }

    @AfterClass
    public static void tearDownDomain() throws Exception {
        DomainClient domainClient = testSupport.getDomainMasterLifecycleUtil().getDomainClient();
        try {
            HostRolesMappingSetup.INSTANCE.tearDown(domainClient);
        } finally {
            try {
                tearDownRoles(domainClient);
            } finally {
                try {
                    removeDeployment1(domainClient);
                } finally {
                    try {
                        setNonCoreMbeanSensitivity(domainClient, false);
                        jmxTask.tearDown(domainClient, SERVER_GROUP_A);
                    } finally {
                        FullRbacProviderTestSuite.stopSupport();
                        testSupport = null;
                    }
                }
            }
        }
    }

    @Test
    @Override
    public void testMonitor() throws Exception {
        test(MONITOR_USER);
    }

    @Test
    @Override
    public void testOperator() throws Exception {
        test(OPERATOR_USER);
    }

    @Test
    @Override
    public void testMaintainer() throws Exception {
        test(MAINTAINER_USER);
    }

    @Test
    @Override
    public void testDeployer() throws Exception {
        test(DEPLOYER_USER);
    }

    @Test
    @Override
    public void testAdministrator() throws Exception {
        test(ADMINISTRATOR_USER);
    }

    @Test
    @Override
    public void testAuditor() throws Exception {
        test(AUDITOR_USER);
    }

    @Test
    @Override
    public void testSuperUser() throws Exception {
        test(SUPERUSER_USER);
    }


    @Test
    public void testSlaveSuperUser() throws Exception {
        test(SLAVE_HOST_USER);
    }

    protected boolean isReadAllowed(String userName) {
        if (MONITOR_USER.equals(userName)
                || DEPLOYER_USER.equals(userName)
                || MAINTAINER_USER.equals(userName)
                || OPERATOR_USER.equals(userName)) {
            return !mbeanSensitivity;
        }
        return ADMINISTRATOR_USER.equals(userName)
                || AUDITOR_USER.equals(userName)
                || SUPERUSER_USER.equals(userName);
    }

    protected boolean isWriteAllowed(String userName) {
        if (MAINTAINER_USER.equals(userName) || OPERATOR_USER.equals(userName)) {
            return !mbeanSensitivity;
        }
        return ADMINISTRATOR_USER.equals(userName) || SUPERUSER_USER.equals(userName);
    }

    private void test(String userName) throws Exception {
        String urlString = System.getProperty("jmx.service.url", "service:jmx:remoting-jmx://"
                        + NetworkUtils.formatPossibleIpv6Address(masterClientConfig.getHostControllerManagementAddress())
                        + ":12345");
        JmxManagementInterface jmx = JmxManagementInterface.create(new JMXServiceURL(urlString),
                userName, RbacAdminCallbackHandler.STD_PASSWORD,
                null // not needed, as the only thing from JmxManagementInterface used in this test is getConnection()
        );
        try {
            getAttribute(userName, jmx);
            setAttribute(userName, jmx);
            dumpServices(userName, jmx);
            operationReadOnly(userName, jmx);
            operationWriteOnly(userName, jmx);
            operationReadWrite(userName, jmx);
            operationUnknown(userName, jmx);
            deactivateMBeanSensitivity();
            getAttribute(userName, jmx);
            setAttribute(userName, jmx);
            dumpServices(userName, jmx);
            operationReadOnly(userName, jmx);
            operationWriteOnly(userName, jmx);
            operationReadWrite(userName, jmx);
            operationUnknown(userName, jmx);
        } finally {
            jmx.close();
        }
    }

    // test utils

    private void getAttribute(String userName, JmxManagementInterface jmx) throws Exception {
        boolean successExpected = isReadAllowed(userName);
        MBeanServerConnection connection = jmx.getConnection();
        ObjectName domain = new ObjectName("java.lang:type=OperatingSystem");
        try {
            Object attribute = connection.getAttribute(domain, "Name");
            assertTrue("Failure was expected", successExpected);
            assertEquals(System.getProperty("os.name"), attribute.toString());
        } catch (JMRuntimeException e) {
            if (e.getMessage().contains("JBAS011360")) {
                assertFalse("Success was expected but failure happened: " + e, successExpected);
            } else {
                throw e;
            }
        }
    }

    private void setAttribute(String userName, JmxManagementInterface jmx) throws Exception {
        boolean successExpected = isWriteAllowed(userName);

        MBeanServerConnection connection = jmx.getConnection();
        ObjectName domain = new ObjectName("java.lang:type=Memory");
        try {
            connection.setAttribute(domain, new Attribute("Verbose", true));
            connection.setAttribute(domain, new Attribute("Verbose", false)); // back to default to not pollute the logs
            assertTrue("Failure was expected", successExpected);
        } catch (JMRuntimeException e) {
            if (e.getMessage().contains("JBAS011360")) {
                assertFalse("Success was expected but failure happened: " + e, successExpected);
            } else {
                throw e;
            }
        }
    }

    private void dumpServices(String userName, JmxManagementInterface jmx) throws Exception {
        boolean successExpected = isWriteAllowed(userName);
        doOperation(successExpected, "jboss.msc:type=container,name=jboss-as", "dumpServices", jmx);
    }

    private void operationReadOnly(String userName, JmxManagementInterface jmx) throws Exception {
        boolean successExpected = isReadAllowed(userName);
        doOperation(successExpected, OBJECT_NAME, "helloReadOnly", jmx);
    }

    private void operationWriteOnly(String userName, JmxManagementInterface jmx) throws Exception {
        boolean successExpected = isWriteAllowed(userName);
        doOperation(successExpected, OBJECT_NAME, "helloWriteOnly", jmx);
    }

    private void operationReadWrite(String userName, JmxManagementInterface jmx) throws Exception {
        boolean successExpected = isWriteAllowed(userName);
        doOperation(successExpected, OBJECT_NAME, "helloReadWrite", jmx);
    }

    private void operationUnknown(String userName, JmxManagementInterface jmx) throws Exception {
        boolean successExpected = isWriteAllowed(userName);
        doOperation(successExpected, OBJECT_NAME, "helloUnknown", jmx);
    }

    private void doOperation(boolean successExpected, String objectName, String operationName, JmxManagementInterface jmx) throws Exception {
        MBeanServerConnection connection = jmx.getConnection();
        ObjectName domain = new ObjectName(objectName);
        try {
            connection.invoke(domain, operationName, ArrayUtils.EMPTY_OBJECT_ARRAY, ArrayUtils.EMPTY_STRING_ARRAY);
            assertTrue("Failure was expected but success happened", successExpected);
        } catch (JMRuntimeException e) {
            if (e.getMessage().contains("JBAS011360")) {
                assertFalse("Success was expected but failure happened: " + e, successExpected);
            } else {
                throw e;
            }
        }
    }

    @Override
    protected boolean isAllowLocalAuth() {
        return false;
    }

    @Override
    protected void configureRoles(ModelNode op, String[] roles) {
        // no-op. Role mapping is done based on the client's authenticated Subject
    }

    static class HostRolesMappingSetup extends UserRolesMappingServerSetupTask {

        private static final Map<String, Set<String>> STANDARD_USERS;

        static {
            Map<String, Set<String>> rolesToUsers = new HashMap<String, Set<String>>();
            rolesToUsers.put(MONITOR_USER, Collections.singleton(MONITOR_USER));
            rolesToUsers.put(OPERATOR_USER, Collections.singleton(OPERATOR_USER));
            rolesToUsers.put(MAINTAINER_USER, Collections.singleton(MAINTAINER_USER));
            rolesToUsers.put(DEPLOYER_USER, Collections.singleton(DEPLOYER_USER));
            rolesToUsers.put(ADMINISTRATOR_USER, Collections.singleton(ADMINISTRATOR_USER));
            rolesToUsers.put(AUDITOR_USER, Collections.singleton(AUDITOR_USER));
            rolesToUsers.put(SUPERUSER_USER, Collections.singleton(SUPERUSER_USER));
            rolesToUsers.put(SLAVE_HOST_ROLE, Collections.singleton(SLAVE_HOST_USER));
            STANDARD_USERS = rolesToUsers;
        }

        static final HostRolesMappingSetup INSTANCE = new HostRolesMappingSetup();

        protected HostRolesMappingSetup() {
            super(STANDARD_USERS);
        }
    }
}
