package org.fcrepo.server.security.xacml.pdp.data; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStreamWriter; import java.io.StringReader; import java.io.UnsupportedEncodingException; import java.io.Writer; import java.net.URISyntaxException; import java.net.URLDecoder; import java.net.URLEncoder; import java.nio.charset.Charset; import java.util.Arrays; import java.util.Comparator; import java.util.HashMap; import java.util.Map; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantReadWriteLock; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import org.fcrepo.server.security.xacml.pdp.finder.policy.PolicyReader; import org.fcrepo.server.security.xacml.util.AttributeBean; import org.fcrepo.utilities.xml.SunXmlSerializers; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.w3c.dom.Document; import org.w3c.dom.Node; import org.xml.sax.InputSource; import org.xml.sax.SAXException; import org.xmldb.api.DatabaseManager; import org.xmldb.api.base.Collection; import org.xmldb.api.base.Database; import org.xmldb.api.base.Resource; import org.xmldb.api.base.ResourceIterator; import org.xmldb.api.base.ResourceSet; import org.xmldb.api.base.XMLDBException; import org.xmldb.api.modules.CollectionManagementService; import org.xmldb.api.modules.XMLResource; import org.xmldb.api.modules.XPathQueryService; import org.jboss.security.xacml.sunxacml.AbstractPolicy; import org.jboss.security.xacml.sunxacml.EvaluationCtx; import org.jboss.security.xacml.sunxacml.ParsingException; import org.jboss.security.xacml.sunxacml.finder.PolicyFinder; /** * A PolicyIndex based on an XPath XML database. * * This implementation only tested on eXist, but as only generic xmldb API methods have * been used it should work with some customisation on other XML databases that support * the xmldb API. Customisations will be required for driver configuration, indexing, * and potentially organisation of collections (eg root collection name). * * Concurrency handled with a ReentrantReadWriteLock (although eXist does natively have * some concurrency support). * * @author Stephen Bayliss * @version $Id$ */ @Deprecated public class ExistPolicyIndex extends XPathPolicyIndex implements PolicyIndex { private static final Logger log = LoggerFactory.getLogger(ExistPolicyIndex.class.getName()); // path of eXist root collection (within which policies collection stored) private static final String ROOT_COLLECTION_PATH = "/db"; // eXist index config document name private static final String INDEX_DOCUMENT_NAME = "collection.xconf"; // string URI of database private String m_databaseURI; // name of collection to store FeSL policies in private String m_collectionName; // path of policies collection URI (nb: needs to include base collection - /db) private String m_collectionPath ; // path to collection containing index config document private String m_indexCollectionPath; // admin credentials private String m_user; private String m_password; // the main policies collection object used to perform queries, adds, updates etc protected Collection m_collection; // allow multiple read threads, block reading when writing, allow single write thread private static final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock(); private static final Lock readLock = rwl.readLock(); private static final Lock writeLock = rwl.writeLock(); protected ExistPolicyIndex(PolicyReader policyReader) throws PolicyIndexException { super(policyReader); // initialise from config, set up driver // create collection if needed try { writeLock.lock(); initCollection(); } finally { writeLock.unlock(); } } @Override public String addPolicy(String name, String document) throws PolicyIndexException { String Id = nameToId(name); XMLResource res; try { writeLock.lock(); // check it doesn't already exist res = (XMLResource) m_collection.getResource(Id); if (res != null ) { throw new PolicyIndexException("Tried to add an already-existing resource " + name); } // create the new resource res = (XMLResource) m_collection.createResource(Id, XMLResource.RESOURCE_TYPE); // set the resource content res.setContentAsDOM(createDocument(document)); // add it m_collection.storeResource(res); } catch (XMLDBException e) { log.error("Error adding resource " + name + " " + e.getMessage(), e); throw new PolicyIndexException("Error adding resource " + name + " " + e.getMessage(), e); } finally { writeLock.unlock(); } return name; } @Override public boolean clear() throws PolicyIndexException { try { writeLock.lock(); deleteCollection(); initCollection(); } finally { writeLock.unlock(); } return true; } @Override public boolean contains(String policyName) throws PolicyIndexException { try { readLock.lock(); return (m_collection.getResource(nameToId(policyName)) != null); } catch (XMLDBException e) { log.error("Error determining if db contains " + policyName + " - " + e.getMessage(), e); throw new PolicyIndexException("Error determining if db contains " + policyName + " - " + e.getMessage(), e); } finally { readLock.unlock(); } } @Override public boolean deletePolicy(String name) throws PolicyIndexException { String Id = nameToId(name); try { writeLock.lock(); Resource res = m_collection.getResource(Id); if (res == null) { log.warn("Attempted to delete non-existing resource " + name); return false; } m_collection.removeResource(res); } catch (XMLDBException e) { log.error("Error deleting resource " + name + " " + e.getMessage(), e); throw new PolicyIndexException("Error deleting resource " + name + " " + e.getMessage(), e); } finally { writeLock.unlock(); } return true; } @Override public Map<String, AbstractPolicy> getPolicies(EvaluationCtx eval, PolicyFinder policyFinder) throws PolicyIndexException { Map<String, java.util.Collection<AttributeBean>> attributeMap; try { // get evaluation context attributes to query on attributeMap = getAttributeMap(eval); } catch (URISyntaxException e) { log.error("Error getting attribute map " + e.getMessage(), e); throw new PolicyIndexException("Error getting attribute map " + e.getMessage(), e); } Map<String, AbstractPolicy> documents = new HashMap<String, AbstractPolicy>(); // generate the xpath query and variables String query = getXpath(attributeMap); Map<String, String> variables = getXpathVariables(attributeMap); try { readLock.lock(); // do the query ResourceSet resources = doQuery(query, variables); try { // get each result ResourceIterator ri = resources.getIterator(); while (ri.hasMoreResources()) { XMLResource res = (XMLResource)ri.nextResource(); // get the result's document name (the query result just contains content as selected by the xpath query) String id = res.getDocumentId(); log.trace("Query matched document: " + IdToName(id)); Document policyDoc = m_policyReader.readPolicy(((String)m_collection.getResource(id).getContent()).getBytes("UTF-8")); documents.put(IdToName(id), handleDocument(policyDoc, policyFinder)); } } catch (XMLDBException e) { log.error("Error retrieving query results " + e.getMessage(), e); throw new PolicyIndexException("Error retrieving query results " + e.getMessage(), e); } catch (ParsingException e) { log.error("Error retrieving query results " + e.getMessage(), e); throw new PolicyIndexException("Error retrieving query results " + e.getMessage(), e); } catch (UnsupportedEncodingException e) { // Should never happen throw new RuntimeException("Unsupported encoding " + e.getMessage(), e); } } finally { readLock.unlock(); } return documents; } @Override public AbstractPolicy getPolicy(String name, PolicyFinder policyFinder) throws PolicyIndexException { try { readLock.lock(); XMLResource resource = (XMLResource) m_collection.getResource(nameToId(name)); if (resource == null) { log.error("Attempting to get non-existant resource " + name); throw new PolicyIndexException("Attempting to get non-existant resource " + name); } Document policyDoc = m_policyReader.readPolicy(((String)resource.getContent()).getBytes("UTF-8")); return handleDocument(policyDoc, policyFinder); } catch (UnsupportedEncodingException e) { // Should never happen throw new RuntimeException("Unsupported encoding " + e.getMessage(), e); } catch (XMLDBException e) { log.error("Error getting policy " + name + " " + e.getMessage(), e); throw new PolicyIndexException("Error getting policy " + name + " " + e.getMessage(), e); } catch (ParsingException e) { log.error("Error getting policy " + name + " " + e.getMessage(), e); throw new PolicyIndexException("Error getting policy " + name + " " + e.getMessage(), e); } finally { readLock.unlock(); } } @Override public boolean updatePolicy(String name, String newDocument) throws PolicyIndexException { String Id = nameToId(name); try { writeLock.lock(); // get the resource XMLResource res = (XMLResource) m_collection.getResource(Id); if (res == null ) { log.error("Tried to update non-existing resource " + name); throw new PolicyIndexException("Tried to update non-existing resource " + name); } // set the resource content res.setContentAsDOM(createDocument(newDocument)); // update it m_collection.storeResource(res); } catch (XMLDBException e) { log.error("Error updating resource " + name + " " + e.getMessage(), e); throw new PolicyIndexException("Error updating resource " + name + " " + e.getMessage(), e); } finally { writeLock.unlock(); } return true; } /** * get XML document supplied as w3c dom Node as bytes * * @param node * @return * @throws PolicyIndexException */ @Deprecated protected static byte[] nodeToByte(Node node) throws PolicyIndexException { ByteArrayOutputStream out = new ByteArrayOutputStream(); Writer output = new OutputStreamWriter(out, Charset.forName("UTF-8")); try { SunXmlSerializers.writePrettyPrintWithDecl(node, output); output.close(); } catch (IOException e) { throw new PolicyIndexException("Failed to serialise node " + e.getMessage(), e); } return out.toByteArray(); } // utility methods to convert between a document name and a form that eXist accepts (ie URL-encoded) protected static String nameToId(String name) { try { return URLEncoder.encode(name, "UTF-8"); } catch (UnsupportedEncodingException e) { // should not happen throw new RuntimeException("Unsupported encoding", e); } } protected static String IdToName(String Id) { try { return URLDecoder.decode(Id, "UTF-8"); } catch (UnsupportedEncodingException e) { // should not happen throw new RuntimeException("Unsupported encoding", e); } } /** * sorts a string array in descending order of length * @param s * @return */ protected static String[] sortDescending(String[] s) { Arrays.sort(s, new Comparator<String>() { @Override public int compare(String o1, String o2) { if (o1.length() < o2.length()) return 1; if (o1.length() > o2.length()) return -1; return 0; } } ); return s; } protected ResourceSet doQuery(String query, Map<String, String> variables) throws PolicyIndexException { try { XPathQueryService queryService = (XPathQueryService) m_collection.getService("XPathQueryService", "1.0"); // set namespaces for (String prefix : PolicyIndexBase.namespaces.keySet()) { queryService.setNamespace(prefix, PolicyIndexBase.namespaces.get(prefix)); } // note: eXist extensions support "declareVariable", but the base API does not // so we substitute the variables here to avoid dependency on the eXist implementation (and libraries) // do in descending order as some variable names could be substrings of others String[] varNames = sortDescending((variables.keySet().toArray(new String[0]))); for (String name : varNames) { // nb, treat as strings (variables currently does not specify type) query = query.replace("$" + name, "\"" + variables.get(name) + "\""); } log.trace("XPath query with variables substituted:\n" + query); /* eXist impl version, not used, to remove dependency on eXist API // set the xpath variables if supplied if (variables != null) { for (String name : variables.keySet()) { String value = variables.get(name); // query service needs to be eXist extension to xmldb queryService.declareVariable(name, value); } } */ long start = System.nanoTime(); ResourceSet results = queryService.query(query); log.debug("XPath query time: " + (System.nanoTime() - start) + "ns"); return results; } catch (XMLDBException e) { log.error("Error running query " + e.getMessage(), e); throw new PolicyIndexException("Error running query " + e.getMessage(), e); } } /** * Create a collection given a full path to the collection. The collection path must include * the root collection. Intermediate collections in the path are created if they do not * already exist. * * @param collectionPath * @param rootCollection * @return * @throws PolicyIndexException */ protected Collection createCollectionPath(String collectionPath, Collection rootCollection) throws PolicyIndexException { try { if (rootCollection.getParentCollection() != null) { throw new PolicyIndexException("Collection supplied is not a root collection"); } String rootCollectionName = rootCollection.getName(); if (!collectionPath.startsWith(rootCollectionName)) { throw new PolicyIndexException("Collection path " + collectionPath + " does not start from root collection - " + rootCollectionName ); } // strip root collection from path, obtain each individual collection name in the path String pathToCreate = collectionPath.substring(rootCollectionName.length()); String[] collections = pathToCreate.split("/"); // iterate each and create as necessary Collection nextCollection = rootCollection; for (String collectionName : collections ) { Collection childCollection = nextCollection.getChildCollection(collectionName); if (childCollection != null) { // child exists childCollection = nextCollection.getChildCollection(collectionName); } else { // does not exist, create it CollectionManagementService mgtService = (CollectionManagementService) nextCollection.getService("CollectionManagementService", "1.0"); childCollection = mgtService.createCollection(collectionName); log.debug("Created collection " + collectionName); } if (nextCollection.isOpen()) { nextCollection.close(); } nextCollection = childCollection; } return nextCollection; } catch (XMLDBException e) { log.error("Error creating collections from path " + e.getMessage(), e); throw new PolicyIndexException("Error creating collections from path " + e.getMessage(), e); } } public void setDatabaseURI(String databaseURI) { m_databaseURI = databaseURI; } public void setCollectionName(String collectionName) { m_collectionName = collectionName; m_indexCollectionPath = ROOT_COLLECTION_PATH +"/system/config/db/" + m_collectionName; // string URI form of collection URI (nb: needs to include base collection - /db) m_collectionPath = ROOT_COLLECTION_PATH + "/" + m_collectionName; } public void setUser(String user) { m_user = user; } public void setPassword(String password) { m_password = password; } public void init() throws PolicyIndexException { initDatabase(); initCollection(); } public void initDatabase() throws PolicyIndexException { String databaseImplClassName = "org.exist.xmldb.DatabaseImpl"; Class<?> cl; try { cl = Class.forName(databaseImplClassName); } catch (ClassNotFoundException e) { log.error("Class not found - check xmldb driver classes are on classpath " + e.getMessage()); throw new PolicyIndexException("Class not found - check xmldb driver classes are on classpath " + e.getMessage(), e); } Database database; try { database = (Database)cl.newInstance(); } catch (Exception e) { log.error("Error instantiating xmldb driver", e); throw new PolicyIndexException("Error instantiating xmldb driver", e); } try { database.setProperty("create-database", "true"); DatabaseManager.registerDatabase(database); } catch (XMLDBException e) { throw new PolicyIndexException("Error registering xmldb driver " + e.getMessage(), e); } } protected void initCollection() throws PolicyIndexException { try { // try to get FeSL policy collection m_collection = DatabaseManager.getCollection(m_databaseURI + m_collectionPath, m_user, m_password); // if it doesn't exist, create it and create the index document if this doesn't already exist if (m_collection == null) { // get root collection Collection rootCol = DatabaseManager.getCollection(m_databaseURI + ROOT_COLLECTION_PATH ,m_user, m_password); CollectionManagementService mgtService = (CollectionManagementService) rootCol.getService("CollectionManagementService", "1.0"); // first create the index for the collection Collection indexCollection = DatabaseManager.getCollection(m_databaseURI + m_indexCollectionPath, m_user, m_password); if (indexCollection == null) { log.debug("creating index collection"); indexCollection = createCollectionPath(m_indexCollectionPath, rootCol); } if (rootCol.isOpen()) rootCol.close(); XMLResource ixd = (XMLResource) indexCollection.getResource(INDEX_DOCUMENT_NAME); // get an already-existing index config document; or create if it doesn't exist if (ixd == null ) { ixd = (XMLResource) indexCollection.createResource(INDEX_DOCUMENT_NAME, XMLResource.RESOURCE_TYPE); } // Note: although eXist allows full path-based indexes, recommendation is to use // qname-based indexes for efficiency, therefore the following defines indexing // on resource attribute IDs and values throughout rather than following the // (dbxml-style) index definition. // Thus the index configuration in the config file is not followed. String configDoc = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" + "<collection xmlns=\"http://exist-db.org/collection-config/1.0\">" + "<index xmlns:p=\"urn:oasis:names:tc:xacml:2.0:policy:schema:os\">" + "<create qname=\"p:AttributeValue\" type=\"xs:string\"/>" + "<create qname=\"@AttributeId\" type=\"xs:string\"/>" + "</index>" + "</collection>"; Document config = createDocument(configDoc); log.debug("Storing index document"); ixd.setContentAsDOM(config); indexCollection.storeResource(ixd); indexCollection.close(); // create the collection log.debug("Creating policy collection"); m_collection = mgtService.createCollection(m_collectionName); } } catch (XMLDBException e) { throw new PolicyIndexException("Error getting/creating policy collection " + e.getMessage(), e); } } /** * delete the policy collection from the database * @throws PolicyIndexException */ protected void deleteCollection() throws PolicyIndexException { // get root collection management service Collection rootCol; try { rootCol = DatabaseManager.getCollection(m_databaseURI + ROOT_COLLECTION_PATH ,m_user, m_password); CollectionManagementService mgtService = (CollectionManagementService) rootCol.getService("CollectionManagementService", "1.0"); // delete the collection mgtService.removeCollection(m_collectionName); log.debug("Policy collection deleted"); } catch (XMLDBException e) { throw new PolicyIndexException("Error deleting collection " + e.getMessage(), e); } } // create an XML Document from the policy document protected static Document createDocument(String document) throws PolicyIndexException { // parse policy document and create dom DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); factory.setNamespaceAware(true); DocumentBuilder builder; try { builder = factory.newDocumentBuilder(); Document doc = builder.parse(new InputSource(new StringReader(document))); return doc; } catch (ParserConfigurationException e) { throw new PolicyIndexException(e); } catch (SAXException e) { throw new PolicyIndexException(e); } catch (IOException e) { throw new PolicyIndexException(e); } } public void close() { try { m_collection.close(); } catch (XMLDBException e) { log.warn("Error closing connection " + e.getMessage(), e); } } }