1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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
55
56
57
58
59
60 public class ShibbolethAttributeResolver extends BaseReloadableService implements
61 AttributeResolver<SAMLProfileRequestContext> {
62
63
64 public static final Collection<Class> PLUGIN_TYPES = Arrays.asList(new Class[] {DataConnector.class,
65 AttributeDefinition.class, PrincipalConnector.class,});
66
67
68 private final Logger log = LoggerFactory.getLogger(ShibbolethAttributeResolver.class.getName());
69
70
71 private Map<String, DataConnector> dataConnectors;
72
73
74 private Map<String, AttributeDefinition> definitions;
75
76
77 private Map<String, PrincipalConnector> principalConnectors;
78
79
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
89
90
91
92 public Map<String, AttributeDefinition> getAttributeDefinitions() {
93 return definitions;
94 }
95
96
97
98
99
100
101 public Map<String, DataConnector> getDataConnectors() {
102 return dataConnectors;
103 }
104
105
106
107
108
109
110 public Map<String, PrincipalConnector> getPrincipalConnectors() {
111 return principalConnectors;
112 }
113
114
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
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
165
166
167
168
169
170
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
190
191
192
193
194
195
196
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
228 resolveDependencies(effectiveConnector, resolutionContext);
229
230 return effectiveConnector.resolve(resolutionContext);
231 }
232
233
234
235
236
237
238
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
260
261
262
263
264
265
266
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
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
298
299
300
301
302
303
304
305
306
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
324 definition = new ContextualAttributeDefinition(definition);
325
326
327 resolutionContext.getResolvedPlugins().put(attributeID, definition);
328 }
329 }
330
331
332 resolveDependencies(definition, resolutionContext);
333
334
335 BaseAttribute attribute = definition.resolve(resolutionContext);
336 log.debug("Resolved attribute {} containing {} values", attributeID, attribute.getValues().size());
337 return attribute;
338 }
339
340
341
342
343
344
345
346
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
363 dataConnector = new ContextualDataConnector(dataConnector);
364
365
366 resolutionContext.getResolvedPlugins().put(connectorID, dataConnector);
367 }
368 }
369
370
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
399
400
401
402
403
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
419
420
421
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
434 if (resolvedAttribute == null) {
435 attributeItr.remove();
436 continue;
437 }
438
439
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
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
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
475
476
477
478
479 protected void addVertex(DirectedGraph<ResolutionPlugIn, DefaultEdge> graph, ResolutionPlugIn<?> plugin) {
480 graph.addVertex(plugin);
481 ResolutionPlugIn<?> dependency = null;
482
483
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
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 }