View Javadoc

    1   /*
    2    * Copyright [2005] [University Corporation for Advanced Internet Development, Inc.]
    3    *
    4    * Licensed under the Apache License, Version 2.0 (the "License");
    5    * you may not use this file except in compliance with the License.
    6    * You may obtain a copy of the License at
    7    *
    8    * http://www.apache.org/licenses/LICENSE-2.0
    9    *
    10   * Unless required by applicable law or agreed to in writing, software
    11   * distributed under the License is distributed on an "AS IS" BASIS,
    12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13   * See the License for the specific language governing permissions and
    14   * limitations under the License.
    15   */
    16  package edu.internet2.middleware.shibboleth.wayf;
    17
    18  import java.io.File;
    19  import java.lang.reflect.Constructor;
    20  import java.net.MalformedURLException;
    21  import java.net.URL;
    22  import java.util.ArrayList;
    23  import java.util.Collection;
    24  import java.util.Enumeration;
    25  import java.util.HashMap;
    26  import java.util.HashSet;
    27  import java.util.List;
    28  import java.util.Map;
    29  import java.util.Set;
    30  import java.util.StringTokenizer;
    31  import java.util.TreeMap;
    32
    33  import org.opensaml.saml2.metadata.EntitiesDescriptor;
    34  import org.opensaml.saml2.metadata.EntityDescriptor;
    35  import org.opensaml.saml2.metadata.IDPSSODescriptor;
    36  import org.opensaml.saml2.metadata.Organization;
    37  import org.opensaml.saml2.metadata.OrganizationDisplayName;
    38  import org.opensaml.saml2.metadata.OrganizationName;
    39  import org.opensaml.saml2.metadata.RoleDescriptor;
    40  import org.opensaml.saml2.metadata.SPSSODescriptor;
    41  import org.opensaml.saml2.metadata.provider.FileBackedHTTPMetadataProvider;
    42  import org.opensaml.saml2.metadata.provider.FilesystemMetadataProvider;
    43  import org.opensaml.saml2.metadata.provider.MetadataFilter;
    44  import org.opensaml.saml2.metadata.provider.MetadataFilterChain;
    45  import org.opensaml.saml2.metadata.provider.MetadataProvider;
    46  import org.opensaml.saml2.metadata.provider.MetadataProviderException;
    47  import org.opensaml.saml2.metadata.provider.ObservableMetadataProvider;
    48  import org.opensaml.xml.XMLObject;
    49  import org.opensaml.xml.parse.ParserPool;
    50  import org.slf4j.Logger;
    51  import org.slf4j.LoggerFactory;
    52  import org.w3c.dom.Element;
    53  import org.w3c.dom.NodeList;
    54
    55  import edu.internet2.middleware.shibboleth.common.ShibbolethConfigurationException;
    56  import edu.internet2.middleware.shibboleth.wayf.plugins.Plugin;
    57  import edu.internet2.middleware.shibboleth.wayf.plugins.PluginMetadataParameter;
    58  import edu.internet2.middleware.shibboleth.wayf.plugins.provider.BindingFilter;
    59
    60  /**
    61   *
    62   * Represents a collection of related sites as desribed by a single soirce of metadata.
    63   * This is usually a federation.  When the WAYF looks to see which IdP sites to show,
    64   * it trims the list so as to not show IdP's which do not trust the SP.
    65   *
    66   * This class is opaque outside this file.  The three static methods getSitesLists,
    67   * searchForMatchingOrigins and lookupIdP provide mechansims for accessing
    68   * collections of IdPSiteSets.
    69   *
    70   */
    71
    72  public class IdPSiteSet implements ObservableMetadataProvider.Observer {
    73
    74      /** Handle for error output. */
    75      private static final Logger LOG = LoggerFactory.getLogger(IdPSiteSet.class.getName());
    76
    77      /** The OpenSaml metadata source. */
    78      private ObservableMetadataProvider metadata;
    79
    80      /** Is the named SP in the current metadata set? */
    81      private Set<String> spNames = new HashSet<String>(0);
    82
    83      /** Is the named IdP in the current metadata set? */
    84      private Set<String> idpNames = new HashSet<String>(0);
    85
    86      /** What does the configuration identify this as? */
    87      private final String identifier;
    88
    89      /** What name should we display for this set of entities? */
    90      private final String displayName;
    91
    92      /** Where does the metadata exist? */
    93      private String location;
    94
    95      /** What parameters do we pass in to which plugin? */
    96      private final Map<Plugin, PluginMetadataParameter> plugins = new HashMap<Plugin, PluginMetadataParameter>();
    97
    98      /**
    99       * Create a new IdPSiteSet as described by the supplied XML segment.
    100      * @param el - configuration details.
    101      * @param parserPool - the parsers we initialized above.
    102      * @param warnOnBadBinding if we just warn or give an error if an SP has bad entry points.
    103      * @throws ShibbolethConfigurationException - if something goes wrong.
    104      */
    105     protected IdPSiteSet(Element el, ParserPool parserPool, boolean warnOnBadBinding) throws ShibbolethConfigurationException {
    106
    107         String spoolSpace;
    108         String delayString;
    109
    110         this.identifier = el.getAttribute("identifier");
    111         this.displayName = el.getAttribute("displayName");
    112         location = el.getAttribute("url");
    113         if (null == location || location.length() == 0) {
    114             //
    115             // Sigh for a few releases this was documented as URI
    116             //
    117             location = el.getAttribute("url");
    118         }
    119         spoolSpace = el.getAttribute("backingFile");
    120         delayString = el.getAttribute("timeout");
    121
    122         //
    123         // Configure the filters (before the metadata so we can add them before we start reading)
    124         //
    125         String ident;
    126         String className;
    127         ident = "<not specified>";
    128         className = "<not specified>";
    129         MetadataFilterChain filterChain = null;
    130         filterChain = new MetadataFilterChain();
    131         try {
    132             NodeList itemElements = el.getElementsByTagNameNS(XMLConstants.CONFIG_NS, "Filter");
    133             List <MetadataFilter> filters = new ArrayList<MetadataFilter>(1 + itemElements.getLength());
    134
    135             //
    136             // We always have a binding filter
    137             //
    138             filters.add(new BindingFilter(warnOnBadBinding));
    139
    140             for (int i = 0; i < itemElements.getLength(); i++) {
    141                 Element element = (Element) itemElements.item(i);
    142
    143                 ident = "<not specified>";
    144                 className = "<not specified>";
    145
    146                 ident = element.getAttribute("identifier");
    147
    148                 if (null == ident || ident.equals("")) {
    149                     LOG.error("Could not load filter with no identifier");
    150                     continue;
    151                 }
    152
    153                 className = element.getAttribute("type");
    154                 if (null == className || className.equals("")) {
    155                     LOG.error("Filter " + identifier + " did not have a valid type");
    156                 }
    157                 //
    158                 // So try to get hold of the Filter
    159                 //
    160                 Class<MetadataFilter> filterClass = (Class<MetadataFilter>) Class.forName(className);
    161                 Class[] classParams = {Element.class};
    162                 Constructor<MetadataFilter> constructor = filterClass.getConstructor(classParams);
    163                 Object[] constructorParams = {element};
    164
    165                 filters.add(constructor.newInstance(constructorParams));
    166             }
    167             filterChain.setFilters(filters);
    168         } catch (Exception e) {
    169             LOG.error("Could not load filter " + ident + "()" + className + ") for " + this.identifier, e);
    170             throw new ShibbolethConfigurationException("Could not load filter", e);
    171         }
    172
    173         LOG.info("Loading Metadata for " + displayName);
    174         try {
    175             int delay;
    176             delay = 30000;
    177             if (null != delayString && !"".equals(delayString)) {
    178                 delay = Integer.parseInt(delayString);
    179             }
    180
    181             URL url = new URL(location);
    182             if ("file".equalsIgnoreCase(url.getProtocol())){
    183                 FilesystemMetadataProvider provider = new FilesystemMetadataProvider(new File(url.getFile()));
    184                 provider.setParserPool(parserPool);
    185                 if (null != filterChain) {
    186                     provider.setMetadataFilter(filterChain);
    187                 }
    188                 provider.initialize();
    189                 metadata = provider;
    190             } else {
    191                 if (spoolSpace == null || "".equals(spoolSpace)) {
    192                     throw new ShibbolethConfigurationException("backingFile must be specified for " + identifier);
    193                 }
    194
    195                 FileBackedHTTPMetadataProvider provider;
    196
    197                 provider = new FileBackedHTTPMetadataProvider(location, delay, spoolSpace);
    198                 provider.setParserPool(parserPool);
    199                 if (null != filterChain) {
    200                     provider.setMetadataFilter(filterChain);
    201                 }
    202                 provider.initialize();
    203                 metadata = provider;
    204             }
    205         } catch (MetadataProviderException e) {
    206             throw new ShibbolethConfigurationException("Could not read " + location, e);
    207         } catch (NumberFormatException e) {
    208             throw new ShibbolethConfigurationException("Badly formed timeout " + delayString, e);
    209         } catch (MalformedURLException e) {
    210             throw new ShibbolethConfigurationException("Badly formed url ", e);
    211         }
    212         metadata.getObservers().add(this);
    213         onEvent(metadata);
    214     }
    215
    216     /**
    217      * Based on 1.2 Origin.isMatch.  There must have been a reason for it...
    218      * [Kindas of] support for the search function in the wayf.  This return many false positives
    219      * but given the aim is to provide input for a pull down list...
    220      *
    221      * @param entity   The entity to match.
    222      * @param str      The patten to match against.
    223      * @param config   Provides list of tokens to not lookup
    224      * @return         Whether this entity matches
    225      */
    226
    227     private static boolean isMatch(EntityDescriptor entity, String str, HandlerConfig config) {
    228
    229         Enumeration input = new StringTokenizer(str);
    230         while (input.hasMoreElements()) {
    231             String currentToken = (String) input.nextElement();
    232
    233             if (config.isIgnoredForMatch(currentToken)) {
    234                 continue;
    235             }
    236
    237             currentToken = currentToken.toLowerCase();
    238
    239             if (entity.getEntityID().indexOf(currentToken) > -1) {
    240                 return true;
    241             }
    242
    243             Organization org = entity.getOrganization();
    244
    245             if (org != null) {
    246
    247                 List <OrganizationName> orgNames = org.getOrganizationNames();
    248                 for (OrganizationName name : orgNames) {
    249                     if (name.getName().getLocalString().toLowerCase().indexOf(currentToken) > -1) {
    250                         return true;
    251                     }
    252                 }
    253
    254                 List <OrganizationDisplayName> orgDisplayNames = org.getDisplayNames();
    255                 for (OrganizationDisplayName name : orgDisplayNames) {
    256                     if (name.getName().getLocalString().toLowerCase().indexOf(currentToken) > -1) {
    257                         return true;
    258                     }
    259                 }
    260             }
    261         }
    262         return false;
    263     }
    264
    265     /**
    266      * Return all the Idp in the provided entities descriptor.  If SearchMatches
    267      * is non null it is populated with whatever of the IdPs matches the search string
    268      * (as noted above).
    269      * @param searchString to match with
    270      * @param config parameter to mathing
    271      * @param searchMatches if non null is filled with such of the sites which match the string
    272      * @return the sites which fit.
    273      */
    274     protected Map<String, IdPSite> getIdPSites(String searchString,
    275                                                HandlerConfig config,
    276                                                Collection<IdPSite> searchMatches)
    277     {
    278         XMLObject object;
    279         List <EntityDescriptor> entities;
    280         try {
    281             object = metadata.getMetadata();
    282         } catch (MetadataProviderException e) {
    283             LOG.error("Metadata for " + location + "could not be read", e);
    284             return null;
    285         }
    286
    287         if (object == null) {
    288             return null;
    289         }
    290
    291         //
    292         // Fill in entities approptiately
    293         //
    294
    295         if (object instanceof EntityDescriptor) {
    296             entities = new ArrayList<EntityDescriptor>(1);
    297             entities.add((EntityDescriptor) object);
    298         } else if (object instanceof EntitiesDescriptor) {
    299
    300             EntitiesDescriptor entitiesDescriptor = (EntitiesDescriptor) object;
    301
    302             entities = entitiesDescriptor.getEntityDescriptors();
    303         } else {
    304            return null;
    305         }
    306
    307         //
    308         // populate the result (and the searchlist) from the entities list
    309         //
    310
    311         TreeMap<String, IdPSite> result = new TreeMap <String,IdPSite>();
    312
    313         for (EntityDescriptor entity : entities) {
    314
    315             if (entity.isValid() && hasIdPRole(entity)) {
    316
    317                 IdPSite site = new IdPSite(entity);
    318                 result.put(site.getName(), site);
    319                 if (searchMatches != null && isMatch(entity, searchString, config)) {
    320
    321                     searchMatches.add(site);
    322                 }
    323
    324             }
    325         } // iterate over all entities
    326         return result;
    327     }
    328
    329
    330     /**
    331      * Return this sites (internal) identifier.
    332      * @return the identifier
    333      */
    334     protected String getIdentifier() {
    335         return identifier;
    336     }
    337
    338     /**
    339      * Return the human friendly name for this siteset.
    340      * @return The friendly name
    341      */
    342     protected String getDisplayName() {
    343         return displayName;
    344     }
    345
    346     /**
    347      * We do not need to look at a set if it doesn't know about the given SP.  However if
    348      * no SP is given (as per 1.1) then we do need to look.  This calls lets us know whether
    349      * this set is a canddiate for looking into.
    350      * @param SPName the Sp we are interested in.
    351      * @return whether the site contains the SP.
    352      */
    353     protected boolean containsSP(String SPName) {
    354
    355         //
    356         // Deal with the case where we do *not* want to search by
    357         // SP (also handles the 1.1 case)
    358         //
    359
    360         if ((SPName == null) || (SPName.length() == 0)) {
    361             return true;
    362         }
    363
    364         //
    365         // Get hold of the current object list so as to provoke observer to fire
    366         // if needs be.
    367         //
    368
    369         XMLObject object;
    370         try {
    371             object = metadata.getMetadata();
    372         } catch (MetadataProviderException e) {
    373             return false;
    374         }
    375         //
    376         // Now lookup
    377         //
    378
    379         if (object instanceof EntitiesDescriptor ||
    380             object instanceof EntityDescriptor) {
    381             return spNames.contains(SPName);
    382         } else {
    383             return false;
    384         }
    385     }
    386
    387     /**
    388      * For plugin handling we need to know quickly if a metadataset contains the idp.
    389      * @param IdPName the IdP we are interested in.
    390      * @return whether the site contains the IdP.
    391      *
    392      */
    393
    394     protected boolean containsIdP(String IdPName) {
    395
    396         if ((IdPName == null) || (IdPName.length() == 0)) {
    397             return true;
    398         }
    399
    400         //
    401         // Get hold of the current object list so as to provoke observer to fire
    402         // if needs be.
    403         //
    404
    405         XMLObject object;
    406         try {
    407             object = metadata.getMetadata();
    408         } catch (MetadataProviderException e) {
    409             return false;
    410         }
    411         if (object instanceof EntitiesDescriptor ||
    412             object instanceof EntityDescriptor) {
    413             return idpNames.contains(IdPName);
    414         } else {
    415             return false;
    416         }
    417     }
    418
    419     //
    420     // Now deal with plugins - these are delcared to use but we are
    421     // responsible for their parameter
    422     //
    423
    424     /**
    425      * Declares a plugin to the siteset.
    426      * @param plugin what to declare
    427      */
    428     protected void addPlugin(Plugin plugin) {
    429
    430         if (plugins.containsKey(plugin)) {
    431             return;
    432         }
    433
    434         PluginMetadataParameter param = plugin.refreshMetadata(metadata);
    435
    436         plugins.put(plugin, param);
    437     }
    438
    439     /**
    440      * Return the parameter that this plugin uses.
    441      * @param plugin
    442      * @return teh parameter.
    443      */
    444     protected PluginMetadataParameter paramFor(Plugin plugin) {
    445         return plugins.get(plugin);
    446     }
    447
    448
    449     /* (non-Javadoc)
    450      * @see org.opensaml.saml2.metadata.provider.ObservableMetadataProvider.Observer#onEvent(org.opensaml.saml2.metadata.provider.MetadataProvider)
    451      */
    452     public void onEvent(MetadataProvider provider) {
    453         Set<String> spNameSet = new HashSet<String>(0);
    454         Set<String> idpNameSet = new HashSet<String>(0);
    455
    456         XMLObject obj;
    457         try {
    458             obj = provider.getMetadata();
    459         } catch (MetadataProviderException e) {
    460             LOG.error("Couldn't read metadata for " + location, e);
    461             return;
    462         }
    463         if ((obj instanceof EntitiesDescriptor)) {
    464             EntitiesDescriptor entitiesDescriptor = (EntitiesDescriptor) obj;
    465
    466             for (EntityDescriptor entity : entitiesDescriptor.getEntityDescriptors()) {
    467                 if (hasSPRole(entity)) {
    468                     spNameSet.add(entity.getEntityID());
    469                 }
    470                 if (hasIdPRole(entity)) {
    471                     idpNameSet.add(entity.getEntityID());
    472                 }
    473             }
    474         } else if (obj instanceof EntityDescriptor) {
    475             EntityDescriptor entity = (EntityDescriptor) obj;
    476             if (hasSPRole(entity)) {
    477                 spNameSet.add(entity.getEntityID());
    478             }
    479             if (hasIdPRole(entity)) {
    480                 idpNameSet.add(entity.getEntityID());
    481             }
    482         } else {
    483             LOG.error("Metadata for " + location + " isn't <EntitiesDescriptor> or <EntityDescriptor>");
    484             return;
    485         }
    486         //
    487         // Now that we have the new set sorted out commit it in
    488         //
    489         this.spNames = spNameSet;
    490         this.idpNames = idpNameSet;
    491
    492         for (Plugin plugin:plugins.keySet()) {
    493             plugins.put(plugin, plugin.refreshMetadata(provider));
    494         }
    495     }
    496
    497     /**
    498      * Enumerate all the roles and see whether this entity can be an IdP.
    499      * @param entity
    500      * @return true if one of the roles that entity has is IdPSSO
    501      */
    502     private static boolean hasIdPRole(EntityDescriptor entity) {
    503         List<RoleDescriptor> roles = entity.getRoleDescriptors();
    504
    505         for (RoleDescriptor role:roles) {
    506            if (role instanceof IDPSSODescriptor) {
    507                //
    508                // So the entity knows how to be some sort of an Idp
    509                //
    510                return true;
    511            }
    512         }
    513         return false;
    514     }
    515
    516     /**
    517      * Enumerate all the roles and see whether this entity can be an SP.
    518      * @param entity
    519      * @return true if one of the roles that entity has is SPSSO
    520      */
    521     private static boolean hasSPRole(EntityDescriptor entity) {
    522         List<RoleDescriptor> roles = entity.getRoleDescriptors();
    523
    524         for (RoleDescriptor role:roles) {
    525            if (role instanceof SPSSODescriptor) {
    526                //
    527                // "I can do that"
    528                //
    529                return true;
    530            }
    531         }
    532         return false;
    533     }
    534
    535     /**
    536      * Return the idpSite for the given entity name.
    537      * @param idpName the entityname to look up
    538      * @return the associated idpSite
    539      * @throws WayfException
    540      */
    541     protected IdPSite getSite(String idpName) throws WayfException {
    542
    543         try {
    544             return new IdPSite(metadata.getEntityDescriptor(idpName));
    545         } catch (MetadataProviderException e) {
    546             String s = "Couldn't resolve " + idpName + " in "  + getDisplayName();
    547             LOG.error(s, e);
    548             throw new WayfException(s, e);
    549         }
    550     }
    551
    552     protected EntityDescriptor getEntity(String name) throws WayfException {
    553         try {
    554             return metadata.getEntityDescriptor(name);
    555         } catch (MetadataProviderException e) {
    556             String s = "Couldn't resolve " + name + " in "  + getDisplayName();
    557             LOG.error(s, e);
    558             throw new WayfException(s, e);
    559         }
    560
    561     }
    562 }
    563