/*
* File: PEP.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 melcoe.fedora.pep.rest;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import melcoe.fedora.pep.AuthzDeniedException;
import melcoe.fedora.pep.ContextHandler;
import melcoe.fedora.pep.ContextHandlerImpl;
import melcoe.fedora.pep.PEPException;
import melcoe.fedora.pep.rest.filters.DataResponseWrapper;
import melcoe.fedora.pep.rest.filters.ParameterRequestWrapper;
import melcoe.fedora.pep.rest.filters.RESTFilter;
import org.apache.log4j.Logger;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import com.sun.xacml.ctx.RequestCtx;
import com.sun.xacml.ctx.ResponseCtx;
import com.sun.xacml.ctx.Result;
import fedora.common.Constants;
/**
* This is the PEP for the REST interface.
*
* @author nishen@melcoe.mq.edu.au
*/
public final class PEP
implements Filter {
private static Logger log = Logger.getLogger(PEP.class.getName());
private FilterConfig filterConfig = null;
private Map<String, RESTFilter> filters = null;
private ContextHandler ctxHandler = null;
/*
* (non-Javadoc)
* @see javax.servlet.Filter#doFilter(javax.servlet.ServletRequest,
* javax.servlet.ServletResponse, javax.servlet.FilterChain)
*/
public void doFilter(ServletRequest request,
ServletResponse response,
FilterChain chain) throws IOException,
ServletException {
// if a response has already been committed, bypass this filter...
if (response.isCommitted()) {
if (log.isDebugEnabled()) {
log
.debug("Response has already been committed. Bypassing PEP.");
}
// continuing the chain once auth has failed causes errors. Short
// circuiting the path here.
// remove this if it causes problems
// chain.doFilter(request, response);
return;
}
// Need to make sure we are dealing with HttpServlets
if (!(request instanceof HttpServletRequest)
|| !(response instanceof HttpServletResponse)) {
log.error("Servlets are not HttpServlets!");
throw new ServletException("Servlets are not HttpServlets!");
}
ServletOutputStream out = null;
ParameterRequestWrapper req = null;
DataResponseWrapper res = null;
// the request and response context
RequestCtx reqCtx = null;
ResponseCtx resCtx = null;
String uri = ((HttpServletRequest) request).getRequestURI();
String servletPath = ((HttpServletRequest) request).getServletPath();
if (log.isDebugEnabled()) {
log.debug("Incoming URI: " + uri);
log.debug("Incoming servletPath: " + servletPath);
}
// get the filter (or null if no filter)
RESTFilter filter = getFilter(servletPath);
try {
// handle the request if we have a filter
if (filter != null) {
// get a handle for the original OutputStream
out = response.getOutputStream();
// substitute our own request object that manages parameters
try {
req =
new ParameterRequestWrapper((HttpServletRequest) request);
} catch (Exception e) {
throw new PEPException(e);
}
// substitute our own response object that captures the data
res = new DataResponseWrapper(((HttpServletResponse) response));
if (log.isDebugEnabled()) {
log.debug("Filtering URI: [" + req.getRequestURI()
+ "] with: [" + filter.getClass().getName() + "]");
}
reqCtx = filter.handleRequest(req, res);
if (reqCtx != null) {
resCtx = ctxHandler.evaluate(reqCtx);
enforce(resCtx);
}
// pass the request along to the next chain...
chain.doFilter(req, res);
} else {
// no filter, just use the original request/response...
chain.doFilter(request, response);
}
// handle the response if we have a filter
if (filter != null) {
reqCtx = filter.handleResponse(req, res);
if (reqCtx != null) {
resCtx = ctxHandler.evaluate(reqCtx);
enforce(resCtx);
}
out.write(res.getData());
out.flush();
out.close();
}
} catch (AuthzDeniedException ae) {
if (!res.isCommitted()
&& (req.getRemoteUser() == null || "".equals(req
.getRemoteUser().trim()))) {
loginForm(res);
} else {
denyAccess((HttpServletResponse) response, ae.getMessage());
}
} catch (PEPException pe) {
throw new ServletException("Error evaluating request", pe);
}
}
/*
* (non-Javadoc)
* @see javax.servlet.Filter#init(javax.servlet.FilterConfig)
*/
public void init(FilterConfig filterCfg) throws ServletException {
try {
ctxHandler = ContextHandlerImpl.getInstance();
} catch (PEPException pe) {
log.error("Error obtaining ContextHandler", pe);
throw new ServletException("Error obtaining ContextHandler", pe);
}
log.info("Initialising Servlet Filter: " + PEP.class.getName());
filterConfig = filterCfg;
// exit if no config. Should always have a config.
if (filterConfig == null) {
log.error("No config found!");
throw new ServletException("No config found for filter (filterConfig)");
}
loadFilters();
}
/*
* (non-Javadoc)
* @see javax.servlet.Filter#destroy()
*/
public void destroy() {
log.info("Destroying Servlet Filter: " + PEP.class.getName());
filterConfig = null;
filters = null;
ctxHandler = null;
}
private void loadFilters() throws ServletException {
filters = new HashMap<String, RESTFilter>();
try {
// get the PEP configuration
File configPEPFile =
new File(Constants.FEDORA_HOME,
"server/config/config-melcoe-pep.xml");
InputStream is = new FileInputStream(configPEPFile);
if (is == null) {
throw new PEPException("Could not locate config file: config-melcoe-pep.xml");
}
DocumentBuilderFactory factory =
DocumentBuilderFactory.newInstance();
DocumentBuilder docBuilder = factory.newDocumentBuilder();
Document doc = docBuilder.parse(is);
Node node = doc.getElementsByTagName("handlers-rest").item(0);
NodeList nodes = node.getChildNodes();
for (int x = 0; x < nodes.getLength(); x++) {
Node n = nodes.item(x);
if (n.getNodeType() == Node.ELEMENT_NODE
&& "handler".equals(n.getNodeName())) {
String opn =
n.getAttributes().getNamedItem("operation")
.getNodeValue();
String cls =
n.getAttributes().getNamedItem("class")
.getNodeValue();
if (opn == null || "".equals(opn)) {
throw new PEPException("Cannot have a missing or empty operation attribute");
}
if (cls == null || "".equals(cls)) {
throw new PEPException("Cannot have a missing or empty class attribute");
}
try {
Class<?> filterClass = Class.forName(cls);
RESTFilter filter =
(RESTFilter) filterClass.newInstance();
filters.put(opn, filter);
if (log.isDebugEnabled()) {
log.debug("filter added to filter map: " + opn
+ "/" + cls);
}
} catch (ClassNotFoundException e) {
if (log.isDebugEnabled()) {
log.debug("filterClass not found for: " + cls);
}
} catch (InstantiationException ie) {
log.error("Could not instantiate filter: " + cls);
throw new ServletException(ie.getMessage(), ie);
} catch (IllegalAccessException iae) {
log.error("Could not instantiate filter: " + cls);
throw new ServletException(iae.getMessage(), iae);
}
}
}
} catch (Exception e) {
log.fatal("Failed to initialse the PEP for REST");
log.fatal(e.getMessage(), e);
throw new ServletException(e.getMessage(), e);
}
}
/**
* Obtains a filter from the filter map. If the filter does not exist in the
* filter map, then an attempt to create the required filter is made and if
* successful it is added to the filter map.
*
* @param servletPath
* the servletPath of incoming servlet request
* @return the filter to use
* @throws ServletException
*/
private RESTFilter getFilter(String servletPath) throws ServletException {
RESTFilter filter = filters.get(servletPath);
if (filter != null && log.isDebugEnabled())
log.debug("obtaining filter: " + filter.getClass().getName());
return filter;
}
/**
* Enforces a decision returned from the PDP.
*
* @param res
* the XACML response
* @throws AuthzDeniedException
*/
private void enforce(ResponseCtx res) throws AuthzDeniedException {
@SuppressWarnings("unchecked")
Set<Result> results = res.getResults();
for (Result r : results) {
if (r.getDecision() != Result.DECISION_PERMIT) {
log.debug("Denying access: " + r.getDecision());
switch (r.getDecision()) {
case Result.DECISION_DENY:
throw new AuthzDeniedException("Deny");
case Result.DECISION_INDETERMINATE:
throw new AuthzDeniedException("Indeterminate");
case Result.DECISION_NOT_APPLICABLE:
throw new AuthzDeniedException("NotApplicable");
default:
}
}
}
log.debug("Permitting access!");
}
/**
* Outputs an access denied message.
*
* @param out
* the output stream to send the message to
* @param message
* the message to send
*/
private void denyAccess(HttpServletResponse response, String message)
throws IOException {
StringBuilder sb = new StringBuilder();
sb.append("Fedora: 403 " + message.toUpperCase());
response.reset();
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.setContentType("text/plain");
response.setContentLength(sb.length());
ServletOutputStream out = response.getOutputStream();
out.write(sb.toString().getBytes());
out.flush();
out.close();
}
/**
* Sends a 401 error to the browser. This forces a login screen to be
* displayed allowing the user to login.
*
* @param response
* the response to set the headers and status
*/
private void loginForm(HttpServletResponse response) {
response.reset();
response.addHeader("WWW-Authenticate",
"Basic realm=\"!!Fedora Repository Server\"");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
}
}