/* The contents of this file are subject to the license and copyright terms * detailed in the license directory at the root of the source tree (also * available online at http://fedora-commons.org/license/). */ package org.fcrepo.server.security.xacml.pdp.data; import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.util.List; import java.util.Map; import org.apache.commons.io.IOUtils; import org.fcrepo.common.Constants; import org.fcrepo.common.MalformedPIDException; import org.fcrepo.common.PID; import org.fcrepo.server.Context; import org.fcrepo.server.ReadOnlyContext; import org.fcrepo.server.Server; import org.fcrepo.server.access.Access; import org.fcrepo.server.access.ObjectProfile; import org.fcrepo.server.errors.ObjectNotInLowlevelStorageException; import org.fcrepo.server.errors.ServerException; import org.fcrepo.server.management.Management; import org.fcrepo.server.security.PolicyParser; import org.fcrepo.server.security.xacml.pdp.MelcoePDPException; import org.fcrepo.server.utilities.StreamUtility; import org.fcrepo.server.validation.ValidationUtility; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.xml.sax.SAXException; /** * A PolicyStore for managing policies stored as Fedora digital objects. * * Mainly used by the initial load of the bootstrap policies and by the rebuilder. * * For searching for policies see PolicyIndex.java and its implementations. * * @author Stephen Bayliss * @version $Id$ */ public class FedoraPolicyStore extends AbstractPolicyStore implements PolicyStore { private static final Logger log = LoggerFactory.getLogger(FedoraPolicyStore.class.getName()); private static final String XACML20_POLICY_NS = Constants.XACML2_POLICY_SCHEMA.OS.uri; public static final String FESL_POLICY_DATASTREAM = "FESLPOLICY"; public static String FESL_BOOTSTRAP_POLICY_NAMESPACE = "fedora-policy"; // escaping to use for ":" in policy names private static final String PID_SEPARATOR_ESCAPED = "%3A"; // = "__"; // read from config file: private String pidNamespace = ""; private String contentModel = ""; private String datastreamControlGroup = ""; private String collection = ""; private String collectionRelationship = ""; private boolean validateSchema = false; private Map<String,String> schemaLocations = null; private final PolicyUtils utils = new PolicyUtils(); protected Server fedoraServer; protected Management apiMService; protected Access apiAService; public FedoraPolicyStore(Server server) throws PolicyStoreException { this.fedoraServer = server; this.apiMService = (Management)server.getBean("org.fcrepo.server.management.Management"); this.apiAService = (Access)server.getBean("org.fcrepo.server.access.Access"); } @Override public void init() throws PolicyStoreException, FileNotFoundException { if (log.isDebugEnabled()) { Runtime runtime = Runtime.getRuntime(); log.debug("Total memory: " + runtime.totalMemory() / 1024); log.debug("Free memory: " + runtime.freeMemory() / 1024); log.debug("Max memory: " + runtime.maxMemory() / 1024); } super.init(); // if no pid namespace was specified, use the default specified in fedora.fcfg if (pidNamespace.isEmpty()) { pidNamespace = fedoraServer.getModule("org.fcrepo.server.storage.DOManager").getParameter("pidNamespace"); } // check control group was supplied if (datastreamControlGroup.isEmpty()) { throw new PolicyStoreException("No control group for policy datastreams was specified in FedoraPolicyStore configuration"); } if (validateSchema) { String schemaLocation = schemaLocations.get(XACML20_POLICY_NS); if ( schemaLocation == null) { throw new PolicyStoreException("Configuration error - no policy schema specified"); } try{ String serverHome = fedoraServer.getHomeDir().getCanonicalPath() + File.separator; String schemaPath = ((schemaLocation) .startsWith(File.separator) ? "" : serverHome) + schemaLocation; FileInputStream in = new FileInputStream(schemaPath); PolicyParser policyParser = new PolicyParser(in); ValidationUtility.setFeslPolicyParser(policyParser); } catch (IOException ioe) { throw new PolicyStoreException(ioe.getMessage(),ioe); } catch (SAXException se) { throw new PolicyStoreException(se.getMessage(),se); } } } public void setPidNamespace( String pidNamespace ) { this.pidNamespace = pidNamespace; } public void setContentModel( String contentModel ) { this.contentModel = contentModel; } public void setDatastreamControlGroup(String datastreamControlGroup){ this.datastreamControlGroup = datastreamControlGroup; } public void setCollection(String collection){ this.collection = collection; } public void setCollectionRelationship(String collectionRelationship){ this.collectionRelationship = collectionRelationship; } // schema config properties public void setSchemaValidation(boolean validate){ this.validateSchema = validate; log.info("Initialising validation " + Boolean.toString(validate)); ValidationUtility.setValidateFeslPolicy(validate); } /** * Map policy schema URIs to locations for the schema document * @param schemaLocation * @throws IOException * @throws SAXException */ public void setSchemaLocations(Map<String,String> schemaLocation) throws IOException, SAXException{ this.schemaLocations = schemaLocation; } /* * (non-Javadoc) * @see * org.fcrepo.server.security.xacml.pdp.data.PolicyDataManager#addPolicy * (java.io.File) */ @Override public String addPolicy(File f) throws PolicyStoreException { return addPolicy(f, null); } /* * (non-Javadoc) * @see * org.fcrepo.server.security.xacml.pdp.data.PolicyDataManager#addPolicy * (java.io.File, java.lang.String) */ @Override public String addPolicy(File f, String name) throws PolicyStoreException { try { return addPolicy(utils.fileToString(f), name); } catch (MelcoePDPException e) { throw new PolicyStoreException(e); } } /* * (non-Javadoc) * @see * org.fcrepo.server.security.xacml.pdp.data.PolicyDataManager#addPolicy * (java.lang.String) */ @Override public String addPolicy(String document) throws PolicyStoreException { return addPolicy(document, null); } /* * (non-Javadoc) * @see * org.fcrepo.server.security.xacml.pdp.data.PolicyDataManager#addPolicy * (java.lang.String, java.lang.String) */ @Override public String addPolicy(String document, String name) throws PolicyStoreException { String policyName; if (name == null || name.isEmpty()) { // no policy name, derive from document // (note: policy ID is mandatory according to schema) try { policyName = utils.getPolicyName(document); } catch (MelcoePDPException e) { throw new PolicyStoreException("Could not get policy name from policy", e); } // if name from document contains pid separator, escape it if (name.contains(":")) { name = name.replace(":", PID_SEPARATOR_ESCAPED); } } else { policyName = name; } String pid = this.getPID(policyName); ObjectProfile objectProfile = null; try { objectProfile = this.apiAService.getObjectProfile(getContext(), pid, null); } catch (ObjectNotInLowlevelStorageException e) { } catch (ServerException e) { throw new PolicyStoreException("Add: error getting object profile for " + pid + " - " + e.getMessage(), e); } if (objectProfile != null) { // object exists, check state if (objectProfile.objectState != "D") { throw new PolicyStoreException("Add: attempting to add policy " + pid + " but it already exists"); } // deleted object: set state to active and do an update instead try { this.apiMService .modifyObject(getContext(), pid, "A", objectProfile.objectLabel, objectProfile.objectOwnerId, "Fedora policy manager: Adding policy by activating deleted object",null); } catch (ServerException e) { throw new PolicyStoreException("Add: " + e.getMessage(), e); } this.updatePolicy(policyName, document); return pid; } else { // create new object // if control group is M - managed - we need a temp location for the datastream String dsLocationOrContent = null; if (datastreamControlGroup.equals("M")) { try { ByteArrayInputStream is = new ByteArrayInputStream(document.getBytes("UTF-8")); dsLocationOrContent = apiMService.putTempStream(getContext(), is); } catch (Exception e) { throw new PolicyStoreException("Add: error generating temp datastream location - " + e .getMessage(), e); } } else { dsLocationOrContent = document; } try { return apiMService .ingest(getContext(), new ByteArrayInputStream(getFOXMLPolicyTemplate(pid, "XACML policy " + policyName, contentModel, collection, collectionRelationship, dsLocationOrContent, datastreamControlGroup) .getBytes("UTF-8")), "Fedora Policy Manager creating policy", Constants.FOXML1_1.uri, "UTF-8", ""); } catch (Exception e) { throw new PolicyStoreException("Add: error ingesting " + pid + " - " + e.getMessage(), e); } } } /* * (non-Javadoc) * @see * org.fcrepo.server.security.xacml.pdp.data.PolicyDataManager#deletePolicy * (java.lang.String) */ @Override public boolean deletePolicy(String name) throws PolicyStoreException { String pid = this.getPID(name); if (!contains(name)) { throw new PolicyStoreException("Delete: object " + pid + " not found."); } try { this.apiMService.modifyObject(getContext(), pid, "D", null, null, "Deleting policy " + pid, null); } catch (ServerException e) { throw new PolicyStoreException("Delete: error deleting policy " + pid + " - " + e.getMessage(), e); } return true; } /* * (non-Javadoc) * @see * org.fcrepo.server.security.xacml.pdp.data.PolicyDataManager#updatePolicy * (java.lang.String, java.lang.String) */ @Override public boolean updatePolicy(String name, String newDocument) throws PolicyStoreException { String pid = this.getPID(name); if (!contains(name)) { throw new PolicyStoreException("Update: policy " + pid + " not found"); } if (datastreamControlGroup.equals("X")) { // inline, modify by value try { this.apiMService .modifyDatastreamByValue(getContext(), pid, FESL_POLICY_DATASTREAM, null, null, null, null, new ByteArrayInputStream(newDocument .getBytes("UTF-8")), "DISABLED", null, "Modifying policy " + pid, null); } catch (Exception e) { throw new PolicyStoreException("Update: error modifying datastream by value for " + pid + " - " + e.getMessage(), e); } } else if (datastreamControlGroup.equals("M")) { // managed, generate temp location, modify by reference String dsLocation = null; try { ByteArrayInputStream is = new ByteArrayInputStream(newDocument.getBytes("UTF-8")); dsLocation = apiMService.putTempStream(getContext(), is); } catch (Exception e) { throw new PolicyStoreException("Update: error generating temp datastream location - " + e.getMessage(), e); } try { apiMService.modifyDatastreamByReference(getContext(), pid, FESL_POLICY_DATASTREAM, null, null, null, null, dsLocation, "DISABLED", null, "Modifying policy " + pid, null); } catch (ServerException e) { throw new PolicyStoreException("Update: error modifying datastream by reference for " + pid + " - " + e.getMessage(), e); } } else { throw new PolicyStoreException("Update: Invalid datastream control group " + datastreamControlGroup + " - use M or X"); } return true; } /* * (non-Javadoc) * @see * org.fcrepo.server.security.xacml.pdp.data.PolicyDataManager#getPolicy * (java.lang.String) */ @Override public byte[] getPolicy(String name) throws PolicyStoreException { String pid = getPID(name); if (!contains(name)) { throw new PolicyStoreException("Get: policy " + pid + " does not exist."); } try { InputStream is = apiAService.getDatastreamDissemination(getContext(), pid, FESL_POLICY_DATASTREAM, null).getStream(); return IOUtils.toByteArray(is); } catch (Exception e) { throw new PolicyStoreException("Get: error reading policy " + pid + " - " + e.getMessage(), e); } } /** * Check if the policy identified by policyName exists. * * @param policyName * @return true iff the policy store contains a policy identified as * policyName * @throws PolicyStoreException */ @Override public boolean contains(String policyName) throws PolicyStoreException { // search for policy - active and inactive String pid = this.getPID(policyName); ObjectProfile objectProfile = null; try { objectProfile = this.apiAService.getObjectProfile(getContext(), pid, null); } catch (ObjectNotInLowlevelStorageException e) { } catch (ServerException e) { throw new PolicyStoreException("Add: error getting object profile for " + pid + " - " + e.getMessage(), e); } if (objectProfile == null) { // no object found return false; } else { if (objectProfile.objectState.equals("A") || objectProfile.objectState.equals("I")) { // active or inactive object found - policy exists return true; } else { // deleted object - policy does not exist return false; } } } /** * Check if the policy identified by policyName exists. * * @param policy * @return true iff the policy store contains a policy with the same * PolicyId * @throws PolicyStoreException */ @Override public boolean contains(File policy) throws PolicyStoreException { try { return contains(utils.getPolicyName(policy)); } catch (MelcoePDPException e) { throw new PolicyStoreException(e); } } /* * (non-Javadoc) * @see * org.fcrepo.server.security.xacml.pdp.data.PolicyDataManager#listPolicies * () */ @Override public List<String> listPolicies() throws PolicyStoreException { // not implemented (is it ever used?) return null; } /** * Given a policy name (corresponds to Policy identifier in XACML), generate a PID * * If the name already contains a pid namespace, use that, otherwise use the default * * @param name * @return normalized name (a PID) * @throws PolicyStoreException * @throws MalformedPIDException */ private String getPID(String name) throws PolicyStoreException { String pid; // only bootstrap policies specify the PID namespace, all others follow the config if (name.startsWith(FESL_BOOTSTRAP_POLICY_NAMESPACE + ":")) { pid = name; } else { // TODO: would be nice to have the PID class contain a method for this // (could be used as a generic method instead of the from-filename etc methods) // name might contain non-legal PID characters so encode them StringBuffer out = new StringBuffer(); for (int i = 0; i < name.length(); i++) { char c = name.charAt(i); // valid pid characters if (isAlphaNum(c) || c == '-' || c == '.' || c == '~' || c == '_') { out.append(c); } else { // do each byte // FIXME: percent-encoding causing various issues // (not least with web admin client) out.append("_"); /* byte[] bytes; try { bytes = Character.toString(c).getBytes("UTF-8"); } catch (UnsupportedEncodingException e) { // should never happen throw new RuntimeException(e); } for (byte b : bytes ) { // percent-encode the byte out.append("%"); out.append(hexChar[(b >>> 4) & 0xf]); out.append(hexChar[b & 0xf]); } */ } } pid = pidNamespace + ":" + out.toString(); } try { // just in case... return PID.normalize(pid); } catch (MalformedPIDException e) { throw new PolicyStoreException("Invalid policy name '" + name + "'. Could not create a valid PID from this name: " + e.getMessage(), e); } } private Context getContext() throws PolicyStoreException { try { return ReadOnlyContext.getContext(null, "fedoraBootstrap", null, false); } catch (Exception e) { throw new PolicyStoreException(e.getMessage(), e); } } /** * Generate FOXML for a new policy object * @param pid * @param label * @param contentModel * @param policyOrLocation * @param controlGroup * @return * @throws PolicyStoreException */ private static String getFOXMLPolicyTemplate(String pid, String label, String contentModel, String collection, String collectionRelationship, String policyOrLocation, String controlGroup) throws PolicyStoreException { StringBuilder foxml = new StringBuilder(1024); foxml.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"); foxml.append("<foxml:digitalObject VERSION=\"1.1\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n"); foxml.append(" xmlns:foxml=\"info:fedora/fedora-system:def/foxml#\"\n"); foxml.append(" xsi:schemaLocation=\"" + Constants.FOXML.uri + " " + Constants.FOXML1_1.xsdLocation + "\""); foxml.append("\n PID=\""); StreamUtility.enc(pid, foxml); foxml.append("\">\n <foxml:objectProperties>\n"); foxml.append(" <foxml:property NAME=\"info:fedora/fedora-system:def/model#state\" VALUE=\"A\"/>\n"); foxml.append(" <foxml:property NAME=\"info:fedora/fedora-system:def/model#label\" VALUE=\""); StreamUtility.enc(label, foxml); foxml.append("\"/>\n </foxml:objectProperties>\n"); // RELS-EXT specifying content model - if present, collection relationship if present // but not for bootstrap policies if (!pid.startsWith(FESL_BOOTSTRAP_POLICY_NAMESPACE + ":")) { if (!contentModel.isEmpty() || !collection.isEmpty()) { foxml.append("<foxml:datastream ID=\"RELS-EXT\" CONTROL_GROUP=\"X\">"); foxml.append("<foxml:datastreamVersion FORMAT_URI=\"info:fedora/fedora-system:FedoraRELSExt-1.0\" ID=\"RELS-EXT.0\" MIMETYPE=\"application/rdf+xml\" LABEL=\"RDF Statements about this object\">"); foxml.append(" <foxml:xmlContent>"); foxml.append(" <rdf:RDF xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\" xmlns:rdfs=\"http://www.w3.org/2000/01/rdf-schema#\" xmlns:fedora-model=\"info:fedora/fedora-system:def/model#\" xmlns:rel=\"info:fedora/fedora-system:def/relations-external#\">"); foxml.append(" <rdf:Description rdf:about=\"" + "info:fedora/"); StreamUtility.enc(pid, foxml); foxml.append("\">"); if (!contentModel.isEmpty()) { foxml.append(" <fedora-model:hasModel rdf:resource=\""); StreamUtility.enc(contentModel, foxml); foxml.append("\"/>"); } if (!collection.isEmpty()) { foxml.append(" <rel:"); StreamUtility.enc(collectionRelationship, foxml); foxml.append(" rdf:resource=\""); StreamUtility.enc(collection, foxml); foxml.append("\"/>"); } foxml.append(" </rdf:Description>"); foxml.append(" </rdf:RDF>"); foxml.append(" </foxml:xmlContent>"); foxml.append(" </foxml:datastreamVersion>"); foxml.append("</foxml:datastream>"); } } // the POLICY datastream foxml.append("<foxml:datastream ID=\"" + FESL_POLICY_DATASTREAM + "\" CONTROL_GROUP=\"" + controlGroup + "\">"); foxml.append("<foxml:datastreamVersion ID=\"POLICY.0\" MIMETYPE=\"text/xml\" LABEL=\"XACML policy datastream\">"); if (controlGroup.equals("M")) { foxml.append(" <foxml:contentLocation REF=\"" + policyOrLocation + "\" TYPE=\"" + org.fcrepo.server.storage.types.Datastream.DS_LOCATION_TYPE_URL + "\"/>"); } else if (controlGroup.equals("X")) { foxml.append(" <foxml:xmlContent>"); foxml.append(policyOrLocation); foxml.append(" </foxml:xmlContent>"); } else { throw new PolicyStoreException("Generating new object XML: Invalid control group: " + controlGroup + " - use X or M."); } foxml.append(" </foxml:datastreamVersion>"); foxml.append("</foxml:datastream>"); foxml.append("</foxml:digitalObject>"); return foxml.toString(); } private static boolean isAlphaNum(char c) { return c >= '0' && c <= '9' || c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z'; } }