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

import java.security.Principal;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import javax.jcr.RepositoryException;
import javax.jcr.UnsupportedRepositoryOperationException;
import javax.jcr.Value;
import javax.jcr.security.AccessControlEntry;
import javax.jcr.security.AccessControlException;
import javax.jcr.security.AccessControlManager;
import javax.jcr.security.Privilege;

import org.apache.jackrabbit.api.JackrabbitSession;
import org.apache.jackrabbit.api.JackrabbitWorkspace;
import org.apache.jackrabbit.api.security.JackrabbitAccessControlEntry;
import org.apache.jackrabbit.api.security.JackrabbitAccessControlList;
import org.apache.jackrabbit.api.security.authorization.PrivilegeCollection;
import org.apache.jackrabbit.api.security.user.Authorizable;
import org.apache.jackrabbit.commons.jackrabbit.authorization.AccessControlUtils;
import org.apache.jackrabbit.oak.exercise.ExerciseUtility;
import org.apache.jackrabbit.oak.security.authorization.accesscontrol.InvalidTestPrincipal;
import org.apache.jackrabbit.oak.spi.security.principal.PrincipalImpl;
import org.apache.jackrabbit.test.AbstractJCRTest;
import org.apache.jackrabbit.test.NotExecutableException;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import static org.junit.Assert.assertArrayEquals;

/**
 * <pre>
 * Module: Authorization (Access Control Management)
 * =============================================================================
 *
 * Title: AccessControlList Implementation Details
 * -----------------------------------------------------------------------------
 *
 * Goal:
 * Understand some of the implementation details applied by the default
 * access control list provided by the Oak access control management.
 *
 * Exercises:
 *
 * - {@link #testAddEntryTwice()}
 *   Adding the same ACE twice does not work.
 *   Verify the expectation by looking at the ACEs exposed by the list.
 *
 * - {@link #testUpdateAndComplementary()}
 *   The default implementation of the JackrabbitAccessControlList interface
 *   performs some optimization upon ACE-addition.
 *   Walk through the setup and complete the test case such that it passes.
 *
 * - {@link #testAddEntryWithInvalidPrincipals()}
 *   This tests creates a list of invalid principals for which adding an ACE
 *   will fail.
 *
 *   Question: Can you explain for each of these principals why?
 *
 * - {@link #testAddEntriesWithCustomKnownPrincipal()}
 *   Here we use a custom principal implementation as well but a principal
 *   with the given name is actually known.
 *   Walk through the test and complete it such that it passes.
 *
 * - {@link #testAddEntryWithInvalidPrivilege()}
 *   Walk through the test and explain why creating ACE for the given list of
 *   privilege arrays must fail.
 *
 * - {@link #testRemoveInvalidEntry()}
 *   Walk through the removal and explain why removing an ACE with the same
 *   characteristics is expected to fail.
 *
 *
 * Additional Exercises:
 * -----------------------------------------------------------------------------
 *
 * The JCR specification mandates the that the principal used to create an ACE
 * is known to the system.
 *
 * - Investigate how the Oak repository can be configured such that creating
 *   ACEs with unknown principals would still succeed.
 *
 *   Question: Can you name the configuration option and list the allowed values? What are the differences?
 *   Question: Can you find other places in the access control management code
 *             base where this is being used?
 *   Question: Can you imagine the use cases for such a different or relaxed behaviour?
 *
 *
 * Related Exercises:
 * -----------------------------------------------------------------------------
 *
 * - {@link L6_AccessControlContentTest}
 *
 * </pre>
 */
public class L5_AccessControlListImplTest extends AbstractJCRTest {

    private AccessControlManager acMgr;
    private JackrabbitAccessControlList acl;

    private Principal testPrincipal;
    private Privilege[] testPrivileges;

    @Override
    protected void setUp() throws Exception {
        super.setUp();

        acMgr = superuser.getAccessControlManager();

        testPrincipal = ExerciseUtility.createTestGroup(((JackrabbitSession) superuser).getUserManager()).getPrincipal();
        superuser.save();

        acl = AccessControlUtils.getAccessControlList(superuser, testRoot);
        if (acl == null) {
            throw new NotExecutableException();
        }

        testPrivileges = AccessControlUtils.privilegesFromNames(acMgr, Privilege.JCR_READ, Privilege.JCR_WRITE);
    }

    @Override
    protected void tearDown() throws Exception {
        try {
            Authorizable testGroup = ((JackrabbitSession) superuser).getUserManager().getAuthorizable(testPrincipal);
            if (testGroup != null) {
                testGroup.remove();
                superuser.save();
            }
        } finally {
            super.tearDown();
        }
    }

    public void testAddEntryTwice() throws Exception {
        acl.addEntry(testPrincipal, testPrivileges, true, Collections.<String, Value>emptyMap());

        boolean expectedResult = false; // EXERCISE
        assertEquals(expectedResult, acl.addEntry(testPrincipal, testPrivileges, true, Collections.<String, Value>emptyMap()));

        // EXERCISE : verify the size of the ACL.
    }

    public void testUpdateAndComplementary() throws Exception {
        Privilege[] readPriv = AccessControlUtils.privilegesFromNames(acMgr, Privilege.JCR_READ);
        Privilege[] writePriv = AccessControlUtils.privilegesFromNames(acMgr, Privilege.JCR_WRITE);
        Privilege[] acReadPriv = AccessControlUtils.privilegesFromNames(acMgr, Privilege.JCR_READ_ACCESS_CONTROL);

        assertTrue(acl.addEntry(testPrincipal, readPriv, true));
        assertTrue(acl.addEntry(testPrincipal, writePriv, true));
        assertTrue(acl.addEntry(testPrincipal, acReadPriv, true));

        int expectedSize = -1; // EXERCISE
        assertEquals(expectedSize, acl.size());

        assertTrue(acl.addEntry(testPrincipal, readPriv, false));

        expectedSize = -1; // EXERCISE
        assertEquals(expectedSize, acl.size());


        AccessControlEntry[] entries = acl.getAccessControlEntries();

        Privilege[] expectedPrivileges = null; // EXERCISE
        assertArrayEquals(expectedPrivileges, entries[0].getPrivileges());

        Privilege[] expectedPrivileges1 = null; // EXERCISE
        assertArrayEquals(expectedPrivileges1, entries[1].getPrivileges());
    }

    public void testAddEntryWithInvalidPrincipals() throws Exception {
        // EXERCISE: explain for each principal in the list why using it for an ACE fails
        List<Principal> invalidPrincipals = Arrays.asList(
                new InvalidTestPrincipal("unknown"),
                null,
                    new PrincipalImpl(""),
                    new Principal() {
                        @Override
                        public String getName() {
                            return "unknown";
                        }
                });

        for (Principal principal : invalidPrincipals) {
            try {
                acl.addAccessControlEntry(principal, testPrivileges);
                fail("Adding an ACE with an invalid principal should fail");
            } catch (AccessControlException e) {
                // success
            }
        }
    }

    public void testAddEntriesWithCustomKnownPrincipal()  throws Exception {
        Principal oakPrincipal = new PrincipalImpl(testPrincipal.getName());
        Principal principal = new Principal() {
            @Override
            public String getName() {
                return testPrincipal.getName();
            }
        };

        assertTrue(acl.addAccessControlEntry(oakPrincipal, AccessControlUtils.privilegesFromNames(acMgr, Privilege.JCR_READ)));
        assertTrue(acl.addAccessControlEntry(principal, AccessControlUtils.privilegesFromNames(acMgr, Privilege.JCR_READ_ACCESS_CONTROL)));

        int expectedLength = -1; // EXERCISE
        assertEquals(expectedLength, acl.getAccessControlEntries().length);
    }

    public void testAddEntryWithInvalidPrivilege() throws Exception {
        String privilegeName = "AccessControlListImplTestPrivilege";
        Privilege customPriv = ((JackrabbitWorkspace) superuser.getWorkspace()).getPrivilegeManager().registerPrivilege(privilegeName, true, new String[0]);

        // EXERCISE : walks through this test and explain why adding those ACEs fails.
        List<Privilege[]> invalidPrivileges = Arrays.asList(
                new Privilege[0],
                null,
                new Privilege[] {customPriv}
        );

        for (Privilege[] privs : invalidPrivileges) {
            try {
                acl.addAccessControlEntry(testPrincipal, privs);
                fail("Adding an ACE with invalid privilege array should fail.");
            } catch (AccessControlException e) {
                // success
            }
        }
    }

    public void testRemoveInvalidEntry() throws RepositoryException {
        assertTrue(AccessControlUtils.addAccessControlEntry(superuser, testRoot, testPrincipal, testPrivileges, true));

        // EXERCISE : walk through the removal and explain the expected behaviour.
        try {
            acl.removeAccessControlEntry(new JackrabbitAccessControlEntry() {
                public boolean isAllow() {
                    return false;
                }

                @NotNull
                public String[] getRestrictionNames() {
                    return new String[0];
                }

                @Nullable
                public Value getRestriction(@NotNull String restrictionName) {
                    return null;
                }

                @Nullable
                public Value[] getRestrictions(@NotNull String restrictionName) {
                    return null;
                }

                @Override
                public @NotNull PrivilegeCollection getPrivilegeCollection() throws RepositoryException {
                    throw new UnsupportedRepositoryOperationException();
                }

                public Principal getPrincipal() {
                    return testPrincipal;
                }

                public Privilege[] getPrivileges() {
                    return testPrivileges;
                }
            });
            fail("Passing an unknown ACE should fail");
        } catch (AccessControlException e) {
            // success
        }
    }
}