/*
 * 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.jcr.security.authorization;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.Principal;
import java.util.Arrays;
import java.util.List;
import javax.jcr.ImportUUIDBehavior;
import javax.jcr.Node;
import javax.jcr.RepositoryException;
import javax.jcr.security.AccessControlEntry;
import javax.jcr.security.AccessControlException;
import javax.jcr.security.AccessControlList;
import javax.jcr.security.AccessControlManager;
import javax.jcr.security.AccessControlPolicy;
import javax.jcr.security.AccessControlPolicyIterator;
import javax.jcr.security.Privilege;

import org.apache.jackrabbit.api.security.JackrabbitAccessControlEntry;
import org.apache.jackrabbit.api.security.JackrabbitAccessControlList;
import org.apache.jackrabbit.oak.spi.security.principal.EveryonePrincipal;
import org.apache.jackrabbit.test.AbstractJCRTest;
import org.jetbrains.annotations.Nullable;

public class AccessControlImporterTest extends AbstractJCRTest {

    private static final String VALID_PRINCIPAL_NAME = "anonymous";

    public static final String XML_POLICY_TREE = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
            "<sv:node sv:name=\"test\" xmlns:mix=\"http://www.jcp.org/jcr/mix/1.0\" xmlns:nt=\"http://www.jcp.org/jcr/nt/1.0\" xmlns:fn_old=\"http://www.w3.org/2004/10/xpath-functions\" xmlns:fn=\"http://www.w3.org/2005/xpath-functions\" xmlns:xs=\"http://www.w3.org/2001/XMLSchema\" xmlns:sv=\"http://www.jcp.org/jcr/sv/1.0\" xmlns:rep=\"internal\" xmlns:jcr=\"http://www.jcp.org/jcr/1.0\">" +
            "  <sv:property sv:name=\"jcr:primaryType\" sv:type=\"Name\"><sv:value>nt:unstructured</sv:value></sv:property>" +
            "  <sv:property sv:name=\"jcr:mixinTypes\" sv:type=\"Name\">" +
            "     <sv:value>rep:AccessControllable</sv:value>" +
            "  </sv:property>" +
            "  <sv:node sv:name=\"rep:policy\">" +
            "     <sv:property sv:name=\"jcr:primaryType\" sv:type=\"Name\"><sv:value>rep:ACL</sv:value></sv:property>" +
            "     <sv:node sv:name=\"allow\">" +
            "         <sv:property sv:name=\"jcr:primaryType\" sv:type=\"Name\">" +
            "             <sv:value>rep:GrantACE</sv:value>" +
            "         </sv:property>" +
            "         <sv:property sv:name=\"rep:principalName\" sv:type=\"String\">" +
            "             <sv:value>"+EveryonePrincipal.NAME+"</sv:value>" +
            "         </sv:property>" +
            "         <sv:property sv:name=\"rep:privileges\" sv:type=\"Name\">" +
            "             <sv:value>jcr:write</sv:value>" +
            "         </sv:property>" +
            "     </sv:node>" +
            "  </sv:node>" +
            "</sv:node>";

    public static final String XML_POLICY_TREE_2 = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
            "<sv:node sv:name=\"rep:policy\" " +
            "xmlns:mix=\"http://www.jcp.org/jcr/mix/1.0\" xmlns:nt=\"http://www.jcp.org/jcr/nt/1.0\" xmlns:fn_old=\"http://www.w3.org/2004/10/xpath-functions\" xmlns:fn=\"http://www.w3.org/2005/xpath-functions\" xmlns:xs=\"http://www.w3.org/2001/XMLSchema\" xmlns:sv=\"http://www.jcp.org/jcr/sv/1.0\" xmlns:rep=\"internal\" xmlns:jcr=\"http://www.jcp.org/jcr/1.0\">" +
            "<sv:property sv:name=\"jcr:primaryType\" sv:type=\"Name\">" +
            "<sv:value>rep:ACL</sv:value>" +
            "</sv:property>" +
            "<sv:node sv:name=\"allow\">" +
            "<sv:property sv:name=\"jcr:primaryType\" sv:type=\"Name\">" +
            "<sv:value>rep:GrantACE</sv:value>" +
            "</sv:property>" +
            "<sv:property sv:name=\"rep:principalName\" sv:type=\"String\">" +
            "<sv:value>"+EveryonePrincipal.NAME+"</sv:value>" +
            "</sv:property>" +
            "<sv:property sv:name=\"rep:privileges\" sv:type=\"Name\">" +
            "<sv:value>jcr:write</sv:value>" +
            "</sv:property>" +
            "</sv:node>" +
            "</sv:node>";

    public static final String XML_POLICY_TREE_3 = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
            "<sv:node sv:name=\"rep:policy\" " +
            "xmlns:mix=\"http://www.jcp.org/jcr/mix/1.0\" xmlns:nt=\"http://www.jcp.org/jcr/nt/1.0\" xmlns:fn_old=\"http://www.w3.org/2004/10/xpath-functions\" xmlns:fn=\"http://www.w3.org/2005/xpath-functions\" xmlns:xs=\"http://www.w3.org/2001/XMLSchema\" xmlns:sv=\"http://www.jcp.org/jcr/sv/1.0\" xmlns:rep=\"internal\" xmlns:jcr=\"http://www.jcp.org/jcr/1.0\">" +
            "<sv:property sv:name=\"jcr:primaryType\" sv:type=\"Name\">" +
            "<sv:value>rep:ACL</sv:value>" +
            "</sv:property>" +
            "<sv:node sv:name=\"allow\">" +
            "<sv:property sv:name=\"jcr:primaryType\" sv:type=\"Name\">" +
            "<sv:value>rep:GrantACE</sv:value>" +
            "</sv:property>" +
            "<sv:property sv:name=\"rep:principalName\" sv:type=\"String\">" +
            "<sv:value>"+EveryonePrincipal.NAME+"</sv:value>" +
            "</sv:property>" +
            "<sv:property sv:name=\"rep:privileges\" sv:type=\"Name\">" +
            "<sv:value>jcr:write</sv:value>" +
            "</sv:property>" +
            "</sv:node>" +
            "<sv:node sv:name=\"allow0\">" +
            "<sv:property sv:name=\"jcr:primaryType\" sv:type=\"Name\">" +
            "<sv:value>rep:GrantACE</sv:value>" +
            "</sv:property>" +
            "<sv:property sv:name=\"rep:principalName\" sv:type=\"String\">" +
            "<sv:value>"+VALID_PRINCIPAL_NAME+"</sv:value>" +
            "</sv:property>" +
            "<sv:property sv:name=\"rep:privileges\" sv:type=\"Name\">" +
            "<sv:value>jcr:write</sv:value>" +
            "</sv:property>" +
            "</sv:node>" +
            "</sv:node>";

    public static final String XML_POLICY_TREE_5 = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
            "<sv:node sv:name=\"rep:policy\" " +
            "xmlns:mix=\"http://www.jcp.org/jcr/mix/1.0\" xmlns:nt=\"http://www.jcp.org/jcr/nt/1.0\" xmlns:fn_old=\"http://www.w3.org/2004/10/xpath-functions\" xmlns:fn=\"http://www.w3.org/2005/xpath-functions\" xmlns:xs=\"http://www.w3.org/2001/XMLSchema\" xmlns:sv=\"http://www.jcp.org/jcr/sv/1.0\" xmlns:rep=\"internal\" xmlns:jcr=\"http://www.jcp.org/jcr/1.0\">" +
            "<sv:property sv:name=\"jcr:primaryType\" sv:type=\"Name\">" +
            "<sv:value>rep:ACL</sv:value>" +
            "</sv:property>" +
            "<sv:node sv:name=\"allow0\">" +
            "<sv:property sv:name=\"jcr:primaryType\" sv:type=\"Name\">" +
            "<sv:value>rep:GrantACE</sv:value>" +
            "</sv:property>" +
            "<sv:property sv:name=\"rep:principalName\" sv:type=\"String\">" +
            "<sv:value>"+VALID_PRINCIPAL_NAME+"</sv:value>" +
            "</sv:property>" +
            "<sv:property sv:name=\"rep:privileges\" sv:type=\"Name\">" +
            "<sv:value>jcr:write</sv:value>" +
            "</sv:property>" +
            "</sv:node>" +
            "</sv:node>";

    public static final String XML_REPO_POLICY_TREE = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
            "<sv:node sv:name=\"rep:repoPolicy\" " +
            "xmlns:mix=\"http://www.jcp.org/jcr/mix/1.0\" xmlns:nt=\"http://www.jcp.org/jcr/nt/1.0\" xmlns:fn_old=\"http://www.w3.org/2004/10/xpath-functions\" xmlns:fn=\"http://www.w3.org/2005/xpath-functions\" xmlns:xs=\"http://www.w3.org/2001/XMLSchema\" xmlns:sv=\"http://www.jcp.org/jcr/sv/1.0\" xmlns:rep=\"internal\" xmlns:jcr=\"http://www.jcp.org/jcr/1.0\">" +
            "<sv:property sv:name=\"jcr:primaryType\" sv:type=\"Name\">" +
            "<sv:value>rep:ACL</sv:value>" +
            "</sv:property>" +
            "<sv:node sv:name=\"allow\">" +
            "<sv:property sv:name=\"jcr:primaryType\" sv:type=\"Name\">" +
            "<sv:value>rep:GrantACE</sv:value>" +
            "</sv:property>" +
            "<sv:property sv:name=\"rep:principalName\" sv:type=\"String\">" +
            "<sv:value>"+VALID_PRINCIPAL_NAME+"</sv:value>" +
            "</sv:property>" +
            "<sv:property sv:name=\"rep:privileges\" sv:type=\"Name\">" +
            "<sv:value>jcr:workspaceManagement</sv:value>" +
            "</sv:property>" +
            "</sv:node>" +
            "</sv:node>";

    public static final String XML_POLICY_ONLY = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
            "<sv:node sv:name=\"test\" xmlns:mix=\"http://www.jcp.org/jcr/mix/1.0\" xmlns:nt=\"http://www.jcp.org/jcr/nt/1.0\" xmlns:fn_old=\"http://www.w3.org/2004/10/xpath-functions\" xmlns:fn=\"http://www.w3.org/2005/xpath-functions\" xmlns:xs=\"http://www.w3.org/2001/XMLSchema\" xmlns:sv=\"http://www.jcp.org/jcr/sv/1.0\" xmlns:rep=\"internal\" xmlns:jcr=\"http://www.jcp.org/jcr/1.0\">" +
            "  <sv:property sv:name=\"jcr:primaryType\" sv:type=\"Name\"><sv:value>nt:unstructured</sv:value></sv:property>" +
            "  <sv:property sv:name=\"jcr:mixinTypes\" sv:type=\"Name\">" +
            "     <sv:value>rep:AccessControllable</sv:value>" +
            "  </sv:property>" +
            "  <sv:node sv:name=\"rep:policy\">" +
            "     <sv:property sv:name=\"jcr:primaryType\" sv:type=\"Name\"><sv:value>rep:ACL</sv:value></sv:property>" +
            "  </sv:node>" +
            "</sv:node>";

    protected void doImport(String parentPath, String xml) throws IOException, RepositoryException {
        InputStream in = new ByteArrayInputStream(xml.getBytes("UTF-8"));
        if (isSessionImport()) {
            superuser.importXML(parentPath, in, ImportUUIDBehavior.IMPORT_UUID_COLLISION_THROW);
        } else {
            superuser.save();
            superuser.getWorkspace().importXML(parentPath, in, ImportUUIDBehavior.IMPORT_UUID_COLLISION_THROW);
        }
    }

    protected boolean isSessionImport() {
        return true;
    }

    private Node createImportTarget() throws RepositoryException {
        Node target = testRootNode.addNode(nodeName1);
        target.addMixin("rep:AccessControllable");
        if (!isSessionImport()) {
            superuser.save();
        }
        return target;
    }

    private Node createImportTargetWithPolicy(@Nullable Principal principal) throws RepositoryException {
        Node target = testRootNode.addNode("test", "test:sameNameSibsFalseChildNodeDefinition");
        AccessControlManager acMgr = superuser.getAccessControlManager();
        for (AccessControlPolicyIterator it = acMgr.getApplicablePolicies(target.getPath()); it.hasNext(); ) {
            AccessControlPolicy policy = it.nextAccessControlPolicy();
            if (policy instanceof AccessControlList) {
                if (principal != null) {
                    Privilege[] privs = new Privilege[]{acMgr.privilegeFromName(Privilege.JCR_LOCK_MANAGEMENT)};
                    ((AccessControlList) policy).addAccessControlEntry(principal, privs);
                }
                acMgr.setPolicy(target.getPath(), policy);
            }
        }
        if (!isSessionImport()) {
            superuser.save();
        }
        return target;
    }

    /**
     * Imports a resource-based ACL containing a single entry.
     *
     * @throws Exception
     */
    public void testImportACL() throws Exception {
        try {
            Node target = testRootNode;
            doImport(target.getPath(), XML_POLICY_TREE);

            assertTrue(target.hasNode("test"));
            String path = target.getNode("test").getPath();

            AccessControlManager acMgr = superuser.getAccessControlManager();
            AccessControlPolicy[] policies = acMgr.getPolicies(path);

            assertEquals(1, policies.length);
            assertTrue(policies[0] instanceof JackrabbitAccessControlList);

            AccessControlEntry[] entries = ((JackrabbitAccessControlList) policies[0]).getAccessControlEntries();
            assertEquals(1, entries.length);

            AccessControlEntry entry = entries[0];
            assertEquals(EveryonePrincipal.NAME, entry.getPrincipal().getName());
            assertEquals(1, entry.getPrivileges().length);
            assertEquals(acMgr.privilegeFromName(Privilege.JCR_WRITE), entry.getPrivileges()[0]);

            if (entry instanceof JackrabbitAccessControlEntry) {
                assertTrue(((JackrabbitAccessControlEntry) entry).isAllow());
            }

        } finally {
            superuser.refresh(false);
        }
    }

    public void testImportACLOnly() throws Exception {
        try {
            Node target = createImportTarget();

            doImport(target.getPath(), XML_POLICY_TREE_3);

            String path = target.getPath();

            AccessControlManager acMgr = superuser.getAccessControlManager();
            AccessControlPolicy[] policies = acMgr.getPolicies(path);

            assertEquals(1, policies.length);
            assertTrue(policies[0] instanceof JackrabbitAccessControlList);

            AccessControlEntry[] entries = ((JackrabbitAccessControlList) policies[0]).getAccessControlEntries();
            assertEquals(2, entries.length);

            AccessControlEntry entry = entries[0];
            assertEquals(EveryonePrincipal.NAME, entry.getPrincipal().getName());
            assertEquals(1, entry.getPrivileges().length);
            assertEquals(acMgr.privilegeFromName(Privilege.JCR_WRITE), entry.getPrivileges()[0]);

            entry = entries[1];
            assertEquals(VALID_PRINCIPAL_NAME, entry.getPrincipal().getName());
            assertEquals(1, entry.getPrivileges().length);
            assertEquals(acMgr.privilegeFromName(Privilege.JCR_WRITE), entry.getPrivileges()[0]);

            if (entry instanceof JackrabbitAccessControlEntry) {
                assertTrue(((JackrabbitAccessControlEntry) entry).isAllow());
            }
        } finally {
            superuser.refresh(false);
        }
    }

    public void testImportACLRemoveACE() throws Exception {
        try {
            Node target = createImportTarget();

            doImport(target.getPath(), XML_POLICY_TREE_3);
            doImport(target.getPath(), XML_POLICY_TREE_5);

            String path = target.getPath();

            AccessControlManager acMgr = superuser.getAccessControlManager();
            AccessControlPolicy[] policies = acMgr.getPolicies(path);

            assertEquals(1, policies.length);
            assertTrue(policies[0] instanceof JackrabbitAccessControlList);

            AccessControlEntry[] entries = ((JackrabbitAccessControlList) policies[0]).getAccessControlEntries();
            assertEquals(1, entries.length);

            AccessControlEntry entry = entries[0];
            assertEquals(VALID_PRINCIPAL_NAME, entry.getPrincipal().getName());
            assertEquals(1, entry.getPrivileges().length);
            assertEquals(acMgr.privilegeFromName(Privilege.JCR_WRITE), entry.getPrivileges()[0]);

            if (entry instanceof JackrabbitAccessControlEntry) {
                assertTrue(((JackrabbitAccessControlEntry) entry).isAllow());
            }
        } finally {
            superuser.refresh(false);
        }
    }

    /**
     * Imports a resource-based ACL containing a single entry for a policy that
     * already exists: expected outcome its that the existing ACE is replaced.
     */
    public void testImportPolicyExists() throws Exception {
        try {
            Node target = createImportTargetWithPolicy(EveryonePrincipal.getInstance());
            doImport(target.getPath(), XML_POLICY_TREE_2);

            AccessControlManager acMgr = superuser.getAccessControlManager();
            AccessControlPolicy[] policies = acMgr.getPolicies(target.getPath());
            assertEquals(1, policies.length);
            assertTrue(policies[0] instanceof JackrabbitAccessControlList);

            AccessControlEntry[] entries = ((JackrabbitAccessControlList) policies[0]).getAccessControlEntries();
            assertEquals(1, entries.length);

            AccessControlEntry entry = entries[0];
            assertEquals(EveryonePrincipal.getInstance().getName(), entry.getPrincipal().getName());
            List<Privilege> privs = Arrays.asList(entry.getPrivileges());
            assertEquals(1, privs.size());
            assertEquals(acMgr.privilegeFromName(Privilege.JCR_WRITE), entry.getPrivileges()[0]);

            if (entry instanceof JackrabbitAccessControlEntry) {
                assertTrue(((JackrabbitAccessControlEntry) entry).isAllow());
            }
        } finally {
            superuser.refresh(false);
        }
    }

    /**
     * Imports an empty resource-based ACL for a policy that already exists.
     *
     * @throws Exception
     */
    public void testImportEmptyExistingPolicy() throws Exception {
        try {
            Node target = createImportTargetWithPolicy(null);
            doImport(target.getPath(), XML_POLICY_ONLY);

            AccessControlPolicy[] policies = superuser.getAccessControlManager().getPolicies(target.getPath());

            assertEquals(1, policies.length);
            assertTrue(policies[0] instanceof JackrabbitAccessControlList);

            AccessControlEntry[] entries = ((JackrabbitAccessControlList) policies[0]).getAccessControlEntries();
            assertEquals(0, entries.length);

        } finally {
            superuser.refresh(false);
        }
    }

    /**
     * Repo level acl must be imported underneath the root node.
     *
     * @throws Exception
     */
    public void testImportRepoACLAtRoot() throws Exception {
        Node target = superuser.getRootNode();
        AccessControlManager acMgr = superuser.getAccessControlManager();
        try {
            // need to add mixin. in contrast to only using JCR API to retrieve
            // and set the policies the protected item import only is called if
            // the node to be imported is defined to be protected. however, if
            // the root node doesn't have the mixin assigned the defining node
            // type of the imported policy nodes will be rep:root (unstructured)
            // and the items will not be detected as being protected.
            target.addMixin("rep:RepoAccessControllable");
            if (!isSessionImport()) {
                superuser.save();
            }

            doImport(target.getPath(), XML_REPO_POLICY_TREE);

            AccessControlPolicy[] policies = acMgr.getPolicies(null);

            assertEquals(1, policies.length);
            assertTrue(policies[0] instanceof JackrabbitAccessControlList);

            AccessControlEntry[] entries = ((JackrabbitAccessControlList) policies[0]).getAccessControlEntries();
            assertEquals(1, entries.length);
            assertEquals(1, entries[0].getPrivileges().length);
            assertEquals(acMgr.privilegeFromName("jcr:workspaceManagement"), entries[0].getPrivileges()[0]);

            assertTrue(target.hasNode("rep:repoPolicy"));
            assertTrue(target.hasNode("rep:repoPolicy/allow"));

            // clean up again
            acMgr.removePolicy(null, policies[0]);
            assertFalse(target.hasNode("rep:repoPolicy"));
            assertFalse(target.hasNode("rep:repoPolicy/allow"));

        } finally {
            if (isSessionImport()) {
                superuser.refresh(false);
            } else {
                superuser.save();
            }
            assertEquals(0, acMgr.getPolicies(null).length);
        }
    }

    /**
     * Make sure repo-level acl is not imported below any other node than the
     * root node.
     *
     * @throws Exception
     */
    public void testImportRepoACLAtTestNode() throws Exception {
        try {
            Node target = testRootNode.addNode("test");
            target.addMixin("rep:RepoAccessControllable");

            doImport(target.getPath(), XML_REPO_POLICY_TREE);

            assertTrue(target.hasNode("rep:repoPolicy"));
            assertFalse(target.hasNode("rep:repoPolicy/allow0"));

            Node n = target.getNode("rep:repoPolicy");
            assertEquals("rep:RepoAccessControllable", n.getDefinition().getDeclaringNodeType().getName());

            try {
                superuser.save();
                fail("Importing repo policy to non-root node must fail");
            } catch (AccessControlException e) {
                // success
            }
        } finally {
            superuser.refresh(false);
        }
    }
}
