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