View Javadoc

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