/*
 * 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.user;

import org.apache.jackrabbit.JcrConstants;
import org.apache.jackrabbit.api.security.user.Authorizable;
import org.apache.jackrabbit.api.security.user.User;
import org.apache.jackrabbit.api.security.user.UserManager;
import org.apache.jackrabbit.oak.AbstractSecurityTest;
import org.apache.jackrabbit.oak.api.CommitFailedException;
import org.apache.jackrabbit.oak.api.PropertyState;
import org.apache.jackrabbit.oak.api.Tree;
import org.apache.jackrabbit.oak.api.Type;
import org.apache.jackrabbit.oak.commons.UUIDUtils;
import org.apache.jackrabbit.oak.plugins.memory.MemoryNodeStore;
import org.apache.jackrabbit.oak.plugins.memory.PropertyStates;
import org.apache.jackrabbit.oak.plugins.tree.TreeUtil;
import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
import org.apache.jackrabbit.oak.spi.commit.Validator;
import org.apache.jackrabbit.oak.spi.security.ConfigurationParameters;
import org.apache.jackrabbit.oak.spi.security.user.UserConstants;
import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
import org.apache.jackrabbit.oak.spi.state.NodeState;
import org.apache.jackrabbit.util.Text;
import org.jetbrains.annotations.NotNull;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

import static java.util.Objects.requireNonNull;
import static org.apache.jackrabbit.JcrConstants.JCR_PRIMARYTYPE;
import static org.apache.jackrabbit.JcrConstants.JCR_UUID;
import static org.apache.jackrabbit.JcrConstants.NT_UNSTRUCTURED;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

/**
 * @since OAK 1.0
 */
public class UserValidatorTest extends AbstractSecurityTest implements UserConstants {

    private String userPath;

    @Before
    public void before() throws Exception {
        super.before();
        userPath = getTestUser().getPath();
    }

    @After
    public void after() throws Exception {
        try {
            root.refresh();
        } finally {
            super.after();
        }
    }

    @NotNull
    private UserValidatorProvider createValidatorProvider() {
        return new UserValidatorProvider(getConfig(), getRootProvider(), getTreeProvider());
    }

    @NotNull
    private UserValidator createUserValidator(@NotNull Tree before, @NotNull Tree after) {
        UserValidatorProvider uvp = createValidatorProvider();
        // force creation of membership provider
        uvp.getRootValidator(getTreeProvider().asNodeState(before), getTreeProvider().asNodeState(after), new CommitInfo("sid", null));
        return new UserValidator(before, after, uvp);
    }

    private void assertRemoveProperty(@NotNull String name, int expectedCode) throws CommitFailedException {
        try {
            Tree userTree = root.getTree(userPath);
            userTree.removeProperty(name);
            root.commit();
        } catch (CommitFailedException e) {
            assertEquals(expectedCode, e.getCode());
            assertTrue(e.isConstraintViolation());
            throw e;
        }
    }

    private void assertChangeProperty(@NotNull String name, @NotNull String value, int expectedCode) throws CommitFailedException {
        try {
            Tree userTree = root.getTree(userPath);
            userTree.setProperty(name, value);
            root.commit();
        } catch (CommitFailedException e) {
            assertEquals(expectedCode, e.getCode());
            assertTrue(e.isConstraintViolation());
            throw e;
        }
    }

    @NotNull
    private ConfigurationParameters getConfig() {
        return getUserConfiguration().getParameters();
    }

    @Test(expected = CommitFailedException.class)
    public void removePassword() throws Exception {
        assertRemoveProperty(REP_PASSWORD, 25);
    }

    @Test(expected = CommitFailedException.class)
    public void removePrincipalName() throws Exception {
        assertRemoveProperty(REP_PRINCIPAL_NAME, 22);
    }

    @Test(expected = CommitFailedException.class)
    public void removePrincipalName2() throws Exception {
        try {
            Tree userTree = root.getTree(userPath);
            UserValidator validator = createUserValidator(userTree, userTree);
            validator.propertyDeleted(userTree.getProperty(REP_PRINCIPAL_NAME));
        } catch (CommitFailedException e) {
            assertEquals(25, e.getCode());
            assertTrue(e.isConstraintViolation());
            throw e;
        }
    }

    @Test(expected = CommitFailedException.class)
    public void removeAuthorizableId() throws Exception {
        assertRemoveProperty(REP_AUTHORIZABLE_ID, 25);
    }

    @Test(expected = CommitFailedException.class)
    public void createWithoutPrincipalName() throws Exception {
        User user = getUserManager(root).createUser("withoutPrincipalName", "pw");
        Tree tree = root.getTree(user.getPath());
        tree.removeProperty(REP_PRINCIPAL_NAME);
        root.commit();
    }

    @Test(expected = CommitFailedException.class)
    public void createWithoutPrincipalName2() throws Exception {
        Tree userTree = root.getTree(userPath);
        userTree.removeProperty(REP_PRINCIPAL_NAME);
        NodeState userState = getTreeProvider().asNodeState(userTree);

        try {
            Tree tree = root.getTree(userPath).getParent();
            createUserValidator(tree, tree).childNodeAdded(userTree.getName(), userState);
        } catch (CommitFailedException e) {
            assertEquals(26, e.getCode());
            throw e;
        }
    }

    @Test
    public void createWithoutAuthorizableId() throws Exception {
        User user = getUserManager(root).createUser("withoutId", "pw");
        Tree tree = root.getTree(user.getPath());
        tree.removeProperty(REP_AUTHORIZABLE_ID);
        root.commit();

        assertNotNull(getUserManager(root).getAuthorizable("withoutId"));
    }

    @Test(expected = CommitFailedException.class)
    public void createWithInvalidUUID() throws Exception {
        User user = getUserManager(root).createUser("withInvalidUUID", "pw");
        Tree tree = root.getTree(user.getPath());
        tree.setProperty(JcrConstants.JCR_UUID, UUID.randomUUID().toString());
        root.commit();
    }

    @Test(expected = CommitFailedException.class)
    public void createSystemUserWithPw() throws Exception {
        try {
            User user = getUserManager(root).createSystemUser("withPw", null);
            Tree tree = root.getTree(user.getPath());
            tree.setProperty(REP_PASSWORD, "pw", Type.STRING);
            root.commit();
        } catch (CommitFailedException e) {
            assertEquals(32, e.getCode());
            throw e;
        }
    }

    @Test(expected = CommitFailedException.class)
    public void createSystemUserWithPwNode() throws Exception {
        try {
            User user = getUserManager(root).createSystemUser("withPwNode", null);
            Tree tree = root.getTree(user.getPath());
            TreeUtil.addChild(tree, REP_PWD, NT_REP_PASSWORD);
            root.commit();
        } catch (CommitFailedException e) {
            assertEquals(33, e.getCode());
            throw e;
        }
    }

    @Test(expected = CommitFailedException.class)
    public void changeUUID() throws Exception {
        assertChangeProperty(JcrConstants.JCR_UUID, UUID.randomUUID().toString(), 23);
    }

    @Test
    public void changeUUIDValid() throws Exception {
        Tree userTree = root.getTree(userPath);
        UserValidator validator = createUserValidator(userTree, userTree);
        validator.propertyChanged(PropertyStates.createProperty(JCR_UUID, "invalidBefore"), userTree.getProperty(JCR_UUID));
    }

    @Test(expected = CommitFailedException.class)
    public void changePrincipalName() throws Exception {
        assertChangeProperty(REP_PRINCIPAL_NAME, "another", 22);
    }

    @Test(expected = CommitFailedException.class)
    public void changeAuthorizableId() throws Exception {
        assertChangeProperty(REP_AUTHORIZABLE_ID, "modified", 22);
    }

    @Test(expected = CommitFailedException.class)
    public void changePasswordToPlainText() throws Exception {
        assertChangeProperty(REP_PASSWORD, "plaintext", 24);
    }

    @Test(expected = CommitFailedException.class)
    public void changePasswordToPlainText2() throws Exception {
        Tree beforeTree = when(mock(Tree.class).getProperty(JCR_PRIMARYTYPE)).thenReturn(PropertyStates.createProperty(JCR_PRIMARYTYPE, NT_UNSTRUCTURED, Type.NAME)).getMock();
        Tree afterTree = root.getTree(userPath);

        UserValidator uv = new UserValidator(beforeTree, afterTree, new UserValidatorProvider(ConfigurationParameters.EMPTY, getRootProvider(), getTreeProvider()));
        PropertyState plainTextAfter = PropertyStates.createProperty(REP_PASSWORD, "pw");
        uv.propertyChanged(afterTree.getProperty(REP_PASSWORD), plainTextAfter);
    }

    @Test(expected = CommitFailedException.class)
    public void changePrimaryType() throws Exception {
        try {
            Tree userTree = root.getTree(userPath);
            UserValidator validator = createUserValidator(userTree, userTree);
            validator.propertyChanged(userTree.getProperty(JCR_PRIMARYTYPE), PropertyStates.createProperty(JCR_PRIMARYTYPE, NT_REP_GROUP));
        } catch (CommitFailedException e) {
            assertEquals(28, e.getCode());
            throw e;
        }
    }

    @Test
    public void changePrimaryTypeValid() throws Exception {
        Tree userTree = root.getTree(userPath);
        UserValidator validator = createUserValidator(userTree, userTree);
        validator.propertyChanged(PropertyStates.createProperty(JCR_PRIMARYTYPE, NT_REP_GROUP), userTree.getProperty(JCR_PRIMARYTYPE));
    }

    @Test(expected = CommitFailedException.class)
    public void testRemoveAdminUser() throws Exception {
        String adminId = getConfig().getConfigValue(PARAM_ADMIN_ID, DEFAULT_ADMIN_ID);
        UserManager userMgr = getUserManager(root);
        Authorizable admin = userMgr.getAuthorizable(adminId);
        if (admin == null) {
            admin = userMgr.createUser(adminId, adminId);
            root.commit();
        }

        root.getTree(admin.getPath()).remove();
        root.commit();
    }

    @Test(expected = CommitFailedException.class)
    public void testRemoveAdminUserFolder() throws Exception {
        String adminId = getConfig().getConfigValue(PARAM_ADMIN_ID, DEFAULT_ADMIN_ID);
        UserManager userMgr = getUserManager(root);
        Authorizable admin = userMgr.getAuthorizable(adminId);
        if (admin == null) {
            admin = userMgr.createUser(adminId, adminId);
            root.commit();
        }

        try {
            root.getTree(admin.getPath()).getParent().remove();
            root.commit();
        } catch (CommitFailedException e) {
            assertEquals(27, e.getCode());
            throw e;
        }
    }

    @Test(expected = CommitFailedException.class)
    public void testDisableAdminUser() throws Exception {
        try {
            String adminId = getConfig().getConfigValue(PARAM_ADMIN_ID, DEFAULT_ADMIN_ID);
            UserManager userMgr = getUserManager(root);
            Authorizable admin = userMgr.getAuthorizable(adminId);
            if (admin == null) {
                admin = userMgr.createUser(adminId, adminId);
                root.commit();
            }

            root.getTree(admin.getPath()).setProperty(REP_DISABLED, "disabled");
            root.commit();
        } catch (CommitFailedException e) {
            assertEquals(20, e.getCode());
            throw e;
        }
    }

    @Test
    public void testDisableAdminUserNonExistingTree() throws Exception {
        String adminId = getConfig().getConfigValue(PARAM_ADMIN_ID, DEFAULT_ADMIN_ID);
        Authorizable admin = getUserManager(root).getAuthorizable(adminId);

        Tree userTree = root.getTree(requireNonNull(admin).getPath());
        UserValidator validator = createUserValidator(userTree, userTree);
        userTree.remove();

        validator.propertyAdded(PropertyStates.createProperty(REP_DISABLED, "disabled"));
    }

    @Test
    public void testEnforceHierarchy() {
        List<String> invalid = new ArrayList<>();
        invalid.add("/");
        invalid.add("/jcr:system");
        String groupRoot = getConfig().getConfigValue(PARAM_GROUP_PATH, DEFAULT_GROUP_PATH);
        invalid.add(groupRoot);
        String userRoot = getConfig().getConfigValue(PARAM_USER_PATH, DEFAULT_USER_PATH);
        invalid.add(Text.getRelativeParent(userRoot, 1));
        invalid.add(userPath);
        invalid.add(userPath + "/folder");

        UserProvider up = new UserProvider(root, getUserConfiguration().getParameters());
        for (String path : invalid) {
            try {
                Tree parent = root.getTree(path);
                if (!parent.exists()) {
                    String[] segments = Text.explode(path, '/', false);
                    parent = root.getTree("/");
                    for (String segment : segments) {
                        Tree next = parent.getChild(segment);
                        if (!next.exists()) {
                            next = parent.addChild(segment);
                            next.setProperty(JCR_PRIMARYTYPE, NT_REP_AUTHORIZABLE_FOLDER, Type.NAME);
                            parent = next;
                        }
                    }
                }
                Tree userTree = parent.addChild("testUser");
                userTree.setProperty(JCR_PRIMARYTYPE, NT_REP_USER, Type.NAME);
                userTree.setProperty(JcrConstants.JCR_UUID, up.getContentID("testUser"));
                userTree.setProperty(REP_PRINCIPAL_NAME, "testUser");
                root.commit();
                fail("Invalid hierarchy should be detected");

            } catch (CommitFailedException e) {
                // success
            } finally {
                root.refresh();
            }
        }
    }

    @Test(expected = CommitFailedException.class)
    public void testCreateNestedUser() throws Exception {
        Tree userTree = root.getTree(getTestUser().getPath());
        Tree profile = TreeUtil.addChild(userTree, "profile", JcrConstants.NT_UNSTRUCTURED);
        Tree nested = TreeUtil.addChild(profile, "nested", UserConstants.NT_REP_USER);
        nested.setProperty(UserConstants.REP_PRINCIPAL_NAME, "nested");
        nested.setProperty(UserConstants.REP_AUTHORIZABLE_ID, "nested");
        nested.setProperty(JcrConstants.JCR_UUID, UUIDUtils.generateUUID("nested"));
        try {
            root.commit();
            fail("Creating nested users must be detected.");
        } catch (CommitFailedException e) {
            // success
            assertEquals(29, e.getCode());
            throw e;
        }
    }

    @Test(expected = CommitFailedException.class)
    public void testCreateNestedUser2Steps() throws Exception {
        Tree userTree = root.getTree(getTestUser().getPath());
        Tree profile = TreeUtil.addChild(userTree, "profile", JcrConstants.NT_UNSTRUCTURED);
        Tree nested = TreeUtil.addChild(profile, "nested", JcrConstants.NT_UNSTRUCTURED);
        nested.setProperty(UserConstants.REP_PRINCIPAL_NAME, "nested");
        nested.setProperty(UserConstants.REP_AUTHORIZABLE_ID, "nested");
        nested.setProperty(JcrConstants.JCR_UUID, UUIDUtils.generateUUID("nested"));
        root.commit();

        try {
            nested.setProperty(JCR_PRIMARYTYPE, UserConstants.NT_REP_USER, Type.NAME);
            root.commit();
            fail("Creating nested users must be detected.");
        } catch (CommitFailedException e) {
            // success
            assertEquals(29, e.getCode());
            throw e;
        }
    }

    @Test
    public void hiddenNodeAdded() throws CommitFailedException {
        UserValidatorProvider provider = createValidatorProvider();
        MemoryNodeStore store = new MemoryNodeStore();
        NodeState root = store.getRoot();
        NodeBuilder builder = root.builder();
        NodeBuilder test = builder.child("test");
        NodeBuilder hidden = test.child(":hidden");

        Validator validator = provider.getRootValidator(
                root, builder.getNodeState(), CommitInfo.EMPTY);
        Validator childValidator = validator.childNodeAdded(
                "test", test.getNodeState());
        assertNotNull(childValidator);

        Validator hiddenValidator = childValidator.childNodeAdded(
                ":hidden", hidden.getNodeState());
        assertNull(hiddenValidator);
    }

    @Test
    public void hiddenNodeChanged() throws CommitFailedException {
        UserValidatorProvider provider = createValidatorProvider();
        MemoryNodeStore store = new MemoryNodeStore();
        NodeBuilder builder = store.getRoot().builder();
        builder.child("test").child(":hidden");
        NodeState root = builder.getNodeState();

        NodeBuilder test = root.builder().child("test");
        NodeBuilder hidden = test.child(":hidden");
        hidden.child("added");

        Validator validator = provider.getRootValidator(
                root, builder.getNodeState(), CommitInfo.EMPTY);
        Validator childValidator = validator.childNodeChanged(
                "test", root.getChildNode("test"), test.getNodeState());
        assertNotNull(childValidator);

        Validator hiddenValidator = childValidator.childNodeChanged(
                ":hidden", root.getChildNode("test").getChildNode(":hidden"), hidden.getNodeState());
        assertNull(hiddenValidator);
    }

    @Test
    public void hiddenNodeDeleted() throws CommitFailedException {
        UserValidatorProvider provider = createValidatorProvider();
        MemoryNodeStore store = new MemoryNodeStore();
        NodeBuilder builder = store.getRoot().builder();
        builder.child("test").child(":hidden");
        NodeState root = builder.getNodeState();

        builder = root.builder();
        NodeBuilder test = builder.child("test");
        test.child(":hidden").remove();

        Validator validator = provider.getRootValidator(
                root, builder.getNodeState(), CommitInfo.EMPTY);
        Validator childValidator = validator.childNodeChanged(
                "test", root.getChildNode("test"), test.getNodeState());
        assertNotNull(childValidator);

        Validator hiddenValidator = childValidator.childNodeDeleted(
                ":hidden", root.getChildNode("test").getChildNode(":hidden"));
        assertNull(hiddenValidator);
    }
}