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
56   * refine 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: {}", attributeRequestContext
137                 .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                 plugin.validate();
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      * Resolves the principal name for the subject of the request.
164      * 
165      * @param requestContext current request context
166      * 
167      * @return principal name for the subject of the request
168      * 
169      * @throws AttributeResolutionException thrown if the subject identifier information can not be resolved into a
170      *             principal name
171      */
172     public String resolvePrincipalName(SAMLProfileRequestContext requestContext) throws AttributeResolutionException {
173         String nameIdFormat = getNameIdentifierFormat(requestContext.getSubjectNameIdentifier());
174 
175         log.debug("Resolving principal name from name identifier of format: {}", nameIdFormat);
176 
177         PrincipalConnector effectiveConnector = null;
178         for (PrincipalConnector connector : principalConnectors.values()) {
179             if (connector.getFormat().equals(nameIdFormat)) {
180                 if (connector.getRelyingParties().contains(requestContext.getInboundMessageIssuer())) {
181                     effectiveConnector = connector;
182                     break;
183                 }
184 
185                 if (connector.getRelyingParties().isEmpty()) {
186                     effectiveConnector = connector;
187                 }
188             }
189         }
190 
191         if (effectiveConnector == null) {
192             throw new AttributeResolutionException(
193                     "No principal connector available to resolve a subject name with format " + nameIdFormat
194                             + " for relying party " + requestContext.getInboundMessageIssuer());
195         }
196         log.debug("Using principal connector {} to resolve principal name.", effectiveConnector.getId());
197         effectiveConnector = new ContextualPrincipalConnector(effectiveConnector);
198 
199         ShibbolethResolutionContext resolutionContext = new ShibbolethResolutionContext(requestContext);
200 
201         // resolve all the connectors dependencies
202         resolveDependencies(effectiveConnector, resolutionContext);
203 
204         return effectiveConnector.resolve(resolutionContext);
205     }
206 
207     /**
208      * Gets the format of the name identifier used to identify the subject.
209      * 
210      * @param nameIdentifier name identifier used to identify the subject
211      * 
212      * @return format of the name identifier used to identify the subject
213      */
214     protected String getNameIdentifierFormat(SAMLObject nameIdentifier) {
215         String subjectNameFormat = null;
216 
217         if (nameIdentifier instanceof NameIdentifier) {
218             NameIdentifier identifier = (NameIdentifier) nameIdentifier;
219             subjectNameFormat = identifier.getFormat();
220         } else if (nameIdentifier instanceof NameID) {
221             NameID identifier = (NameID) nameIdentifier;
222             subjectNameFormat = identifier.getFormat();
223         }
224 
225         if (DatatypeHelper.isEmpty(subjectNameFormat)) {
226             subjectNameFormat = "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified";
227         }
228 
229         return subjectNameFormat;
230     }
231 
232     /**
233      * Resolves the attributes requested in the resolution context or all attributes if no specific attributes were
234      * requested. This method does not remove dependency only attributes or attributes that do not contain values.
235      * 
236      * @param resolutionContext current resolution context
237      * 
238      * @return resolved attributes
239      * 
240      * @throws AttributeResolutionException thrown if the attributes could not be resolved
241      */
242     protected Map<String, BaseAttribute> resolveAttributes(ShibbolethResolutionContext resolutionContext)
243             throws AttributeResolutionException {
244         Collection<String> attributeIDs = resolutionContext.getAttributeRequestContext().getRequestedAttributesIds();
245         Map<String, BaseAttribute> resolvedAttributes = new HashMap<String, BaseAttribute>();
246 
247         // if no attributes requested, then resolve everything
248         if (attributeIDs == null || attributeIDs.isEmpty()) {
249             log.debug("Specific attributes for principal {} were not requested, resolving all attributes.",
250                     resolutionContext.getAttributeRequestContext().getPrincipalName());
251             attributeIDs = getAttributeDefinitions().keySet();
252         }
253 
254         Lock readLock = getReadWriteLock().readLock();
255         readLock.lock();
256 	try {
257 	    for (String attributeID : attributeIDs) {
258 		BaseAttribute resolvedAttribute = resolveAttribute(attributeID, resolutionContext);
259 		if (resolvedAttribute != null) {
260 		    resolvedAttributes.put(resolvedAttribute.getId(), resolvedAttribute);
261 		}
262 	    }
263 	} finally {
264 	    readLock.unlock();
265 	}
266 
267         return resolvedAttributes;
268     }
269 
270     /**
271      * Resolve the {@link AttributeDefinition} which has the specified ID. The definition is then added to the
272      * {@link ShibbolethResolutionContext} for use by other {@link ResolutionPlugIn}s and the resolution of the
273      * specified definition is added to <code>resolvedAttributes</code> to be returned by the resolver.
274      * 
275      * @param attributeID id of the attribute definition to resolve
276      * @param resolutionContext resolution context that we are working in
277      * 
278      * @return resolution of the specified attribute definition
279      * 
280      * @throws AttributeResolutionException if unable to resolve the requested attribute definition
281      */
282     protected BaseAttribute resolveAttribute(String attributeID, ShibbolethResolutionContext resolutionContext)
283             throws AttributeResolutionException {
284 
285         AttributeDefinition definition = resolutionContext.getResolvedAttributeDefinitions().get(attributeID);
286 
287         if (definition == null) {
288             log.debug("Resolving attribute {} for principal {}", attributeID, resolutionContext
289                     .getAttributeRequestContext().getPrincipalName());
290 
291             definition = getAttributeDefinitions().get(attributeID);
292             if (definition == null) {
293                 log.warn("{} requested attribute {} but no attribute definition exists for that attribute",
294                         resolutionContext.getAttributeRequestContext().getInboundMessageIssuer(), attributeID);
295                 return null;
296             } else {
297                 // wrap attribute definition for use within the given resolution context
298                 definition = new ContextualAttributeDefinition(definition);
299 
300                 // register definition as resolved for this resolution context
301                 resolutionContext.getResolvedPlugins().put(attributeID, definition);
302             }
303         }
304 
305         // resolve all the definitions dependencies
306         resolveDependencies(definition, resolutionContext);
307 
308         // return the actual resolution of the definition
309         BaseAttribute attribute = definition.resolve(resolutionContext);
310         log.debug("Resolved attribute {} containing {} values", attributeID, attribute.getValues().size());
311         return attribute;
312     }
313 
314     /**
315      * Resolve the {@link DataConnector} which has the specified ID and add it to the resolution context.
316      * 
317      * @param connectorID id of the data connector to resolve
318      * @param resolutionContext resolution context that we are working in
319      * 
320      * @throws AttributeResolutionException if unable to resolve the requested connector
321      */
322     protected void resolveDataConnector(String connectorID, ShibbolethResolutionContext resolutionContext)
323             throws AttributeResolutionException {
324 
325         DataConnector dataConnector = resolutionContext.getResolvedDataConnectors().get(connectorID);
326 
327         if (dataConnector == null) {
328             log.debug("Resolving data connector {} for principal {}", connectorID, resolutionContext
329                     .getAttributeRequestContext().getPrincipalName());
330 
331             dataConnector = getDataConnectors().get(connectorID);
332             if (dataConnector == null) {
333                 log.warn("{} requested to resolve data connector {} but does not have such a data connector", getId(),
334                         connectorID);
335             } else {
336                 // wrap connector for use within the given resolution context
337                 dataConnector = new ContextualDataConnector(dataConnector);
338 
339                 // register connector as resolved for this resolution context
340                 resolutionContext.getResolvedPlugins().put(connectorID, dataConnector);
341             }
342         }
343 
344         // resolve all the connectors dependencies
345         resolveDependencies(dataConnector, resolutionContext);
346 
347         try {
348             dataConnector.resolve(resolutionContext);
349         } catch (AttributeResolutionException e) {
350             String failoverDataConnectorId = dataConnector.getFailoverDependencyId();
351             
352             if (DatatypeHelper.isEmpty(failoverDataConnectorId)) {
353                 log.error("Received the following error from data connector " + dataConnector.getId()
354                         + ", no failover data connector available", e);
355                 throw e;
356             }
357 
358             log.warn("Received the following error from data connector " + dataConnector.getId()
359                     + ", trying its failover connector " + failoverDataConnectorId, e.getMessage());
360             log.debug("Error recieved from data connector " + dataConnector.getId(), e);
361             resolveDataConnector(failoverDataConnectorId, resolutionContext);
362 
363             DataConnector failoverConnector = resolutionContext.getResolvedDataConnectors()
364                     .get(failoverDataConnectorId);
365             log.debug("Using failover connector {} in place of {} for the remainder of this resolution",
366                     failoverConnector.getId(), connectorID);
367             resolutionContext.getResolvedPlugins().put(connectorID, failoverConnector);
368         }
369     }
370 
371     /**
372      * Resolves all the dependencies for a given plugin.
373      * 
374      * @param plugin plugin whose dependencies should be resolved
375      * @param resolutionContext current resolution context
376      * 
377      * @throws AttributeResolutionException thrown if there is a problem resolving a dependency
378      */
379     protected void resolveDependencies(ResolutionPlugIn<?> plugin, ShibbolethResolutionContext resolutionContext)
380             throws AttributeResolutionException {
381 
382         for (String dependency : plugin.getDependencyIds()) {
383             if (dataConnectors.containsKey(dependency)) {
384                 resolveDataConnector(dependency, resolutionContext);
385             } else if (definitions.containsKey(dependency)) {
386                 resolveAttribute(dependency, resolutionContext);
387             }
388         }
389     }
390 
391     /**
392      * Removes attributes that contain no values or those which are dependency only.
393      * 
394      * @param resolvedAttributes attribute set to clean up
395      * @param resolutionContext current resolution context
396      */
397     protected void cleanResolvedAttributes(Map<String, BaseAttribute> resolvedAttributes,
398             ShibbolethResolutionContext resolutionContext) {
399         AttributeDefinition attributeDefinition;
400 
401         Iterator<Entry<String, BaseAttribute>> attributeItr = resolvedAttributes.entrySet().iterator();
402         BaseAttribute<?> resolvedAttribute;
403         Set<Object> values;
404         while (attributeItr.hasNext()) {
405             resolvedAttribute = attributeItr.next().getValue();
406 
407             // remove nulls
408             if (resolvedAttribute == null) {
409                 attributeItr.remove();
410                 continue;
411             }
412 
413             // remove dependency-only attributes
414             attributeDefinition = getAttributeDefinitions().get(resolvedAttribute.getId());
415             if (attributeDefinition.isDependencyOnly()) {
416                 log.debug("Removing dependency-only attribute {} from resolution result for principal {}.",
417                         resolvedAttribute.getId(), resolutionContext.getAttributeRequestContext().getPrincipalName());
418                 attributeItr.remove();
419                 continue;
420             }
421 
422             // remove value-less attributes
423             if (resolvedAttribute.getValues().size() == 0) {
424                 log.debug("Removing attribute {} from resolution result for principal {}.  It contains no values.",
425                         resolvedAttribute.getId(), resolutionContext.getAttributeRequestContext().getPrincipalName());
426                 attributeItr.remove();
427                 continue;
428             }
429 
430             // remove duplicate attribute values
431             Iterator<?> valueItr = resolvedAttribute.getValues().iterator();
432             values = new HashSet<Object>();
433             while (valueItr.hasNext()) {
434                 Object value = valueItr.next();
435                 if (!values.add(value)) {
436                     log.debug("Removing duplicate value {} of attribute {} from resolution result", value,
437                             resolvedAttribute.getId());
438                     valueItr.remove();
439                 }
440             }
441         }
442     }
443 
444     /**
445      * Add a resolution plug-in and dependencies to a directed graph.
446      * 
447      * @param graph directed graph
448      * @param plugin plug-in to add
449      */
450     protected void addVertex(DirectedGraph<ResolutionPlugIn, DefaultEdge> graph, ResolutionPlugIn<?> plugin) {
451         graph.addVertex(plugin);
452         ResolutionPlugIn<?> dependency = null;
453 
454         // add edges for dependencies
455         for (String id : plugin.getDependencyIds()) {
456             if (dataConnectors.containsKey(id)) {
457                 dependency = dataConnectors.get(id);
458             } else if (definitions.containsKey(id)) {
459                 dependency = definitions.get(id);
460             }
461 
462             if (dependency != null) {
463                 graph.addVertex(dependency);
464                 graph.addEdge(plugin, dependency);
465             }
466         }
467     }
468 
469     /** {@inheritDoc} */
470     protected void onNewContextCreated(ApplicationContext newServiceContext) throws ServiceException {
471         String[] beanNames;
472 
473         Map<String, DataConnector> oldDataConnectors = dataConnectors;
474         Map<String, DataConnector> newDataConnectors = new HashMap<String, DataConnector>();
475         DataConnector dConnector;
476         beanNames = newServiceContext.getBeanNamesForType(DataConnector.class);
477         log.debug("Loading {} data connectors", beanNames.length);
478         for (String beanName : beanNames) {
479             dConnector = (DataConnector) newServiceContext.getBean(beanName);
480             newDataConnectors.put(dConnector.getId(), dConnector);
481         }
482 
483         Map<String, AttributeDefinition> oldAttributeDefinitions = definitions;
484         Map<String, AttributeDefinition> newAttributeDefinitions = new HashMap<String, AttributeDefinition>();
485         AttributeDefinition aDefinition;
486         beanNames = newServiceContext.getBeanNamesForType(AttributeDefinition.class);
487         log.debug("Loading {} attribute definitions", beanNames.length);
488         for (String beanName : beanNames) {
489             aDefinition = (AttributeDefinition) newServiceContext.getBean(beanName);
490             newAttributeDefinitions.put(aDefinition.getId(), aDefinition);
491         }
492 
493         Map<String, PrincipalConnector> oldPrincipalConnectors = principalConnectors;
494         Map<String, PrincipalConnector> newPrincipalConnectors = new HashMap<String, PrincipalConnector>();
495         PrincipalConnector pConnector;
496         beanNames = newServiceContext.getBeanNamesForType(PrincipalConnector.class);
497         log.debug("Loading {} principal connectors", beanNames.length);
498         for (String beanName : beanNames) {
499             pConnector = (PrincipalConnector) newServiceContext.getBean(beanName);
500             newPrincipalConnectors.put(pConnector.getId(), pConnector);
501         }
502 
503         try {
504             dataConnectors = newDataConnectors;
505             definitions = newAttributeDefinitions;
506             principalConnectors = newPrincipalConnectors;
507             validate();
508         } catch (AttributeResolutionException e) {
509             dataConnectors = oldDataConnectors;
510             definitions = oldAttributeDefinitions;
511             principalConnectors = oldPrincipalConnectors;
512             throw new ServiceException(getId() + " configuration is not valid, retaining old configuration", e);
513         }
514     }
515 }