package org.fcrepo.test.fesl.policy; import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertTrue; import static junit.framework.Assert.fail; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.PropertyResourceBundle; import java.util.ResourceBundle; import java.util.regex.Matcher; import java.util.regex.Pattern; import junit.framework.JUnit4TestAdapter; import org.fcrepo.client.FedoraClient; import org.fcrepo.common.Constants; import org.fcrepo.test.FedoraServerTestCase; import org.fcrepo.test.fesl.util.AuthorizationDeniedException; import org.fcrepo.test.fesl.util.FedoraUtil; import org.fcrepo.test.fesl.util.HttpUtils; import org.fcrepo.test.fesl.util.LoadDataset; import org.fcrepo.test.fesl.util.PolicyUtils; import org.fcrepo.test.fesl.util.RemoveDataset; import org.junit.After; import org.junit.AfterClass; import org.junit.Assume; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class TestPolicies extends FedoraServerTestCase implements Constants { private static final Logger logger = LoggerFactory.getLogger(TestPolicies.class); private static final String PROPERTIES = "fedora"; private static FedoraClient s_client; private HttpUtils httpUtils = null; private static String ri_impl; //private FedoraAPIM apim = null; private PolicyUtils policyUtils = null; //private static PolicyStoreService polMan = null; // was: PolicyStore public static junit.framework.Test suite() { return new JUnit4TestAdapter(TestPolicies.class); } @BeforeClass public static void bootStrap() throws Exception { s_client = getFedoraClient(); ri_impl = getRIImplementation(); } @AfterClass public static void cleanUp() { s_client.shutdown(); } @Before public void setUp() { PropertyResourceBundle prop = (PropertyResourceBundle) ResourceBundle.getBundle(PROPERTIES); String username = prop.getString("fedora.admin.username"); String password = prop.getString("fedora.admin.password"); //String fedoraUrl = prop.getString("fedora.url"); String fedoraUrl = FedoraUtil.getBaseURL(); try { if (logger.isDebugEnabled()) { logger.debug("Setting up..."); } policyUtils = new PolicyUtils(s_client); //PolicyStoreFactory f = new PolicyStoreFactory(); //polMan = f.newPolicyStore(); //polMan = new PolicyStoreService(); httpUtils = new HttpUtils(fedoraUrl, username, password); // Load the admin policy to give us rights to add objects // FIXME: redundant, bootstrap policies will allow this String policyId = policyUtils.addPolicy("test-access-admin.xml"); LoadDataset.load("fesl", fedoraUrl, username, password); // httpUtils.get("/fedora/risearch?flush=true"); // Now that objects are loaded, remove the policy policyUtils.delPolicy(policyId); } catch (Exception e) { logger.error(e.getMessage(), e); fail(e.getMessage()); } } @After public void tearDown() { PropertyResourceBundle prop = (PropertyResourceBundle) ResourceBundle.getBundle(PROPERTIES); String username = prop.getString("fedora.admin.username"); String password = prop.getString("fedora.admin.password"); //String fedoraUrl = prop.getString("fedora.url"); String fedoraUrl = FedoraUtil.getBaseURL(); try { if (logger.isDebugEnabled()) { logger.debug("Tearing down..."); } httpUtils.shutdown(); //PolicyStoreFactory f = new PolicyStoreFactory(); //polMan = f.newPolicyStore(); //polMan = new PolicyStoreService(); // Load the admin policy to give us rights to remove objects String policyId = policyUtils.addPolicy("test-access-admin.xml"); RemoveDataset.remove("fesl", fedoraUrl, username, password); // Now that objects are loaded, remove the policy policyUtils.delPolicy(policyId); httpUtils.shutdown(); } catch (Exception e) { logger.error(e.getMessage(), e); fail(e.getMessage()); } } @Test public void testAdminGetDeny() throws Exception { // getting object test:1000007 but applying policy // to parent object (test:1000006) first String policyId = policyUtils.addPolicy("test-policy-00.xml"); // NOTE: system property fedora.fesl.pep_nocachevariable MUST be set to true to disable policy evaluation results caching try { // object try { String url = "/fedora/objects/test:1000007?format=xml"; String response = httpUtils.get(url); logger.debug("http response:\n" + response); fail("Access was permitted when it should have been denied: " + url); } catch (AuthorizationDeniedException e) { } // expected // list datastreams try { String url = "/fedora/objects/test:1000007/datastreams"; String response = httpUtils.get(url); logger.debug("http response:\n" + response); fail("Access was permitted when it should have been denied: " + url); } catch (AuthorizationDeniedException e) { } // expected // datastream profile try { String url = "/fedora/objects/test:1000007/datastreams/DC"; String response = httpUtils.get(url); logger.debug("http response:\n" + response); fail("Access was permitted when it should have been denied: " + url); } catch (AuthorizationDeniedException e) { } // expected // datastream content try { String url = "/fedora/objects/test:1000007/datastreams/DC/content"; String response = httpUtils.get(url); logger.debug("http response:\n" + response); fail("Access was permitted when it should have been denied: " + url); } catch (AuthorizationDeniedException e) { } // expected // list methods try { String url = "/fedora/objects/test:1000007/methods"; String response = httpUtils.get(url); logger.debug("http response:\n" + response); fail("Access was permitted when it should have been denied: " + url); } catch (AuthorizationDeniedException e) { } // expected // get method content try { String url = "/fedora/objects/test:1000007/methods/fedora-system:3/viewDublinCore"; String response = httpUtils.get(url); logger.debug("http response:\n" + response); fail("Access was permitted when it should have been denied: " + url); } catch (AuthorizationDeniedException e) { } // expected // TODO: extend for all REST methods } finally { policyUtils.delPolicy(policyId); } } @Test public void testAdminGetPermit() throws Exception { // getting object test:1000007 but applying policy // to parent object (test:1000006) first String policyId = policyUtils.addPolicy("test-policy-01.xml"); try { String url = "/fedora/objects/test:1000007?format=xml"; String response = httpUtils.get(url); if (logger.isDebugEnabled()) { logger.debug("http response:\n" + response); } boolean check = response.contains("<objLabel>Dexter</objLabel>"); assertTrue("Expected object data not found", check); } catch (AuthorizationDeniedException e) { // PEP caching must be disabled (previously cached results will invalidate test) fail("Authorization denied. (Check that system property fedora.fesl.pep_nocache is set to true)"); } catch (Exception e) { throw e; } finally { policyUtils.delPolicy(policyId); } } /* * Tests based on resource attributes sourced via the resource index. * Note the attributes must be defined in the pdp/config/config-attribute-finder.xml. * * Attributes tested for are * - dc:subject * - object/datastream state * * See the test-objects.txt in the same directory as the test objects to see which * objects have which attributes. * * Each pair of policies tested in the individual tests implement the same access conditions, * but do so through different implementations; ie using different xacml resource attributes * declared in the FedoraRIattribute finder configuration file. * * The different xacml resource id attribute declarations exercise sourcing attributes using: * - simple Fedora relationships * - TQL queries * - SPARQL queries * - SPO queries * */ @Test public void testRIAttributesRels1() throws Exception { doAttributesTest("test-policy-state-rel1.xml", "test-policy-subject-rel1.xml"); } @Test public void testRIAttributesRels2() throws Exception { doAttributesTest("test-policy-state-rel2.xml", "test-policy-subject-rel2.xml"); } @Test public void testRIAttributesTQL() throws Exception { /* skip if MPTTriplestore implementation */ Assume.assumeTrue(! "localPostgresMPTTriplestore".equals(ri_impl)); doAttributesTest("test-policy-state-itql.xml", "test-policy-subject-itql.xml"); } @Test public void testRIAttributesSPARQL() throws Exception { /* skip if MPTTriplestore implementation */ Assume.assumeTrue(! "localPostgresMPTTriplestore".equals(ri_impl)); doAttributesTest("test-policy-state-sparql.xml", "test-policy-subject-sparql.xml"); } @Test public void testRIAttributesSPO() throws Exception { doAttributesTest("test-policy-state-spo.xml", "test-policy-subject-spo.xml"); } private void doAttributesTest(String statePolicy, String subjectPolicy) throws Exception { // A. object/datastream state String [] pids = new String[]{ "test:1000000", "test:1000001", "test:1000002", "test:1000003", "test:1000004", "test:1000005", "test:1000006", "test:1000007", "test:1000008", "test:1000009", "test:1000010", "test:1000011", "test:1000012" }; Perms allPids = new Perms(); allPids.addAll(Arrays.asList(pids)); // check permissions before adding policy PermissionTest perms = new PermissionTest(1000000, 1000012, "test", "DC"); assertEquals("Allowed objects count (no policies)", 0, perms.object().allowed().mismatch(pids,true).length); assertEquals("Allowed DC datastreams count (no policies)", 0, perms.datastream().allowed().mismatch(pids,true).length); // load policy String policyId = policyUtils.addPolicy(statePolicy); try { perms = new PermissionTest(1000000, 1000012, "test", "DC"); // objects that should be denied access String [] denied = new String[]{ "test:1000004", "test:1000005", "test:1000006", "test:1000007", "test:1000008", "test:1000009" }; String [] deniedVideos = new String[]{ "test:1000000", // not a video "test:1000004", "test:1000005", "test:1000006", "test:1000007", "test:1000008", "test:1000009" }; String [] allowed = allPids.mismatch(denied, false); String [] mismatches = perms.object().denied().mismatch(denied, false); assertEquals(getAccessErrorMessage(subjectPolicy, "objects", "denied", mismatches), 0, mismatches.length); allowed = allPids.mismatch(deniedVideos, false); mismatches = perms.object().listed().mismatch(allowed, true); assertEquals(getAccessErrorMessage(subjectPolicy, "objects", "listed", mismatches), 0, mismatches.length); // datastreams that should be denied access denied = new String[]{ "test:1000008", "test:1000009", "test:1000010", "test:1000011", "test:1000012" }; mismatches = perms.datastream().denied().mismatch(denied, true); assertEquals(getAccessErrorMessage(subjectPolicy, "datastreams", "denied", mismatches), 0, mismatches.length); } finally { policyUtils.delPolicy(policyId); } // B. dc:subject attributes // check permissions before adding policy perms = new PermissionTest(1000000, 1000012, "test", "DC"); assertEquals("Allowed objects count (no policies)", perms.pidCount(), perms.object().allowed().size()); assertEquals("Allowed DC datastreams count (no policies)", perms.pidCount(), perms.datastream().allowed().size()); // load policy policyId = policyUtils.addPolicy(subjectPolicy); try { perms = new PermissionTest(1000000, 1000012, "test", "DC"); // objects that should be allowed access (deny=if pid divisible by two and/or three by using dc:subject attrs that indicate this) String [] allowed = new String[]{ "test:1000001", "test:1000003", "test:1000007", "test:1000009" }; String [] mismatches = perms.object().allowed().mismatch(allowed, true); assertEquals(getAccessErrorMessage(subjectPolicy, "objects", "allowed", mismatches), 0, mismatches.length); mismatches = perms.object().listed().mismatch(allowed, true); assertEquals(getAccessErrorMessage(subjectPolicy, "objects", "listed", mismatches), 0, mismatches.length); // same for datastream access, as subject attribute is retrieved for the object, not for the datastream mismatches = perms.datastream().allowed().mismatch(allowed, true); assertEquals(getAccessErrorMessage(subjectPolicy, "datastreams", "allowed", mismatches), 0, mismatches.length); } finally { policyUtils.delPolicy(policyId); } } private static String getAccessErrorMessage(String subjectPolicy, String type, String verb, String[] mismatches){ StringBuilder sb = new StringBuilder(); sb.append(subjectPolicy).append(": Access ").append(verb).append(" for ").append(type).append("["); for (int i=0; i<mismatches.length; i++){ sb.append(mismatches[i]); if (i < mismatches.length - 1) sb.append(','); } sb.append(']'); return sb.toString(); } // utility class for performing object and datastream access tests on a range of pids // representing the results in an [object | datastream] / [allowed | denied] / set of matching pids // structure class PermissionTest { private final EntityPerms m_object = new EntityPerms(); private final EntityPerms m_datastream = new EntityPerms(); private final int m_first; private final int m_last; private final String m_pidns; private final String m_dsid; PermissionTest(int first, int last, String pidNamespace, String dsid) throws Exception { m_first = first; m_last = last; m_pidns = pidNamespace; m_dsid = dsid; for (int i = m_first; i <= m_last; i++) { String pid = m_pidns + ":" + i; // test object access String url = "/fedora/objects/" + pid + "?format=xml"; try { httpUtils.get(url); // if we got here, it was allowed, so... m_object.allowed().add(pid); } catch (AuthorizationDeniedException e) { // access was denied m_object.denied().add(pid); } // test datastream access if (!m_dsid.isEmpty()) { url = "/fedora/objects/" + pid + "/datastreams/" + m_dsid + "?format=xml"; try { httpUtils.get(url); // if we got here, it was allowed, so... m_datastream.allowed().add(pid); } catch (AuthorizationDeniedException e) { // access was denied m_datastream.denied().add(pid); } // Now check that the datastreams are being filtered correctly from listing url = "/fedora/objects?resultFormat=xml&query=type%7Evideo"; String response = httpUtils.get(url); Matcher matcher = Pattern.compile("<pid>(.*)<\\/pid>").matcher(response); while(matcher.find()){ m_object.listed().add(matcher.group(1)); } } } // sanity check - allowed + denied = number of objects tested if (m_object.allowed().size() + m_object.denied().size() != pidCount()) { fail("Error in checking permissions - total of allowed and denied objects does not equal number of objects tested"); throw new RuntimeException("Should not happen"); } if (!m_dsid.isEmpty()) { if (m_datastream.allowed().size() + m_datastream.denied().size() != pidCount()) { fail("Error in checking permissions - total of allowed and denied datastreams does not equal number of object datastreams tested"); throw new RuntimeException("Also should not happen"); } } } public EntityPerms object() { return m_object; } public EntityPerms datastream() { return m_datastream; } public int pidCount() { return m_last - m_first + 1; } } // holds two sets of pids, one for objects for which access was allowed, one for denied class EntityPerms { private final Perms m_allowed = new Perms(); private final Perms m_denied = new Perms(); private final Perms m_listed = new Perms(); public Perms allowed() { return m_allowed; } public Perms denied() { return m_denied; } public Perms listed() { return m_listed; } } // holds a set of pids with utility method for comparing to string array class Perms extends HashSet<String>{ private static final long serialVersionUID = 3747931619024146008L; public boolean containsAll(String[] items) { return this.containsAll(Arrays.asList(items)); } public boolean containsOnly(String[] items) { return ((this.size() == items.length) && containsAll(items)); } public boolean containsAny(String [] items) { boolean result = false; for (String item:items) { result |= contains(item);} return result; } /* * returns a string representation of difference between the set members and the supplied array. * Return empty string "" if items match */ public String[] mismatch(String[] items, boolean indicate) { ArrayList<String> res = new ArrayList<String>(); if (containsOnly(items)) return res.toArray(new String[0]); // they match // expected items not present in this set for (String item : items) { if (!contains(item)){ if (indicate) res.add("-" + item); else res.add(item); } } // items in set not present in supplied array List<String> asList = Arrays.asList(items); for (String item : this) { if (!asList.contains(item)){ if (indicate) res.add("+" + item); else res.add(item); } } return res.toArray(new String[0]); } } }