/*
* eXist Open Source Native XML Database
* Copyright (C) 2001-06 The eXist Project
* http://exist-db.org
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*
* $Id$
*/
package org.exist.security.xacml;
import com.sun.xacml.AbstractPolicy;
import com.sun.xacml.Indenter;
import com.sun.xacml.ParsingException;
import com.sun.xacml.Policy;
import com.sun.xacml.PolicyReference;
import com.sun.xacml.PolicySet;
import com.sun.xacml.PolicyTreeElement;
import com.sun.xacml.ProcessingException;
import com.sun.xacml.Target;
import com.sun.xacml.cond.Apply;
import com.sun.xacml.ctx.Status;
import com.sun.xacml.finder.PolicyFinderResult;
import java.io.CharArrayWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.net.URI;
import java.net.URL;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import org.apache.log4j.Logger;
import org.exist.EXistException;
import org.exist.collections.Collection;
import org.exist.collections.IndexInfo;
import org.exist.collections.triggers.TriggerException;
import org.exist.dom.DefaultDocumentSet;
import org.exist.dom.DocumentImpl;
import org.exist.dom.DocumentSet;
import org.exist.dom.MutableDocumentSet;
import org.exist.dom.NodeSet;
import org.exist.dom.QName;
import org.exist.dom.StoredNode;
import org.exist.numbering.NodeId;
import org.exist.security.PermissionDeniedException;
import org.exist.storage.BrokerPool;
import org.exist.storage.DBBroker;
import org.exist.storage.NativeValueIndex;
import org.exist.storage.UpdateListener;
import org.exist.storage.txn.TransactionManager;
import org.exist.storage.txn.Txn;
import org.exist.xmldb.XmldbURI;
import org.exist.xquery.Constants;
import org.exist.xquery.XPathException;
import org.exist.xquery.value.AnyURIValue;
import org.exist.xquery.value.AtomicValue;
import org.exist.xquery.value.Sequence;
import org.apache.commons.io.output.ByteArrayOutputStream;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
/**
* This class contains utility methods for working with XACML
* in eXist.
*/
public class XACMLUtil implements UpdateListener
{
private static final Logger LOG = Logger.getLogger(ExistPolicyModule.class);
private static final Map<String, AbstractPolicy> POLICY_CACHE = Collections.synchronizedMap(new HashMap<String, AbstractPolicy>(8));
private static final XmldbURI[] samplePolicyDocs = { XmldbURI.create("policies/main_modules_policy.xml"),
XmldbURI.create("policies/builtin_policy.xml"), XmldbURI.create("policies/external_modules_policy.xml"),
XmldbURI.create("policies/reflection_policy.xml") };
private ExistPDP pdp;
@SuppressWarnings("unused")
private XACMLUtil() {}
XACMLUtil(ExistPDP pdp)
{
if(pdp == null)
{throw new NullPointerException("ExistPDP cannot be null");}
this.pdp = pdp;
pdp.getBrokerPool().getNotificationService().subscribe(this);
}
protected void initializePolicyCollection()
{
DBBroker broker = null;
try {
final BrokerPool pool = pdp.getBrokerPool();
broker = pool.get(pool.getSecurityManager().getSystemSubject());
initializePolicyCollection(broker);
} catch(final PermissionDeniedException pde) {
LOG.error(pde.getMessage(), pde);
} catch(final EXistException ee) {
LOG.error("Could not get broker pool to initialize policy collection", ee);
} finally {
pdp.getBrokerPool().release(broker);
}
}
private void initializePolicyCollection(DBBroker broker) throws PermissionDeniedException
{
final Collection policyCollection = getPolicyCollection(broker);
if(policyCollection == null)
{return;} //warning generated by getPolicyCollection, no need to duplicate here
if(policyCollection.getDocumentCount(broker) == 0)
{
final Boolean loadDefaults = (Boolean)broker.getConfiguration().getProperty(XACMLConstants.LOAD_DEFAULT_POLICIES_PROPERTY);
if(loadDefaults == null || loadDefaults.booleanValue())
{storeDefaultPolicies(broker);}
}
}
//UpdateListener method
/**
* This method is called by the <code>NotificationService</code>
* when documents are updated in the databases. If a document
* is removed or updated from the policy collection, it is removed
* from the policy cache.
*/
public void documentUpdated(DocumentImpl document, int event)
{
if(inPolicyCollection(document) && (event == UpdateListener.REMOVE || event == UpdateListener.UPDATE))
{POLICY_CACHE.remove(document.getURI().toString());}
}
public void nodeMoved(NodeId oldNodeId, StoredNode newNode) {
// not relevant
}
public void unsubscribe() {
// not relevant
}
/**
* Returns true if the specified document is in the policy collection.
* This does not check subcollections.
*
* @param document The document in question
* @return if the document is in the policy collection
*/
public static boolean inPolicyCollection(DocumentImpl document)
{
return XACMLConstants.POLICY_COLLECTION_URI.equals(document.getCollection().getURI());
}
/**
* Performs any necessary cleanup operations. Generally only
* called if XACML has been disabled.
*/
public void close()
{
pdp.getBrokerPool().getNotificationService().unsubscribe(this);
}
/**
* Gets the policy (or policy set) specified by the given id.
*
* @param type The type of id reference:
* PolicyReference.POLICY_REFERENCE for a policy reference
* or PolicyReference.POLICYSET_REFERENCE for a policy set
* reference.
* @param idReference The id of the policy (or policy set) to
* retrieve
* @param broker the broker to use to access the database
* @return The referenced policy.
* @throws ProcessingException if there is an error finding
* the policy (or policy set).
* @throws XPathException
*/
public AbstractPolicy findPolicy(DBBroker broker, URI idReference, int type) throws ParsingException, ProcessingException, XPathException, PermissionDeniedException
{
final QName idAttributeQName = getIdAttributeQName(type);
if(idAttributeQName == null)
{throw new NullPointerException("Invalid reference type: " + type);}
final DocumentImpl policyDoc = getPolicyDocument(broker, idAttributeQName, idReference);
if(policyDoc == null)
{return null;}
return getPolicyDocument(policyDoc);
}
/**
* This method returns all policy documents in the policies collection.
* If recursive is true, policies in subcollections are returned as well.
*
* @param broker the broker to use to access the database
* @param recursive true if policies in subcollections should be
* returned as well
* @return All policy documents in the policies collection
*/
public static DocumentSet getPolicyDocuments(DBBroker broker, boolean recursive) throws PermissionDeniedException
{
final Collection policyCollection = getPolicyCollection(broker);
if(policyCollection == null)
{return null;}
final int documentCount = policyCollection.getDocumentCount(broker);
if(documentCount == 0)
{return null;}
final MutableDocumentSet documentSet = new DefaultDocumentSet(documentCount);
return policyCollection.allDocs(broker, documentSet, recursive);
}
/**
* Gets the policy collection or creates it if it does not exist.
*
* @param broker The broker to use to access the database.
* @return A <code>Collection</code> object for the policy collection.
*/
public static Collection getPolicyCollection(DBBroker broker)
{
try{
Collection policyCollection = broker.getCollection(XACMLConstants.POLICY_COLLECTION_URI);
if(policyCollection == null)
{
final TransactionManager transact = broker.getBrokerPool().getTransactionManager();
final Txn txn = transact.beginTransaction();
try
{
policyCollection = broker.getOrCreateCollection(txn, XACMLConstants.POLICY_COLLECTION_URI);
broker.saveCollection(txn, policyCollection);
transact.commit(txn);
}
catch (final IOException e) {
transact.abort(txn);
LOG.error("Error creating policy collection", e);
return null;
} catch (final EXistException e) {
transact.abort(txn);
LOG.error("Error creating policy collection", e);
return null;
} catch (final PermissionDeniedException e) {
transact.abort(txn);
LOG.error("Error creating policy collection", e);
return null;
} catch (final TriggerException e) {
transact.abort(txn);
LOG.error("Error creating policy collection", e);
return null;
} finally {
transact.close(txn);
}
}
return policyCollection;
} catch (final PermissionDeniedException e) {
LOG.error("Error creating policy collection", e);
return null;
}
}
/**
* Returns the single policy (or policy set) document that has the
* attribute specified by attributeQName with the value
* attributeValue, null if none match, or throws a
* <code>ProcessingException</code> if more than one match. This is
* performed by a QName range index lookup and so it requires a range
* index to be given on the attribute.
*
* @param attributeQName The name of the attribute
* @param attributeValue The value of the attribute
* @param broker the broker to use to access the database
* @return The referenced policy.
* @throws ProcessingException if there is an error finding
* the policy (or policy set) documents.
* @throws XPathException if there is an error performing
* the index lookup
*/
public DocumentImpl getPolicyDocument(DBBroker broker, QName attributeQName, URI attributeValue) throws ProcessingException, XPathException, PermissionDeniedException
{
final DocumentSet documentSet = getPolicyDocuments(broker, attributeQName, attributeValue);
final int documentCount = (documentSet == null) ? 0 : documentSet.getDocumentCount();
if(documentCount == 0)
{
LOG.warn("Could not find " + attributeQName.getLocalName() + " '" + attributeValue + "'", null);
return null;
}
if(documentCount > 1)
{
throw new ProcessingException("Too many applicable policies for " + attributeQName.getLocalName() + " '" + attributeValue + "'");
}
return (DocumentImpl)documentSet.getDocumentIterator().next();
}
/**
* Gets all policy (or policy set) documents that have the
* attribute specified by attributeQName with the value
* attributeValue. This is performed by a QName range index
* lookup and so it requires a range index to be given
* on the attribute.
*
* @param attributeQName The name of the attribute
* @param attributeValue The value of the attribute
* @param broker the broker to use to access the database
* @return The referenced policy.
* @throws ProcessingException if there is an error finding
* the policy (or policy set) documents.
* @throws XPathException if there is an error performing the
* index lookup
*/
public DocumentSet getPolicyDocuments(DBBroker broker, QName attributeQName, URI attributeValue) throws ProcessingException, XPathException, PermissionDeniedException
{
if(attributeQName == null)
{return null;}
if(attributeValue == null)
{return null;}
final AtomicValue comparison = new AnyURIValue(attributeValue);
final DocumentSet documentSet = getPolicyDocuments(broker, true);
final NodeSet nodeSet = documentSet.docsToNodeSet();
final NativeValueIndex valueIndex = broker.getValueIndex();
final Sequence results = valueIndex.find(null, Constants.EQ, documentSet, null, NodeSet.ANCESTOR, attributeQName, comparison);
// Sequence results = index.findByQName(attributeQName, comparison, nodeSet);
//TODO : should we honour (# exist:force-index-use #) ?
return (results == null) ? null : results.getDocumentSet();
}
/**
* Gets the name of the attribute that specifies the policy
* (if type == PolicyReference.POLICY_REFERENCE) or
* the policy set (if type == PolicyReference.POLICYSET_REFERENCE).
*
* @param type The type of id reference:
* PolicyReference.POLICY_REFERENCE for a policy reference
* or PolicyReference.POLICYSET_REFERENCE for a policy set
* reference.
* @return The attribute name for the reference type
*/
public static QName getIdAttributeQName(int type)
{
if(type == PolicyReference.POLICY_REFERENCE)
{return new QName(XACMLConstants.POLICY_ID_LOCAL_NAME, XACMLConstants.XACML_POLICY_NAMESPACE);}
else if(type == PolicyReference.POLICYSET_REFERENCE)
{return new QName(XACMLConstants.POLICY_SET_ID_LOCAL_NAME, XACMLConstants.XACML_POLICY_NAMESPACE);}
else
{return null;}
}
//logs the specified message and exception
//then, returns a result with status Indeterminate and the given message
/**
* Convenience method for errors occurring while processing. The message
* and exception are logged and a <code>PolicyFinderResult</code> is
* generated with Status.STATUS_PROCESSING_ERROR as the error condition
* and the message as the message.
*
* @param message The message describing the error.
* @param t The cause of the error, may be null
* @return A <code>PolicyFinderResult</code> representing the error.
*/
public static PolicyFinderResult errorResult(String message, Throwable t)
{
LOG.warn(message, t);
return new PolicyFinderResult(new Status(Collections.singletonList(Status.STATUS_PROCESSING_ERROR), message));
}
/**
* Obtains a parsed representation of the specified XACML Policy or PolicySet
* document. If the document has already been parsed, this method returns the
* cached <code>AbstractPolicy</code>. Otherwise, it unmarshals the document into
* an <code>AbstractPolicy</code> and caches it.
*
* @param policyDoc the policy (or policy set) document
* for which a parsed representation should be obtained
* @return a parsed policy (or policy set)
* @throws ParsingException if an error occurs while parsing the specified document
*/
public AbstractPolicy getPolicyDocument(DocumentImpl policyDoc) throws ParsingException
{
//TODO: use xmldbUri
final String name = policyDoc.getURI().toString();
AbstractPolicy policy = (AbstractPolicy)POLICY_CACHE.get(name);
if(policy == null)
{
policy = parsePolicyDocument(policyDoc);
POLICY_CACHE.put(name, policy);
}
return policy;
}
/**
* Parses a DOM representation of a policy document into an
* <code>AbstractPolicy</code>.
*
* @param policyDoc The DOM <code>Document</code> representing
* the XACML policy or policy set.
* @return The parsed policy
* @throws ParsingException if there is an error parsing the document
*/
public AbstractPolicy parsePolicyDocument(Document policyDoc) throws ParsingException
{
final Element root = policyDoc.getDocumentElement();
final String name = root.getTagName();
if(name.equals(XACMLConstants.POLICY_SET_ELEMENT_LOCAL_NAME))
{return PolicySet.getInstance(root, pdp.getPDPConfig().getPolicyFinder());}
else if(name.equals(XACMLConstants.POLICY_ELEMENT_LOCAL_NAME))
{return Policy.getInstance(root);}
else
{throw new ParsingException("The root element of the policy document must be '" + XACMLConstants.POLICY_SET_ID_LOCAL_NAME + "' or '" + XACMLConstants.POLICY_SET_ID_LOCAL_NAME + "', was: '" + name + "'");}
}
/**
* Escapes characters that are not allowed in various places
* in XML by replacing all invalid characters with
* <code>getEscape(c)</code>.
*
* @param buffer The <code>StringBuffer</code> containing
* the text to escape in place.
*/
public static void XMLEscape(StringBuffer buffer)
{
if(buffer == null)
{return;}
char c;
String escape;
for(int i = 0; i < buffer.length();)
{
c = buffer.charAt(i);
escape = getEscape(c);
if(escape == null)
{i++;}
else
{
buffer.replace(i, i+1, escape);
i += escape.length();
}
}
}
/**
* Escapes characters that are not allowed in various
* places in XML. Characters are replaced by the
* corresponding entity. The characters &, <,
* >, ", and ' are escaped.
*
* @param c The character to escape.
* @return A <code>String</code> representing the
* escaped character or null if the character does
* not need to be escaped.
*/
public static String getEscape(char c)
{
switch(c)
{
case '&': return "&";
case '<': return "<";
case '>': return ">";
case '\"': return """;
case '\'': return "'";
default: return null;
}
}
/**
* Escapes characters that are not allowed in various places
* in XML by replacing all invalid characters with
* <code>getEscape(c)</code>.
*
* @param in The <code>String</code> containing
* the text to escape in place.
*/
public static String XMLEscape(String in)
{
if(in == null)
{return null;}
final StringBuffer temp = new StringBuffer(in);
XMLEscape(temp);
return temp.toString();
}
/**
* Serializes the specified <code>PolicyTreeElement</code> to a
* <code>String</code> as XML. The XML is indented if indent
* is true.
*
* @param element The <code>PolicyTreeElement</code> to serialize
* @param indent If the XML should be indented
* @return The XML representation of the element
*/
public static String serialize(PolicyTreeElement element, boolean indent)
{
if(element == null)
{return "";}
final ByteArrayOutputStream out = new ByteArrayOutputStream();
if(indent)
{element.encode(out, new Indenter());}
else
{element.encode(out);}
return out.toString();
}
/**
* Serializes the specified <code>Target</code> to a
* <code>String</code> as XML. The XML is indented if indent
* is true.
*
* @param target The <code>Target</code> to serialize
* @param indent If the XML should be indented
* @return The XML representation of the target
*/
public static String serialize(Target target, boolean indent)
{
if(target == null)
{return "";}
final ByteArrayOutputStream out = new ByteArrayOutputStream();
if(indent)
{target.encode(out, new Indenter());}
else
{target.encode(out);}
return out.toString();
}
/**
* Serializes the specified <code>Apply</code> to a
* <code>String</code> as XML. The XML is indented if indent
* is true.
*
* @param apply The <code>Apply</code> to serialize
* @param indent If the XML should be indented
* @return The XML representation of the apply
*/
public static String serialize(Apply apply, boolean indent)
{
if(apply == null)
{return "";}
final ByteArrayOutputStream out = new ByteArrayOutputStream();
if(indent)
{apply.encode(out, new Indenter());}
else
{apply.encode(out);}
return out.toString();
}
/**
* Stores the default policies
*
* @param broker The broker with which to access the database
*/
public static void storeDefaultPolicies(DBBroker broker)
{
LOG.debug("Storing default XACML policies");
for(int i = 0; i < samplePolicyDocs.length; ++i)
{
final XmldbURI docPath = samplePolicyDocs[i];
try
{
storePolicy(broker, docPath);
}
catch(final IOException ioe)
{
LOG.warn("IO Error storing default policy '" + docPath + "'", ioe);
}
catch(final EXistException ee)
{
LOG.warn("IO Error storing default policy '" + docPath + "'", ee);
}
}
}
/**
* Stores the resource at docPath into the policies collection.
*
* @param broker The broker with which to access the database
* @param docPath The location of the resource
* @throws EXistException
*/
public static void storePolicy(DBBroker broker, XmldbURI docPath) throws EXistException, IOException
{
final XmldbURI docName = docPath.lastSegment();
final URL url = XACMLUtil.class.getResource(docPath.toString());
if(url == null)
{return;}
final String content = toString(url.openStream());
if(content == null)
{return;}
final Collection collection = getPolicyCollection(broker);
if(collection == null)
{return;}
final TransactionManager transact = broker.getBrokerPool().getTransactionManager();
final Txn txn = transact.beginTransaction();
try
{
final IndexInfo info = collection.validateXMLResource(txn, broker, docName, content);
//TODO : unlock the collection here ?
collection.store(txn, broker, info, content, false);
transact.commit(txn);
}
catch(final Exception e)
{
transact.abort(txn);
if(e instanceof EXistException)
{throw (EXistException)e;}
throw new EXistException("Error storing policy '" + docPath + "'", e);
} finally {
transact.close(txn);
}
}
/** Reads an <code>InputStream</code> into a string.
* @param in The stream to read into a string.
* @return The stream as a string
* @throws IOException
*/
public static String toString(InputStream in) throws IOException
{
if(in == null)
{return null;}
final Reader reader = new InputStreamReader(in);
final char[] buffer = new char[100];
final CharArrayWriter writer = new CharArrayWriter(1000);
int read;
while((read = reader.read(buffer)) > -1)
writer.write(buffer, 0, read);
return writer.toString();
}
public void debug() {
// left empty
}
}