/*
 * 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.spi.security.authorization.principalbased.impl;

import org.apache.jackrabbit.guava.common.collect.ImmutableList;
import org.apache.jackrabbit.guava.common.collect.ImmutableSet;
import org.apache.jackrabbit.api.security.JackrabbitAccessControlList;
import org.apache.jackrabbit.api.security.JackrabbitAccessControlManager;
import org.apache.jackrabbit.api.security.JackrabbitAccessControlPolicy;
import org.apache.jackrabbit.api.security.user.User;
import org.apache.jackrabbit.oak.api.CommitFailedException;
import org.apache.jackrabbit.oak.api.Root;
import org.apache.jackrabbit.oak.api.Tree;
import org.apache.jackrabbit.oak.api.Type;
import org.apache.jackrabbit.oak.commons.PathUtils;
import org.apache.jackrabbit.oak.plugins.tree.TreeUtil;
import org.apache.jackrabbit.oak.spi.nodetype.NodeTypeConstants;
import org.apache.jackrabbit.oak.spi.security.authorization.AuthorizationConfiguration;
import org.apache.jackrabbit.oak.spi.security.authorization.accesscontrol.ImmutableACL;
import org.apache.jackrabbit.oak.spi.security.authorization.accesscontrol.ReadPolicy;
import org.apache.jackrabbit.oak.spi.security.authorization.permission.PermissionConstants;
import org.apache.jackrabbit.oak.spi.security.privilege.PrivilegeConstants;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.junit.Before;
import org.junit.Test;

import javax.jcr.AccessDeniedException;
import javax.jcr.RepositoryException;
import javax.jcr.SimpleCredentials;
import javax.jcr.security.AccessControlEntry;
import javax.jcr.security.AccessControlException;
import javax.jcr.security.AccessControlPolicy;
import javax.jcr.security.Privilege;
import java.security.Principal;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import static org.apache.jackrabbit.oak.spi.security.authorization.principalbased.impl.Constants.NT_REP_PRINCIPAL_ENTRY;
import static org.apache.jackrabbit.oak.spi.security.authorization.principalbased.impl.Constants.REP_EFFECTIVE_PATH;
import static org.apache.jackrabbit.oak.spi.security.authorization.principalbased.impl.Constants.REP_PRINCIPAL_POLICY;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

/**
 * Tests for PrincipalBasedAccessControlManager where the editing session (based on a regular user with default
 * permission evaluation) lacks permissions to read/modify access control on the target system-principal.
 */
public class AccessControlManagerLimitedUserTest extends AbstractPrincipalBasedTest implements PrivilegeConstants {

    Principal systemPrincipal;
    String systemPrincipalPath;

    Principal testPrincipal;
    Root testRoot;
    JackrabbitAccessControlManager testAcMgr;

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

        User systemUser = getTestSystemUser();
        systemPrincipalPath = systemUser.getPath();
        systemPrincipal = getTestSystemUser().getPrincipal();

        testPrincipal = createTestPrincipal();

        setupContentTrees(TEST_OAK_PATH);

        // grant test-user full read access (but not read-access control!)
        grant(testPrincipal, PathUtils.ROOT_PATH, JCR_READ);

        // trigger creation of principal policy with testPrincipal with 2 random entries
        PrincipalPolicyImpl policy = setupPrincipalBasedAccessControl(systemPrincipal, testContentJcrPath, JCR_NODE_TYPE_MANAGEMENT);
        addPrincipalBasedEntry(policy, null, JCR_NAMESPACE_MANAGEMENT);
        root.commit();

        testRoot = createTestRoot();
        testAcMgr = createAccessControlManager(testRoot);
    }

    @Override
    public void after() throws Exception {
        try {
            if (testRoot != null) {
                testRoot.getContentSession().close();
            }
        } finally {
            super.after();
        }
    }

    Principal createTestPrincipal() throws Exception {
        return getTestUser().getPrincipal();
    }

    Root createTestRoot() throws Exception {
        User testUser = getTestUser();
        return login(new SimpleCredentials(testUser.getID(), testUser.getID().toCharArray())).getLatestRoot();
    }

    void grant(@NotNull Principal principal, @Nullable String path, @NotNull String... privNames) throws Exception {
        addDefaultEntry(path, principal, privNames);
    }

    private static void assertEmptyPolicies(@NotNull AccessControlPolicy[] policies) {
        assertEquals(0, policies.length);
    }

    private static void assertPolicies(@NotNull AccessControlPolicy[] policies, Class<? extends JackrabbitAccessControlList> expectedClass, int expectedSize, int expectedEntrySize) {
        assertEquals(expectedSize, policies.length);
        if (expectedSize > 0) {
            assertTrue(expectedClass.isAssignableFrom(policies[0].getClass()));
            assertEquals(expectedEntrySize, ((JackrabbitAccessControlList) policies[0]).size());
        }
    }

    @NotNull
    private String getPolicyPath() throws Exception {
        JackrabbitAccessControlPolicy[] policies = createAccessControlManager(root).getPolicies(systemPrincipal);
        assertEquals(1, policies.length);
        return PathUtils.concat(((PrincipalPolicyImpl) policies[0]).getOakPath(), REP_PRINCIPAL_POLICY);
    }

    @NotNull
    private String getEntryPath() throws Exception {
        Tree policyTree = root.getTree(getPolicyPath());
        assertTrue(policyTree.exists());

        for (Tree child : policyTree.getChildren()) {
            if (Utils.isPrincipalEntry(child)) {
                return child.getPath();
            }
        }
        throw new RepositoryException("unable to locate policy entry");
    }

    @Test(expected = AccessDeniedException.class)
    public void testGetApplicableByPath() throws RepositoryException {
        testAcMgr.getApplicablePolicies(testJcrPath);
    }

    @Test(expected = AccessDeniedException.class)
    public void testGetPoliciesByPath() throws RepositoryException {
        testAcMgr.getPolicies(testJcrPath);
    }

    @Test(expected = AccessDeniedException.class)
    public void testGetEffectiveByPathNoAccess() throws RepositoryException {
        testAcMgr.getEffectivePolicies(testJcrPath);
    }

    @Test(expected = AccessDeniedException.class)
    public void testGetEffectiveByNullPath() throws RepositoryException {
        testAcMgr.getEffectivePolicies((String) null);
    }

    @Test(expected = AccessDeniedException.class)
    public void testGetEffectiveByRooyPath() throws RepositoryException {
        testAcMgr.getEffectivePolicies(PathUtils.ROOT_PATH);
    }

    @Test(expected = AccessDeniedException.class)
    public void testGetEffectiveByPathReadAccessControlOnPrincipal() throws Exception {
        // grant testuser read-access control on testPrincipal-path but NOT on effective paths null and /oak:content
        grant(testPrincipal, systemPrincipalPath, JCR_READ_ACCESS_CONTROL);
        root.commit();
        testRoot.refresh();

        // since default permission evaluation is in charge for 'testUser' -> access to full principal policy is now
        // granted
        AccessControlPolicy[] effective = testAcMgr.getEffectivePolicies(testJcrPath);
        assertEquals(1, effective.length);
        assertTrue(effective[0] instanceof PrincipalPolicyImpl);
    }

    @Test
    public void testGetEffectiveByPathMissingReadAccessControlOnPrincipal() throws Exception {
        // test-user: granted read-access-control on effective null-path
        grant(testPrincipal, null, JCR_READ_ACCESS_CONTROL);
        // test-user: granted read-access-control on effective /oak:content
        grant(testPrincipal, PathUtils.getAncestorPath(testJcrPath, 3), JCR_READ_ACCESS_CONTROL);
        root.commit();
        testRoot.refresh();

        assertEmptyPolicies(testAcMgr.getEffectivePolicies((String) null));
        assertEmptyPolicies(testAcMgr.getEffectivePolicies(testJcrPath));
    }

    @Test(expected = AccessDeniedException.class)
    public void testGetApplicableByPrincipalNoAccess() throws RepositoryException {
        testAcMgr.getApplicablePolicies(systemPrincipal);
    }

    @Test(expected = AccessDeniedException.class)
    public void testGetPoliciesByPrincipalNoAccess() throws RepositoryException {
        testAcMgr.getPolicies(systemPrincipal);
    }

    @Test(expected = AccessDeniedException.class)
    public void testGetEffectiveByPrincipalNoAccess() throws RepositoryException {
        testAcMgr.getEffectivePolicies(ImmutableSet.of(systemPrincipal));
    }

    @Test
    public void testGetPoliciesByPrincipal() throws Exception {
        // grant testuser read-access control on testPrincipal-path but NOT on effective paths null and /oak:content
        grant(testPrincipal, systemPrincipalPath, JCR_READ_ACCESS_CONTROL);
        root.commit();
        testRoot.refresh();

        // no read-ac permission on effective paths
        assertPolicies(testAcMgr.getPolicies(systemPrincipal), PrincipalPolicyImpl.class, 1, 2);

        // grant testuser read-access control on /oak:content
        grant(testPrincipal, testContentJcrPath, JCR_READ_ACCESS_CONTROL);
        root.commit();
        testRoot.refresh();
        assertPolicies(testAcMgr.getPolicies(systemPrincipal), PrincipalPolicyImpl.class, 1, 2);

        // additionally grant testuser read-access control on null
        grant(testPrincipal, null, JCR_READ_ACCESS_CONTROL);
        root.commit();
        testRoot.refresh();
        assertPolicies(testAcMgr.getPolicies(systemPrincipal), PrincipalPolicyImpl.class, 1, 2);
    }

    @Test
    public void testGetEffectiveByPrincipal() throws Exception {
        // grant testuser read-access control on testPrincipal-path but NOT on effective paths null and /oak:content
        grant(testPrincipal, systemPrincipalPath, JCR_READ_ACCESS_CONTROL);
        root.commit();
        testRoot.refresh();

        // read-access-control is only granted for the principalpolicy itself
        assertPolicies(testAcMgr.getEffectivePolicies(ImmutableSet.of(systemPrincipal)), ImmutableACL.class, 1, 2);

        // grant testuser read-access control on /oak:content
        grant(testPrincipal, testContentJcrPath, JCR_READ_ACCESS_CONTROL);
        root.commit();
        testRoot.refresh();
        assertPolicies(testAcMgr.getEffectivePolicies(ImmutableSet.of(systemPrincipal)), ImmutableACL.class, 1, 2);

        // additionally grant testuser read-access control on null
        grant(testPrincipal, null, JCR_READ_ACCESS_CONTROL);
        root.commit();
        testRoot.refresh();
        assertPolicies(testAcMgr.getEffectivePolicies(ImmutableSet.of(systemPrincipal)), ImmutableACL.class, 1, 2);

        // make sure test-user has read-accesscontrol permission on any readable-path
        grant(testPrincipal, PathUtils.ROOT_PATH, JCR_READ_ACCESS_CONTROL);
        root.commit();
        testRoot.refresh();

        AccessControlPolicy[] effective = testAcMgr.getEffectivePolicies(ImmutableSet.of(systemPrincipal));;
        assertPolicies(effective, ImmutableACL.class, 2, 2);
        assertTrue(effective[1] instanceof ReadPolicy);
    }

    @Test(expected = AccessDeniedException.class)
    public void testSetPolicyMissingModifyAccessControlOnPrincipal() throws Exception {
        grant(testPrincipal, systemPrincipalPath, JCR_READ_ACCESS_CONTROL);
        root.commit();
        testRoot.refresh();

        PrincipalPolicyImpl policy = (PrincipalPolicyImpl) testAcMgr.getPolicies(systemPrincipal)[0];
        policy.addEntry(null, privilegesFromNames(JCR_WORKSPACE_MANAGEMENT));

        testAcMgr.setPolicy(policy.getPath(), policy);
    }

    @Test(expected = AccessDeniedException.class)
    public void testSetPolicyMissingModifyAccessControlOnEffectivePath() throws Exception {
        grant(testPrincipal, systemPrincipalPath, JCR_READ_ACCESS_CONTROL, JCR_MODIFY_ACCESS_CONTROL);
        root.commit();
        testRoot.refresh();

        PrincipalPolicyImpl policy = (PrincipalPolicyImpl) testAcMgr.getPolicies(systemPrincipal)[0];
        policy.addEntry(null, privilegesFromNames(JCR_WORKSPACE_MANAGEMENT));
        policy.addEntry(testJcrPath, privilegesFromNames(JCR_READ));

        testAcMgr.setPolicy(policy.getPath(), policy);
    }

    @Test(expected = AccessDeniedException.class)
    public void testSetPolicyMissingModifyAccessControlOnEffectivePath2() throws Exception {
        grant(testPrincipal, systemPrincipalPath, JCR_READ_ACCESS_CONTROL, JCR_MODIFY_ACCESS_CONTROL);
        grant(testPrincipal, testContentJcrPath, JCR_READ_ACCESS_CONTROL, JCR_MODIFY_ACCESS_CONTROL);
        root.commit();
        testRoot.refresh();

        PrincipalPolicyImpl policy = (PrincipalPolicyImpl) testAcMgr.getPolicies(systemPrincipal)[0];
        policy.addEntry(null, privilegesFromNames(JCR_WORKSPACE_MANAGEMENT));
        policy.addEntry(testJcrPath, privilegesFromNames(JCR_READ));

        testAcMgr.setPolicy(policy.getPath(), policy);
    }

    @Test
    public void testSetPolicy() throws Exception {
        grant(testPrincipal, systemPrincipalPath, JCR_READ_ACCESS_CONTROL, JCR_MODIFY_ACCESS_CONTROL);
        grant(testPrincipal, testContentJcrPath, JCR_READ_ACCESS_CONTROL, JCR_MODIFY_ACCESS_CONTROL);
        grant(testPrincipal, null, JCR_READ_ACCESS_CONTROL, JCR_MODIFY_ACCESS_CONTROL);
        root.commit();
        testRoot.refresh();

        PrincipalPolicyImpl policy = (PrincipalPolicyImpl) testAcMgr.getPolicies(systemPrincipal)[0];
        policy.addEntry(null, privilegesFromNames(JCR_WORKSPACE_MANAGEMENT));
        policy.addEntry(testJcrPath, privilegesFromNames(JCR_READ));

        testAcMgr.setPolicy(policy.getPath(), policy);
        testRoot.commit();
    }

    @Test(expected = AccessDeniedException.class)
    public void testRemovePolicyMissingModifyAccessControlOnPrincipal() throws Exception {
        grant(testPrincipal, systemPrincipalPath, JCR_READ_ACCESS_CONTROL);
        root.commit();
        testRoot.refresh();

        PrincipalPolicyImpl policy = (PrincipalPolicyImpl) testAcMgr.getPolicies(systemPrincipal)[0];
        testAcMgr.removePolicy(policy.getPath(), policy);
    }

    @Test(expected = AccessDeniedException.class)
    public void testRemovePolicyMissingModifyAccessControlOnEffectivePath() throws Exception {
        grant(testPrincipal, systemPrincipalPath, JCR_READ_ACCESS_CONTROL, JCR_MODIFY_ACCESS_CONTROL);
        root.commit();
        testRoot.refresh();

        PrincipalPolicyImpl policy = (PrincipalPolicyImpl) testAcMgr.getPolicies(systemPrincipal)[0];
        testAcMgr.removePolicy(policy.getPath(), policy);
    }

    @Test(expected = AccessDeniedException.class)
    public void testRemovePolicyMissingModifyAccessControlOnEffectivePath2() throws Exception {
        grant(testPrincipal, systemPrincipalPath, JCR_READ_ACCESS_CONTROL, JCR_MODIFY_ACCESS_CONTROL);
        grant(testPrincipal, testContentJcrPath, JCR_READ_ACCESS_CONTROL, JCR_MODIFY_ACCESS_CONTROL);
        root.commit();
        testRoot.refresh();

        PrincipalPolicyImpl policy = (PrincipalPolicyImpl) testAcMgr.getPolicies(systemPrincipal)[0];
        testAcMgr.removePolicy(policy.getPath(), policy);
    }

    @Test
    public void testRemovePolicy() throws Exception {
        grant(testPrincipal, systemPrincipalPath, JCR_READ_ACCESS_CONTROL, JCR_MODIFY_ACCESS_CONTROL);
        grant(testPrincipal, null, JCR_READ_ACCESS_CONTROL, JCR_MODIFY_ACCESS_CONTROL);
        grant(testPrincipal, testContentJcrPath, JCR_READ_ACCESS_CONTROL, JCR_MODIFY_ACCESS_CONTROL);
        root.commit();
        testRoot.refresh();

        PrincipalPolicyImpl policy = (PrincipalPolicyImpl) testAcMgr.getPolicies(systemPrincipal)[0];
        testAcMgr.removePolicy(policy.getPath(), policy);
        testRoot.commit();
    }

    @Test
    public void testRemovePolicyWithNonEntryChild() throws Exception {
        // clear all entries from policy
        PrincipalPolicyImpl policy = getPrincipalPolicyImpl(systemPrincipal, getAccessControlManager(root));
        for (AccessControlEntry entry : policy.getAccessControlEntries()) {
            policy.removeAccessControlEntry(entry);
        }
        getAccessControlManager(root).setPolicy(policy.getPath(), policy);
        // grant permission to read/modify policy
        grant(testPrincipal, systemPrincipalPath, JCR_READ_ACCESS_CONTROL, JCR_MODIFY_ACCESS_CONTROL);
        root.commit();
        testRoot.refresh();

        // transiently add non-entry tree
        Tree policyTree = testRoot.getTree(getPolicyPath());
        assertFalse(policyTree.getChildren().iterator().hasNext());
        TreeUtil.addChild(policyTree, "nonEntry", NodeTypeConstants.NT_OAK_UNSTRUCTURED);

        policy = getPrincipalPolicyImpl(systemPrincipal, testAcMgr);
        testAcMgr.removePolicy(policy.getPath(), policy);
        testRoot.commit();
    }

    @Test(expected = AccessControlException.class)
    public void testRemovePolicyMissingEffectivePaths() throws Exception {
        // grant permission to read/modify policy
        grant(testPrincipal, systemPrincipalPath, JCR_READ_ACCESS_CONTROL, JCR_MODIFY_ACCESS_CONTROL);
        root.commit();
        testRoot.refresh();

        // first read policy
        PrincipalPolicyImpl policy = getPrincipalPolicyImpl(systemPrincipal, testAcMgr);

        // transiently remove rep:effectivePath properties from all entries.
        Tree policyTree = testRoot.getTree(getPolicyPath());
        for (Tree child : policyTree.getChildren()) {
            child.removeProperty(REP_EFFECTIVE_PATH);
        }

        // removing policy must fail, because effective paths cannot be evaluated
        testAcMgr.removePolicy(policy.getPath(), policy);
    }

    @Test(expected = AccessDeniedException.class)
    public void testHasPrivilegeSystemUser() throws Exception {
        // test session has no access
        testAcMgr.hasPrivileges(testContentJcrPath, ImmutableSet.of(systemPrincipal), privilegesFromNames(JCR_NODE_TYPE_MANAGEMENT));
    }

    @Test(expected = AccessDeniedException.class)
    public void testHasPrivilegeSystemUserWithPartialReadAc() throws Exception {
        // grant read-ac access on principal policy (but not on targetPath)
        grant(testPrincipal, systemPrincipalPath, JCR_READ_ACCESS_CONTROL);
        root.commit();
        testRoot.refresh();

        testAcMgr.hasPrivileges(testContentJcrPath, ImmutableSet.of(systemPrincipal), privilegesFromNames(JCR_NODE_TYPE_MANAGEMENT));
    }

    @Test
    public void testHasPrivilegeSystemUserWithPartialReadAc2() throws Exception {
        // grant read-ac access on effective path -> no entries accessible
        grant(testPrincipal, testContentJcrPath, JCR_READ_ACCESS_CONTROL);
        root.commit();
        testRoot.refresh();

        assertFalse(testAcMgr.hasPrivileges(testContentJcrPath, ImmutableSet.of(systemPrincipal), privilegesFromNames(JCR_NODE_TYPE_MANAGEMENT)));
    }

    @Test
    public void testHasPrivilegeSystemUserWithReadAc() throws Exception {
        // grant read-ac access on effective path and on principal policy
        grant(testPrincipal, testContentJcrPath, JCR_READ_ACCESS_CONTROL);
        grant(testPrincipal, systemPrincipalPath, JCR_READ_ACCESS_CONTROL);
        root.commit();
        testRoot.refresh();

        // default model lacks jcr:nodeTypeManagement privilege -> not granted
        assertFalse(testAcMgr.hasPrivileges(testContentJcrPath, ImmutableSet.of(systemPrincipal), privilegesFromNames(JCR_NODE_TYPE_MANAGEMENT)));

        addDefaultEntry(testContentJcrPath, systemPrincipal, JCR_READ, JCR_NODE_TYPE_MANAGEMENT);
        root.commit();
        testRoot.refresh();

        // once default model grants permissions as well -> granted
        assertTrue(testAcMgr.hasPrivileges(testContentJcrPath, ImmutableSet.of(systemPrincipal), privilegesFromNames(JCR_NODE_TYPE_MANAGEMENT)));

        // but combination read/nt-mgt is not granted because jcr:read is missing on principal-based setup.
        assertFalse(testAcMgr.hasPrivileges(testContentJcrPath, ImmutableSet.of(systemPrincipal), privilegesFromNames(JCR_READ, JCR_NODE_TYPE_MANAGEMENT)));
    }

    @Test(expected = AccessDeniedException.class)
    public void testGetPrivilegeSystemUser() throws Exception {
        // test session has no access
        testAcMgr.getPrivileges(null, ImmutableSet.of(systemPrincipal));
    }

    @Test(expected = AccessDeniedException.class)
    public void testGetPrivilegeSystemUserWithPartialReadAc() throws Exception {
        // grant read ac on principal policy but not on target path
        grant(testPrincipal, systemPrincipalPath, JCR_READ_ACCESS_CONTROL);
        root.commit();
        testRoot.refresh();

        testAcMgr.getPrivileges(null, ImmutableSet.of(systemPrincipal));
    }

    @Test
    public void testGetPrivilegeSystemUserWithPartialReadAc2() throws Exception {
        // grant read-ac access on target path (but not on principal policy)
        grant(testPrincipal, null, JCR_READ_ACCESS_CONTROL);
        root.commit();
        testRoot.refresh();

        assertEquals(0, testAcMgr.getPrivileges(null, ImmutableSet.of(systemPrincipal)).length);
    }

    @Test
    public void testGetPrivilegeSystemUserWithWithReadAc() throws Exception {
        // grant read-ac access on effective path and on principal policy
        grant(testPrincipal, null, JCR_READ_ACCESS_CONTROL);
        grant(testPrincipal, systemPrincipalPath, JCR_READ_ACCESS_CONTROL);
        root.commit();
        testRoot.refresh();

        // default model lacks jcr:nodeTypeManagement privilege -> not granted
        assertArrayEquals(new Privilege[0], testAcMgr.getPrivileges(null, ImmutableSet.of(systemPrincipal)));

        addDefaultEntry(null, systemPrincipal, JCR_NAMESPACE_MANAGEMENT, REP_PRIVILEGE_MANAGEMENT);
        root.commit();
        testRoot.refresh();

        // once default model grants namespace-mgt privilege as well -> granted
        // but not rep:privilegeMgt because the principal-based model doesn't grant that one
        assertArrayEquals(privilegesFromNames(JCR_NAMESPACE_MANAGEMENT), testAcMgr.getPrivileges(null, ImmutableSet.of(systemPrincipal)));
    }

    @Test
    public void testReadPolicyTree() throws Exception {
        Tree policyTree = testRoot.getTree(getPolicyPath());
        assertNotNull(policyTree);
        assertFalse(policyTree.exists());
    }

    @Test
    public void testReadPolicyTreeWithReadAc() throws Exception {
        // grant read-ac access and check again
        grant(testPrincipal, systemPrincipalPath, JCR_READ_ACCESS_CONTROL);
        root.commit();
        testRoot.refresh();

        Tree policyTree = testRoot.getTree(getPolicyPath());
        assertNotNull(policyTree);
        assertTrue(policyTree.exists());
    }

    @Test
    public void testReadEntryTree() throws Exception {
        Tree entryTree = testRoot.getTree(getEntryPath());
        assertNotNull(entryTree);
        assertFalse(entryTree.exists());
    }

    @Test
    public void testReadEntryTreeWithReadAc() throws Exception {
        // grant read-ac access and check again
        grant(testPrincipal, systemPrincipalPath, JCR_READ_ACCESS_CONTROL);
        root.commit();
        testRoot.refresh();

        Tree entryTree = testRoot.getTree(getEntryPath());
        assertNotNull(entryTree);
        assertTrue(entryTree.exists());
    }

    @Test(expected = CommitFailedException.class)
    public void testAddEntryTree() throws Exception {
        // grant read-ac access on principal policy
        grant(testPrincipal, systemPrincipalPath, JCR_READ_ACCESS_CONTROL);
        root.commit();
        testRoot.refresh();

        try {
            Tree policyTree = testRoot.getTree(getPolicyPath());
            Tree entry = TreeUtil.addChild(policyTree, "entry", NT_REP_PRINCIPAL_ENTRY);
            entry.setProperty(REP_EFFECTIVE_PATH, TEST_OAK_PATH, Type.PATH);
            entry.setProperty(Constants.REP_PRIVILEGES, ImmutableList.of(JCR_ADD_CHILD_NODES), Type.NAMES);
            testRoot.commit();
        } catch (CommitFailedException e) {
            assertEquals(CommitFailedException.ACCESS, e.getType());
            assertEquals(3, e.getCode());
            throw e;
        }
    }

    @Test(expected = CommitFailedException.class)
    public void testAddEntryTreeModAcOnSystemPrincipal() throws Exception {
        // grant read-ac + mod-ac access on principal policy
        grant(testPrincipal, systemPrincipalPath, JCR_READ_ACCESS_CONTROL, JCR_MODIFY_ACCESS_CONTROL);
        root.commit();
        testRoot.refresh();

        try {
            Tree policyTree = testRoot.getTree(getPolicyPath());
            Tree entry = TreeUtil.addChild(policyTree, "entry", NT_REP_PRINCIPAL_ENTRY);
            entry.setProperty(REP_EFFECTIVE_PATH, TEST_OAK_PATH, Type.PATH);
            entry.setProperty(Constants.REP_PRIVILEGES, ImmutableList.of(JCR_ADD_CHILD_NODES), Type.NAMES);
            testRoot.commit();
        } catch (CommitFailedException e) {
            assertEquals(CommitFailedException.ACCESS, e.getType());
            assertEquals(3, e.getCode());
            throw e;
        }
    }

    @Test(expected = IllegalStateException.class)
    public void testAddEntryTreeModAcOnEffectivePath() throws Exception {
        // grant read-ac + mod-ac access on effective path only -> cannot read principal policy
        grant(testPrincipal, testJcrPath, JCR_READ_ACCESS_CONTROL, JCR_MODIFY_ACCESS_CONTROL);
        root.commit();
        testRoot.refresh();

        Tree policyTree = testRoot.getTree(getPolicyPath());
        Tree entry = TreeUtil.addChild(policyTree, "entry", NT_REP_PRINCIPAL_ENTRY);

    }

    @Test
    public void testAddEntryTreeFullModAc() throws Exception {
        grant(testPrincipal, testJcrPath, JCR_READ_ACCESS_CONTROL, JCR_MODIFY_ACCESS_CONTROL);
        grant(testPrincipal, systemPrincipalPath, JCR_READ_ACCESS_CONTROL, JCR_MODIFY_ACCESS_CONTROL);
        root.commit();
        testRoot.refresh();

        Tree policyTree = testRoot.getTree(getPolicyPath());
        Tree entry = TreeUtil.addChild(policyTree, "entry", NT_REP_PRINCIPAL_ENTRY);
        entry.setProperty(REP_EFFECTIVE_PATH, TEST_OAK_PATH, Type.PATH);
        entry.setProperty(Constants.REP_PRIVILEGES, ImmutableList.of(JCR_ADD_CHILD_NODES), Type.NAMES);
        testRoot.commit();
    }

    @Test(expected = CommitFailedException.class)
    public void testRemovePolicyTree() throws Exception {
        // grant read-ac access on principal policy
        grant(testPrincipal, systemPrincipalPath, JCR_READ_ACCESS_CONTROL);
        root.commit();
        testRoot.refresh();

        try {
            testRoot.getTree(getPolicyPath()).remove();
            testRoot.commit();
        } catch (CommitFailedException e) {
            assertEquals(CommitFailedException.ACCESS, e.getType());
            assertEquals(0, e.getCode());
            throw e;
        }
    }

    @Test(expected = CommitFailedException.class)
    public void testRemovePolicyTreeWithModAcOnSystemPrincipal() throws Exception {
        // grant read-ac + mod-ac access on principal policy but not on target paths
        grant(testPrincipal, systemPrincipalPath, JCR_READ_ACCESS_CONTROL, JCR_MODIFY_ACCESS_CONTROL);
        root.commit();
        testRoot.refresh();

        try {
            testRoot.getTree(getPolicyPath()).remove();
            testRoot.commit();
        } catch (CommitFailedException e) {
            assertEquals(CommitFailedException.ACCESS, e.getType());
            assertEquals(3, e.getCode());
            throw e;
        }
    }

    @Test(expected = CommitFailedException.class)
    public void testRemovePolicyTreeWithModAcOnOneEffectivePath() throws Exception {
        // grant read-ac + mod-ac access on principal policy and on testcontent target paths (but not on null path)
        grant(testPrincipal, systemPrincipalPath, JCR_READ_ACCESS_CONTROL, JCR_MODIFY_ACCESS_CONTROL);
        grant(testPrincipal, testContentJcrPath, JCR_MODIFY_ACCESS_CONTROL);
        root.commit();
        testRoot.refresh();

        try {
            testRoot.getTree(getPolicyPath()).remove();
            testRoot.commit();
        } catch (CommitFailedException e) {
            assertEquals(CommitFailedException.ACCESS, e.getType());
            assertEquals(3, e.getCode());
            throw e;
        }
    }

    @Test(expected = CommitFailedException.class)
    public void testRemovePolicyTreeWithModAcOnOneEffectivePath2() throws Exception {
        // grant read-ac + mod-ac access on principal policy and on nul target paths (but not on testcontent path)
        grant(testPrincipal, systemPrincipalPath, JCR_READ_ACCESS_CONTROL, JCR_MODIFY_ACCESS_CONTROL);
        grant(testPrincipal, null, JCR_MODIFY_ACCESS_CONTROL);
        root.commit();
        testRoot.refresh();

        try {
            testRoot.getTree(getPolicyPath()).remove();
            testRoot.commit();
        } catch (CommitFailedException e) {
            assertEquals(CommitFailedException.ACCESS, e.getType());
            assertEquals(3, e.getCode());
            throw e;
        }
    }

    @Test(expected = CommitFailedException.class)
    public void testRemoveEmptyPolicyTree() throws Exception {
        PrincipalPolicyImpl policy = getPrincipalPolicyImpl(systemPrincipal, getAccessControlManager(root));
        for (AccessControlEntry entry : policy.getEntries()) {
            policy.removeAccessControlEntry(entry);
        }
        getAccessControlManager(root).setPolicy(policy.getPath(), policy);
        // grant permission to read policy
        grant(testPrincipal, systemPrincipalPath, JCR_READ_ACCESS_CONTROL);

        root.commit();
        testRoot.refresh();

        Tree policyTree = testRoot.getTree(getPolicyPath());
        assertTrue(policyTree.exists());
        policyTree.remove();
        testRoot.commit();
    }

    @Test(expected = CommitFailedException.class)
    public void testRemoveEntryTree() throws Exception {
        // grant read-ac access on principal policy
        grant(testPrincipal, systemPrincipalPath, JCR_READ_ACCESS_CONTROL);
        root.commit();
        testRoot.refresh();

        try {
            testRoot.getTree(getEntryPath()).remove();
            testRoot.commit();
        } catch (CommitFailedException e) {
            assertEquals(CommitFailedException.ACCESS, e.getType());
            assertEquals(3, e.getCode());
            throw e;
        }
    }

    @Test(expected = CommitFailedException.class)
    public void testRemoveEntryTreeModAcOnSystemPrincipal() throws Exception {
        // grant read-ac + mod-ac access on principal policy but not on target path
        grant(testPrincipal, systemPrincipalPath, JCR_READ_ACCESS_CONTROL, JCR_MODIFY_ACCESS_CONTROL);
        root.commit();
        testRoot.refresh();

        try {
            testRoot.getTree(getEntryPath()).remove();
            testRoot.commit();
        } catch (CommitFailedException e) {
            assertEquals(CommitFailedException.ACCESS, e.getType());
            assertEquals(3, e.getCode());
            throw e;
        }
    }

    @Test(expected = CommitFailedException.class)
    public void testRemoveEntryTreeModAcOnEffectivePath() throws Exception {
        // grant read-ac on principal policy but not mod-ac
        // on target path grant mod-ac
        grant(testPrincipal, systemPrincipalPath, JCR_READ_ACCESS_CONTROL);
        grant(testPrincipal, testContentJcrPath, JCR_MODIFY_ACCESS_CONTROL);
        root.commit();
        testRoot.refresh();

        try {
            testRoot.getTree(getEntryPath()).remove();
            testRoot.commit();
        } catch (CommitFailedException e) {
            assertEquals(CommitFailedException.ACCESS, e.getType());
            assertEquals(0, e.getCode());
            throw e;
        }
    }

    @Test
    public void testRemoveEntryTreeFullModAc() throws Exception {
        // grant read-ac and mod-ac on principal policy
        // on target path grant mod-ac
        grant(testPrincipal, systemPrincipalPath, JCR_READ_ACCESS_CONTROL, JCR_MODIFY_ACCESS_CONTROL);
        grant(testPrincipal, testContentJcrPath, JCR_MODIFY_ACCESS_CONTROL);
        root.commit();
        testRoot.refresh();

        testRoot.getTree(getEntryPath()).remove();
        testRoot.commit();
    }

    @Test
    public void testGetEffectivePrincipalsReadablePaths() throws Exception {
        Set<String> readablePaths = getConfig(AuthorizationConfiguration.class).getParameters().getConfigValue(PermissionConstants.PARAM_READ_PATHS, PermissionConstants.DEFAULT_READ_PATHS);
        String[] paths = readablePaths.stream().map(oakPath -> namePathMapper.getJcrPath(oakPath)).distinct().toArray(String[]::new);
        assertEquals(3, paths.length);

        // readpolicy must not be included if editing session doesn't have sufficient permission on readable paths.
        try {
            getAccessControlManager(testRoot).getEffectivePolicies(Collections.singleton(systemPrincipal), paths);
            fail("AccessDenied exception expected");
        } catch (AccessDeniedException e) {
            // success
        }
    }
}