/* 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 fedora.utilities.install.container;
import java.io.IOException;
import java.io.Serializable;
import java.io.Writer;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import fedora.server.config.webxml.ContextParam;
import fedora.server.config.webxml.Filter;
import fedora.server.config.webxml.FilterMapping;
import fedora.server.config.webxml.InitParam;
import fedora.server.config.webxml.SecurityConstraint;
import fedora.server.config.webxml.Servlet;
import fedora.server.config.webxml.ServletMapping;
import fedora.server.config.webxml.UserDataConstraint;
import fedora.server.config.webxml.WebResourceCollection;
import fedora.server.config.webxml.WebXML;
import fedora.utilities.install.InstallOptions;
/**
* Configures the web.xml for Fedora. This class does not create a complete
* web.xml document from scratch. It assumes that the constructor-provided
* webXML file has already defined the base set of servlets, servlet-mapping,
* etc. Specifically, we expect the web.xml located in
* fcrepo-webapp-fedora/main/webapp/WEB-INF/web.xml.
*
* @author Edwin Shin
*/
public class FedoraWebXML {
protected final String FEDORA_GENERATED =
"Fedora-generated security-constraint";
private final String CONFIDENTIAL = "CONFIDENTIAL";
private final String FILTER_AUTHN = "EnforceAuthnFilter";
private final String FILTER_RESTAPI = "RestApiAuthnFilter";
private final String FILTER_PEP = "PEPFilter";
private final String FILTER_PEP_CLASS = "melcoe.fedora.pep.rest.PEP";
private final String FILTER_SETUP = "SetupFilter";
private final String FILTER_XMLUSERFILE = "XmlUserfileFilter";
private final String FILTER_FINALIZE = "FinalizeFilter";
private final String FILTER_JAAS = "AuthFilterJAAS";
private final String[] FILTER_APIA_SERVLET_NAMES =
new String[] {"AccessServlet", "DescribeRepositoryServlet",
"FieldSearchServlet", "GetObjectHistoryServlet",
"ListDatastreamsServlet", "ListMethodsServlet",
"MethodParameterResolverServlet", "OAIProviderServlet",
"ReportServlet", "RISearchServlet"};
private final String[] FILTER_APIA_URL_PATTERNS =
new String[] {"/services/access"};
private final String[] FILTER_APIM_SERVLET_NAMES =
new String[] {"AxisServlet", "ControlServlet", "GetNextPIDServlet",
"UploadServlet"};
private final String[] FILTER_APIM_URL_PATTERNS =
new String[] {"/getDSAuthenticated", "/index.html",
"/services/management"};
private final String[] SC_APIA_URL_PATTERNS =
new String[] {"/", "/describe", "/get/*", "/getAccessParmResolver",
"/getObjectHistory/*", "/listDatastreams/*",
"/listMethods/*", "/oai", "/report", "/risearch",
"/search", "/services/access", "/wsdl", "*.jsp"};
private final String[] SC_APIM_URL_PATTERNS =
new String[] {"/index.html", "/getDSAuthenticated",
"/management/getNextPID", "/management/upload",
"/services/management", "*.jws"};
private final Map<String,String> FESL_SERVLET_MAPPINGS =
new HashMap<String,String>() {
private static final long serialVersionUID = 1L;
{put("UserServlet", "/user");
}
};
//FIXME for FeSL, what about UserServlet and /user url-pattern?
private final WebXMLOptions options;
private final WebXML fedoraWebXML;
public FedoraWebXML(String webXML, InstallOptions options) {
this(webXML, new WebXMLOptions(options));
}
/**
*
* @param webXML path to the webXML file
* @param options
*/
public FedoraWebXML(String webXML, WebXMLOptions options) {
this.options = options;
fedoraWebXML = fedora.server.config.webxml.WebXML.getInstance(webXML);
setFedoraHome();
setFilters();
setServletMappings();
setFilterMappings();
Collections.sort(fedoraWebXML.getFilterMappings(),
new FilterMappingComparator());
setSecurityConstraints();
}
private void setFilters() {
Filter f = new Filter();
f.setFilterName(FILTER_PEP);
f.setFilterClass(FILTER_PEP_CLASS);
if (options.requireFesl()) {
fedoraWebXML.addFilter(f);
} else {
fedoraWebXML.removeFilter(f);
}
}
/**
* Set the servlet-mappings.
*/
private void setServletMappings() {
if (options.requireFesl()) {
for (String servletName : FESL_SERVLET_MAPPINGS.keySet()) {
addServletMapping(servletName, FESL_SERVLET_MAPPINGS.get(servletName));
}
} else {
for (String servletName : FESL_SERVLET_MAPPINGS.keySet()) {
removeServletMapping(servletName, FESL_SERVLET_MAPPINGS.get(servletName));
}
}
}
/**
* Set the filter-mappings. The filter-mappings for APIM are always set.
*/
private void setFilterMappings() {
addFilterMappings(FILTER_APIM_SERVLET_NAMES, FILTER_APIM_URL_PATTERNS);
// AuthN filter for all REST API methods
FilterMapping fmAll = new FilterMapping();
fmAll.setFilterName(FILTER_AUTHN);
fmAll.addServletName("RestServlet");
// AuthN filter for REST API methods corresponding to API-M
FilterMapping fmAPIM = new FilterMapping();
fmAPIM.setFilterName(FILTER_RESTAPI);
fmAPIM.addServletName("RestServlet");
if (options.requireApiaAuth()) {
addFilterMappings(FILTER_APIA_SERVLET_NAMES,
FILTER_APIA_URL_PATTERNS);
fedoraWebXML.addFilterMapping(fmAll);
} else {
removeFilterMappings(FILTER_APIA_SERVLET_NAMES,
FILTER_APIA_URL_PATTERNS);
fedoraWebXML.addFilterMapping(fmAPIM);
}
// FeSL
if (options.requireFesl()) {
setFeslFilterMappings();
}
}
private void setSecurityConstraints() {
if (options.requireApimSSL()) {
addUserDataConstraint(SC_APIM_URL_PATTERNS);
} else {
removeUserDataConstraint(SC_APIM_URL_PATTERNS);
}
if (options.requireApiaSSL()) {
addUserDataConstraint(SC_APIA_URL_PATTERNS);
} else {
removeUserDataConstraint(SC_APIA_URL_PATTERNS);
}
}
private void addServletMapping(String servletName, String urlPattern) {
List<ServletMapping> servletMappings =
fedoraWebXML.getServletMappings();
for (ServletMapping servletMapping : servletMappings) {
if (servletMapping.getServletName().equals(servletName)) {
if (servletMapping.getUrlPatterns().contains(urlPattern)) {
return; // servlet-mapping already exists, no need to add
}
}
}
ServletMapping servletMapping = new ServletMapping();
servletMapping.setServletName(servletName);
servletMapping.addUrlPattern(urlPattern);
fedoraWebXML.addServletMapping(servletMapping);
}
/**
* Removes the servlet-mapping with the given servlet-name and url-pattern.
*
* @param servletName
* the servlet-name to match
* @param urlPattern
* the url-pattern to match (or null to match any)
*/
private void removeServletMapping(String servletName, String urlPattern) {
ServletMapping servletMapping;
Iterator<ServletMapping> servletMappings =
fedoraWebXML.getServletMappings().iterator();
while (servletMappings.hasNext()) {
servletMapping = servletMappings.next();
if (servletName == null || servletName.length() == 0
|| servletMapping.getServletName().equals(servletName)) {
if (urlPattern == null || urlPattern.length() == 0
|| servletMapping.getUrlPatterns().contains(urlPattern)) {
servletMappings.remove();
}
}
}
}
/**
* Adds a user-data-constraint with transport-guarantee CONFIDENTIAL to the
* security-constraint that contains <code>urlPatterns</code>. If an
* existing security-constraint contains a partial match against urlPatterns
* (i.e., a subset or a superset), the matching url-patterns will be removed
* and a new security-constraint containing urlPatterns will be created. If
* no security-constraint contains <code>urlPatterns</code>, a new
* security-constraint block will be created.
*
* @param urlPatterns
*/
private void addUserDataConstraint(String[] urlPatterns) {
Set<String> targetSet = new HashSet<String>(Arrays.asList(urlPatterns));
Set<String> candidateSet;
boolean hasUserDataConstraint = false;
Set<SecurityConstraint> removalSet = new HashSet<SecurityConstraint>();
for (SecurityConstraint sc : fedoraWebXML.getSecurityConstraints()) {
candidateSet = new HashSet<String>();
for (WebResourceCollection wrc : sc.getWebResourceCollections()) {
candidateSet.addAll(wrc.getUrlPatterns());
}
if (targetSet.equals(candidateSet)) {
if (!hasUserDataConstraint) {
if (sc.getUserDataConstraint() == null) {
sc
.setUserDataConstraint(new UserDataConstraint(CONFIDENTIAL));
} else if (sc.getUserDataConstraint()
.getTransportGuarantee() == null
|| !sc.getUserDataConstraint()
.getTransportGuarantee()
.equals(CONFIDENTIAL)) {
sc.getUserDataConstraint()
.setTransportGuarantee(CONFIDENTIAL);
}
hasUserDataConstraint = true;
} else {
removalSet.add(sc);
}
} else if (targetSet.containsAll(candidateSet)
|| candidateSet.containsAll(targetSet)) {
candidateSet.removeAll(targetSet);
if (candidateSet.isEmpty()) {
removalSet.add(sc);
}
}
}
for (SecurityConstraint sc : removalSet) {
fedoraWebXML.removeSecurityConstraint(sc);
}
if (!hasUserDataConstraint) {
WebResourceCollection wrc = new WebResourceCollection();
wrc.addDescription(FEDORA_GENERATED);
for (String urlPattern : targetSet) {
wrc.addUrlPattern(urlPattern);
}
SecurityConstraint sc = new SecurityConstraint();
sc.addWebResourceCollection(wrc);
sc.setUserDataConstraint(new UserDataConstraint(CONFIDENTIAL));
fedoraWebXML.addSecurityConstraint(sc);
}
}
/**
* Removes the user-data-constraint if the security-constraint contains
* <code>urlPatterns</code>.
*
* @param urlPatterns
* The array of url-patterns to match.
*/
private void removeUserDataConstraint(String[] urlPatterns) {
List<String> up = Arrays.asList(urlPatterns);
scLoop: for (SecurityConstraint sc : fedoraWebXML
.getSecurityConstraints()) {
for (WebResourceCollection wrc : sc.getWebResourceCollections()) {
if (wrc.getUrlPatterns().containsAll(up)) {
sc.setUserDataConstraint(null);
break scLoop;
}
}
}
}
private void addFilterMappings(String[] servletNames, String[] urlPatterns) {
Set<String> servlets = new HashSet<String>(Arrays.asList(servletNames));
Set<String> urls = new HashSet<String>(Arrays.asList(urlPatterns));
for (FilterMapping fMap : fedoraWebXML.getFilterMappings()) {
if (fMap.getFilterName().equals(FILTER_AUTHN)) {
for (String servletName : fMap.getServletNames()) {
servlets.remove(servletName);
}
for (String urlPattern : fMap.getUrlPatterns()) {
urls.remove(urlPattern);
}
}
}
for (String servletName : servlets) {
FilterMapping fm = new FilterMapping();
fm.setFilterName(FILTER_AUTHN);
fm.addServletName(servletName);
fedoraWebXML.addFilterMapping(fm);
}
for (String urlPattern : urls) {
FilterMapping fm = new FilterMapping();
fm.setFilterName(FILTER_AUTHN);
fm.addUrlPattern(urlPattern);
fedoraWebXML.addFilterMapping(fm);
}
}
private void removeFilterMappings(String[] servletNames,
String[] urlPatterns) {
Set<String> servlets = new HashSet<String>(Arrays.asList(servletNames));
Set<String> urls = new HashSet<String>(Arrays.asList(urlPatterns));
fedora.server.config.webxml.WebXML fedoraWebXML =
fedora.server.config.webxml.WebXML.getInstance();
for (FilterMapping fMap : fedoraWebXML.getFilterMappings()) {
if (fMap.getFilterName().equals(FILTER_AUTHN)) {
for (String servletName : fMap.getServletNames()) {
if (servlets.contains(servletName)) {
fMap.removeServletName(servletName);
}
}
for (String urlPattern : fMap.getUrlPatterns()) {
if (urls.contains(urlPattern)) {
fMap.removeUrlPattern(urlPattern);
}
}
if (fMap.getServletNames().size() == 0
&& fMap.getUrlPatterns().size() == 0) {
fedoraWebXML.removeFilterMapping(fMap);
}
}
}
}
/**
* Set the filter mappings required by FeSL.
* This involves replacing the legacy policy enforcement filter, FILTER_AUTHN,
* with PEP_FILTER as well as removing some unneeded filters and adding the
* FILTER_JAAS filter.
* It is assumed that the actual servlets or url-patterns that need filter
* mapping have already been declared previously, with the exception of
* FILTER_JAAS.
*/
private void setFeslFilterMappings() {
Collection<String> toDelete = new HashSet<String>();
toDelete.add(FILTER_SETUP);
toDelete.add(FILTER_XMLUSERFILE);
toDelete.add(FILTER_FINALIZE);
Collection<String> toReplace = new HashSet<String>();
toReplace.add(FILTER_AUTHN);
toReplace.add(FILTER_RESTAPI);
String filterName;
FilterMapping fMap;
Iterator<FilterMapping> filterMappings =
fedoraWebXML.getFilterMappings().iterator();
while (filterMappings.hasNext()) {
fMap = filterMappings.next();
filterName = fMap.getFilterName();
if (toReplace.contains(filterName)) {
fMap.setFilterName(FILTER_PEP);
} else if (toDelete.contains(filterName)) {
filterMappings.remove();
}
}
fMap = new FilterMapping();
fMap.setFilterName(FILTER_JAAS);
fMap.addUrlPattern("/*");
fedoraWebXML.addFilterMapping(fMap);
}
/**
* Sets all context-param/param-value and init-param/param-value elements
* where param-name=fedora.home
*/
private void setFedoraHome() {
for (Servlet servlet : fedoraWebXML.getServlets()) {
for (InitParam param : servlet.getInitParams()) {
if (param.getParamName().equals("fedora.home")) {
param.setParamValue(options.getFedoraHome()
.getAbsolutePath());
}
}
}
for (ContextParam contextParam : fedoraWebXML.getContextParams()) {
if (contextParam.getParamName().equals("fedora.home")) {
contextParam.setParamValue(options.getFedoraHome()
.getAbsolutePath());
}
}
}
public void write(Writer outputWriter) throws IOException {
fedoraWebXML.write(outputWriter);
}
/**
* Ensures that SETUP_FILTER is first, followed by XMLUSERFILE_FILTER, and
* FINALIZE_FILTER is last.
*
* @author Edwin Shin
*/
class FilterMappingComparator
implements Comparator<FilterMapping>, Serializable {
private static final long serialVersionUID = 1L;
private static final String SETUP_FILTER = "SetupFilter";
private static final String XMLUSERFILE_FILTER = "XmlUserfileFilter";
private static final String FINALIZE_FILTER = "FinalizeFilter";
private static final String WILDCARD_URL_PATTERN = "/*";
public int compare(FilterMapping fm1, FilterMapping fm2) {
String fn1 = fm1.getFilterName();
String fn2 = fm2.getFilterName();
List<String> sn1 = fm1.getServletNames();
List<String> sn2 = fm2.getServletNames();
List<String> up1 = fm1.getUrlPatterns();
List<String> up2 = fm2.getUrlPatterns();
// SETUP_FILTER goes first
if (fn1.equals(SETUP_FILTER) && !up1.isEmpty()
&& up1.get(0).equals(WILDCARD_URL_PATTERN)) {
return -1;
}
if (fn2.equals(SETUP_FILTER) && !up2.isEmpty()
&& up2.get(0).equals(WILDCARD_URL_PATTERN)) {
return 1;
}
// XMLUSERFILE_FILTER goes second
if (fn1.equals(XMLUSERFILE_FILTER) && !up1.isEmpty()
&& up1.get(0).equals(WILDCARD_URL_PATTERN)) {
return -1;
}
if (fn2.equals(XMLUSERFILE_FILTER) && !up2.isEmpty()
&& up2.get(0).equals(WILDCARD_URL_PATTERN)) {
return 1;
}
// FINALIZE_FILTER goes last
if (fn1.equals(FINALIZE_FILTER) && !up1.isEmpty()
&& up1.get(0).equals(WILDCARD_URL_PATTERN)) {
return 1;
}
if (fn2.equals(FINALIZE_FILTER) && !up2.isEmpty()
&& up2.get(0).equals(WILDCARD_URL_PATTERN)) {
return -1;
}
// Other WILDCARD_URL_PATTERN filter-mappings start at 3rd place
if (!up1.isEmpty() && up1.get(0).equals(WILDCARD_URL_PATTERN)
&& !up2.isEmpty()
&& up2.get(0).equals(WILDCARD_URL_PATTERN)) {
return fn1.compareTo(fn2);
}
if (!up1.isEmpty() && up1.get(0).equals(WILDCARD_URL_PATTERN)) {
return -1;
}
if (!up2.isEmpty() && up2.get(0).equals(WILDCARD_URL_PATTERN)) {
return 1;
}
int c = fn1.compareTo(fn2);
if (c != 0) {
return c;
} else {
// i.e., we put filter-mappings with servlet-names ahead of
// filter-mappings with url-patterns
if (!sn1.isEmpty() && sn2.isEmpty()) {
return -1;
}
if (sn1.isEmpty() && !sn2.isEmpty()) {
return 1;
}
if (!sn1.isEmpty() && !sn2.isEmpty()) {
return sn1.get(0).compareToIgnoreCase(sn2.get(0));
}
if (!up1.isEmpty() && !up2.isEmpty()) {
return up1.get(0).compareToIgnoreCase(up2.get(0));
}
}
if (fm1.equals(fm2)) {
return 0;
}
return fn1.compareTo(fn2);
}
}
}