/*
* File: FindObjects.java
*
* Copyright 2007 Macquarie E-Learning Centre Of Excellence
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.fcrepo.server.security.xacml.pep.rest.objectshandlers;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.xml.namespace.NamespaceContext;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import org.fcrepo.common.Constants;
import org.fcrepo.server.security.RequestCtx;
import org.fcrepo.server.security.xacml.MelcoeXacmlException;
import org.fcrepo.server.security.xacml.pep.PEPException;
import org.fcrepo.server.security.xacml.pep.ResourceAttributes;
import org.fcrepo.server.security.xacml.pep.rest.filters.AbstractFilter;
import org.fcrepo.server.security.xacml.pep.rest.filters.DataResponseWrapper;
import org.fcrepo.server.security.xacml.pep.rest.filters.ResponseHandlingRESTFilter;
import org.fcrepo.server.security.xacml.util.LogUtil;
import org.fcrepo.server.utilities.CXFUtility;
import org.fcrepo.utilities.XmlTransformUtility;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.tidy.Tidy;
import org.jboss.security.xacml.sunxacml.attr.AttributeValue;
import org.jboss.security.xacml.sunxacml.ctx.ResponseCtx;
import org.jboss.security.xacml.sunxacml.ctx.Result;
import org.jboss.security.xacml.sunxacml.ctx.Status;
/**
* Filter to handle the FindObjects operation.
*
* @author nish.naidoo@gmail.com
*/
public class FindObjects
extends AbstractFilter implements ResponseHandlingRESTFilter {
private static final Logger logger =
LoggerFactory.getLogger(FindObjects.class);
private static final NamespaceContext TYPES_NAMESPACE = new TypesNamespaceContext();
private static final XPathFactory XPATH_FACTORY = XPathFactory.newInstance();
private static final DocumentBuilderFactory BUILDER_FACTORY = DocumentBuilderFactory.newInstance();
static {
// the default namespace must be prefixed to be accessible to xpath
BUILDER_FACTORY.setNamespaceAware(true);
}
private Transformer xFormer = null;
private Tidy tidy = null;
/**
* Default constructor.
*
* @throws PEPException
*/
public FindObjects()
throws PEPException {
super();
try {
xFormer = XmlTransformUtility.getTransformer();
} catch (Exception e) {
throw new PEPException("Error initialising SearchFilter", e);
}
tidy = new Tidy();
tidy.setShowWarnings(false);
tidy.setQuiet(true);
}
/*
* (non-Javadoc)
* @see
* org.fcrepo.server.security.xacml.pep.rest.filters.RESTFilter#handleRequest(javax.servlet
* .http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
*/
@Override
public RequestCtx handleRequest(HttpServletRequest request,
HttpServletResponse response)
throws IOException, ServletException {
RequestCtx req = null;
Map<URI, AttributeValue> resAttr;
Map<URI, AttributeValue> actions = new HashMap<URI, AttributeValue>();
try {
resAttr = ResourceAttributes.getRepositoryResources();
actions.put(Constants.ACTION.ID.getURI(),
Constants.ACTION.FIND_OBJECTS
.getStringAttribute());
actions.put(Constants.ACTION.API.getURI(),
Constants.ACTION.APIA.getStringAttribute());
req =
getContextHandler().buildRequest(getSubjects(request),
actions,
resAttr,
getEnvironment(request));
LogUtil.statLog(request.getRemoteUser(),
Constants.ACTION.FIND_OBJECTS.uri,
Constants.FEDORA_REPOSITORY_PID.uri,
null);
} catch (Exception e) {
logger.error(e.getMessage(),e);
CXFUtility.getFault(e);
}
return req;
}
/*
* (non-Javadoc)
* @see
* org.fcrepo.server.security.xacml.pep.rest.filters.RESTFilter#handleResponse(javax.servlet
* .http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
*/
@Override
public RequestCtx handleResponse(HttpServletRequest request,
HttpServletResponse response)
throws IOException, ServletException {
if (request.getParameter("terms") == null
&& request.getParameter("query") == null
&& request.getParameter("sessionToken") == null) {
return null;
}
DataResponseWrapper res = (DataResponseWrapper) response;
byte[] data = res.getData();
byte[] result = null;
String body = new String(data);
if (body.startsWith("<html>")) {
logger.debug("filtering html");
result = filterHTML(request, res);
} else if (body.startsWith("<?xml")) {
logger.debug("filtering xml");
result = filterXML(request, res);
} else {
logger.debug("not filtering due to unexpected output: {}", body);
result = data;
}
res.setData(result);
return null;
}
/**
* Parses an XML based response and removes the items that are not
* permitted.
*
* @param request
* the http servlet request
* @param response
* the http servlet response
* @return the new response body without non-permissable objects.
* @throws ServletException
*/
private byte[] filterXML(HttpServletRequest request,
DataResponseWrapper response)
throws ServletException {
byte[] data = response.getData();
DocumentBuilder docBuilder = null;
Document doc = null;
try {
synchronized(BUILDER_FACTORY){
docBuilder =
BUILDER_FACTORY.newDocumentBuilder();
}
doc = docBuilder.parse(new ByteArrayInputStream(data));
} catch (Exception e) {
throw new ServletException(e);
}
XPath xpath;
synchronized(XPATH_FACTORY){
xpath = XPATH_FACTORY.newXPath();
}
xpath.setNamespaceContext(TYPES_NAMESPACE);
NodeList rows = null;
try {
rows =
(NodeList) xpath
.evaluate("/:result/:resultList/:objectFields/:pid",
doc,
XPathConstants.NODESET);
} catch (XPathExpressionException xpe) {
throw new ServletException("Error parsing XML for search results: ",
xpe);
}
if (rows.getLength() == 0) {
logger.debug("No results to filter.");
return data;
}
Map<String, Node> pids = new HashMap<String, Node>();
for (int x = 0; x < rows.getLength(); x++) {
Node pid = rows.item(x);
pids.put(pid.getFirstChild().getNodeValue(), pid.getParentNode());
}
Set<Result> results = evaluatePids(pids.keySet(), request, response);
for (Result r : results) {
String resource = r.getResource();
if (resource == null || resource.isEmpty()) {
logger.warn("This resource has no resource identifier in the xacml response results!");
} else if (logger.isDebugEnabled()) {
logger.debug("Checking: {}", resource);
}
int lastSlash = resource.lastIndexOf('/');
String rid = resource.substring(lastSlash+1);
if (r.getStatus().getCode().contains(Status.STATUS_OK)
&& r.getDecision() != Result.DECISION_PERMIT) {
Node node = pids.get(rid);
node.getParentNode().removeChild(node);
logger.debug("Removing: {} [{}]", resource, rid);
}
}
// since namespaces are disabled, set the attribute explicitly
doc.getDocumentElement().setAttribute("xmlns", "http://www.fedora.info/definitions/1/0/types/");
Source src = new DOMSource(doc);
ByteArrayOutputStream os = new ByteArrayOutputStream();
javax.xml.transform.Result dst = new StreamResult(os);
try {
xFormer.transform(src, dst);
} catch (TransformerException te) {
throw new ServletException("error generating output", te);
}
return os.toByteArray();
}
/**
* Parses an HTML based response and removes the items that are not
* permitted.
*
* @param request
* the http servlet request
* @param response
* the http servlet response
* @return the new response body without non-permissable objects.
* @throws ServletException
*/
private byte[] filterHTML(HttpServletRequest request,
DataResponseWrapper response)
throws ServletException {
byte[] data = response.getData();
InputStream is = new ByteArrayInputStream(data);
Document doc = tidy.parseDOM(is, null);
XPath xpath;
synchronized(XPATH_FACTORY) {
xpath = XPATH_FACTORY.newXPath();
}
NodeList rows = null;
try {
rows =
(NodeList) xpath
.evaluate("/html/body/center/center/table/tr",
doc,
XPathConstants.NODESET);
} catch (XPathExpressionException xpe) {
throw new ServletException("Error parsing HTML for search results: ",
xpe);
}
// only the header row, no results.
if (rows.getLength() == 1) {
logger.debug("No results to filter.");
return data;
}
NodeList headers = rows.item(0).getChildNodes();
int numHeaders = headers.getLength();
int pidHeader = -1;
// ensure we have 'pid' in the list and also that it exists
for (int x = 0; x < numHeaders; x++) {
String header =
headers.item(x).getFirstChild().getFirstChild()
.getNodeValue();
if ("pid".equals(header)) {
pidHeader = x;
}
}
if (pidHeader == -1) {
throw new ServletException("pid field not in result list!");
}
Map<String, Node> pids = new HashMap<String, Node>();
// start from 1 to skip the header row.
for (int x = 1; x < rows.getLength(); x++) {
NodeList elements = rows.item(x).getChildNodes();
Node pidParentA = elements.item(pidHeader).getFirstChild();
if (pidParentA != null && pidParentA.getNodeName().equals("a")) {
String pid = pidParentA.getFirstChild().getNodeValue();
pids.put(pid, rows.item(x));
}
}
Set<Result> results = evaluatePids(pids.keySet(), request, response);
for (Result r : results) {
String resource = r.getResource();
if (resource == null || resource.isEmpty()) {
logger.warn("This resource has no resource identifier in the xacml response results!");
resource = "";
} else {
logger.debug("Checking: {}", resource);
}
int lastSlash = resource.lastIndexOf('/');
String rid = resource.substring(lastSlash + 1);
if (r.getStatus().getCode().contains(Status.STATUS_OK)
&& r.getDecision() != Result.DECISION_PERMIT) {
Node node = pids.get(rid);
node.getParentNode().removeChild(node.getNextSibling());
node.getParentNode().removeChild(node);
logger.debug("Removing: {} [{}]", resource, rid);
}
}
Source src = new DOMSource(doc);
ByteArrayOutputStream os = new ByteArrayOutputStream();
javax.xml.transform.Result dst = new StreamResult(os);
try {
xFormer.transform(src, dst);
} catch (TransformerException te) {
throw new ServletException("error generating output", te);
}
return os.toByteArray();
}
/**
* Takes a given list of PID's and evaluates them.
*
* @param pids
* the list of pids to check
* @param request
* the http servlet request
* @param response
* the http servlet resposne
* @return a set of XACML results.
* @throws ServletException
*/
private Set<Result> evaluatePids(Set<String> pids,
HttpServletRequest request,
DataResponseWrapper response)
throws ServletException {
RequestCtx[] requests = new RequestCtx[pids.size()];
int ix = 0;
for (String pid : pids) {
logger.debug("Checking: {}", pid);
Map<URI, AttributeValue> actions =
new HashMap<URI, AttributeValue>();
Map<URI, AttributeValue> resAttr;
try {
actions
.put(Constants.ACTION.ID.getURI(),
Constants.ACTION.LIST_OBJECT_IN_FIELD_SEARCH_RESULTS
.getStringAttribute());
actions.put(Constants.ACTION.API.getURI(),
Constants.ACTION.APIA.getStringAttribute());
resAttr = ResourceAttributes.getResources(pid);
RequestCtx req =
getContextHandler()
.buildRequest(getSubjects(request),
actions,
resAttr,
getEnvironment(request));
requests[ix++] = req;
} catch (Exception e) {
logger.error(e.getMessage(), e);
throw new ServletException(e.getMessage(), e);
}
}
ResponseCtx resCtx = null;
try {
logger.debug("Number of requests: {}", requests.length);
resCtx =
getContextHandler().evaluateBatch(requests);
} catch (MelcoeXacmlException pe) {
throw new ServletException("Error evaluating pids: "
+ pe.getMessage(), pe);
}
@SuppressWarnings("unchecked")
Set<Result> results = resCtx.getResults();
return results;
}
static class TypesNamespaceContext implements NamespaceContext {
static List<String> PREFIXES = Arrays.asList(new String[]{"types",""});
static List<String> XSI_PREFIXES = Arrays.asList(new String[]{"xsi"});
static ArrayList<String> EMPTY = new ArrayList<String>(0);
@Override
public String getNamespaceURI(String prefix) {
if ("types".equals(prefix) || (prefix != null && prefix.isEmpty())) {
return "http://www.fedora.info/definitions/1/0/types/";
}
if ("xsi".equals(prefix)){
return "http://www.w3.org/2001/XMLSchema-instance";
}
return "";
}
@Override
public String getPrefix(String uri) {
if ("http://www.fedora.info/definitions/1/0/types/".equals(uri)) {
return "types";
}
if ("http://www.w3.org/2001/XMLSchema-instance".equals(uri)){
return "xsi";
}
return null;
}
@Override
public Iterator<String> getPrefixes(String uri) {
if("http://www.fedora.info/definitions/1/0/types/".equals(uri)) {
return PREFIXES.iterator();
}
if ("http://www.w3.org/2001/XMLSchema-instance".equals(uri)){
return XSI_PREFIXES.iterator();
}
return EMPTY.iterator();
}
}
}