/*
 * Copyright 2002-2016 the original author or authors.
 *
 * Licensed 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.springframework.security.acls.jdbc;

import static org.assertj.core.api.Assertions.*;

import net.sf.ehcache.Cache;
import net.sf.ehcache.CacheManager;
import net.sf.ehcache.Ehcache;
import org.junit.*;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.SingleConnectionDataSource;
import org.springframework.security.acls.domain.*;
import org.springframework.security.acls.model.Acl;
import org.springframework.security.acls.model.AuditableAccessControlEntry;
import org.springframework.security.acls.model.MutableAcl;
import org.springframework.security.acls.model.NotFoundException;
import org.springframework.security.acls.model.ObjectIdentity;
import org.springframework.security.acls.model.Permission;
import org.springframework.security.acls.model.Sid;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.util.FileCopyUtils;

import java.util.*;

/**
 * Tests {@link BasicLookupStrategy}
 *
 * @author Andrei Stefan
 */
public class BasicLookupStrategyTests {

	private static final Sid BEN_SID = new PrincipalSid("ben");
	private static final String TARGET_CLASS = "org.springframework.security.acls.TargetObject";

	// ~ Instance fields
	// ================================================================================================

	private static JdbcTemplate jdbcTemplate;
	private BasicLookupStrategy strategy;
	private static SingleConnectionDataSource dataSource;
	private static CacheManager cacheManager;

	// ~ Methods
	// ========================================================================================================
	@BeforeClass
	public static void initCacheManaer() {
		cacheManager = CacheManager.create();
		cacheManager.addCache(new Cache("basiclookuptestcache", 500, false, false, 30, 30));
	}

	@BeforeClass
	public static void createDatabase() throws Exception {
		dataSource = new SingleConnectionDataSource("jdbc:hsqldb:mem:lookupstrategytest", "sa", "", true);
		dataSource.setDriverClassName("org.hsqldb.jdbcDriver");
		jdbcTemplate = new JdbcTemplate(dataSource);

		Resource resource = new ClassPathResource("createAclSchema.sql");
		String sql = new String(FileCopyUtils.copyToByteArray(resource.getInputStream()));
		jdbcTemplate.execute(sql);
	}

	@AfterClass
	public static void dropDatabase() throws Exception {
		dataSource.destroy();
	}

	@AfterClass
	public static void shutdownCacheManager() {
		cacheManager.removalAll();
		cacheManager.shutdown();
	}

	@Before
	public void populateDatabase() {
		String query = "INSERT INTO acl_sid(ID,PRINCIPAL,SID) VALUES (1,1,'ben');"
				+ "INSERT INTO acl_class(ID,CLASS) VALUES (2,'" + TARGET_CLASS + "');"
				+ "INSERT INTO acl_object_identity(ID,OBJECT_ID_CLASS,OBJECT_ID_IDENTITY,PARENT_OBJECT,OWNER_SID,ENTRIES_INHERITING) VALUES (1,2,100,null,1,1);"
				+ "INSERT INTO acl_object_identity(ID,OBJECT_ID_CLASS,OBJECT_ID_IDENTITY,PARENT_OBJECT,OWNER_SID,ENTRIES_INHERITING) VALUES (2,2,101,1,1,1);"
				+ "INSERT INTO acl_object_identity(ID,OBJECT_ID_CLASS,OBJECT_ID_IDENTITY,PARENT_OBJECT,OWNER_SID,ENTRIES_INHERITING) VALUES (3,2,102,2,1,1);"
				+ "INSERT INTO acl_entry(ID,ACL_OBJECT_IDENTITY,ACE_ORDER,SID,MASK,GRANTING,AUDIT_SUCCESS,AUDIT_FAILURE) VALUES (1,1,0,1,1,1,0,0);"
				+ "INSERT INTO acl_entry(ID,ACL_OBJECT_IDENTITY,ACE_ORDER,SID,MASK,GRANTING,AUDIT_SUCCESS,AUDIT_FAILURE) VALUES (2,1,1,1,2,0,0,0);"
				+ "INSERT INTO acl_entry(ID,ACL_OBJECT_IDENTITY,ACE_ORDER,SID,MASK,GRANTING,AUDIT_SUCCESS,AUDIT_FAILURE) VALUES (3,2,0,1,8,1,0,0);"
				+ "INSERT INTO acl_entry(ID,ACL_OBJECT_IDENTITY,ACE_ORDER,SID,MASK,GRANTING,AUDIT_SUCCESS,AUDIT_FAILURE) VALUES (4,3,0,1,8,0,0,0);";
		jdbcTemplate.execute(query);
	}

	@Before
	public void initializeBeans() {
		EhCacheBasedAclCache cache = new EhCacheBasedAclCache(getCache(),
				new DefaultPermissionGrantingStrategy(new ConsoleAuditLogger()),
				new AclAuthorizationStrategyImpl(new SimpleGrantedAuthority("ROLE_USER")));
		AclAuthorizationStrategy authorizationStrategy = new AclAuthorizationStrategyImpl(
				new SimpleGrantedAuthority("ROLE_ADMINISTRATOR"));
		strategy = new BasicLookupStrategy(dataSource, cache, authorizationStrategy,
				new DefaultPermissionGrantingStrategy(new ConsoleAuditLogger()));
		strategy.setPermissionFactory(new DefaultPermissionFactory());
	}

	@After
	public void emptyDatabase() {
		String query = "DELETE FROM acl_entry;" + "DELETE FROM acl_object_identity WHERE ID = 7;"
				+ "DELETE FROM acl_object_identity WHERE ID = 6;" + "DELETE FROM acl_object_identity WHERE ID = 5;"
				+ "DELETE FROM acl_object_identity WHERE ID = 4;" + "DELETE FROM acl_object_identity WHERE ID = 3;"
				+ "DELETE FROM acl_object_identity WHERE ID = 2;" + "DELETE FROM acl_object_identity WHERE ID = 1;"
				+ "DELETE FROM acl_class;" + "DELETE FROM acl_sid;";
		jdbcTemplate.execute(query);
	}

	private Ehcache getCache() {
		Ehcache cache = cacheManager.getCache("basiclookuptestcache");
		cache.removeAll();
		return cache;
	}

	@Test
	public void testAclsRetrievalWithDefaultBatchSize() throws Exception {
		ObjectIdentity topParentOid = new ObjectIdentityImpl(TARGET_CLASS, new Long(100));
		ObjectIdentity middleParentOid = new ObjectIdentityImpl(TARGET_CLASS, new Long(101));
		// Deliberately use an integer for the child, to reproduce bug report in
		// SEC-819
		ObjectIdentity childOid = new ObjectIdentityImpl(TARGET_CLASS, Integer.valueOf(102));

		Map<ObjectIdentity, Acl> map = this.strategy
				.readAclsById(Arrays.asList(topParentOid, middleParentOid, childOid), null);
		checkEntries(topParentOid, middleParentOid, childOid, map);
	}

	@Test
	public void testAclsRetrievalFromCacheOnly() throws Exception {
		ObjectIdentity topParentOid = new ObjectIdentityImpl(TARGET_CLASS, Integer.valueOf(100));
		ObjectIdentity middleParentOid = new ObjectIdentityImpl(TARGET_CLASS, new Long(101));
		ObjectIdentity childOid = new ObjectIdentityImpl(TARGET_CLASS, new Long(102));

		// Objects were put in cache
		strategy.readAclsById(Arrays.asList(topParentOid, middleParentOid, childOid), null);

		// Let's empty the database to force acls retrieval from cache
		emptyDatabase();
		Map<ObjectIdentity, Acl> map = this.strategy
				.readAclsById(Arrays.asList(topParentOid, middleParentOid, childOid), null);

		checkEntries(topParentOid, middleParentOid, childOid, map);
	}

	@Test
	public void testAclsRetrievalWithCustomBatchSize() throws Exception {
		ObjectIdentity topParentOid = new ObjectIdentityImpl(TARGET_CLASS, new Long(100));
		ObjectIdentity middleParentOid = new ObjectIdentityImpl(TARGET_CLASS, Integer.valueOf(101));
		ObjectIdentity childOid = new ObjectIdentityImpl(TARGET_CLASS, new Long(102));

		// Set a batch size to allow multiple database queries in order to
		// retrieve all
		// acls
		this.strategy.setBatchSize(1);
		Map<ObjectIdentity, Acl> map = this.strategy
				.readAclsById(Arrays.asList(topParentOid, middleParentOid, childOid), null);
		checkEntries(topParentOid, middleParentOid, childOid, map);
	}

	private void checkEntries(ObjectIdentity topParentOid, ObjectIdentity middleParentOid, ObjectIdentity childOid,
			Map<ObjectIdentity, Acl> map) throws Exception {
		assertThat(map).hasSize(3);

		MutableAcl topParent = (MutableAcl) map.get(topParentOid);
		MutableAcl middleParent = (MutableAcl) map.get(middleParentOid);
		MutableAcl child = (MutableAcl) map.get(childOid);

		// Check the retrieved versions has IDs
		assertThat(topParent.getId()).isNotNull();
		assertThat(middleParent.getId()).isNotNull();
		assertThat(child.getId()).isNotNull();

		// Check their parents were correctly retrieved
		assertThat(topParent.getParentAcl()).isNull();
		assertThat(middleParent.getParentAcl().getObjectIdentity()).isEqualTo(topParentOid);
		assertThat(child.getParentAcl().getObjectIdentity()).isEqualTo(middleParentOid);

		// Check their ACEs were correctly retrieved
		assertThat(topParent.getEntries()).hasSize(2);
		assertThat(middleParent.getEntries()).hasSize(1);
		assertThat(child.getEntries()).hasSize(1);

		// Check object identities were correctly retrieved
		assertThat(topParent.getObjectIdentity()).isEqualTo(topParentOid);
		assertThat(middleParent.getObjectIdentity()).isEqualTo(middleParentOid);
		assertThat(child.getObjectIdentity()).isEqualTo(childOid);

		// Check each entry
		assertThat(topParent.isEntriesInheriting()).isTrue();
		assertThat(Long.valueOf(1)).isEqualTo(topParent.getId());
		assertThat(new PrincipalSid("ben")).isEqualTo(topParent.getOwner());
		assertThat(Long.valueOf(1)).isEqualTo(topParent.getEntries().get(0).getId());
		assertThat(topParent.getEntries().get(0).getPermission()).isEqualTo(BasePermission.READ);
		assertThat(topParent.getEntries().get(0).getSid()).isEqualTo(new PrincipalSid("ben"));
		assertThat(((AuditableAccessControlEntry) topParent.getEntries().get(0)).isAuditFailure()).isFalse();
		assertThat(((AuditableAccessControlEntry) topParent.getEntries().get(0)).isAuditSuccess()).isFalse();
		assertThat((topParent.getEntries().get(0)).isGranting()).isTrue();

		assertThat(Long.valueOf(2)).isEqualTo(topParent.getEntries().get(1).getId());
		assertThat(topParent.getEntries().get(1).getPermission()).isEqualTo(BasePermission.WRITE);
		assertThat(topParent.getEntries().get(1).getSid()).isEqualTo(new PrincipalSid("ben"));
		assertThat(((AuditableAccessControlEntry) topParent.getEntries().get(1)).isAuditFailure()).isFalse();
		assertThat(((AuditableAccessControlEntry) topParent.getEntries().get(1)).isAuditSuccess()).isFalse();
		assertThat(topParent.getEntries().get(1).isGranting()).isFalse();

		assertThat(middleParent.isEntriesInheriting()).isTrue();
		assertThat(Long.valueOf(2)).isEqualTo(middleParent.getId());
		assertThat(new PrincipalSid("ben")).isEqualTo(middleParent.getOwner());
		assertThat(Long.valueOf(3)).isEqualTo(middleParent.getEntries().get(0).getId());
		assertThat(middleParent.getEntries().get(0).getPermission()).isEqualTo(BasePermission.DELETE);
		assertThat(middleParent.getEntries().get(0).getSid()).isEqualTo(new PrincipalSid("ben"));
		assertThat(((AuditableAccessControlEntry) middleParent.getEntries().get(0)).isAuditFailure()).isFalse();
		assertThat(((AuditableAccessControlEntry) middleParent.getEntries().get(0)).isAuditSuccess()).isFalse();
		assertThat(middleParent.getEntries().get(0).isGranting()).isTrue();

		assertThat(child.isEntriesInheriting()).isTrue();
		assertThat(Long.valueOf(3)).isEqualTo(child.getId());
		assertThat(new PrincipalSid("ben")).isEqualTo(child.getOwner());
		assertThat(Long.valueOf(4)).isEqualTo(child.getEntries().get(0).getId());
		assertThat(child.getEntries().get(0).getPermission()).isEqualTo(BasePermission.DELETE);
		assertThat(new PrincipalSid("ben")).isEqualTo(child.getEntries().get(0).getSid());
		assertThat(((AuditableAccessControlEntry) child.getEntries().get(0)).isAuditFailure()).isFalse();
		assertThat(((AuditableAccessControlEntry) child.getEntries().get(0)).isAuditSuccess()).isFalse();
		assertThat((child.getEntries().get(0)).isGranting()).isFalse();
	}

	@Test
	public void testAllParentsAreRetrievedWhenChildIsLoaded() throws Exception {
		String query = "INSERT INTO acl_object_identity(ID,OBJECT_ID_CLASS,OBJECT_ID_IDENTITY,PARENT_OBJECT,OWNER_SID,ENTRIES_INHERITING) VALUES (4,2,103,1,1,1);";
		jdbcTemplate.execute(query);

		ObjectIdentity topParentOid = new ObjectIdentityImpl(TARGET_CLASS, Long.valueOf(100));
		ObjectIdentity middleParentOid = new ObjectIdentityImpl(TARGET_CLASS, Long.valueOf(101));
		ObjectIdentity childOid = new ObjectIdentityImpl(TARGET_CLASS, Long.valueOf(102));
		ObjectIdentity middleParent2Oid = new ObjectIdentityImpl(TARGET_CLASS, Long.valueOf(103));

		// Retrieve the child
		Map<ObjectIdentity, Acl> map = this.strategy.readAclsById(Arrays.asList(childOid), null);

		// Check that the child and all its parents were retrieved
		assertThat(map.get(childOid)).isNotNull();
		assertThat(map.get(childOid).getObjectIdentity()).isEqualTo(childOid);
		assertThat(map.get(middleParentOid)).isNotNull();
		assertThat(map.get(middleParentOid).getObjectIdentity()).isEqualTo(middleParentOid);
		assertThat(map.get(topParentOid)).isNotNull();
		assertThat(map.get(topParentOid).getObjectIdentity()).isEqualTo(topParentOid);

		// The second parent shouldn't have been retrieved
		assertThat(map.get(middleParent2Oid)).isNull();
	}

	/**
	 * Test created from SEC-590.
	 */
	@Test
	public void testReadAllObjectIdentitiesWhenLastElementIsAlreadyCached() throws Exception {
		String query = "INSERT INTO acl_object_identity(ID,OBJECT_ID_CLASS,OBJECT_ID_IDENTITY,PARENT_OBJECT,OWNER_SID,ENTRIES_INHERITING) VALUES (4,2,104,null,1,1);"
				+ "INSERT INTO acl_object_identity(ID,OBJECT_ID_CLASS,OBJECT_ID_IDENTITY,PARENT_OBJECT,OWNER_SID,ENTRIES_INHERITING) VALUES (5,2,105,4,1,1);"
				+ "INSERT INTO acl_object_identity(ID,OBJECT_ID_CLASS,OBJECT_ID_IDENTITY,PARENT_OBJECT,OWNER_SID,ENTRIES_INHERITING) VALUES (6,2,106,4,1,1);"
				+ "INSERT INTO acl_object_identity(ID,OBJECT_ID_CLASS,OBJECT_ID_IDENTITY,PARENT_OBJECT,OWNER_SID,ENTRIES_INHERITING) VALUES (7,2,107,5,1,1);"
				+ "INSERT INTO acl_entry(ID,ACL_OBJECT_IDENTITY,ACE_ORDER,SID,MASK,GRANTING,AUDIT_SUCCESS,AUDIT_FAILURE) VALUES (5,4,0,1,1,1,0,0)";
		jdbcTemplate.execute(query);

		ObjectIdentity grandParentOid = new ObjectIdentityImpl(TARGET_CLASS, new Long(104));
		ObjectIdentity parent1Oid = new ObjectIdentityImpl(TARGET_CLASS, new Long(105));
		ObjectIdentity parent2Oid = new ObjectIdentityImpl(TARGET_CLASS, Integer.valueOf(106));
		ObjectIdentity childOid = new ObjectIdentityImpl(TARGET_CLASS, Integer.valueOf(107));

		// First lookup only child, thus populating the cache with grandParent,
		// parent1
		// and child
		List<Permission> checkPermission = Arrays.asList(BasePermission.READ);
		List<Sid> sids = Arrays.asList(BEN_SID);
		List<ObjectIdentity> childOids = Arrays.asList(childOid);

		strategy.setBatchSize(6);
		Map<ObjectIdentity, Acl> foundAcls = strategy.readAclsById(childOids, sids);

		Acl foundChildAcl = foundAcls.get(childOid);
		assertThat(foundChildAcl).isNotNull();
		assertThat(foundChildAcl.isGranted(checkPermission, sids, false)).isTrue();

		// Search for object identities has to be done in the following order:
		// last
		// element have to be one which
		// is already in cache and the element before it must not be stored in
		// cache
		List<ObjectIdentity> allOids = Arrays.asList(grandParentOid, parent1Oid, parent2Oid, childOid);
		try {
			foundAcls = strategy.readAclsById(allOids, sids);

		} catch (NotFoundException notExpected) {
			fail("It shouldn't have thrown NotFoundException");
		}

		Acl foundParent2Acl = foundAcls.get(parent2Oid);
		assertThat(foundParent2Acl).isNotNull();
		assertThat(foundParent2Acl.isGranted(checkPermission, sids, false)).isTrue();
	}

	@Test(expected = IllegalArgumentException.class)
	public void nullOwnerIsNotSupported() {
		String query = "INSERT INTO acl_object_identity(ID,OBJECT_ID_CLASS,OBJECT_ID_IDENTITY,PARENT_OBJECT,OWNER_SID,ENTRIES_INHERITING) VALUES (4,2,104,null,null,1);";

		jdbcTemplate.execute(query);

		ObjectIdentity oid = new ObjectIdentityImpl(TARGET_CLASS, new Long(104));

		strategy.readAclsById(Arrays.asList(oid), Arrays.asList(BEN_SID));
	}

	@Test
	public void testCreatePrincipalSid() {
		Sid result = strategy.createSid(true, "sid");

		assertThat(result.getClass()).isEqualTo(PrincipalSid.class);
		assertThat(((PrincipalSid) result).getPrincipal()).isEqualTo("sid");
	}

	@Test
	public void testCreateGrantedAuthority() {
		Sid result = strategy.createSid(false, "sid");

		assertThat(result.getClass()).isEqualTo(GrantedAuthoritySid.class);
		assertThat(((GrantedAuthoritySid) result).getGrantedAuthority()).isEqualTo("sid");
	}

}
