--- /dev/null
+/*
+ * ServiceProviderConfig.java
+ *
+ * A ServiceProviderConfig object holds an instance of the Shibboleth
+ * configuration data from the main configuration file and from all
+ * secondary files referenced by the main file.
+ *
+ * The configuration file is typically processed during Context
+ * initialization. In a Servlet environment, this is done from
+ * the ServletContextInitializer, while in JUnit testing it is
+ * done during test setup (unless you are testing configuration
+ * in which case it is part of the test itself). This occurs
+ * during init() processing and is inheritly synchronized.
+ *
+ * Initialization is a two step process. First create an
+ * instance of this class, then find a path to the configuration
+ * file and call loadConfigObjects().
+ *
+ * In addition to the option of loading external classes that
+ * implement one of the Plugin service interfaces by providing
+ * the name of the class in the type= attribute of the Plugin
+ * configuration XML, there is also a manual wiring interface.
+ * Create an implimenting object yourself, then add it to the
+ * configuration by passing an identifying URI and the object
+ * to a addOrReplaceXXXImplementation() method.
+ *
+ * These wiring calls are agressive, while the XML is passive.
+ * If the wiring call is made before loadConfigObject(), then
+ * XML referencing this same URI will find that it has already
+ * been loaded and use it. Alternately, if the call is made
+ * after loadConfigObject() then the XML will have processed
+ * the URI, loaded the file, and built an implimenting object.
+ * However, the wiring call will replace that object with the
+ * one provided in the call. Obviously making these calls
+ * first will be slightly more efficient, and is necessary if
+ * the XML configuration specifies URIs that will be provided
+ * by the wiring call and are not represented by any actual file.
+ *
+ * After initialization completes, this object and its arrays
+ * and collections should be structurally immutable. A map or
+ * array should not be changed by adding or removing members.
+ * Thus iteration over the collections can be unsynchronized.
+ * The reference to a value in the map can be swapped for a
+ * new object, because this doesn't effect iteration.
+ *
+ * Any method may obtain a copy of the current ServiceProviderConfig
+ * object by calling ServiceProviderContext.getServiceProviderConfig().
+ * This reference should only be held locally (for the duration
+ * of the request). Should the entire Shibboleth configuration file
+ * be reparsed (because of a dynamic update), then a new reference will
+ * be stored in the SPContext. Picking up a new reference for each
+ * request ensures that a program uses the latest configuration.
+ *
+ * When a secondary file (Metadata, Trust, AAP, etc.) is reloaded,
+ * a new object is constructed for that interface and the entry in
+ * the corresponding Map of providers of that interface is replaced.
+ * Therefore, non-local variables must only store the URI for such
+ * objects. A method can pass the URI to the Map lookup method and
+ * obtain a local variable reference to the current implementing
+ * object which can be used during the processing of the current
+ * request.
+ *
+ * Note: The URI for a secondary file cannot change just by
+ * reloading the file, but it can change if this main configuration
+ * file object is rebuilt. Therefore, if an external object stores
+ * a URI for a plugin object, it must be prepared for the Map lookup
+ * to return null. This would indicate that the main configuration
+ * file has been reloaded and the previously valid URI now no longer
+ * points to any implementing object.
+ *
+ * XML configuration data is parsed into two formats. First, it
+ * is processed by an ordinary JAXP XML parser into a W3C DOM format.
+ * The parser may validate the XML against an XSD schema, but the
+ * resulting DOM is "untyped". The XML becomes a tree of Element,
+ * Attribute, and Text nodes. The program must still convert an
+ * attribute or text string to a number, date, boolean, or any other
+ * data type even through the XSD declares that it must be of that
+ * type. The program must also search through the elements of the tree
+ * to find specific names for expected contents.
+ *
+ * This module then subjects the DOM to a secondary parse through
+ * some classes generated by compiling the XSD file with tools
+ * provided by the Apache XML Bean project. This turns the valid
+ * XML into strongly typed Java objects. A class is generated to
+ * represent every data type defined in the XSD schemas. Attribute
+ * values and child elements become properties of the objects of
+ * these classes. The XMLBean objects simplify the configuration
+ * logic.
+ *
+ * If the configuration file directly reflected the program logic,
+ * the XML Bean classes would probably be enough. However, there
+ * are two important considerations:
+ *
+ * First, the Metadata is in transition. Currently we support an
+ * ad-hoc format defined by Shibboleth. However, it is expected
+ * that this will change in the next release to support a new
+ * standard accompanying SAML 2.0. The program logic anticipates
+ * this change, and is largely designed around concepts and
+ * structures of the new SAML standard. The actual configuration
+ * file and XML Beans support the old format, which must be mapped
+ * into this new structure.
+ *
+ * Second, secondary configuration elements (Credentials, Trust,
+ * Metadata, AAP, etc.) are "Pluggable" components. There is a
+ * built-in implementation of these services based on the XML
+ * configuration described in the Shibboleth documentation.
+ * However, the administrator can provide other classes that
+ * implement the required interfaces by simply coding the class
+ * name in the type= parameter of the XML element referencing the
+ * plugin. In this case we don't know the format of the opaque
+ * XML and simply pass it to the plugin.
+ *
+ *
+ * Dependencies: Requires XML Beans and the generated classes.
+ * Requires access to XSD schema files for configuration file formats.
+ * Logic depends on the order of enums in the XSD files.
+ *
+ * Error Strategy: A failure parsing the main configuration file
+ * prevents further processing. However, a problem parsing a plug-in
+ * configuration file should be noted while processing continues.
+ * This strategy reports all the errors in all the files to the log
+ * rather than stopping at the first error.
+ *
+ * --------------------
+ * Copyright 2002, 2004
+ * University Corporation for Advanced Internet Development, Inc.
+ * All rights reserved
+ * [Thats all we have to say to protect ourselves]
+ * Your permission to use this code is governed by "The Shibboleth License".
+ * A copy may be found at http://shibboleth.internet2.edu/license.html
+ * [Nothing in copyright law requires license text in every file.]
+ */
+
+package edu.internet2.middleware.shibboleth.serviceprovider;
+
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.TreeMap;
+
+import org.apache.log4j.Logger;
+import org.apache.xerces.parsers.DOMParser;
+import org.apache.xmlbeans.XmlException;
+import org.apache.xmlbeans.XmlOptions;
+import org.opensaml.SAMLAssertion;
+import org.opensaml.SAMLAttribute;
+import org.opensaml.SAMLAttributeStatement;
+import org.opensaml.SAMLObject;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.xml.sax.ErrorHandler;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+import org.xml.sax.SAXParseException;
+
+import x0.maceShibboleth1.AttributeAcceptancePolicyDocument;
+import x0.maceShibbolethTargetConfig1.ApplicationDocument;
+import x0.maceShibbolethTargetConfig1.PluggableType;
+import x0.maceShibbolethTargetConfig1.RequestMapDocument;
+import x0.maceShibbolethTargetConfig1.ShibbolethTargetConfigDocument;
+import x0.maceShibbolethTargetConfig1.ApplicationDocument.Application;
+import x0.maceShibbolethTargetConfig1.ApplicationsDocument.Applications;
+import x0.maceShibbolethTargetConfig1.HostDocument.Host;
+import x0.maceShibbolethTargetConfig1.PathDocument.Path;
+import x0.maceShibbolethTargetConfig1.SHIREDocument.SHIRE;
+import x0.maceShibbolethTargetConfig1.ShibbolethTargetConfigDocument.ShibbolethTargetConfig;
+import edu.internet2.middleware.shibboleth.common.AAP;
+import edu.internet2.middleware.shibboleth.common.AttributeRule;
+import edu.internet2.middleware.shibboleth.common.Credentials;
+import edu.internet2.middleware.shibboleth.common.ShibResource;
+import edu.internet2.middleware.shibboleth.common.ShibbolethConfigurationException;
+import edu.internet2.middleware.shibboleth.common.XML;
+import edu.internet2.middleware.shibboleth.metadata.EntityDescriptor;
+import edu.internet2.middleware.shibboleth.metadata.EntityLocator;
+import edu.internet2.middleware.shibboleth.metadata.Metadata;
+import edu.internet2.middleware.shibboleth.metadata.Provider;
+import edu.internet2.middleware.shibboleth.metadata.ProviderRole;
+
+/**
+ * Load the configuration files into objects, index them, and return them on request.
+ *
+ * <p>A new instance of the ServiceProviderConfig object can be created to
+ * parse a new version of the configuration file. It can then be swapped
+ * into the ServiceProviderContext reference and will be picked up by
+ * subsequent requests.</p>
+ *
+ * @author Howard Gilbert
+ */
+public class ServiceProviderConfig {
+
+
+ private static final String INLINEURN = "urn:inlineBS:ID";
+ private static Logger log = Logger.getLogger(ServiceProviderConfig.class);
+
+ private ShibbolethTargetConfig // The XMLBean from the main config file
+ config = null; // (i.e. shibboleth.xml)
+
+
+ /*
+ * The following Maps reference objects that implement a plugin
+ * interface indexed by their URI. There are builtin objects
+ * created from inline or external XML files, but external
+ * objects implementing the interfaces may be injected by
+ * calling the addOrReplaceXXX method. Public access to these
+ * Maps is indirect, through methods the ApplicationInfo object
+ * for a given configured or default application.
+ */
+
+ // Note EntityLocator extends and renames the old "Metadata" interface
+ private Map/*<String, EntityLocator>*/ entityLocators =
+ new TreeMap/*<String, EntityLocator>*/();
+
+ public void addOrReplaceMetadataImplementor(String uri, EntityLocator m) {
+ entityLocators.put(uri, m);
+ }
+
+ public EntityLocator getMetadataImplementor(String uri) {
+ return (EntityLocator) entityLocators.get(uri);
+ }
+
+ private Map/*<String, AAP>*/ attributePolicies =
+ new TreeMap/*<String, AAP>*/();
+
+ public void addOrReplaceAAPImplementor(String uri, AAP a) {
+ attributePolicies.put(uri,a);
+ }
+
+ public AAP getAAPImplementor(String uri) {
+ return (AAP) attributePolicies.get(uri);
+ }
+
+ private Map/*<String, ITrust>*/ certificateValidators =
+ new TreeMap/*<String, ITrust>*/();
+
+ public void addOrReplaceTrustImplementor(String uri, ITrust t) {
+ certificateValidators.put(uri,t);
+ }
+
+ public ITrust getTrustImplementor(String uri) {
+ return (ITrust) certificateValidators.get(uri);
+ }
+
+
+ /*
+ * Objects created from the <Application(s)> elements.
+ * They manage collections of URI-Strings that index the
+ * previous maps to return Metadata, Trust, and AAP info
+ * applicable to this applicationId.
+ */
+ private Map/*<String, ApplicationInfo>*/ applications =
+ new TreeMap/*<String, ApplicationInfo>*/();
+
+ // Default application info from <Applications> element
+ private ApplicationInfo defaultApplicationInfo = null;
+
+ public ApplicationInfo getApplication(String applicationId) {
+ ApplicationInfo app=null;
+ app = (ApplicationInfo) applications.get(applicationId);
+ if (app==null) // If no specific match, return default
+ return defaultApplicationInfo;
+ return app;
+ }
+
+
+ // Objects created from single configuration file elements
+ private Credentials credentials = null;
+ private RequestMapDocument.RequestMap requestMap = null;
+
+
+ /*
+ * A few constants
+ */
+ private final String SCHEMADIR = "/schemas/";
+ private final String MAINSCHEMA = SCHEMADIR + XML.MAIN_SHEMA_ID;
+ private final String METADATASCHEMA = SCHEMADIR + XML.SHIB_SCHEMA_ID;
+ private final String TRUSTSCHEMA = SCHEMADIR + XML.TRUST_SCHEMA_ID;
+ private final String AAPSCHEMA = SCHEMADIR + XML.SHIB_SCHEMA_ID;
+
+ private static final String XMLTRUSTPROVIDERTYPE =
+ "edu.internet2.middleware.shibboleth.common.provider.XMLTrust";
+ private static final String XMLAAPPROVIDERTYPE =
+ "edu.internet2.middleware.shibboleth.target.provider.XMLAAP";
+ private static final String XMLFEDERATIONPROVIDERTYPE =
+ "edu.internet2.middleware.shibboleth.common.provider.XMLMetadata";
+ private static final String XMLREVOCATIONPROVIDERTYPE =
+ "edu.internet2.middleware.shibboleth.common.provider.XMLRevocation";
+ private static final String XMLREQUESTMAPPROVIDERTYPE =
+ "edu.internet2.middleware.shibboleth.target.provider.XMLRequestMap";
+ private static final String XMLCREDENTIALSPROVIDERTYPE =
+ "edu.internet2.middleware.shibboleth.common.Credentials";
+
+
+
+ private DOMParser parser = new DOMParser();
+
+ /**
+ * The constructor prepares for, but does not parse the configuration.
+ *
+ * @throws ShibbolethConfigurationException
+ * if XML Parser cannot be initialized (Classpath problem)
+ */
+ public ServiceProviderConfig()
+ throws ShibbolethConfigurationException {
+ try {
+ parser.setFeature("http://xml.org/sax/features/validation", true);
+ parser.setFeature("http://apache.org/xml/features/validation/schema", true);
+
+
+ parser.setErrorHandler(new ErrorHandler() {
+
+ public void error(SAXParseException arg0) throws SAXException {
+ throw new SAXException("Error parsing xml file: " + arg0);
+ }
+
+ public void fatalError(SAXParseException arg0) throws SAXException {
+ throw new SAXException("Error parsing xml file: " + arg0);
+ }
+
+ public void warning(SAXParseException arg0) throws SAXException {
+ throw new SAXException("Error parsing xml file: " + arg0);
+ }
+ });
+
+ } catch (SAXException e) {
+ log.error("Unable to setup a workable XML parser: " + e);
+ throw new ShibbolethConfigurationException("Unable to setup a workable XML parser.");
+ }
+ }
+
+ /**
+ * loadConfigObjects must be called once to parse the configuration.
+ *
+ * <p>To reload a modified configuration file, create and load a second
+ * object and swap the reference in the context object.</p>
+ *
+ * @param configFilePath URL or resource name of file
+ * @return the DOM Document
+ * @throws ShibbolethConfigurationException
+ * if there was an error loading the file
+ */
+ public synchronized void loadConfigObjects(String configFilePath)
+ throws ShibbolethConfigurationException {
+
+ if (config!=null) {
+ log.error("ServiceProviderConfig.loadConfigObjects may not be called twice for the same object.");
+ throw new ShibbolethConfigurationException("Cannot reload configuration into same object.");
+ }
+
+ Document configDoc;
+ try {
+ configDoc = loadDom(configFilePath, MAINSCHEMA);
+ } catch (InternalConfigurationException e) {
+ throw new ShibbolethConfigurationException("XML error in "+configFilePath);
+ }
+ loadConfigBean(configDoc);
+
+ return;
+ }
+
+ /*
+ * Given a URL, determine its ApplicationId from the RequestMap config.
+ *
+ * <p>Note: This is not a full implementation of all RequestMap
+ * configuration options. Other functions will be added as needed.</p>
+ */
+ public String mapRequest(String urlreq) {
+ String applicationId = "default";
+ URL url;
+
+ try {
+ url = new URL(urlreq);
+ } catch (MalformedURLException e) {
+ return applicationId;
+ }
+
+ String urlscheme = url.getProtocol();
+ String urlhostname = url.getHost();
+ String urlpath = url.getPath();
+ int urlport = url.getPort();
+ if (urlport==0) {
+ if (urlscheme.equals("http"))
+ urlport=80;
+ else if (urlscheme.equals("https"))
+ urlport=443;
+ }
+
+ // find Host entry for this virtual server
+ Host[] hostArray = requestMap.getHostArray();
+ for (int ihost=0;ihost<hostArray.length;ihost++) {
+ Host host = hostArray[ihost];
+ String hostScheme = host.getScheme().toString();
+ String hostName = host.getName();
+ String hostApplicationId = host.getApplicationId();
+ long hostport = host.getPort();
+ if (hostport==0) {
+ if (hostScheme.equals("http"))
+ hostport=80;
+ else if (hostScheme.equals("https"))
+ hostport=443;
+ }
+
+ if (!urlscheme.equals(hostScheme) ||
+ !urlhostname.equals(hostName)||
+ urlport!=hostport)
+ continue;
+
+ // find Path entry for this subdirectory
+ Path[] pathArray = host.getPathArray();
+ if (hostApplicationId!=null)
+ applicationId=hostApplicationId;
+ for (int i=0;i<pathArray.length;i++) {
+ String dirname = pathArray[i].getName();
+ if (urlpath.equals(dirname)||
+ urlpath.startsWith(dirname+"/")){
+ String pthid= pathArray[i].getApplicationId();
+ if (pthid!=null)
+ applicationId=pthid;
+ }
+ }
+ }
+
+ return applicationId;
+ }
+
+ /**
+ * <p>Parse the main configuration file DOM into XML Bean</p>
+ *
+ * <p>Automatically load secondary configuration files designated
+ * by URLs in the main configuration file</p>
+ *
+ * @throws ShibbolethConfigurationException
+ */
+ private void loadConfigBean(Document configDoc)
+ throws ShibbolethConfigurationException {
+ boolean anyError=false;
+ ShibbolethTargetConfigDocument configBeanDoc;
+ try {
+ // reprocess the already validated DOM to create a bean with typed fields
+ // dump the trash (comments, processing instructions, extra whitespace)
+ configBeanDoc = ShibbolethTargetConfigDocument.Factory.parse(configDoc,
+ new XmlOptions().setLoadStripComments().setLoadStripProcinsts().setLoadStripWhitespace());
+ config=configBeanDoc.getShibbolethTargetConfig();
+ } catch (XmlException e) {
+ // Since the DOM was already validated against the schema, errors will not typically occur here
+ log.error("Error while parsing shibboleth configuration");
+ throw new ShibbolethConfigurationException("Error while parsing shibboleth configuration");
+ }
+
+ // Extract the "root Element" object from the "Document" object
+ ShibbolethTargetConfig config = configBeanDoc.getShibbolethTargetConfig();
+
+ Applications apps = config.getApplications(); // <Applications>
+
+
+
+ /*
+ * Create an <Application> id "default" from <Applications>
+ */
+ ApplicationDocument defaultAppDoc =
+ // Create a new XMLBeans "Document" level object
+ ApplicationDocument.Factory.newInstance();
+ ApplicationDocument.Application defaultApp =
+ // Add an XMLBeans "root Element" object to the Document
+ defaultAppDoc.addNewApplication();
+ // set or copy over fields from unrelated Applications object
+ defaultApp.setId("default");
+ defaultApp.setAAPProviderArray(apps.getAAPProviderArray());
+ defaultApp.setAttributeDesignatorArray(apps.getAttributeDesignatorArray());
+ defaultApp.setAudienceArray(apps.getAudienceArray());
+ defaultApp.setCredentialUse(apps.getCredentialUse());
+ defaultApp.setErrors(apps.getErrors());
+ defaultApp.setFederationProviderArray(apps.getFederationProviderArray());
+ defaultApp.setProviderId(apps.getProviderId());
+ defaultApp.setRevocationProviderArray(apps.getRevocationProviderArray());
+ defaultApp.setSessions(apps.getSessions());
+ defaultApp.setSignedAssertions(apps.getSignedAssertions());
+ defaultApp.setSignedResponse(apps.getSignedResponse());
+ defaultApp.setSignRequest(apps.getSignRequest());
+ defaultApp.setTrustProviderArray(apps.getTrustProviderArray());
+
+ /*
+ * Now process secondary files configured in the applications
+ */
+ anyError |= processApplication(defaultApp);
+
+ Application[] apparray = apps.getApplicationArray();
+ for (int i=0;i<apparray.length;i++){
+ Application tempapp = apparray[i];
+ applications.put(tempapp.getId(),tempapp);
+ anyError |= processApplication(tempapp);
+ }
+
+ /*
+ * Now process other secondary files
+ */
+ anyError |= processCredentials();
+ anyError |= processPluggableRequestMapProvider();
+
+ if (anyError)
+ throw new ShibbolethConfigurationException("Errors processing configuration file, see log");
+ }
+
+
+ /**
+ * Routine to handle CredentialProvider
+ *
+ * <p>Note: This only handles in-line XML.
+ * Also, Credentials was an existing Origin class, so it doesn't
+ * implement the new PluggableConfigurationComponent interface and
+ * can't be loaded by generic plugin support.
+ * </p>
+ */
+ private boolean processCredentials() {
+ boolean anyError=false;
+ PluggableType[] pluggable = config.getCredentialsProviderArray();
+ for (int i=0;i<pluggable.length;i++) {
+ String pluggabletype = pluggable[i].getType();
+ if (!pluggabletype.equals(
+ "edu.internet2.middleware.shibboleth.common.Credentials")) {
+ log.error("Unsupported CredentialsProvider type "+pluggabletype);
+ anyError=true;
+ continue;
+ }
+ PluggableType credentialsProvider = pluggable[i];
+ Node fragment = credentialsProvider.newDomNode();
+ // newDomNode returns the current node wrapped in a Fragment
+ try {
+ Node credentialsProviderNode = fragment.getFirstChild();
+ Node credentialsNode=credentialsProviderNode.getFirstChild();
+ credentials = new Credentials((Element)credentialsNode);
+ } catch(Exception e) {
+ log.error("Cannot process Credentials element of Shibboleth configuration");
+ log.error(e);
+ anyError=true;
+ continue;
+ }
+ }
+ return anyError;
+ }
+
+ /**
+ * Find and load secondary configuration files referenced in an Application(s)
+ *
+ * @param app Application object
+ * @throws ShibbolethConfigurationException
+ */
+ private boolean processApplication(Application app)
+ throws ShibbolethConfigurationException {
+
+ boolean anyError=false;
+
+ String applicationId = app.getId();
+
+ ApplicationInfo appinfo = new ApplicationInfo(app);
+
+ anyError |= processPluggableMetadata(appinfo);
+ anyError |= processPluggableAAPs(appinfo);
+ anyError |= processPluggableTrusts(appinfo);
+
+ applications.put(applicationId, appinfo);
+
+ return anyError;
+ }
+
+ /**
+ * Generic code to create an object of a Pluggable type that implements
+ * a configuration interface.
+ *
+ * <p>The configuration schema defines "PluggableType" as a type of
+ * XML element that has opaque contents and attributes "type" and
+ * "uri". If the uri attribute is omitted, then the configuration
+ * data is inline XML content. The XSD files typically define the
+ * format of pluggable configuration elements, but without binding
+ * them to the PluggableType element that may contain them.</p>
+ *
+ * <p>The implimentation of pluggable objects is provided by
+ * external classes. There are "builtin" classes provided with
+ * Shibboleth (XMLMetadataImpl, XMLTrustImpl, XMLAAPImpl) that
+ * provide examples of how this is done. By design, others can
+ * provide their own classes just by putting the class name as
+ * the value of the type attribute.</p>
+ *
+ * <p>This routine handles the common setup. It creates objects
+ * of one of the builtin types, or it uses Class.forName() to
+ * access a user specified class. It then locates either the
+ * inline XML elements or the external XML file. It passes the
+ * XML to initialize the object. Finally, a reference to the
+ * object is stored in the appropriate Map.</p>
+ *
+ * <p>The objects created implement two interfaces. Mostly they
+ * implement a configuration interface (EntityDescriptor, ITrust,
+ * AAP, etc). However, for the purpose of this routine they also
+ * must be declared to implement PluggableConfigurationComponent
+ * and provide an initialize() method that parses a DOM Node
+ * containing their root XML configuration element.</p>
+ *
+ * @param pluggable XMLBean for element defined in XSD to be of "PluggableType"
+ * @param implclass java.lang.Class of Builtin implementation class
+ * @param interfaceClass java.lang.Class of Interface
+ * @param builtinName alias type to choose Builtin imlementation
+ * @param uriMap ApplicationInfo Map for this interface
+ * @return
+ */
+ private
+ String
+ processPluggable(
+ PluggableType pluggable,
+ Class implclass,
+ Class interfaceClass,
+ String builtinName,
+ String schemaname,
+ Map /*<String,PluggableConfigurationComponent>*/uriMap
+ ) {
+
+ String pluggabletype = pluggable.getType();
+
+ if (!pluggabletype.equals(builtinName)) {
+ // Not the builtin type, try to load user class by name
+ try {
+ implclass = Class.forName(pluggabletype);
+ } catch (ClassNotFoundException e) {
+ log.error("Type value "+pluggabletype+" not found as supplied Java class");
+ return null;
+ }
+ if (!interfaceClass.isAssignableFrom(implclass)||
+ !PluggableConfigurationComponent.class.isAssignableFrom(implclass)) {
+ log.error(pluggabletype+" class does not support required interfaces.");
+ return null;
+ }
+ }
+
+ PluggableConfigurationComponent impl;
+ try {
+ impl = (PluggableConfigurationComponent) implclass.newInstance();
+ } catch (Exception e) {
+ log.error("Unable to instantiate "+pluggabletype);
+ return null;
+ }
+
+ String uri = pluggable.getUri();
+ if (uri==null) { // inline
+
+ uri=genDummyUri();
+ try {
+ Node fragment = pluggable.newDomNode(); // XML-Fragment node
+ Node pluggableNode = fragment.getFirstChild(); // PluggableType
+ Node contentNode=pluggableNode.getFirstChild();// root element
+ impl.initialize(contentNode);
+ } catch (Exception e) {
+ log.error("XML error " + e);
+ return null;
+ }
+
+ } else { // external file
+
+ if (uriMap.get(uri)!=null) { // Already parsed this file
+ return "";
+ }
+
+ String tempname = impl.getSchemaPathname();
+ if (tempname!=null)
+ schemaname=tempname;
+
+ try {
+ Document extdoc = loadDom(uri,schemaname);
+ impl.initialize(extdoc);
+ } catch (Exception e) {
+ log.error("XML error " + e);
+ return null;
+ }
+ }
+
+ uriMap.put(uri,impl);
+ return uri;
+ }
+
+
+
+ /**
+ * Handle a FederationProvider
+ */
+ private boolean processPluggableMetadata(ApplicationInfo appinfo) {
+ boolean anyError = false;
+ PluggableType[] pluggable = appinfo.getApplicationConfig().getFederationProviderArray();
+ for (int i = 0;i<pluggable.length;i++) {
+ String uri = processPluggable(pluggable[i],
+ XMLMetadataImpl.class,
+ EntityLocator.class,
+ XMLFEDERATIONPROVIDERTYPE,
+ METADATASCHEMA,
+ entityLocators);
+ if (uri==null)
+ anyError=true;
+ else if (uri.length()>0) {
+ appinfo.addGroupUri(uri);
+ }
+ }
+ return anyError;
+ }
+
+ /**
+ * Reload XML Metadata configuration after file changed.
+ * @param uri Path to Metadata XML configuration
+ * @return true if file reloaded.
+ */
+ public boolean reloadFederation(String uri) {
+ if (getMetadataImplementor(uri)!=null||
+ uri.startsWith(INLINEURN))
+ return false;
+ try {
+ Document sitedoc = loadDom(uri,METADATASCHEMA);
+ XMLMetadataImpl impl = new XMLMetadataImpl();
+ impl.initialize(sitedoc);
+ addOrReplaceMetadataImplementor(uri,impl);
+ } catch (Exception e) {
+ log.error("Error while parsing Metadata file "+uri);
+ log.error("XML error " + e);
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Handle an AAPProvider element with
+ * type="edu.internet2.middleware.shibboleth.common.provider.XMLAAP"
+ * @throws InternalConfigurationException
+ */
+ private boolean processPluggableAAPs(ApplicationInfo appinfo){
+ boolean anyError=false;
+ PluggableType[] pluggable = appinfo.getApplicationConfig().getAAPProviderArray();
+ for (int i = 0;i<pluggable.length;i++) {
+ String uri = processPluggable(pluggable[i],
+ XMLAAPImpl.class,
+ AAP.class,
+ XMLAAPPROVIDERTYPE,
+ AAPSCHEMA,
+ attributePolicies);
+ if (uri==null)
+ anyError=true;
+ else if (uri.length()>0) {
+ appinfo.addAapUri(uri);
+ }
+ }
+ return anyError;
+ }
+
+ /**
+ * Reload XML AAP configuration after file changed.
+ * @param uri AAP to Trust XML configuration
+ * @return true if file reloaded.
+ */
+ public boolean reloadAAP(String uri) {
+ if (getAAPImplementor(uri)!=null||
+ uri.startsWith(INLINEURN))
+ return false;
+ try {
+ Document aapdoc = loadDom(uri,AAPSCHEMA);
+ AttributeAcceptancePolicyDocument aap = AttributeAcceptancePolicyDocument.Factory.parse(aapdoc);
+ XMLAAPImpl impl = new XMLAAPImpl();
+ impl.initialize(aapdoc);
+ addOrReplaceAAPImplementor(uri,impl);
+ } catch (Exception e) {
+ log.error("Error while parsing AAP file "+uri);
+ log.error("XML error " + e);
+ return false;
+ }
+ return true;
+ }
+
+
+ /**
+ * Handle a TrustProvider element with
+ * type="edu.internet2.middleware.shibboleth.common.provider.XMLTrust"
+ *
+ * Note: This code builds the semantic structure of trust. That is, it knows
+ * about certificates and keys. The actual logic of Trust (signature generation
+ * and validation) is implemented in a peer object defined in the external
+ * class XMLTrustImpl.
+ *
+ * @throws ShibbolethConfigurationException if X.509 certificate cannot be processed
+ * @throws InternalConfigurationException
+ */
+ private boolean processPluggableTrusts(ApplicationInfo appinfo){
+ boolean anyError=false;
+ PluggableType[] pluggable = appinfo.getApplicationConfig().getTrustProviderArray();
+ for (int i = 0;i<pluggable.length;i++) {
+ String uri = processPluggable(pluggable[i],
+ XMLTrustImpl.class,
+ ITrust.class,
+ XMLTRUSTPROVIDERTYPE,
+ TRUSTSCHEMA,
+ certificateValidators);
+ if (uri==null)
+ anyError=true;
+ else if (uri.length()>0) {
+ appinfo.addTrustUri(uri);
+ }
+ }
+ return anyError;
+ }
+
+ /**
+ * Reload XML Trust configuration after file changed.
+ * @param uri Path to Trust XML configuration
+ * @return true if file reloaded.
+ */
+ public boolean reloadTrust(String uri) {
+ if (getTrustImplementor(uri)!=null||
+ uri.startsWith(INLINEURN))
+ return false;
+ try {
+ Document trustdoc = loadDom(uri,TRUSTSCHEMA);
+ XMLTrustImpl impl = new XMLTrustImpl();
+ impl.initialize(trustdoc);
+ addOrReplaceTrustImplementor(uri,impl);
+ } catch (Exception e) {
+ log.error("Error while parsing Trust file "+uri);
+ log.error("XML error " + e);
+ return false;
+ }
+ return true;
+ }
+
+
+ private boolean processPluggableRequestMapProvider(){
+ SHIRE shire = config.getSHIRE();
+ PluggableType mapProvider = shire.getRequestMapProvider();
+
+ String pluggabletype = mapProvider.getType();
+ if (!pluggabletype.equals(XMLREQUESTMAPPROVIDERTYPE)) {
+ log.error("Unsupported RequestMapProvider type "+pluggabletype);
+ return true;
+ }
+
+ RequestMapDocument requestMapDoc = null;
+ Document mapdoc = null;
+ Element maproot = null;
+ String uri = mapProvider.getUri();
+
+ if (uri==null) { // inline
+
+ uri=genDummyUri();
+ try {
+ Node fragment = mapProvider.newDomNode();
+ Node pluggableNode = fragment.getFirstChild();
+ Node contentNode=pluggableNode.getFirstChild();
+
+ requestMapDoc = RequestMapDocument.Factory.parse(contentNode);
+ } catch (Exception e) {
+ log.error("Error while parsing inline RequestMap");
+ log.error("XML error " + e);
+ return true;
+ }
+
+ } else { // external file
+ try {
+ mapdoc = loadDom(uri,METADATASCHEMA);
+ requestMapDoc = RequestMapDocument.Factory.parse(mapdoc);
+ } catch (Exception e) {
+ log.error("Error while parsing RequestMap file "+uri);
+ log.error("XML error " + e);
+ return true;
+ }
+ }
+
+ requestMap = requestMapDoc.getRequestMap();
+ return false;
+ }
+
+
+
+ /**
+ * Given a file URI, parse it (validated against a schema) to a DOM.
+ *
+ * <p>Note: The exact format of the URI (absolute file path, resource
+ * path, URL) is interpreted here. Changes to URI conventions can be
+ * implemented here and apply throughout the configuration system.</p>
+ *
+ * @param configFilePath Path to the particular configuration file
+ * @param schemaFilePath Path to the schema for this type of file
+ * @return DOM Document object created from the file
+ * @throws ShibbolethConfigurationException if file not found or invalid
+ */
+ private Document loadDom(String configFilePath, String schemaFilePath)
+ throws InternalConfigurationException {
+
+ log.debug("Loading Configuration from (" + configFilePath + ").");
+
+ InputSource insrc;
+ String schemaCannonicalFilePath;
+ ShibResource configResource;
+ ShibResource schemaResource;
+ try {
+ configResource = new ShibResource(configFilePath,this.getClass());
+ insrc = new InputSource(configResource.getInputStream());
+ } catch (Exception e1) {
+ log.error("Configuration file "+configFilePath+" could not be located.");
+ throw new InternalConfigurationException();
+ }
+
+ try {
+ schemaResource = new ShibResource(schemaFilePath,this.getClass());
+ schemaCannonicalFilePath = schemaResource.getFile().getCanonicalPath();
+ } catch (Exception e) {
+ log.error("Schema file "+schemaFilePath+" could not be located.");
+ throw new InternalConfigurationException();
+ }
+
+
+ return loadDom(insrc,schemaCannonicalFilePath);
+
+ }
+
+
+ /**
+ * Parse an InputSource (maybe file, maybe buffer) into a DOM
+ * @param insrc XML InputSource object
+ * @param schemaFilePath
+ * @return DOM Document
+ * @throws ShibbolethConfigurationException
+ */
+ private Document loadDom(InputSource insrc, String schemaFilePath)
+ throws InternalConfigurationException {
+
+ try {
+ parser.setProperty("http://java.sun.com/xml/jaxp/properties/schemaSource",
+ schemaFilePath);
+
+ parser.parse(insrc);
+
+ } catch (SAXException e) {
+ log.error("Error while parsing shibboleth configuration: " + e);
+ throw new InternalConfigurationException();
+ } catch (IOException e) {
+ log.error("Could not load shibboleth configuration file: " + e);
+ throw new InternalConfigurationException();
+ }
+
+ return parser.getDocument();
+
+ }
+
+
+
+
+ private int inlinenum = 1;
+ private String genDummyUri() {
+ return INLINEURN+Integer.toString(inlinenum++);
+ }
+
+
+
+
+
+
+ /**
+ * ApplicationInfo represents the <Application(s)> object, its fields,
+ * and the pluggable configuration elements under it.
+ *
+ * <p>It can return arrays of Metadata, Trust, or AAP providers, but
+ * it also exposes convenience methods that shop the lookup(),
+ * validate(), and trust() calls to each object in the collection
+ * until success or failure is determined.</p>
+ *
+ * <p>For all other parameters, such as Session parameters, you
+ * can fetch the XMLBean by calling getApplicationConf() and
+ * query their value directly.
+ */
+ class ApplicationInfo
+ implements EntityLocator, ITrust {
+
+ private Application applicationConfig;
+ public Application getApplicationConfig() {
+ return applicationConfig;
+ }
+
+ /**
+ * Construct this object from the XML Bean.
+ * @param application XMLBean for Application element
+ */
+ ApplicationInfo(Application application) {
+ this.applicationConfig=application;
+ }
+
+
+ /*
+ * Following the general rule, this object may not keep
+ * direct references to the plugin interface implementors,
+ * but must look them up on every call through their URI keys.
+ * So we keep collections of URI strings instead.
+ */
+ ArrayList groupUris = new ArrayList();
+ ArrayList trustUris = new ArrayList();
+ ArrayList aapUris = new ArrayList();
+
+ void addGroupUri(String uri) {
+ groupUris.add(uri);
+ }
+ void addTrustUri(String uri) {
+ trustUris.add(uri);
+ }
+ void addAapUri(String uri) {
+ aapUris.add(uri);
+ }
+
+ /**
+ * Return the current array of objects that implement the
+ * ...metadata.Metadata interface
+ *
+ * @return Metadata[]
+ */
+ Metadata[] getMetadataProviders() {
+ Iterator iuris = groupUris.iterator();
+ int count = groupUris.size();
+ Metadata[] metadatas = new Metadata[count];
+ for (int i=0;i<count;i++) {
+ String uri =(String) iuris.next();
+ metadatas[i]=getMetadataImplementor(uri);
+ }
+ return metadatas;
+ }
+
+ /**
+ * A convenience function based on the Metadata interface.
+ *
+ * <p>Look for an entity ID through each implementor until the
+ * first one finds locates a describing object.</p>
+ *
+ * <p>Unfortunately, Metadata.lookup() was originally specified to
+ * return a "Provider". In current SAML 2.0 terms, the object
+ * returned should be an EntityDescriptor. So this is the new
+ * function in the new interface that will use the new term, but
+ * it does the same thing.</p>
+ *
+ * @param id ID of the OriginSite entity
+ * @return EntityDescriptor metadata object for that site.
+ */
+ public EntityDescriptor getEntityDescriptor(String id) {
+ Iterator iuris = groupUris.iterator();
+ while (iuris.hasNext()) {
+ String uri =(String) iuris.next();
+ EntityLocator locator=getMetadataImplementor(uri);
+ EntityDescriptor entity = locator.getEntityDescriptor(id);
+ if (entity!=null)
+ return entity;
+ }
+ return null;
+ }
+
+ /**
+ * Convenience function to fulfill Metadata interface contract.
+ *
+ * @param id ID of OriginSite
+ * @return Provider object for that Site.
+ */
+ public Provider lookup(String id) {
+ return getEntityDescriptor(id);
+ }
+
+ /**
+ * Return the current array of objects that implement the ITrust interface
+ *
+ * @return ITrust[]
+ */
+ public ITrust[] getTrustProviders() {
+ Iterator iuris = groupUris.iterator();
+ int count = groupUris.size();
+ ITrust[] trusts = new ITrust[count];
+ for (int i=0;i<count;i++) {
+ String uri =(String) iuris.next();
+ trusts[i]=getTrustImplementor(uri);
+ }
+ return trusts;
+ }
+
+ /**
+ * Return the current array of objects that implement the AAP interface
+ *
+ * @return AAP[]
+ */
+ public AAP[] getAAPProviders() {
+ Iterator iuris = aapUris.iterator();
+ int count = aapUris.size();
+ AAP[] aaps = new AAP[count];
+ for (int i=0;i<count;i++) {
+ String uri =(String) iuris.next();
+ aaps[i]=getAAPImplementor(uri);
+ }
+ return aaps;
+ }
+
+ /**
+ * Convenience function to apply AAP by calling the apply()
+ * method of each AAP implementor.
+ *
+ * <p>Any AAP implementor can delete an assertion or value.
+ * Empty SAML elements get removed from the assertion.
+ * This can yield an AttributeAssertion with no attributes.
+ *
+ * @param entity Origin site that sent the assertion
+ * @param assertion SAML Attribute Assertion
+ */
+ void applyAAP(EntityDescriptor entity, SAMLAssertion assertion) {
+
+ // Foreach AAP in the collection
+ AAP[] providers = getAAPProviders();
+ for (int i=0;i<providers.length;i++) {
+ AAP aap = providers[i];
+ if (aap.isAnyAttribute())
+ continue;
+
+ // Foreach Statement in the Assertion
+ Iterator statements = assertion.getStatements();
+ int istatement=0;
+ while (statements.hasNext()) {
+ Object statement = statements.next();
+ if (statement instanceof SAMLAttributeStatement) {
+ SAMLAttributeStatement attributeStatement =
+ (SAMLAttributeStatement) statement;
+
+ // Foreach Attribute in the AttributeStatement
+ Iterator attributes = attributeStatement.getAttributes();
+ int iattribute=0;
+ while (attributes.hasNext()) {
+ SAMLAttribute attribute =
+ (SAMLAttribute) attributes.next();
+ String name = attribute.getName();
+ String namespace = attribute.getNamespace();
+ AttributeRule rule = aap.lookup(name,namespace);
+ if (rule==null) {
+ // TODO Not sure, but code appears to keep unknown attributes
+ log.warn("No rule found for attribute "+name);
+ iattribute++;
+ continue;
+ }
+ rule.apply(entity,attribute);
+ if (!attribute.getValues().hasNext())
+ attributeStatement.removeAttribute(iattribute);
+ else
+ iattribute++;
+
+ }
+ if (!attributeStatement.getAttributes().hasNext())
+ assertion.removeStatement(istatement);
+ else
+ istatement++;
+ } else {
+ istatement++;
+ }
+ }
+ }
+ }
+
+
+ /**
+ * Returns a collection of attribute names to request from the AA.
+ *
+ * @return Collection of attribute Name values
+ */
+ public Collection getAttributeDesignators() {
+ // TODO Not sure where this should come from
+ return new ArrayList();
+ }
+
+
+ /**
+ * Convenience method implementing ITrust.validate() across
+ * the collection of implementing objects. Returns true if
+ * any Trust implementor approves the signatures in the object.
+ *
+ * <p>In the interface, validate() is passed several arguments
+ * that come from this object. In this function, those
+ * arguments are ignored "this" is used.
+ */
+ public boolean
+ validate(
+ Iterator revocations, // Currently unused
+ ProviderRole role,
+ SAMLObject token,
+ EntityLocator dummy // "this" is an EntityLocator
+ ) {
+
+ // TODO If revocations are supported, "this" will provide them
+
+ ITrust[] trustProviders = getTrustProviders();
+ for (int i=0;i<trustProviders.length;i++) {
+ ITrust trust = trustProviders[i];
+ if (trust.validate(null,role,token,this))
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Simpler version of validate that avoids dummy arguments
+ *
+ * @param role Entity that sent Token (from Metadata)
+ * @param token Signed SAMLObject
+ * @return
+ */
+ public boolean validate(ProviderRole role, SAMLObject token) {
+ return validate(null,role,token,null);
+ }
+
+ /**
+ * A method of ITrust that we must declare to claim that
+ * ApplicationInfo implements ITrust. However, no code in the
+ * ServiceProvider calls this (probably an Origin thing).
+ *
+ * @param revocations
+ * @param role
+ * @return This dummy always returns false.
+ */
+ public boolean attach(Iterator revocations, ProviderRole role) {
+ // Unused
+ return false;
+ }
+
+ }
+
+
+
+ private static class InternalConfigurationException extends Exception {
+ InternalConfigurationException() {
+ super();
+ }
+ }
+
+}