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 metadat6a 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().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().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