/*
 * 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.jackrabbit.oak.security.authorization.composite;

import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import javax.jcr.Session;

import org.apache.jackrabbit.JcrConstants;
import org.apache.jackrabbit.api.JackrabbitSession;
import org.apache.jackrabbit.oak.api.PropertyState;
import org.apache.jackrabbit.oak.api.Root;
import org.apache.jackrabbit.oak.api.Tree;
import org.apache.jackrabbit.oak.commons.PathUtils;
import org.apache.jackrabbit.oak.plugins.memory.PropertyStates;
import org.apache.jackrabbit.oak.plugins.tree.TreeLocation;
import org.apache.jackrabbit.oak.spi.security.authorization.permission.AggregatedPermissionProvider;
import org.apache.jackrabbit.oak.spi.security.authorization.permission.OpenPermissionProvider;
import org.apache.jackrabbit.oak.spi.security.authorization.permission.PermissionProvider;
import org.apache.jackrabbit.oak.spi.security.authorization.permission.Permissions;
import org.apache.jackrabbit.oak.spi.security.authorization.permission.RepositoryPermission;
import org.apache.jackrabbit.oak.spi.security.authorization.permission.TreePermission;
import org.apache.jackrabbit.oak.spi.security.principal.EveryonePrincipal;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.junit.Test;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

/**
 * Test the effect of the 'AND' combination of
 *
 * - default permission provider (which a limited permission setup)
 * - custom provider that always grants full access and supports all permissions.
 *
 * for the {@link #getTestUser()}.
 *
 * The expected result is only the subset of permissions granted by the default
 * provider. The test user must never have full access anywhere.
 * <p>
 * Test the effect of the 'OR'ed combination of
 *
 * - default permission provider (which a limited permission setup)
 * - custom provider that always grants full access and supports all permissions.
 *
 * for the {@link #getTestUser()}.
 *
 * The expected result is the test user will have full access anywhere.

 */
public class CompositeProviderAllTest extends AbstractCompositeProviderTest {

    private CompositePermissionProvider cpp;
    private CompositePermissionProvider cppO;

    @Override
    public void before() throws Exception {
        super.before();

        cpp = createPermissionProvider(getTestUser().getPrincipal(), EveryonePrincipal.getInstance());
        cppO = createPermissionProviderOR(getTestUser().getPrincipal(), EveryonePrincipal.getInstance());
    }

    @Override
    protected AggregatedPermissionProvider getTestPermissionProvider() {
        return new OpenAggregateProvider(root);
    }

    @Override
    @Test
    public void testHasPrivilegesJcrAllOR() throws Exception {
        PermissionProvider pp = createPermissionProviderOR();
        for (String p : NODE_PATHS) {
            Tree tree = readOnlyRoot.getTree(p);
            assertTrue(p, pp.hasPrivileges(tree, JCR_ALL));
        }
    }

    @Override
    @Test
    public void testIsGrantedAll() throws Exception {
        PermissionProvider pp = createPermissionProvider();
        PermissionProvider ppo = createPermissionProviderOR();

        for (String p : NODE_PATHS) {
            Tree tree = readOnlyRoot.getTree(p);
            PropertyState ps = tree.getProperty(JcrConstants.JCR_PRIMARYTYPE);

            assertFalse(p, pp.isGranted(tree, null, Permissions.ALL));
            assertFalse(PathUtils.concat(p, JcrConstants.JCR_PRIMARYTYPE), pp.isGranted(tree, ps, Permissions.ALL));
            assertTrue(p, ppo.isGranted(tree, null, Permissions.ALL));
            assertTrue(PathUtils.concat(p, JcrConstants.JCR_PRIMARYTYPE), ppo.isGranted(tree, ps, Permissions.ALL));
        }
    }

    @Override
    @Test
    public void testHasPrivilegesOnRepoJcrAll() throws Exception {
        PermissionProvider pp = createPermissionProvider();
        assertFalse(pp.hasPrivileges(null, JCR_ALL));
        PermissionProvider ppo = createPermissionProviderOR();
        assertTrue(ppo.hasPrivileges(null, JCR_ALL));
    }

    @Override
    @Test
    public void testIsNotGranted() throws Exception {
        PermissionProvider pp = createPermissionProvider();
        PermissionProvider ppo = createPermissionProviderOR();

        for (String p : NODE_PATHS) {
            Tree tree = readOnlyRoot.getTree(p);
            PropertyState ps = tree.getProperty(JcrConstants.JCR_PRIMARYTYPE);

            assertFalse(p, pp.isGranted(tree, null, Permissions.MODIFY_ACCESS_CONTROL));
            assertFalse(PathUtils.concat(p, JcrConstants.JCR_PRIMARYTYPE), pp.isGranted(tree, ps, Permissions.MODIFY_ACCESS_CONTROL));
            assertTrue(p, ppo.isGranted(tree, null, Permissions.MODIFY_ACCESS_CONTROL));
            assertTrue(PathUtils.concat(p, JcrConstants.JCR_PRIMARYTYPE), ppo.isGranted(tree, ps, Permissions.MODIFY_ACCESS_CONTROL));
        }
    }

    @Override
    @Test
    public void testIsNotGrantedAction() throws Exception {
        PermissionProvider pp = createPermissionProvider();
        PermissionProvider ppo = createPermissionProviderOR();
        String[] actions = new String[]{JackrabbitSession.ACTION_LOCKING, JackrabbitSession.ACTION_MODIFY_ACCESS_CONTROL};

        for (String nodePath : NODE_PATHS) {
            String actionStr = getActionString(actions);
            assertFalse(nodePath, pp.isGranted(nodePath, actionStr));
            assertTrue(nodePath, ppo.isGranted(nodePath, actionStr));

            String propPath = PathUtils.concat(nodePath, JcrConstants.JCR_PRIMARYTYPE);
            assertFalse(propPath, pp.isGranted(propPath, actionStr));
            assertTrue(propPath, ppo.isGranted(propPath, actionStr));

            String nonExPath = PathUtils.concat(nodePath, "nonExisting");
            assertFalse(nonExPath, pp.isGranted(nonExPath, actionStr));
            assertTrue(nonExPath, ppo.isGranted(nonExPath, actionStr));
        }
    }

    @Override
    @Test
    public void testTreePermissionIsGrantedAllOR() throws Exception {
        PermissionProvider pp = createPermissionProviderOR();
        TreePermission parentPermission = TreePermission.EMPTY;

        PropertyState ps = PropertyStates.createProperty("propName", "val");

        for (String path : TP_PATHS) {
            Tree t = readOnlyRoot.getTree(path);
            TreePermission tp = pp.getTreePermission(t, parentPermission);

            assertTrue(tp.isGranted(Permissions.ALL));
            assertTrue(tp.isGranted(Permissions.ALL, ps));

            parentPermission = tp;
        }
    }

    @Override
    @Test
    public void testTreePermissionIsNotGrantedOR() throws Exception {
        PermissionProvider pp = createPermissionProviderOR();
        TreePermission parentPermission = TreePermission.EMPTY;

        PropertyState ps = PropertyStates.createProperty("propName", "val");

        for (String path : TP_PATHS) {
            Tree t = readOnlyRoot.getTree(path);
            TreePermission tp = pp.getTreePermission(t, parentPermission);

            assertFalse(tp.isGranted(Permissions.NO_PERMISSION));
            assertTrue(tp.isGranted(Permissions.MODIFY_ACCESS_CONTROL));
            assertFalse(tp.isGranted(Permissions.NO_PERMISSION, ps));
            assertTrue(tp.isGranted(Permissions.MODIFY_ACCESS_CONTROL, ps));

            parentPermission = tp;
        }
    }

    @Override
    @Test
    public void testTreePermissionCanReadPropertiesOR() throws Exception {
        PermissionProvider pp = createPermissionProviderOR();
        TreePermission parentPermission = TreePermission.EMPTY;

        for (String path : TP_PATHS) {
            TreePermission tp = pp.getTreePermission(readOnlyRoot.getTree(path), parentPermission);
            assertTrue(tp.canReadProperties());

            parentPermission = tp;
        }
    }

    @Override
    @Test
    public void testRepositoryPermissionIsNotGrantedOR() throws Exception {
        RepositoryPermission rp = createPermissionProviderOR().getRepositoryPermission();
        assertTrue(rp.isGranted(Permissions.PRIVILEGE_MANAGEMENT));
        assertTrue(rp.isGranted(Permissions.NAMESPACE_MANAGEMENT | Permissions.PRIVILEGE_MANAGEMENT));
        assertTrue(rp.isGranted(Permissions.WORKSPACE_MANAGEMENT));
        assertTrue(rp.isGranted(Permissions.ALL));
        assertFalse(rp.isGranted(Permissions.NO_PERMISSION));
    }

    @Test
    public void testGetPrivileges() throws Exception {
        for (String p : defPrivileges.keySet()) {
            Set<String> expected = defPrivileges.get(p);
            Tree tree = root.getTree(p);
            assertEquals(p, expected, cpp.getPrivileges(tree));
            assertEquals(p, Set.of(JCR_ALL), cppO.getPrivileges(tree));
        }
    }

    @Test
    public void testGetPrivilegesOnRepo() throws Exception {
        Set<String> privilegeNames = cpp.getPrivileges(null);
        assertEquals(Set.of(JCR_NAMESPACE_MANAGEMENT, JCR_NODE_TYPE_DEFINITION_MANAGEMENT), privilegeNames);
        Set<String> privilegeNamesO = cppO.getPrivileges(null);
        assertEquals(Set.of(JCR_ALL), privilegeNamesO);
    }


    @Test
    public void testHasPrivileges() throws Exception {
        for (String p : defPrivileges.keySet()) {
            Set<String> expected = defPrivileges.get(p);
            Tree tree = root.getTree(p);
            assertTrue(p, cpp.hasPrivileges(tree, expected.toArray(new String[expected.size()])));
            assertTrue(p, cppO.hasPrivileges(tree, expected.toArray(new String[expected.size()])));
        }
    }

    @Test
    public void testHasPrivilegesOnRepo() throws Exception {
        assertTrue(cpp.hasPrivileges(null, JCR_NAMESPACE_MANAGEMENT, JCR_NODE_TYPE_DEFINITION_MANAGEMENT));
        assertTrue(cppO.hasPrivileges(null, JCR_NAMESPACE_MANAGEMENT, JCR_NODE_TYPE_DEFINITION_MANAGEMENT));
    }

    @Test
    public void testIsGranted() throws Exception {
        for (String p : defPermissions.keySet()) {
            long expected = defPermissions.get(p);
            Tree tree = readOnlyRoot.getTree(p);

            assertTrue(p, cpp.isGranted(tree, null, expected));
            assertTrue(p, cppO.isGranted(tree, null, expected));
        }
    }

    @Test
    public void testIsGrantedProperty() throws Exception {
        for (String p : defPermissions.keySet()) {
            long expected = defPermissions.get(p);
            Tree tree = readOnlyRoot.getTree(p);

            assertTrue(p, cpp.isGranted(tree, PROPERTY_STATE, expected));
            assertTrue(p, cppO.isGranted(tree, PROPERTY_STATE, expected));
        }
    }

    @Test
    public void testIsGrantedAction() throws Exception {
        for (String p : defActionsGranted.keySet()) {
            String actionStr = getActionString(defActionsGranted.get(p));
            assertTrue(p + " : " + actionStr, cpp.isGranted(p, actionStr));
            assertTrue(p + " : " + actionStr, cppO.isGranted(p, actionStr));
        }
    }

    @Test
    public void testIsGrantedAction2() throws Exception {
        Map<String, String[]> noAccess = Map.of(
                ROOT_PATH, new String[] {Session.ACTION_READ},
                ROOT_PATH + "jcr:primaryType", new String[] {Session.ACTION_READ, Session.ACTION_SET_PROPERTY},
                "/nonexisting", new String[] {Session.ACTION_READ, Session.ACTION_ADD_NODE},
                TEST_PATH_2, new String[] {Session.ACTION_READ, Session.ACTION_REMOVE},
                TEST_PATH_2 + "/jcr:primaryType", new String[] {Session.ACTION_READ, Session.ACTION_SET_PROPERTY},
                TEST_A_B_C_PATH, new String[] {Session.ACTION_READ, Session.ACTION_REMOVE},
                TEST_A_B_C_PATH + "/noneExisting", new String[] {Session.ACTION_READ, JackrabbitSession.ACTION_REMOVE_NODE},
                TEST_A_B_C_PATH + "/jcr:primaryType", new String[] {JackrabbitSession.ACTION_REMOVE_PROPERTY});

        for (String p : noAccess.keySet()) {
            assertFalse(p, cpp.isGranted(p, getActionString(noAccess.get(p))));
            assertTrue(p, cppO.isGranted(p, getActionString(noAccess.get(p))));
        }
    }

    @Test
    public void testRepositoryPermissionsIsGranted() throws Exception {
        RepositoryPermission rp = cpp.getRepositoryPermission();
        assertTrue(rp.isGranted(Permissions.NAMESPACE_MANAGEMENT));
        assertTrue(rp.isGranted(Permissions.NODE_TYPE_DEFINITION_MANAGEMENT));
        assertTrue(rp.isGranted(Permissions.NAMESPACE_MANAGEMENT | Permissions.NODE_TYPE_DEFINITION_MANAGEMENT));

        RepositoryPermission rpO = cpp.getRepositoryPermission();
        assertTrue(rpO.isGranted(Permissions.NAMESPACE_MANAGEMENT));
        assertTrue(rpO.isGranted(Permissions.NODE_TYPE_DEFINITION_MANAGEMENT));
        assertTrue(rpO.isGranted(Permissions.NAMESPACE_MANAGEMENT | Permissions.NODE_TYPE_DEFINITION_MANAGEMENT));
    }

    @Test
    public void testTreePermissionIsGranted() throws Exception {
        TreePermission parentPermission = TreePermission.EMPTY;

        for (String path : TP_PATHS) {
            TreePermission tp = cpp.getTreePermission(root.getTree(path), parentPermission);
            Long toTest = (defPermissions.containsKey(path)) ? defPermissions.get(path) : defPermissions.get(PathUtils.getAncestorPath(path, 1));
            if (toTest != null) {
                assertTrue(tp.isGranted(toTest));
            }
            parentPermission = tp;
        }
    }

    @Test
    public void testTreePermissionIsGrantedOR() throws Exception {
        TreePermission parentPermission = TreePermission.EMPTY;

        for (String path : TP_PATHS) {
            TreePermission tp = cppO.getTreePermission(root.getTree(path), parentPermission);
            Long toTest = (defPermissions.containsKey(path)) ? defPermissions.get(path) : defPermissions.get(PathUtils.getAncestorPath(path, 1));
            if (toTest != null) {
                assertTrue(tp.isGranted(toTest));
            }
            parentPermission = tp;
        }
    }

    @Test
    public void testTreePermissionIsGrantedProperty() throws Exception {
        TreePermission parentPermission = TreePermission.EMPTY;

        for (String path : TP_PATHS) {
            TreePermission tp = cpp.getTreePermission(readOnlyRoot.getTree(path), parentPermission);
            Long toTest = (defPermissions.containsKey(path)) ? defPermissions.get(path) : defPermissions.get(PathUtils.getAncestorPath(path, 1));
            if (toTest != null) {
                assertTrue(tp.isGranted(toTest, PROPERTY_STATE));
            }
            parentPermission = tp;
        }
    }

    @Test
    public void testTreePermissionIsGrantedPropertyOR() throws Exception {
        TreePermission parentPermission = TreePermission.EMPTY;

        for (String path : TP_PATHS) {
            TreePermission tp = cppO.getTreePermission(readOnlyRoot.getTree(path), parentPermission);
            Long toTest = (defPermissions.containsKey(path)) ? defPermissions.get(path) : defPermissions.get(PathUtils.getAncestorPath(path, 1));
            if (toTest != null) {
                assertTrue(tp.isGranted(toTest, PROPERTY_STATE));
            }
            parentPermission = tp;
        }
    }

    @Test
    public void testTreePermissionCanRead() throws Exception {
        // order is relevant here
        Map<String, Boolean> readMap = new LinkedHashMap<>();
        readMap.put(ROOT_PATH, false);
        readMap.put(TEST_PATH, true);
        readMap.put(TEST_A_PATH, true);
        readMap.put(TEST_A_B_PATH, true);
        readMap.put(TEST_A_B_C_PATH, false);
        readMap.put(TEST_A_B_C_PATH + "/nonexisting", false);

        TreePermission parentPermission = TreePermission.EMPTY;
        for (String nodePath : readMap.keySet()) {
            Tree tree = readOnlyRoot.getTree(nodePath);
            TreePermission tp = cpp.getTreePermission(tree, parentPermission);

            boolean expectedResult = readMap.get(nodePath);
            assertEquals(nodePath, expectedResult, tp.canRead());

            parentPermission = tp;
        }
    }

    @Test
    public void testTreePermissionCanReadOR() throws Exception {
        // order is relevant here
        Map<String, Boolean> readMap = new LinkedHashMap<>();
        readMap.put(ROOT_PATH, true);
        readMap.put(TEST_PATH, true);
        readMap.put(TEST_A_PATH, true);
        readMap.put(TEST_A_B_PATH, true);
        readMap.put(TEST_A_B_C_PATH, true);
        readMap.put(TEST_A_B_C_PATH + "/nonexisting", true);

        TreePermission parentPermission = TreePermission.EMPTY;
        for (String nodePath : readMap.keySet()) {
            Tree tree = readOnlyRoot.getTree(nodePath);
            TreePermission tp = cppO.getTreePermission(tree, parentPermission);

            boolean expectedResult = readMap.get(nodePath);
            assertEquals(nodePath, expectedResult, tp.canRead());

            parentPermission = tp;
        }
    }

    @Test
    public void testTreePermissionCanReadProperty() throws Exception {
        // order is relevant here
        Map<String, Boolean> readMap = new LinkedHashMap<>();
        readMap.put(ROOT_PATH, false);
        readMap.put(TEST_PATH, true);
        readMap.put(TEST_A_PATH, true);
        readMap.put(TEST_A_B_PATH, true);
        readMap.put(TEST_A_B_C_PATH, true);
        readMap.put(TEST_A_B_C_PATH + "/nonexisting", true);

        TreePermission parentPermission = TreePermission.EMPTY;
        for (String nodePath : readMap.keySet()) {
            Tree tree = readOnlyRoot.getTree(nodePath);

            TreePermission tp = cpp.getTreePermission(tree, parentPermission);

            boolean expectedResult = readMap.get(nodePath);
            assertEquals(nodePath, expectedResult, tp.canRead(PROPERTY_STATE));

            parentPermission = tp;
        }
    }

    @Test
    public void testTreePermissionCanReadPropertyOR() throws Exception {
        // order is relevant here
        Map<String, Boolean> readMap = new LinkedHashMap<>();
        readMap.put(ROOT_PATH, true);
        readMap.put(TEST_PATH, true);
        readMap.put(TEST_A_PATH, true);
        readMap.put(TEST_A_B_PATH, true);
        readMap.put(TEST_A_B_C_PATH, true);
        readMap.put(TEST_A_B_C_PATH + "/nonexisting", true);

        TreePermission parentPermission = TreePermission.EMPTY;
        for (String nodePath : readMap.keySet()) {
            Tree tree = readOnlyRoot.getTree(nodePath);

            TreePermission tp = cppO.getTreePermission(tree, parentPermission);

            boolean expectedResult = readMap.get(nodePath);
            assertEquals(nodePath, expectedResult, tp.canRead(PROPERTY_STATE));

            parentPermission = tp;
        }
    }

    /**
     * Custom permission provider that supports all permissions and grants
     * full access for everyone.
     */
    private static final class OpenAggregateProvider extends AbstractAggrProvider {

        private static final PermissionProvider BASE = OpenPermissionProvider.getInstance();

        private OpenAggregateProvider(@NotNull Root root) {
            super(root);
        }

        //-----------------------------------< AggregatedPermissionProvider >---

        @Override
        public boolean isGranted(@NotNull TreeLocation location, long permissions) {
            return true;
        }

        //---------------------------------------------< PermissionProvider >---
        @NotNull
        @Override
        public Set<String> getPrivileges(@Nullable Tree tree) {
            return BASE.getPrivileges(tree);
        }

        @Override
        public boolean hasPrivileges(@Nullable Tree tree, @NotNull String... privilegeNames) {
            return BASE.hasPrivileges(tree, privilegeNames);
        }

        @NotNull
        @Override
        public RepositoryPermission getRepositoryPermission() {
            return BASE.getRepositoryPermission();
        }

        @NotNull
        @Override
        public TreePermission getTreePermission(@NotNull Tree tree, @NotNull TreePermission parentPermission) {
            return BASE.getTreePermission(tree, parentPermission);
        }

        @Override
        public boolean isGranted(@NotNull Tree tree, @Nullable PropertyState property, long permissions) {
            return BASE.isGranted(tree, property, permissions);
        }

        @Override
        public boolean isGranted(@NotNull String oakPath, @NotNull String jcrActions) {
            return BASE.isGranted(oakPath, jcrActions);
        }
    }
}
