/*
 * 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.ranger.authorization.kylin.authorizer;

import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import org.apache.commons.lang.RandomStringUtils;
import org.apache.kylin.common.KylinConfig;
import org.apache.kylin.metadata.project.ProjectInstance;
import org.apache.kylin.metadata.project.ProjectManager;
import org.apache.kylin.metadata.project.RealizationEntry;
import org.apache.kylin.rest.util.AclEvaluate;
import org.junit.AfterClass;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.FixMethodOrder;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.MethodSorters;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.util.ReflectionTestUtils;

/**
*
* Here we plug the Ranger RangerKylinAuthorizer into Kylin.
*
* A custom RangerAdminClient is plugged into Ranger in turn, which loads security policies from a local file.
* These policies were generated in the Ranger Admin UI for a kylin service called "kylinTest":
*
* a) A user "kylin" can do all permissions(contains "ADMIN", "MANAGEMENT", "OPERATION", "QUERY")
* on all kylin projects;
* b) A user "zhangqiang" can do a "ADMIN" on the project "test_project",
* and do a "OPERATION" on the project "kylin_project";
* c) A user "yuwen" can do a "ADMIN" on the project "test_project",
* and do a "OPERATION" on the project "kylin_project";
* d) A user "admin" has role "ROLE_ADMIN",
* and the others have role "ROLE_USER" by mock for test.
*
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = { "classpath*:applicationContext.xml", "classpath*:kylinSecurity.xml" })
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
public class RangerKylinAuthorizerTest {
	private static final Map<String, ProjectInstance> uuid2Projects = new HashMap<>();

	private static final Map<String, ProjectInstance> name2Projects = new HashMap<>();

	private static final String ADMIN = "admin";

	private static final String KYLIN = "kylin";

	private static final String ZHANGQIANG = "zhangqiang";

	private static final String YUWEN = "yuwen";

	private static final String LEARN_PROJECT = "learn_project";

	private static final String TEST_PROJECT = "test_project";

	private static final String KYLIN_PROJECT = "kylin_project";

	private static final String[] PROJECTNAMES = new String[] { LEARN_PROJECT, TEST_PROJECT, KYLIN_PROJECT };

	private static final String ROLE_ADMIN = "ADMIN";

	private static final String ROLE_USER = "USER";

	@Autowired
	private AclEvaluate aclEvaluate;

	@BeforeClass
	public static void setup() throws Exception {
		// set kylin conf path
		System.setProperty(KylinConfig.KYLIN_CONF, "src/test/resources");

		// init kylin projects
		initKylinProjects();

		// mock kylin projects, to match projectUuid and projectName for kylin
		mockKylinProjects();
	}

	@AfterClass
	public static void cleanup() throws Exception {
		// do nothing
	}

	/**
	 * Help function: init kylin projects
	 */
	private static void initKylinProjects() {
		for (String projectName : PROJECTNAMES) {
			ProjectInstance projectInstance = getProjectInstance(projectName);
			name2Projects.put(projectName, projectInstance);

			uuid2Projects.put(projectInstance.getUuid(), projectInstance);
		}
	}

	/**
	 * Help function: mock kylin projects, to match projectUuid and projectName
	 */
	private static void mockKylinProjects() {
		KylinConfig kylinConfig = KylinConfig.getInstanceFromEnv();
		ProjectManager projectManager = mock(ProjectManager.class);

		@SuppressWarnings({ "rawtypes", "unchecked" })
		Map<Class, Object> managersCache = (Map<Class, Object>) ReflectionTestUtils.getField(kylinConfig,
				"managersCache");
		managersCache.put(ProjectManager.class, projectManager);

		Answer<ProjectInstance> answer = new Answer<ProjectInstance>() {
			@Override
			public ProjectInstance answer(InvocationOnMock invocation) throws Throwable {
				Object[] args = invocation.getArguments();
				if (args == null || args.length == 0) {
					return null;
				}
				String uuid = (String) args[0];
				return uuid2Projects.get(uuid);
			}
		};
		when(projectManager.getPrjByUuid(anyString())).thenAnswer(answer);
	}

	/**
	 * Help function: get random project instance for test
	 */
	private static ProjectInstance getRandomProjectInstance() {
		String name = RandomStringUtils.randomAlphanumeric(10) + "-project";
		return getProjectInstance(name);
	}

	/**
	 * Help function: get specific project instance for test
	 */
	private static ProjectInstance getProjectInstance(String name) {
		String owner = null;
		String description = null;
		LinkedHashMap<String, String> overrideProps = null;
		List<RealizationEntry> realizationEntries = null;
		List<String> models = null;

		return ProjectInstance.create(name, owner, description, overrideProps, realizationEntries, models);
	}

	// No.1 hasProjectReadPermission test start
	/**
	 * no credentials read any project failed
	 */
	@Test(expected = AuthenticationCredentialsNotFoundException.class)
	public void readProjectAnyWithoutCredentials() {
		ProjectInstance project = getRandomProjectInstance();
		aclEvaluate.hasProjectReadPermission(project);
	}

	/**
	 * admin read all projects sueecss
	 */
	@Test
	@WithMockUser(username = ADMIN, roles = { ROLE_ADMIN })
	public void readProjectAllAsRoleAdmin() {
		for (ProjectInstance project : uuid2Projects.values()) {
			boolean result = aclEvaluate.hasProjectReadPermission(project);
			Assert.assertTrue(result);
		}
	}

	/**
	 * kylin read all projects success
	 */
	@Test
	@WithMockUser(username = KYLIN, roles = { ROLE_USER })
	public void readProjectAllWithAdminPermission() {
		for (ProjectInstance project : uuid2Projects.values()) {
			boolean result = aclEvaluate.hasProjectReadPermission(project);
			Assert.assertTrue(result);
		}
	}

	/**
	 * zhangqiang read test_project success
	 */
	@Test
	@WithMockUser(username = ZHANGQIANG, roles = { ROLE_USER })
	public void readProjectTestWithAdminPermission() {
		ProjectInstance project = name2Projects.get(TEST_PROJECT);
		boolean result = aclEvaluate.hasProjectReadPermission(project);
		Assert.assertTrue(result);
	}

	/**
	 * zhangqiang read kylin_project success
	 */
	@Test
	@WithMockUser(username = ZHANGQIANG, roles = { ROLE_USER })
	public void readProjectKylinWithOperationPermission() {
		ProjectInstance project = name2Projects.get(KYLIN_PROJECT);
		boolean result = aclEvaluate.hasProjectReadPermission(project);
		Assert.assertTrue(result);
	}

	/**
	 * yuwen read test_project success
	 */
	@Test
	@WithMockUser(username = YUWEN, roles = { ROLE_USER })
	public void readProjectTestWithManagementPermission() {
		ProjectInstance project = name2Projects.get(TEST_PROJECT);
		boolean result = aclEvaluate.hasProjectReadPermission(project);
		Assert.assertTrue(result);
	}

	/**
	 * yuwen read kylin_project success
	 */
	@Test
	@WithMockUser(username = YUWEN, roles = { ROLE_USER })
	public void readProjectKylinWithQueryPermission() {
		ProjectInstance project = name2Projects.get(KYLIN_PROJECT);
		boolean result = aclEvaluate.hasProjectReadPermission(project);
		Assert.assertTrue(result);
	}

	/**
	 * yuwen read learn_project failed
	 */
	@Test
	@WithMockUser(username = YUWEN, roles = { ROLE_USER })
	public void readProjectLearnWithoutPermission() {
		ProjectInstance project = name2Projects.get(LEARN_PROJECT);
		boolean result = aclEvaluate.hasProjectReadPermission(project);
		Assert.assertFalse(result);
	}

	// No.1 hasProjectReadPermission test end

	// No.2 hasProjectOperationPermission test start
	/**
	 * no credentials operation any project failed
	 */
	@Test(expected = AuthenticationCredentialsNotFoundException.class)
	public void operationProjectAnyWithoutCredentials() {
		ProjectInstance project = getRandomProjectInstance();
		aclEvaluate.hasProjectOperationPermission(project);
	}

	/**
	 * admin operation all projects sueecss
	 */
	@Test
	@WithMockUser(username = ADMIN, roles = { ROLE_ADMIN })
	public void operationProjectAllAsRoleAdmin() {
		for (ProjectInstance project : uuid2Projects.values()) {
			boolean result = aclEvaluate.hasProjectOperationPermission(project);
			Assert.assertTrue(result);
		}
	}

	/**
	 * kylin operation all projects success
	 */
	@Test
	@WithMockUser(username = KYLIN, roles = { ROLE_USER })
	public void operationProjectAllWithAdminPermission() {
		for (ProjectInstance project : uuid2Projects.values()) {
			boolean result = aclEvaluate.hasProjectOperationPermission(project);
			Assert.assertTrue(result);
		}
	}

	/**
	 * zhangqiang operation test_project success
	 */
	@Test
	@WithMockUser(username = ZHANGQIANG, roles = { ROLE_USER })
	public void operationProjectTestWithAdminPermission() {
		ProjectInstance project = name2Projects.get(TEST_PROJECT);
		boolean result = aclEvaluate.hasProjectOperationPermission(project);
		Assert.assertTrue(result);
	}

	/**
	 * zhangqiang operation kylin_project success
	 */
	@Test
	@WithMockUser(username = ZHANGQIANG, roles = { ROLE_USER })
	public void operationProjectKylinWithOperationPermission() {
		ProjectInstance project = name2Projects.get(KYLIN_PROJECT);
		boolean result = aclEvaluate.hasProjectOperationPermission(project);
		Assert.assertTrue(result);
	}

	/**
	 * yuwen operation test_project success
	 */
	@Test
	@WithMockUser(username = YUWEN, roles = { ROLE_USER })
	public void operationProjectTestWithManagementPermission() {
		ProjectInstance project = name2Projects.get(TEST_PROJECT);
		boolean result = aclEvaluate.hasProjectOperationPermission(project);
		Assert.assertTrue(result);
	}

	/**
	 * yuwen operation kylin_project failed
	 */
	@Test
	@WithMockUser(username = YUWEN, roles = { ROLE_USER })
	public void operationProjectKylinWithoutPermission() {
		ProjectInstance project = name2Projects.get(KYLIN_PROJECT);
		boolean result = aclEvaluate.hasProjectOperationPermission(project);
		Assert.assertFalse(result);
	}

	/**
	 * yuwen operation learn_project failed
	 */
	@Test
	@WithMockUser(username = YUWEN, roles = { ROLE_USER })
	public void operationProjectLearnWithoutPermission() {
		ProjectInstance project = name2Projects.get(LEARN_PROJECT);
		boolean result = aclEvaluate.hasProjectOperationPermission(project);
		Assert.assertFalse(result);
	}

	// No.2 hasProjectOperationPermission test end

	// No.3 hasProjectWritePermission test start
	/**
	 * no credentials write any project failed
	 */
	@Test(expected = AuthenticationCredentialsNotFoundException.class)
	public void writeProjectAnyWithoutCredentials() {
		ProjectInstance project = getRandomProjectInstance();
		aclEvaluate.hasProjectWritePermission(project);
	}

	/**
	 * admin write all projects sueecss
	 */
	@Test
	@WithMockUser(username = ADMIN, roles = { ROLE_ADMIN })
	public void writeProjectAllAsRoleAdmin() {
		for (ProjectInstance project : uuid2Projects.values()) {
			boolean result = aclEvaluate.hasProjectWritePermission(project);
			Assert.assertTrue(result);
		}
	}

	/**
	 * kylin write all projects success
	 */
	@Test
	@WithMockUser(username = KYLIN, roles = { ROLE_USER })
	public void writeProjectAllWithAdminPermission() {
		for (ProjectInstance project : uuid2Projects.values()) {
			boolean result = aclEvaluate.hasProjectWritePermission(project);
			Assert.assertTrue(result);
		}
	}

	/**
	 * zhangqiang write test_project success
	 */
	@Test
	@WithMockUser(username = ZHANGQIANG, roles = { ROLE_USER })
	public void writeProjectTestWithAdminPermission() {
		ProjectInstance project = name2Projects.get(TEST_PROJECT);
		boolean result = aclEvaluate.hasProjectWritePermission(project);
		Assert.assertTrue(result);
	}

	/**
	 * zhangqiang write kylin_project failed
	 */
	@Test
	@WithMockUser(username = ZHANGQIANG, roles = { ROLE_USER })
	public void writeProjectKylinWithoutPermission() {
		ProjectInstance project = name2Projects.get(KYLIN_PROJECT);
		boolean result = aclEvaluate.hasProjectWritePermission(project);
		Assert.assertFalse(result);
	}

	/**
	 * yuwen write test_project success
	 */
	@Test
	@WithMockUser(username = YUWEN, roles = { ROLE_USER })
	public void writeProjectTestWithManagementPermission() {
		ProjectInstance project = name2Projects.get(TEST_PROJECT);
		boolean result = aclEvaluate.hasProjectWritePermission(project);
		Assert.assertTrue(result);
	}

	/**
	 * yuwen write kylin_project failed
	 */
	@Test
	@WithMockUser(username = YUWEN, roles = { ROLE_USER })
	public void writeProjectKylinWithoutPermission2() {
		ProjectInstance project = name2Projects.get(KYLIN_PROJECT);
		boolean result = aclEvaluate.hasProjectWritePermission(project);
		Assert.assertFalse(result);
	}

	/**
	 * yuwen write learn_project failed
	 */
	@Test
	@WithMockUser(username = YUWEN, roles = { ROLE_USER })
	public void writeProjectLearnWithoutPermission() {
		ProjectInstance project = name2Projects.get(LEARN_PROJECT);
		boolean result = aclEvaluate.hasProjectWritePermission(project);
		Assert.assertFalse(result);
	}

	// No.3 hasProjectWritePermission test end

	// No.4 hasProjectAdminPermission test start
	/**
	 * no credentials admin any project failed
	 */
	@Test(expected = AuthenticationCredentialsNotFoundException.class)
	public void adminProjectAnyWithoutCredentials() {
		ProjectInstance project = getRandomProjectInstance();
		aclEvaluate.hasProjectAdminPermission(project);
	}

	/**
	 * admin admin all projects sueecss
	 */
	@Test
	@WithMockUser(username = ADMIN, roles = { ROLE_ADMIN })
	public void adminProjectAllAsRoleAdmin() {
		for (ProjectInstance project : uuid2Projects.values()) {
			boolean result = aclEvaluate.hasProjectAdminPermission(project);
			Assert.assertTrue(result);
		}
	}

	/**
	 * kylin admin all projects success
	 */
	@Test
	@WithMockUser(username = KYLIN, roles = { ROLE_USER })
	public void adminProjectAllWithAdminPermission() {
		for (ProjectInstance project : uuid2Projects.values()) {
			boolean result = aclEvaluate.hasProjectAdminPermission(project);
			Assert.assertTrue(result);
		}
	}

	/**
	 * zhangqiang admin test_project success
	 */
	@Test
	@WithMockUser(username = ZHANGQIANG, roles = { ROLE_USER })
	public void adminProjectTestWithAdminPermission() {
		ProjectInstance project = name2Projects.get(TEST_PROJECT);
		boolean result = aclEvaluate.hasProjectAdminPermission(project);
		Assert.assertTrue(result);
	}

	/**
	 * zhangqiang admin kylin_project failed
	 */
	@Test
	@WithMockUser(username = ZHANGQIANG, roles = { ROLE_USER })
	public void adminProjectKylinWithoutPermission() {
		ProjectInstance project = name2Projects.get(KYLIN_PROJECT);
		boolean result = aclEvaluate.hasProjectAdminPermission(project);
		Assert.assertFalse(result);
	}

	/**
	 * yuwen admin test_project failed
	 */
	@Test
	@WithMockUser(username = YUWEN, roles = { ROLE_USER })
	public void adminProjectTestWithoutPermission() {
		ProjectInstance project = name2Projects.get(TEST_PROJECT);
		boolean result = aclEvaluate.hasProjectAdminPermission(project);
		Assert.assertFalse(result);
	}

	/**
	 * yuwen admin kylin_project failed
	 */
	@Test
	@WithMockUser(username = YUWEN, roles = { ROLE_USER })
	public void adminProjectKylinWithoutPermission2() {
		ProjectInstance project = name2Projects.get(KYLIN_PROJECT);
		boolean result = aclEvaluate.hasProjectAdminPermission(project);
		Assert.assertFalse(result);
	}

	/**
	 * yuwen admin learn_project failed
	 */
	@Test
	@WithMockUser(username = YUWEN, roles = { ROLE_USER })
	public void adminProjectLearnWithoutPermission() {
		ProjectInstance project = name2Projects.get(LEARN_PROJECT);
		boolean result = aclEvaluate.hasProjectAdminPermission(project);
		Assert.assertFalse(result);
	}
	// No.4 hasProjectAdminPermission test end
}