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