/*
 * 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.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import javax.jcr.AccessDeniedException;
import javax.jcr.Node;
import javax.jcr.NodeIterator;
import javax.jcr.query.Query;
import javax.jcr.security.Privilege;

import org.apache.jackrabbit.api.security.JackrabbitAccessControlList;
import org.apache.jackrabbit.api.security.user.Authorizable;
import org.apache.jackrabbit.api.security.user.Group;
import org.apache.jackrabbit.api.security.user.User;
import org.apache.jackrabbit.api.security.user.UserManager;
import org.apache.jackrabbit.commons.JcrUtils;
import org.apache.jackrabbit.commons.jackrabbit.authorization.AccessControlUtils;
import org.apache.jackrabbit.oak.spi.security.principal.EveryonePrincipal;
import org.apache.jackrabbit.oak.spi.security.principal.PrincipalImpl;
import org.apache.jackrabbit.oak.spi.security.privilege.PrivilegeConstants;
import org.apache.jackrabbit.oak.spi.security.user.UserConstants;
import org.apache.jackrabbit.util.Text;
import org.junit.Before;
import org.junit.Test;

/**
 * Testing permission evaluation for user management operations.
 *
 * @since OAK 1.0 As of OAK user mgt related operations require a specific
 *        user management permission (unless the system in configured to behave like
 *        jackrabbit 2x).
 */
public class UserManagementTest extends AbstractEvaluationTest {

    private final String userId = "testUser2";
    private final String groupId = "testGroup2";

    private List<String> authorizablesToRemove = List.of(userId, groupId);

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

        // setup default permissions
        String authPath = "/rep:security/rep:authorizables";
        AccessControlUtils.addAccessControlEntry(superuser, authPath, EveryonePrincipal.getInstance(), privilegesFromName(Privilege.JCR_READ), true);
        superuser.save();
    }

    @Override
    @Before
    public void tearDown() throws Exception {
        try {
            testSession.refresh(false);
            superuser.refresh(false);

            UserManager userMgr = getUserManager(superuser);
            for (String id : authorizablesToRemove) {
                Authorizable a = userMgr.getAuthorizable(id);
                if (a != null) {
                    a.remove();
                }
            }

            superuser.save();
        } finally {
            super.tearDown();
        }
    }

    private void createUser(String userId) throws Exception {
        getUserManager(superuser).createUser(userId, "pw");
        superuser.save();
        testSession.refresh(false);
    }

    @Test
    public void testCreateUserWithoutPermission() throws Exception {
        UserManager testUserMgr = getUserManager(testSession);

        // testSession has read-only access
        try {
            testUserMgr.createUser(userId, "pw");
            testSession.save();
            fail("Test session doesn't have sufficient permission -> creating user should fail.");
        } catch (AccessDeniedException e) {
            // success
        }

        // testSession has write permission but no user-mgt permission
        // -> should still fail
        modify("/", PrivilegeConstants.REP_WRITE, true);
        try {
            testUserMgr.createUser(userId, "pw");
            testSession.save();
            fail("Test session doesn't have sufficient permission -> creating user should fail.");
        } catch (AccessDeniedException e) {
            // success
        }
    }

    @Test
    public void testCreateUser() throws Exception {
        UserManager testUserMgr = getUserManager(testSession);
        modify("/", PrivilegeConstants.REP_USER_MANAGEMENT, true);

        // creating user should succeed
        testUserMgr.createUser(userId, "pw");
        testSession.save();
    }

    @Test
    public void testCreateUser2() throws Exception {
        UserManager testUserMgr = getUserManager(testSession);
        Privilege[] privs = privilegesFromNames(new String[]{PrivilegeConstants.REP_USER_MANAGEMENT, PrivilegeConstants.REP_WRITE});
        allow("/", privs);

        // creating user should succeed
        testUserMgr.createUser(userId, "pw");
        testSession.save();
    }

    @Test
    public void testCreateGroup() throws Exception {
        UserManager testUserMgr = getUserManager(testSession);
        modify("/", PrivilegeConstants.REP_USER_MANAGEMENT, true);

        // creating group should succeed
        Group gr = testUserMgr.createGroup(groupId);
        testSession.save();
    }

    @Test
    public void testCreateGroup2() throws Exception {
        UserManager testUserMgr = getUserManager(testSession);
        Privilege[] privs = privilegesFromNames(new String[]{PrivilegeConstants.REP_USER_MANAGEMENT, PrivilegeConstants.REP_WRITE});
        allow("/", privs);

        // creating group should succeed
        Group gr = testUserMgr.createGroup(groupId);
        testSession.save();
    }

    @Test
    public void testCreateWithoutReadAccess() throws Exception {
        UserManager testUserMgr = getUserManager(testSession);
        deny("/", privilegesFromName(PrivilegeConstants.JCR_READ));
        allow("/", privilegesFromName(PrivilegeConstants.REP_USER_MANAGEMENT));

        try {
            Group gr = testUserMgr.createGroup(groupId);
            testSession.save();
            fail("Creating group without read-access on the folder node should fail");
        } catch (AccessDeniedException e) {
            // success
        }
    }

    @Test
    public void testCreateWithIntermediateReadDeny() throws Exception {
        String path = UserConstants.DEFAULT_GROUP_PATH + "/a/b/c";
        Node groupRoot = JcrUtils.getOrCreateByPath(path, UserConstants.NT_REP_AUTHORIZABLE_FOLDER, superuser);
        superuser.save();

        try {
            deny(UserConstants.DEFAULT_GROUP_PATH, privilegesFromName(Privilege.JCR_READ));

            Privilege[] privs = privilegesFromNames(new String[]{Privilege.JCR_READ, PrivilegeConstants.REP_USER_MANAGEMENT, PrivilegeConstants.REP_WRITE});
            allow(path, privs);

            Group gr = getUserManager(testSession).createGroup(groupId, new PrincipalImpl(groupId), "a/b/c");
            testSession.save();
        } finally {
            superuser.refresh(false);
            superuser.getNode(UserConstants.DEFAULT_GROUP_PATH + "/a").remove();
            JackrabbitAccessControlList acl = AccessControlUtils.getAccessControlList(acMgr, UserConstants.DEFAULT_GROUP_PATH);
            if (acl != null) {
                acMgr.removePolicy(UserConstants.DEFAULT_GROUP_PATH, acl);
            }
            superuser.save();
        }
    }

    @Test
    public void testCreateWithIntermediateReadDeny2() throws Exception {
        String path = UserConstants.DEFAULT_GROUP_PATH + "/a";
        JcrUtils.getOrCreateByPath(path, UserConstants.NT_REP_AUTHORIZABLE_FOLDER, superuser);
        superuser.save();

        try {
            deny(UserConstants.DEFAULT_GROUP_PATH, privilegesFromName(Privilege.JCR_READ));

            Privilege[] privs = privilegesFromNames(new String[]{Privilege.JCR_READ, PrivilegeConstants.REP_USER_MANAGEMENT, PrivilegeConstants.REP_WRITE});
            allow(path, privs);

            Group gr = getUserManager(testSession).createGroup(groupId, new PrincipalImpl(groupId), "a/b/c");
            testSession.save();
        } finally {
            superuser.refresh(false);
            superuser.getNode(UserConstants.DEFAULT_GROUP_PATH + "/a").remove();
            JackrabbitAccessControlList acl = AccessControlUtils.getAccessControlList(acMgr, UserConstants.DEFAULT_GROUP_PATH);
            if (acl != null) {
                acMgr.removePolicy(UserConstants.DEFAULT_GROUP_PATH, acl);
            }
            superuser.save();
        }
    }

    @Test
    public void testChangePasswordWithoutPermission() throws Exception {
        createUser(userId);

        UserManager testUserMgr = getUserManager(testSession);
        User user = (User) testUserMgr.getAuthorizable(userId);
        try {
            user.changePassword("pw2");
            testSession.save();
            fail();
        } catch (AccessDeniedException e) {
            // success
        }
    }

    @Test
    public void testChangePasswordWithoutPermission2() throws Exception {
        createUser(userId);

        modify("/", PrivilegeConstants.REP_WRITE, true);

        UserManager testUserMgr = getUserManager(testSession);
        User user = (User) testUserMgr.getAuthorizable(userId);
        try {
            user.changePassword("pw2");
            testSession.save();
            fail();
        } catch (AccessDeniedException e) {
            // success
        }
    }

    @Test
    public void testChangePassword() throws Exception {
        createUser(userId);

        // after granting user-mgt privilege changing the pw must succeed.
        modify("/", PrivilegeConstants.REP_USER_MANAGEMENT, true);

        UserManager testUserMgr = getUserManager(testSession);
        User user = (User) testUserMgr.getAuthorizable(userId);
        user.changePassword("pw2");
        testSession.save();
    }

    @Test
    public void testDisableUserWithoutPermission() throws Exception {
        createUser(userId);

        UserManager testUserMgr = getUserManager(testSession);
        User user = (User) testUserMgr.getAuthorizable(userId);
        try {
            user.disable("disabled!");
            testSession.save();
            fail();
        } catch (AccessDeniedException e) {
            // success
        }
    }

    @Test
    public void testDisableUserWithoutPermission2() throws Exception {
        createUser(userId);

        modify("/", PrivilegeConstants.REP_WRITE, true);

        UserManager testUserMgr = getUserManager(testSession);
        User user = (User) testUserMgr.getAuthorizable(userId);
        try {
            user.disable("disabled!");
            testSession.save();
            fail();
        } catch (AccessDeniedException e) {
            // success
        }
    }

    @Test
    public void testDisableUser() throws Exception {
        createUser(userId);

        // after granting user-mgt privilege changing the pw must succeed.
        modify("/", PrivilegeConstants.REP_USER_MANAGEMENT, true);

        UserManager testUserMgr = getUserManager(testSession);
        User user = (User) testUserMgr.getAuthorizable(userId);
        user.disable("disabled!");
        testSession.save();
    }

    @Test
    public void testRemoveUserWithoutPermission() throws Exception {
        createUser(userId);

        UserManager testUserMgr = getUserManager(testSession);
        // testSession has read-only access
        try {
            Authorizable a = testUserMgr.getAuthorizable(userId);
            a.remove();
            testSession.save();
            fail("Test session doesn't have sufficient permission to remove a user.");
        } catch (AccessDeniedException e) {
            // success
        }

        // testSession has write permission but no user-mgt permission
        // -> should still fail
        modify("/", PrivilegeConstants.REP_WRITE, true);
        try {
            Authorizable a = testUserMgr.getAuthorizable(userId);
            a.remove();
            testSession.save();
            fail("Test session doesn't have sufficient permission to remove a user.");
        } catch (AccessDeniedException e) {
            // success
        }
    }

    @Test
    public void testRemoveUser() throws Exception {
        createUser(userId);

        // testSession has user-mgt permission -> removal should succeed.
        modify("/", PrivilegeConstants.REP_USER_MANAGEMENT, true);

        UserManager testUserMgr = getUserManager(testSession);
        Authorizable a = testUserMgr.getAuthorizable(userId);
        a.remove();
        testSession.save();
    }

    @Test
    public void testRemoveUser2() throws Exception {
        createUser(userId);

        // testSession has user-mgt permission -> removal should succeed.
        Privilege[] privs = privilegesFromNames(new String[]{
                PrivilegeConstants.REP_USER_MANAGEMENT,
                PrivilegeConstants.REP_WRITE});
        allow("/", privs);

        UserManager testUserMgr = getUserManager(testSession);
        Authorizable a = testUserMgr.getAuthorizable(userId);
        a.remove();
        testSession.save();
    }

    @Test
    public void testChangeUserPropertiesWithoutPermission() throws Exception {
        createUser(userId);

        // testSession has read-only access
        UserManager testUserMgr = getUserManager(testSession);
        try {
            Authorizable a = testUserMgr.getAuthorizable(userId);
            a.setProperty("someProp", testSession.getValueFactory().createValue("value"));
            testSession.save();
            fail("Test session doesn't have sufficient permission to alter user properties.");
        } catch (AccessDeniedException e) {
            // success
        }
    }

    @Test
    public void testChangeUserPropertiesWithoutPermission2() throws Exception {
        createUser(userId);

        // testSession has read and user-mgt permission but lacks permission to
        // alter regular properties
        modify("/", PrivilegeConstants.REP_USER_MANAGEMENT, true);

        UserManager testUserMgr = getUserManager(testSession);
        try {
            Authorizable a = testUserMgr.getAuthorizable(userId);
            a.setProperty("someProp", testSession.getValueFactory().createValue("value"));
            testSession.save();
            fail("Test session doesn't have sufficient permission to alter user properties.");
        } catch (AccessDeniedException e) {
            // success
        }
    }

    @Test
    public void testChangeUserProperties() throws Exception {
        createUser(userId);

        // make sure user can create/modify/remove regular properties
        modify("/", PrivilegeConstants.JCR_MODIFY_PROPERTIES, true);

        UserManager testUserMgr = getUserManager(testSession);
        Authorizable a = testUserMgr.getAuthorizable(userId);
        a.setProperty("someProp", testSession.getValueFactory().createValue("value"));
        testSession.save();

        a.setProperty("someProperty", testSession.getValueFactory().createValue("modified"));
        testSession.save();

        a.removeProperty("someProperty");
        testSession.save();
    }

    /**
     * @see <a href="https://issues.apache.org/jira/browse/JCR-3412">JCR-3412 :
     *      UserManager.findAuthorizables() does not work, if session does not have
     *      read access to common root of all user and groups. </a>
     */
    @Test
    public void testFindAuthorizables() throws Exception {
        String home = Text.getRelativeParent(UserConstants.DEFAULT_USER_PATH, 1);
        deny(home, privilegesFromName(PrivilegeConstants.JCR_READ));
        allow(getUserManager(superuser).getAuthorizable(testSession.getUserID()).getPath(), privilegesFromName(PrivilegeConstants.JCR_ALL));

        UserManager testUserMgr = getUserManager(testSession);
        Iterator<Authorizable> result = testUserMgr.findAuthorizables(UserConstants.REP_PRINCIPAL_NAME, null, UserManager.SEARCH_TYPE_USER);

        Set<String> ids = new HashSet<String>();
        while (result.hasNext()) {
            ids.add(result.next().getID());
        }
        assertFalse(ids.isEmpty());

        NodeIterator nodeIterator = testSession.getWorkspace().getQueryManager().createQuery("/jcr:root//element(*,rep:User)", Query.XPATH).execute().getNodes();
        assertTrue(nodeIterator.hasNext());
        while (nodeIterator.hasNext()) {
            String userId = nodeIterator.nextNode().getProperty(UserConstants.REP_AUTHORIZABLE_ID).getString();
            if (!ids.remove(userId)) {
                fail("UserId " + userId + " missing in result set.");
            }
        }
        assertTrue("Result mismatch", ids.isEmpty());
    }

    @Test
    public void testGlobRestriction() throws Exception {
        String groupHome = Text.getRelativeParent(UserConstants.DEFAULT_GROUP_PATH, 1);
        Privilege[] privs = privilegesFromName(PrivilegeConstants.REP_USER_MANAGEMENT);
        allow(groupHome, privs);
        deny(groupHome, privs, createGlobRestriction("*/" + UserConstants.REP_MEMBERS));

        UserManager testUserMgr = getUserManager(testSession);

        // creating a new group must be allow
        Group gr = testUserMgr.createGroup(groupId);
        testSession.save();

        // modifying group membership must be denied
        try {
            gr.addMember(testUserMgr.getAuthorizable(testSession.getUserID()));
            testSession.save();
            fail();
        } catch (AccessDeniedException e) {
            // success
        } finally {
            testSession.refresh(false);
        }
    }
}