/* 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;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamSource;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import javax.xml.validation.Validator;
import org.apache.commons.pool.PoolableObjectFactory;
import org.apache.commons.pool.impl.SoftReferenceObjectPool;
import org.fcrepo.server.errors.ValidationException;
import org.fcrepo.utilities.XmlTransformUtility;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.xml.sax.ErrorHandler;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.jboss.security.xacml.sunxacml.AbstractPolicy;
import org.jboss.security.xacml.sunxacml.ParsingException;
import org.jboss.security.xacml.sunxacml.Policy;
import org.jboss.security.xacml.sunxacml.PolicySet;
/**
* A validating parser for XACML policies.
* <p>
* This class also provides a commandline XACML validation utility.
* <p>
* NOTE: Although instances may be re-used, this class is not thread-safe.
* Use the <code>copy()</code> method to support concurrent parsing.
*/
public class PolicyParser {
private static final Logger logger = LoggerFactory.getLogger(PolicyParser.class);
private static final String W3C_XML_SCHEMA_NS_URI = "http://www.w3.org/2001/XMLSchema";
// Neither of these factories are thread-safe, so access is synchronized in methods below
private static final SchemaFactory SCHEMA_FACTORY = SchemaFactory.newInstance(W3C_XML_SCHEMA_NS_URI);
private static final ErrorHandler THROW_ALL = new ThrowAllErrorHandler();
private final SoftReferenceObjectPool<Validator> m_validators;
/**
* Creates an instance that will validate according to the given schema.
*
* @param schemaStream the XSD schema to use for schema validation
* @throws IOException if the schema can't be read
* @throws SAXException if the schema isn't valid
*/
public PolicyParser(InputStream schemaStream)
throws IOException, SAXException {
this(getSchema(schemaStream));
}
// actual constructor keeps schema (which is thread safe) for cheap copying
private PolicyParser(Schema schema)
throws SAXException {
m_validators = new SoftReferenceObjectPool<Validator>(new PoolableValidatorFactory(schema));
}
private PolicyParser(SoftReferenceObjectPool<Validator> validators) {
m_validators = validators;
}
/**
* Schema Factory is not thread safe
* @return
*/
private static Schema getSchema(InputStream schemaStream) throws SAXException {
Schema result;
synchronized(SCHEMA_FACTORY){
result = SCHEMA_FACTORY.newSchema(new StreamSource(schemaStream));
}
return result;
}
/**
* Gets a new instance that uses the same schema as this one.
*
* @return a copy of this instance
*/
public PolicyParser copy() {
return new PolicyParser(m_validators);
}
/**
* Parses the given policy and optionally schema validates it.
*
* @param policyStream
* the serialized XACML policy
* @param schemaValidate
* whether to schema validate
* @return the parsed policy.
* @throws ValidationException
* if the given xml is not a valid policy. This will occur if it
* is not well-formed XML, its root element is not named
* <code>Policy</code> or <code>PolicySet</code>, it triggers
* a parse exception in the Sun libraries when constructing an
* <code>AbstractPolicy</code> from the DOM, or (if validation
* is true) it is not schema-valid.
*/
public AbstractPolicy parse(InputStream policyStream,
boolean schemaValidate)
throws ValidationException {
// Parse; die if not well-formed
Document doc = null;
DocumentBuilder domParser = null;
try {
domParser = XmlTransformUtility.borrowDocumentBuilder();
domParser.setErrorHandler(THROW_ALL);
doc = domParser.parse(policyStream);
} catch (Exception e) {
throw new ValidationException("Policy invalid; malformed XML", e);
} finally {
if (domParser != null) {
XmlTransformUtility.returnDocumentBuilder(domParser);
}
}
if (schemaValidate) {
// XSD-validate; die if not schema-valid
Validator validator = null;
try {
validator = m_validators.borrowObject();
validator.validate(new DOMSource(doc));
} catch (Exception e) {
throw new ValidationException("Policy invalid; schema"
+ " validation failed", e);
} finally {
if (validator != null) try {
m_validators.returnObject(validator);
} catch (Exception e) {
logger.warn(e.getMessage(), e);
}
}
}
// Construct AbstractPolicy from doc; die if root isn't "Policy[Set]"
Element root = doc.getDocumentElement();
String rootName = root.getTagName();
try {
if (rootName.equals("Policy")) {
return Policy.getInstance(root);
} else if (rootName.equals("PolicySet")) {
return PolicySet.getInstance(root);
} else {
throw new ValidationException("Policy invalid; root element is "
+ rootName + ", but should be "
+ "Policy or PolicySet");
}
} catch (ParsingException e) {
throw new ValidationException("Policy invalid; failed parsing by "
+ "Sun XACML implementation", e);
}
}
/**
* Command-line utility for validating XACML policies.
* <p>
* Accepts a single argument: the path to the policy instance to validate.
* <p>
* Also requires that the com.sun.xacml.PolicySchema system property points
* to the XACML schema.
*/
public static void main(String[] args) {
if (args.length != 1) {
fail("One argument required: /path/to/xacml-policy-to-validate.xml");
}
final String schemaPathProperty = "com.sun.xacml.PolicySchema";
String schemaPath = System.getProperty(schemaPathProperty);
if (schemaPath == null) {
fail("System property " + schemaPathProperty + " (path to XACML "
+ "schema) must be set. (e.g. -D" + schemaPathProperty
+ "=/path/to/schema)");
}
try {
InputStream instance = getStream(args[0]);
PolicyParser parser = new PolicyParser(getStream(schemaPath));
parser.parse(instance, true);
System.out.println("Validation successful");
System.exit(0);
} catch (ValidationException e) {
if (e.getCause() != null && e.getCause() instanceof SAXParseException) {
fail(e.getCause().getMessage());
} else {
fail(e);
}
} catch (Exception e) {
fail(e);
}
}
private static InputStream getStream(String path) {
try {
return new FileInputStream(path);
} catch (Exception e) {
fail("File not found: " + path);
return null;
}
}
private static void fail(String message) {
System.out.println("ERROR: " + message);
System.out.println("Validation failed");
System.exit(1);
}
private static void fail(Exception e) {
e.printStackTrace();
fail(e.getClass().getName() + ": See above for detail");
}
/**
* This class is a workaround to some shift in the behavior of anonymous inner classes
*
*/
public static class ThrowAllErrorHandler implements ErrorHandler {
public void error(SAXParseException e) throws SAXParseException { throw e; }
public void fatalError(SAXParseException e) throws SAXParseException { throw e; }
public void warning(SAXParseException e) throws SAXParseException { throw e; }
}
public static class PoolableValidatorFactory implements PoolableObjectFactory<Validator> {
private final Schema m_schema;
public PoolableValidatorFactory(Schema schema) {
m_schema = schema;
}
@Override
public Validator makeObject() throws Exception {
return m_schema.newValidator();
}
@Override
public void destroyObject(Validator obj) throws Exception {
// no op
}
@Override
public boolean validateObject(Validator obj) {
// no op
return true;
}
@Override
public void activateObject(Validator obj) throws Exception {
obj.reset();
}
@Override
public void passivateObject(Validator obj) throws Exception {
// no op
}
}
}