View Javadoc

1   /*
2    * Copyright [2006] [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  
17  package edu.internet2.middleware.shibboleth.common.attribute.resolver.provider;
18  
19  import java.util.Arrays;
20  import java.util.Collection;
21  import java.util.HashMap;
22  import java.util.HashSet;
23  import java.util.Iterator;
24  import java.util.Map;
25  import java.util.Set;
26  import java.util.Map.Entry;
27  import java.util.concurrent.locks.Lock;
28  
29  import org.jgrapht.DirectedGraph;
30  import org.jgrapht.alg.CycleDetector;
31  import org.jgrapht.graph.DefaultEdge;
32  import org.jgrapht.graph.SimpleDirectedGraph;
33  import org.opensaml.common.SAMLObject;
34  import org.opensaml.saml1.core.NameIdentifier;
35  import org.opensaml.saml2.core.NameID;
36  import org.opensaml.xml.util.DatatypeHelper;
37  import org.slf4j.Logger;
38  import org.slf4j.LoggerFactory;
39  import org.springframework.context.ApplicationContext;
40  
41  import edu.internet2.middleware.shibboleth.common.attribute.BaseAttribute;
42  import edu.internet2.middleware.shibboleth.common.attribute.resolver.AttributeResolutionException;
43  import edu.internet2.middleware.shibboleth.common.attribute.resolver.AttributeResolver;
44  import edu.internet2.middleware.shibboleth.common.attribute.resolver.provider.attributeDefinition.AttributeDefinition;
45  import edu.internet2.middleware.shibboleth.common.attribute.resolver.provider.attributeDefinition.ContextualAttributeDefinition;
46  import edu.internet2.middleware.shibboleth.common.attribute.resolver.provider.dataConnector.ContextualDataConnector;
47  import edu.internet2.middleware.shibboleth.common.attribute.resolver.provider.dataConnector.DataConnector;
48  import edu.internet2.middleware.shibboleth.common.attribute.resolver.provider.principalConnector.ContextualPrincipalConnector;
49  import edu.internet2.middleware.shibboleth.common.attribute.resolver.provider.principalConnector.PrincipalConnector;
50  import edu.internet2.middleware.shibboleth.common.config.BaseReloadableService;
51  import edu.internet2.middleware.shibboleth.common.profile.provider.SAMLProfileRequestContext;
52  import edu.internet2.middleware.shibboleth.common.service.ServiceException;
53  
54  /**
55   * Primary implementation of {@link AttributeResolver}.
56   * 
57   * "Raw" attributes are gathered by the registered {@link DataConnector}s while the {@link AttributeDefinition}s
58   * refine the raw attributes or create attributes of their own. Connectors and definitions may depend on each other so
59   * implementations must use a directed dependency graph when performing the resolution.
60   */
61  public class ShibbolethAttributeResolver extends BaseReloadableService implements
62          AttributeResolver<SAMLProfileRequestContext> {
63  
64      /** Resolution plug-in types. */
65      public static final Collection<Class> PLUGIN_TYPES = Arrays.asList(new Class[] { DataConnector.class,
66              AttributeDefinition.class, PrincipalConnector.class, });
67  
68      /** Class logger. */
69      private final Logger log = LoggerFactory.getLogger(ShibbolethAttributeResolver.class.getName());
70  
71      /** Data connectors defined for this resolver. */
72      private Map<String, DataConnector> dataConnectors;
73  
74      /** Attribute definitions defined for this resolver. */
75      private Map<String, AttributeDefinition> definitions;
76  
77      /** Principal connectors defined for this resolver. */
78      private Map<String, PrincipalConnector> principalConnectors;
79  
80      /** Constructor. */
81      public ShibbolethAttributeResolver() {
82          super();
83          dataConnectors = new HashMap<String, DataConnector>();
84          definitions = new HashMap<String, AttributeDefinition>();
85          principalConnectors = new HashMap<String, PrincipalConnector>();
86      }
87  
88      /**
89       * Gets the attribute definitions registered with this resolver.
90       * 
91       * @return attribute definitions registered with this resolver
92       */
93      public Map<String, AttributeDefinition> getAttributeDefinitions() {
94          return definitions;
95      }
96  
97      /**
98       * Gets the data connectors registered with this provider.
99       * 
100      * @return data connectors registered with this provider
101      */
102     public Map<String, DataConnector> getDataConnectors() {
103         return dataConnectors;
104     }
105 
106     /**
107      * Gets the principal connectors registered with this resolver.
108      * 
109      * @return principal connectors registered with this resolver
110      */
111     public Map<String, PrincipalConnector> getPrincipalConnectors() {
112         return principalConnectors;
113     }
114 
115     /** {@inheritDoc} */
116     public Map<String, BaseAttribute> resolveAttributes(SAMLProfileRequestContext attributeRequestContext)
117             throws AttributeResolutionException {
118         ShibbolethResolutionContext resolutionContext = new ShibbolethResolutionContext(attributeRequestContext);
119 
120         log.debug("{} resolving attributes for principal {}", getId(), attributeRequestContext.getPrincipalName());
121 
122         if (getAttributeDefinitions().size() == 0) {
123             log.debug("No attribute definitions loaded in {} so no attributes can be resolved for principal {}",
124                     getId(), attributeRequestContext.getPrincipalName());
125             return new HashMap<String, BaseAttribute>();
126         }
127 
128         Lock readLock = getReadWriteLock().readLock();
129         readLock.lock();
130         Map<String, BaseAttribute> resolvedAttributes = null;
131         try {
132             resolvedAttributes = resolveAttributes(resolutionContext);
133             cleanResolvedAttributes(resolvedAttributes, resolutionContext);
134         } finally {
135             readLock.unlock();
136         }
137 
138         log.debug(getId() + " resolved, for principal {}, the attributes: {}", attributeRequestContext
139                 .getPrincipalName(), resolvedAttributes.keySet());
140         return resolvedAttributes;
141     }
142 
143     /** {@inheritDoc} */
144     public void validate() throws AttributeResolutionException {
145         DirectedGraph<ResolutionPlugIn, DefaultEdge> dependencyGraph;
146         CycleDetector<ResolutionPlugIn, DefaultEdge> cycleDetector;
147 
148         dependencyGraph = new SimpleDirectedGraph<ResolutionPlugIn, DefaultEdge>(DefaultEdge.class);
149         cycleDetector = new CycleDetector<ResolutionPlugIn, DefaultEdge>(dependencyGraph);
150 
151         Lock readLock = getReadWriteLock().readLock();
152         readLock.lock();
153         try {
154             for (AttributeDefinition definition : getAttributeDefinitions().values()) {
155                 addVertex(dependencyGraph, definition);
156             }
157 
158             for (DataConnector connector : getDataConnectors().values()) {
159                 addVertex(dependencyGraph, connector);
160             }
161 
162             if (cycleDetector.detectCycles()) {
163                 throw new AttributeResolutionException(getId()
164                         + "configuration contains a resolution plug-in dependency loop.");
165             }
166         } finally {
167             readLock.unlock();
168         }
169     }
170 
171     /**
172      * Resolves the principal name for the subject of the request.
173      * 
174      * @param requestContext current request context
175      * 
176      * @return principal name for the subject of the request
177      * 
178      * @throws AttributeResolutionException thrown if the subject identifier information can not be resolved into a
179      *             principal name
180      */
181     public String resolvePrincipalName(SAMLProfileRequestContext requestContext) throws AttributeResolutionException {
182         String nameIdFormat = getNameIdentifierFormat(requestContext.getSubjectNameIdentifier());
183 
184         log.debug("Resolving principal name from name identifier of format: {}", nameIdFormat);
185 
186         PrincipalConnector effectiveConnector = null;
187         for (PrincipalConnector connector : principalConnectors.values()) {
188             if (connector.getFormat().equals(nameIdFormat)) {
189                 if (connector.getRelyingParties().contains(requestContext.getInboundMessageIssuer())) {
190                     effectiveConnector = connector;
191                     break;
192                 }
193 
194                 if (connector.getRelyingParties().isEmpty()) {
195                     effectiveConnector = connector;
196                 }
197             }
198         }
199 
200         if (effectiveConnector == null) {
201             throw new AttributeResolutionException(
202                     "No principal connector available to resolve a subject name with format " + nameIdFormat
203                             + " for relying party " + requestContext.getInboundMessageIssuer());
204         }
205         log.debug("Using principal connector {} to resolve principal name.", effectiveConnector.getId());
206         effectiveConnector = new ContextualPrincipalConnector(effectiveConnector);
207 
208         ShibbolethResolutionContext resolutionContext = new ShibbolethResolutionContext(requestContext);
209 
210         // resolve all the connectors dependencies
211         resolveDependencies(effectiveConnector, resolutionContext);
212 
213         return effectiveConnector.resolve(resolutionContext);
214     }
215 
216     /**
217      * Gets the format of the name identifier used to identify the subject.
218      * 
219      * @param nameIdentifier name identifier used to identify the subject
220      * 
221      * @return format of the name identifier used to identify the subject
222      */
223     protected String getNameIdentifierFormat(SAMLObject nameIdentifier) {
224         String subjectNameFormat = null;
225 
226         if (nameIdentifier instanceof NameIdentifier) {
227             NameIdentifier identifier = (NameIdentifier) nameIdentifier;
228             subjectNameFormat = identifier.getFormat();
229         } else if (nameIdentifier instanceof NameID) {
230             NameID identifier = (NameID) nameIdentifier;
231             subjectNameFormat = identifier.getFormat();
232         }
233 
234         if (DatatypeHelper.isEmpty(subjectNameFormat)) {
235             subjectNameFormat = "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified";
236         }
237 
238         return subjectNameFormat;
239     }
240 
241     /**
242      * Resolves the attributes requested in the resolution context or all attributes if no specific attributes were
243      * requested. This method does not remove dependency only attributes or attributes that do not contain values.
244      * 
245      * @param resolutionContext current resolution context
246      * 
247      * @return resolved attributes
248      * 
249      * @throws AttributeResolutionException thrown if the attributes could not be resolved
250      */
251     protected Map<String, BaseAttribute> resolveAttributes(ShibbolethResolutionContext resolutionContext)
252             throws AttributeResolutionException {
253         Collection<String> attributeIDs = resolutionContext.getAttributeRequestContext().getRequestedAttributesIds();
254         Map<String, BaseAttribute> resolvedAttributes = new HashMap<String, BaseAttribute>();
255 
256         // if no attributes requested, then resolve everything
257         if (attributeIDs == null || attributeIDs.isEmpty()) {
258             log.debug("Specific attributes for principal {} were not requested, resolving all attributes.",
259                     resolutionContext.getAttributeRequestContext().getPrincipalName());
260             attributeIDs = getAttributeDefinitions().keySet();
261         }
262 
263         Lock readLock = getReadWriteLock().readLock();
264         readLock.lock();
265         for (String attributeID : attributeIDs) {
266             BaseAttribute resolvedAttribute = resolveAttribute(attributeID, resolutionContext);
267             if (resolvedAttribute != null) {
268                 resolvedAttributes.put(resolvedAttribute.getId(), resolvedAttribute);
269             }
270         }
271         readLock.unlock();
272 
273         return resolvedAttributes;
274     }
275 
276     /**
277      * Resolve the {@link AttributeDefinition} which has the specified ID. The definition is then added to the
278      * {@link ShibbolethResolutionContext} for use by other {@link ResolutionPlugIn}s and the resolution of the
279      * specified definition is added to <code>resolvedAttributes</code> to be returned by the resolver.
280      * 
281      * @param attributeID id of the attribute definition to resolve
282      * @param resolutionContext resolution context that we are working in
283      * 
284      * @return resolution of the specified attribute definition
285      * 
286      * @throws AttributeResolutionException if unable to resolve the requested attribute definition
287      */
288     protected BaseAttribute resolveAttribute(String attributeID, ShibbolethResolutionContext resolutionContext)
289             throws AttributeResolutionException {
290 
291         AttributeDefinition definition = resolutionContext.getResolvedAttributeDefinitions().get(attributeID);
292 
293         if (definition == null) {
294             log.debug("Resolving attribute {} for principal {}", attributeID, resolutionContext
295                     .getAttributeRequestContext().getPrincipalName());
296 
297             definition = getAttributeDefinitions().get(attributeID);
298             if (definition == null) {
299                 log.warn("{} requested attribute {} but no attribute definition exists for that attribute",
300                         resolutionContext.getAttributeRequestContext().getInboundMessageIssuer(), attributeID);
301                 return null;
302             } else {
303                 // wrap attribute definition for use within the given resolution context
304                 definition = new ContextualAttributeDefinition(definition);
305 
306                 // register definition as resolved for this resolution context
307                 resolutionContext.getResolvedPlugins().put(attributeID, definition);
308             }
309         }
310 
311         // resolve all the definitions dependencies
312         resolveDependencies(definition, resolutionContext);
313 
314         // return the actual resolution of the definition
315         BaseAttribute attribute = definition.resolve(resolutionContext);
316         log.debug("Resolved attribute {} containing {} values", attributeID, attribute.getValues().size());
317         return attribute;
318     }
319 
320     /**
321      * Resolve the {@link DataConnector} which has the specified ID and add it to the resolution context.
322      * 
323      * @param connectorID id of the data connector to resolve
324      * @param resolutionContext resolution context that we are working in
325      * 
326      * @throws AttributeResolutionException if unable to resolve the requested connector
327      */
328     protected void resolveDataConnector(String connectorID, ShibbolethResolutionContext resolutionContext)
329             throws AttributeResolutionException {
330 
331         DataConnector dataConnector = resolutionContext.getResolvedDataConnectors().get(connectorID);
332 
333         if (dataConnector == null) {
334             log.debug("Resolving data connector {} for principal {}", connectorID, resolutionContext
335                     .getAttributeRequestContext().getPrincipalName());
336 
337             dataConnector = getDataConnectors().get(connectorID);
338             if (dataConnector == null) {
339                 log.warn("{} requested to resolve data connector {} but does not have such a data connector", getId(),
340                         connectorID);
341             } else {
342                 // wrap connector for use within the given resolution context
343                 dataConnector = new ContextualDataConnector(dataConnector);
344 
345                 // register connector as resolved for this resolution context
346                 resolutionContext.getResolvedPlugins().put(connectorID, dataConnector);
347             }
348         }
349 
350         // resolve all the connectors dependencies
351         resolveDependencies(dataConnector, resolutionContext);
352 
353         try {
354             dataConnector.resolve(resolutionContext);
355         } catch (AttributeResolutionException e) {
356             String failoverDataConnectorId = dataConnector.getFailoverDependencyId();
357             if (DatatypeHelper.isEmpty(failoverDataConnectorId)) {
358                 throw e;
359             }
360 
361             log.error("Received the following error from data connector " + dataConnector.getId()
362                     + ", trying its failover connector " + failoverDataConnectorId, e);
363             resolveDataConnector(failoverDataConnectorId, resolutionContext);
364 
365             DataConnector failoverConnector = resolutionContext.getResolvedDataConnectors()
366                     .get(failoverDataConnectorId);
367             log.debug("Using failover connector {} in place of {} for the remainder of this resolution",
368                     failoverConnector.getId(), connectorID);
369             resolutionContext.getResolvedPlugins().put(connectorID, failoverConnector);
370         }
371     }
372 
373     /**
374      * Resolves all the dependencies for a given plugin.
375      * 
376      * @param plugin plugin whose dependencies should be resolved
377      * @param resolutionContext current resolution context
378      * 
379      * @throws AttributeResolutionException thrown if there is a problem resolving a dependency
380      */
381     protected void resolveDependencies(ResolutionPlugIn<?> plugin, ShibbolethResolutionContext resolutionContext)
382             throws AttributeResolutionException {
383 
384         for (String dependency : plugin.getDependencyIds()) {
385             if (dataConnectors.containsKey(dependency)) {
386                 resolveDataConnector(dependency, resolutionContext);
387             } else if (definitions.containsKey(dependency)) {
388                 resolveAttribute(dependency, resolutionContext);
389             }
390         }
391     }
392 
393     /**
394      * Removes attributes that contain no values or those which are dependency only.
395      * 
396      * @param resolvedAttributes attribute set to clean up
397      * @param resolutionContext current resolution context
398      */
399     protected void cleanResolvedAttributes(Map<String, BaseAttribute> resolvedAttributes,
400             ShibbolethResolutionContext resolutionContext) {
401         AttributeDefinition attributeDefinition;
402 
403         Iterator<Entry<String, BaseAttribute>> attributeItr = resolvedAttributes.entrySet().iterator();
404         BaseAttribute<?> resolvedAttribute;
405         Set<Object> values;
406         while (attributeItr.hasNext()) {
407             resolvedAttribute = attributeItr.next().getValue();
408 
409             // remove nulls
410             if (resolvedAttribute == null) {
411                 attributeItr.remove();
412                 continue;
413             }
414 
415             // remove dependency-only attributes
416             attributeDefinition = getAttributeDefinitions().get(resolvedAttribute.getId());
417             if (attributeDefinition.isDependencyOnly()) {
418                 log.debug("Removing dependency-only attribute {} from resolution result for principal {}.",
419                         resolvedAttribute.getId(), resolutionContext.getAttributeRequestContext().getPrincipalName());
420                 attributeItr.remove();
421                 continue;
422             }
423 
424             // remove value-less attributes
425             if (resolvedAttribute.getValues().size() == 0) {
426                 log.debug("Removing attribute {} from resolution result for principal {}.  It contains no values.",
427                         resolvedAttribute.getId(), resolutionContext.getAttributeRequestContext().getPrincipalName());
428                 attributeItr.remove();
429                 continue;
430             }
431 
432             // remove duplicate attribute values
433             Iterator<?> valueItr = resolvedAttribute.getValues().iterator();
434             values = new HashSet<Object>();
435             while (valueItr.hasNext()) {
436                 Object value = valueItr.next();
437                 if (!values.add(value)) {
438                     log.debug("Removing duplicate value {} of attribute {} from resolution result", value,
439                             resolvedAttribute.getId());
440                     valueItr.remove();
441                 }
442             }
443         }
444     }
445 
446     /**
447      * Add a resolution plug-in and dependencies to a directed graph.
448      * 
449      * @param graph directed graph
450      * @param plugin plug-in to add
451      */
452     protected void addVertex(DirectedGraph<ResolutionPlugIn, DefaultEdge> graph, ResolutionPlugIn<?> plugin) {
453         graph.addVertex(plugin);
454         ResolutionPlugIn<?> dependency = null;
455 
456         // add edges for dependencies
457         for (String id : plugin.getDependencyIds()) {
458             if (dataConnectors.containsKey(id)) {
459                 dependency = dataConnectors.get(id);
460             } else if (definitions.containsKey(id)) {
461                 dependency = definitions.get(id);
462             }
463 
464             if (dependency != null) {
465                 graph.addVertex(dependency);
466                 graph.addEdge(plugin, dependency);
467             }
468         }
469     }
470 
471     /** {@inheritDoc} */
472     protected void onNewContextCreated(ApplicationContext newServiceContext) throws ServiceException {
473         String[] beanNames;
474 
475         Map<String, DataConnector> oldDataConnectors = dataConnectors;
476         Map<String, DataConnector> newDataConnectors = new HashMap<String, DataConnector>();
477         DataConnector dConnector;
478         beanNames = newServiceContext.getBeanNamesForType(DataConnector.class);
479         log.debug("Loading {} data connectors", beanNames.length);
480         for (String beanName : beanNames) {
481             dConnector = (DataConnector) newServiceContext.getBean(beanName);
482             newDataConnectors.put(dConnector.getId(), dConnector);
483         }
484 
485         Map<String, AttributeDefinition> oldAttributeDefinitions = definitions;
486         Map<String, AttributeDefinition> newAttributeDefinitions = new HashMap<String, AttributeDefinition>();
487         AttributeDefinition aDefinition;
488         beanNames = newServiceContext.getBeanNamesForType(AttributeDefinition.class);
489         log.debug("Loading {} attribute definitions", beanNames.length);
490         for (String beanName : beanNames) {
491             aDefinition = (AttributeDefinition) newServiceContext.getBean(beanName);
492             newAttributeDefinitions.put(aDefinition.getId(), aDefinition);
493         }
494 
495         Map<String, PrincipalConnector> oldPrincipalConnectors = principalConnectors;
496         Map<String, PrincipalConnector> newPrincipalConnectors = new HashMap<String, PrincipalConnector>();
497         PrincipalConnector pConnector;
498         beanNames = newServiceContext.getBeanNamesForType(PrincipalConnector.class);
499         log.debug("Loading {} principal connectors", beanNames.length);
500         for (String beanName : beanNames) {
501             pConnector = (PrincipalConnector) newServiceContext.getBean(beanName);
502             newPrincipalConnectors.put(pConnector.getId(), pConnector);
503         }
504 
505         try {
506             dataConnectors = newDataConnectors;
507             definitions = newAttributeDefinitions;
508             principalConnectors = newPrincipalConnectors;
509             validate();
510         } catch (AttributeResolutionException e) {
511             dataConnectors = oldDataConnectors;
512             definitions = oldAttributeDefinitions;
513             principalConnectors = oldPrincipalConnectors;
514             throw new ServiceException(getId() + " configuration is not valid, retaining old configuration", e);
515         }
516     }
517 }