/*
 * 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.api.security.user.Group;
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.ContentSession;
import org.apache.jackrabbit.oak.api.Root;
import org.apache.jackrabbit.oak.api.Tree;
import org.apache.jackrabbit.oak.namepath.NamePathMapper;
import org.apache.jackrabbit.oak.plugins.tree.TreeUtil;
import org.apache.jackrabbit.oak.spi.security.ConfigurationParameters;
import org.apache.jackrabbit.oak.spi.security.authentication.ImpersonationCredentials;
import org.apache.jackrabbit.oak.spi.security.authorization.accesscontrol.AccessControlConstants;
import org.apache.jackrabbit.oak.spi.security.principal.AdminPrincipal;
import org.apache.jackrabbit.oak.spi.security.principal.SystemUserPrincipal;
import org.apache.jackrabbit.oak.spi.security.privilege.PrivilegeConstants;
import org.apache.jackrabbit.oak.spi.security.user.UserConfiguration;
import org.apache.jackrabbit.oak.spi.security.user.UserConstants;
import org.apache.jackrabbit.oak.spi.security.user.UserIdCredentials;
import org.apache.jackrabbit.oak.spi.security.user.action.AccessControlAction;
import org.apache.jackrabbit.oak.spi.security.user.action.AuthorizableAction;
import org.apache.jackrabbit.oak.spi.security.user.action.AuthorizableActionProvider;
import org.jetbrains.annotations.Nullable;
import org.junit.Before;
import org.junit.Test;

import javax.jcr.Credentials;
import javax.jcr.SimpleCredentials;
import javax.jcr.UnsupportedRepositoryOperationException;
import javax.jcr.nodetype.ConstraintViolationException;
import javax.security.auth.login.LoginException;
import java.util.Iterator;
import java.util.List;
import java.util.UUID;

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;

/**
 * Implementation specific test wrt system users.
 */
public class SystemUserImplTest extends AbstractSecurityTest {

    private UserManager userMgr;
    private String uid;

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

        userMgr = getUserManager(root);
        uid = "testUser" + UUID.randomUUID();
    }

    @Override
    public void after() throws Exception {
        try {
            root.refresh();
            User user = userMgr.getAuthorizable(uid, User.class);
            if (user != null) {
                user.remove();
                root.commit();
            }
        } finally {
            super.after();
        }
    }

    @Override
    protected ConfigurationParameters getSecurityConfigParameters() {
        return ConfigurationParameters.of(
                UserConfiguration.NAME,
                ConfigurationParameters.of(UserConstants.PARAM_AUTHORIZABLE_ACTION_PROVIDER, (AuthorizableActionProvider) securityProvider -> {
                    AuthorizableAction action = new AccessControlAction();
                    action.init(securityProvider, ConfigurationParameters.of(AccessControlAction.USER_PRIVILEGE_NAMES, new String[]{PrivilegeConstants.JCR_ALL}));
                    return List.of(action);
                }));
    }

    private User createUser(@Nullable String intermediatePath) throws Exception {
        User user = userMgr.createSystemUser(uid, intermediatePath);
        root.commit();
        return user;
    }

    @Test
    public void testCreateSystemUser() throws Exception {
        assertTrue(createUser(null) instanceof SystemUserImpl);
    }

    @Test
    public void testIsAdmin() throws Exception {
        assertFalse(createUser(null).isAdmin());
    }

    @Test
    public void testIsSystemUser() throws Exception {
        assertTrue(createUser(null).isSystemUser());
    }

    @Test
    public void testIsGroup() throws Exception {
        assertFalse(createUser(null).isGroup());
    }

    @Test
    public void testSystemUserTree() throws Exception {
        Tree t = root.getTree(createUser(null).getPath());
        assertFalse(t.hasProperty(UserConstants.REP_PASSWORD));
        assertEquals(UserConstants.NT_REP_SYSTEM_USER, TreeUtil.getPrimaryTypeName(t));
    }

    @Test(expected = IllegalArgumentException.class)
    public void testCheckValidTree() throws Exception {
        User testUser = getTestUser();
        new SystemUserImpl(testUser.getID(), root.getTree(testUser.getPath()), (UserManagerImpl) userMgr);
    }

    @Test
    public void testGetCredentials() throws Exception {
        User user = createUser(null);

        Credentials creds = user.getCredentials();
        assertTrue(creds instanceof UserIdCredentials);

        UserIdCredentials impl = (UserIdCredentials) creds;
        assertEquals(uid, impl.getUserId());
    }

    @Test
    public void testHasNoPassword() throws Exception {
        User user = createUser(null);

        Tree userTree = root.getTree(user.getPath());
        assertFalse(userTree.hasProperty(UserConstants.REP_PASSWORD));
    }

    @Test(expected = UnsupportedRepositoryOperationException.class)
    public void testChangePassword() throws Exception {
        User user = createUser(null);
        user.changePassword("pw");
    }

    @Test(expected = UnsupportedRepositoryOperationException.class)
    public void testChangePassword2() throws Exception {
        User user = createUser(null);
        user.changePassword("pw", "newPw");
    }

    /**
     * @since OAK 1.0 In contrast to Jackrabbit core the intermediate path may
     * not be an absolute path in OAK.
     */
    @Test(expected = ConstraintViolationException.class)
    public void testCreateUserWithAbsolutePath() throws Exception {
        createUser("/any/path/to/the/new/user");
    }

    @Test(expected = ConstraintViolationException.class)
    public void testCreateUserWithAbsolutePath2() throws Exception {
        createUser(UserConstants.DEFAULT_USER_PATH + "/any/path/to/the/new/user");
    }

    @Test
    public void testCreateUserWithAbsolutePath3() throws Exception {
        String userRoot = UserConstants.DEFAULT_USER_PATH + '/' + UserConstants.DEFAULT_SYSTEM_RELATIVE_PATH;
        String path = userRoot + "/any/path/to/the/new/user";

        assertTrue(createUser(path).getPath().startsWith(path));
    }

    @Test(expected = ConstraintViolationException.class)
    public void testCreateUserWithRelativePath() throws Exception {
        createUser("any/path");
    }

    @Test
    public void testCreateUserWithRelativePath2() throws Exception {
        User user = createUser(UserConstants.DEFAULT_SYSTEM_RELATIVE_PATH + "/any/path");

        assertNotNull(user.getID());
        assertTrue(user.getPath().contains("any/path"));
    }

    @Test
    public void testCreateSystemUserWithOtherPath() throws Exception {
        String path = null;
        try {
            Tree t = root.getTree(UserConstants.DEFAULT_USER_PATH);
            Tree systemUserTree = TreeUtil.addChild(t, "systemUser", UserConstants.NT_REP_SYSTEM_USER);
            systemUserTree.setProperty(UserConstants.REP_PRINCIPAL_NAME, "systemUser");
            systemUserTree.setProperty(UserConstants.REP_AUTHORIZABLE_ID, "systemUser");
            path = systemUserTree.getPath();

            root.commit();
            fail();
        } catch (CommitFailedException e) {
            // success
            assertTrue(e.isConstraintViolation());
        } finally {
            root.refresh();
            if (path != null) {
                Tree t = root.getTree(path);
                if (t.exists()) {
                    t.remove();
                    root.commit();
                }
            }
        }
    }

    @Test
    public void testLoginAsSystemUser() throws Exception {
        createUser(null);
        try {
            login(new SimpleCredentials(uid, new char[0])).close();
            fail();
        } catch (LoginException e) {
            // success
        }
    }

    @Test(expected = LoginException.class)
    public void testLoginAsSystemUser2() throws Exception {
        User user = createUser(null);
        login(user.getCredentials()).close();
    }

    @Test
    public void testImpersonateSystemUser() throws Exception {
        createUser(null);
        ContentSession cs = login(new ImpersonationCredentials(new SimpleCredentials(uid, new char[0]), adminSession.getAuthInfo()));
        cs.close();
    }

    @Test
    public void testImpersonateDisabledSystemUser() throws Exception {
        User user = createUser(null);
        user.disable("disabled");
        root.commit();
        try {
            ContentSession cs = login(new ImpersonationCredentials(new SimpleCredentials(uid, new char[0]), adminSession.getAuthInfo()));
            cs.close();
            fail();
        } catch (LoginException e) {
            // success
        }
    }

    @Test
    public void testGetPrincipal() throws Exception {
        User user = createUser(null);
        assertTrue(user.getPrincipal() instanceof SystemUserPrincipal);
    }

    @Test
    public void testAddToGroup() throws Exception {
        User user = createUser(null);

        Group g = null;
        try {
            g = userMgr.createGroup("testGroup");
            g.addMember(user);
            root.commit();

            assertTrue(g.isMember(user));
            assertTrue(g.isDeclaredMember(user));

            boolean isMemberOfG = false;
            Iterator<Group> groups = user.declaredMemberOf();
            while (groups.hasNext() && !isMemberOfG) {
                if (g.getID().equals(groups.next().getID())) {
                    isMemberOfG = true;
                }
            }
            assertTrue(isMemberOfG);
        } finally {
            if (g != null) {
                g.remove();
                root.commit();
            }
        }
    }

    /**
     * Test asserting that {@link AuthorizableAction#onCreate(User, String, Root, NamePathMapper)}
     * is omitted upon calling {@link UserManager#createSystemUser(String, String)}.
     */
    @Test
    public void testOnCreateOmitted() throws Exception {
        User user = createUser(null);

        Tree t = root.getTree(user.getPath());
        assertFalse(t.hasChild(AccessControlConstants.REP_POLICY));
    }

    @Test
    public void testReplacesAdministrator() throws Exception {
        String adminId = getUserConfiguration().getParameters().getConfigValue(UserConstants.PARAM_ADMIN_ID, UserConstants.DEFAULT_ADMIN_ID);
        User admin = userMgr.getAuthorizable(adminId, User.class);
        root.getTree(admin.getPath()).remove();

        User adminAsSystemUser = userMgr.createSystemUser(adminId, null);
        assertTrue(adminAsSystemUser.isAdmin());
        assertTrue(adminAsSystemUser.getPrincipal() instanceof AdminPrincipal);
    }
}
