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.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         for (String attributeID : attributeIDs) {
257             BaseAttribute resolvedAttribute = resolveAttribute(attributeID, resolutionContext);
258             if (resolvedAttribute != null) {
259                 resolvedAttributes.put(resolvedAttribute.getId(), resolvedAttribute);
260             }
261         }
262         readLock.unlock();
263 
264         return resolvedAttributes;
265     }
266 
267     /**
268      * Resolve the {@link AttributeDefinition} which has the specified ID. The definition is then added to the
269      * {@link ShibbolethResolutionContext} for use by other {@link ResolutionPlugIn}s and the resolution of the
270      * specified definition is added to <code>resolvedAttributes</code> to be returned by the resolver.
271      * 
272      * @param attributeID id of the attribute definition to resolve
273      * @param resolutionContext resolution context that we are working in
274      * 
275      * @return resolution of the specified attribute definition
276      * 
277      * @throws AttributeResolutionException if unable to resolve the requested attribute definition
278      */
279     protected BaseAttribute resolveAttribute(String attributeID, ShibbolethResolutionContext resolutionContext)
280             throws AttributeResolutionException {
281 
282         AttributeDefinition definition = resolutionContext.getResolvedAttributeDefinitions().get(attributeID);
283 
284         if (definition == null) {
285             log.debug("Resolving attribute {} for principal {}", attributeID, resolutionContext
286                     .getAttributeRequestContext().getPrincipalName());
287 
288             definition = getAttributeDefinitions().get(attributeID);
289             if (definition == null) {
290                 log.warn("{} requested attribute {} but no attribute definition exists for that attribute",
291                         resolutionContext.getAttributeRequestContext().getInboundMessageIssuer(), attributeID);
292                 return null;
293             } else {
294                 // wrap attribute definition for use within the given resolution context
295                 definition = new ContextualAttributeDefinition(definition);
296 
297                 // register definition as resolved for this resolution context
298                 resolutionContext.getResolvedPlugins().put(attributeID, definition);
299             }
300         }
301 
302         // resolve all the definitions dependencies
303         resolveDependencies(definition, resolutionContext);
304 
305         // return the actual resolution of the definition
306         BaseAttribute attribute = definition.resolve(resolutionContext);
307         log.debug("Resolved attribute {} containing {} values", attributeID, attribute.getValues().size());
308         return attribute;
309     }
310 
311     /**
312      * Resolve the {@link DataConnector} which has the specified ID and add it to the resolution context.
313      * 
314      * @param connectorID id of the data connector to resolve
315      * @param resolutionContext resolution context that we are working in
316      * 
317      * @throws AttributeResolutionException if unable to resolve the requested connector
318      */
319     protected void resolveDataConnector(String connectorID, ShibbolethResolutionContext resolutionContext)
320             throws AttributeResolutionException {
321 
322         DataConnector dataConnector = resolutionContext.getResolvedDataConnectors().get(connectorID);
323 
324         if (dataConnector == null) {
325             log.debug("Resolving data connector {} for principal {}", connectorID, resolutionContext
326                     .getAttributeRequestContext().getPrincipalName());
327 
328             dataConnector = getDataConnectors().get(connectorID);
329             if (dataConnector == null) {
330                 log.warn("{} requested to resolve data connector {} but does not have such a data connector", getId(),
331                         connectorID);
332             } else {
333                 // wrap connector for use within the given resolution context
334                 dataConnector = new ContextualDataConnector(dataConnector);
335 
336                 // register connector as resolved for this resolution context
337                 resolutionContext.getResolvedPlugins().put(connectorID, dataConnector);
338             }
339         }
340 
341         // resolve all the connectors dependencies
342         resolveDependencies(dataConnector, resolutionContext);
343 
344         try {
345             dataConnector.resolve(resolutionContext);
346         } catch (AttributeResolutionException e) {
347             String failoverDataConnectorId = dataConnector.getFailoverDependencyId();
348             
349             if (DatatypeHelper.isEmpty(failoverDataConnectorId)) {
350                 log.error("Received the following error from data connector " + dataConnector.getId()
351                         + ", no failover data connector available", e);
352                 throw e;
353             }
354 
355             log.warn("Received the following error from data connector " + dataConnector.getId()
356                     + ", trying its failover connector " + failoverDataConnectorId, e.getMessage());
357             log.debug("Error recieved from data connector " + dataConnector.getId(), e);
358             resolveDataConnector(failoverDataConnectorId, resolutionContext);
359 
360             DataConnector failoverConnector = resolutionContext.getResolvedDataConnectors()
361                     .get(failoverDataConnectorId);
362             log.debug("Using failover connector {} in place of {} for the remainder of this resolution",
363                     failoverConnector.getId(), connectorID);
364             resolutionContext.getResolvedPlugins().put(connectorID, failoverConnector);
365         }
366     }
367 
368     /**
369      * Resolves all the dependencies for a given plugin.
370      * 
371      * @param plugin plugin whose dependencies should be resolved
372      * @param resolutionContext current resolution context
373      * 
374      * @throws AttributeResolutionException thrown if there is a problem resolving a dependency
375      */
376     protected void resolveDependencies(ResolutionPlugIn<?> plugin, ShibbolethResolutionContext resolutionContext)
377             throws AttributeResolutionException {
378 
379         for (String dependency : plugin.getDependencyIds()) {
380             if (dataConnectors.containsKey(dependency)) {
381                 resolveDataConnector(dependency, resolutionContext);
382             } else if (definitions.containsKey(dependency)) {
383                 resolveAttribute(dependency, resolutionContext);
384             }
385         }
386     }
387 
388     /**
389      * Removes attributes that contain no values or those which are dependency only.
390      * 
391      * @param resolvedAttributes attribute set to clean up
392      * @param resolutionContext current resolution context
393      */
394     protected void cleanResolvedAttributes(Map<String, BaseAttribute> resolvedAttributes,
395             ShibbolethResolutionContext resolutionContext) {
396         AttributeDefinition attributeDefinition;
397 
398         Iterator<Entry<String, BaseAttribute>> attributeItr = resolvedAttributes.entrySet().iterator();
399         BaseAttribute<?> resolvedAttribute;
400         Set<Object> values;
401         while (attributeItr.hasNext()) {
402             resolvedAttribute = attributeItr.next().getValue();
403 
404             // remove nulls
405             if (resolvedAttribute == null) {
406                 attributeItr.remove();
407                 continue;
408             }
409 
410             // remove dependency-only attributes
411             attributeDefinition = getAttributeDefinitions().get(resolvedAttribute.getId());
412             if (attributeDefinition.isDependencyOnly()) {
413                 log.debug("Removing dependency-only attribute {} from resolution result for principal {}.",
414                         resolvedAttribute.getId(), resolutionContext.getAttributeRequestContext().getPrincipalName());
415                 attributeItr.remove();
416                 continue;
417             }
418 
419             // remove value-less attributes
420             if (resolvedAttribute.getValues().size() == 0) {
421                 log.debug("Removing attribute {} from resolution result for principal {}.  It contains no values.",
422                         resolvedAttribute.getId(), resolutionContext.getAttributeRequestContext().getPrincipalName());
423                 attributeItr.remove();
424                 continue;
425             }
426 
427             // remove duplicate attribute values
428             Iterator<?> valueItr = resolvedAttribute.getValues().iterator();
429             values = new HashSet<Object>();
430             while (valueItr.hasNext()) {
431                 Object value = valueItr.next();
432                 if (!values.add(value)) {
433                     log.debug("Removing duplicate value {} of attribute {} from resolution result", value,
434                             resolvedAttribute.getId());
435                     valueItr.remove();
436                 }
437             }
438         }
439     }
440 
441     /**
442      * Add a resolution plug-in and dependencies to a directed graph.
443      * 
444      * @param graph directed graph
445      * @param plugin plug-in to add
446      */
447     protected void addVertex(DirectedGraph<ResolutionPlugIn, DefaultEdge> graph, ResolutionPlugIn<?> plugin) {
448         graph.addVertex(plugin);
449         ResolutionPlugIn<?> dependency = null;
450 
451         // add edges for dependencies
452         for (String id : plugin.getDependencyIds()) {
453             if (dataConnectors.containsKey(id)) {
454                 dependency = dataConnectors.get(id);
455             } else if (definitions.containsKey(id)) {
456                 dependency = definitions.get(id);
457             }
458 
459             if (dependency != null) {
460                 graph.addVertex(dependency);
461                 graph.addEdge(plugin, dependency);
462             }
463         }
464     }
465 
466     /** {@inheritDoc} */
467     protected void onNewContextCreated(ApplicationContext newServiceContext) throws ServiceException {
468         String[] beanNames;
469 
470         Map<String, DataConnector> oldDataConnectors = dataConnectors;
471         Map<String, DataConnector> newDataConnectors = new HashMap<String, DataConnector>();
472         DataConnector dConnector;
473         beanNames = newServiceContext.getBeanNamesForType(DataConnector.class);
474         log.debug("Loading {} data connectors", beanNames.length);
475         for (String beanName : beanNames) {
476             dConnector = (DataConnector) newServiceContext.getBean(beanName);
477             newDataConnectors.put(dConnector.getId(), dConnector);
478         }
479 
480         Map<String, AttributeDefinition> oldAttributeDefinitions = definitions;
481         Map<String, AttributeDefinition> newAttributeDefinitions = new HashMap<String, AttributeDefinition>();
482         AttributeDefinition aDefinition;
483         beanNames = newServiceContext.getBeanNamesForType(AttributeDefinition.class);
484         log.debug("Loading {} attribute definitions", beanNames.length);
485         for (String beanName : beanNames) {
486             aDefinition = (AttributeDefinition) newServiceContext.getBean(beanName);
487             newAttributeDefinitions.put(aDefinition.getId(), aDefinition);
488         }
489 
490         Map<String, PrincipalConnector> oldPrincipalConnectors = principalConnectors;
491         Map<String, PrincipalConnector> newPrincipalConnectors = new HashMap<String, PrincipalConnector>();
492         PrincipalConnector pConnector;
493         beanNames = newServiceContext.getBeanNamesForType(PrincipalConnector.class);
494         log.debug("Loading {} principal connectors", beanNames.length);
495         for (String beanName : beanNames) {
496             pConnector = (PrincipalConnector) newServiceContext.getBean(beanName);
497             newPrincipalConnectors.put(pConnector.getId(), pConnector);
498         }
499 
500         try {
501             dataConnectors = newDataConnectors;
502             definitions = newAttributeDefinitions;
503             principalConnectors = newPrincipalConnectors;
504             validate();
505         } catch (AttributeResolutionException e) {
506             dataConnectors = oldDataConnectors;
507             definitions = oldAttributeDefinitions;
508             principalConnectors = oldPrincipalConnectors;
509             throw new ServiceException(getId() + " configuration is not valid, retaining old configuration", e);
510         }
511     }
512 }