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.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
54
55
56
57
58
59 public class ShibbolethAttributeResolver extends BaseReloadableService implements
60 AttributeResolver<SAMLProfileRequestContext> {
61
62
63 public static final Collection<Class> PLUGIN_TYPES = Arrays.asList(new Class[] { DataConnector.class,
64 AttributeDefinition.class, PrincipalConnector.class, });
65
66
67 private final Logger log = LoggerFactory.getLogger(ShibbolethAttributeResolver.class.getName());
68
69
70 private Map<String, DataConnector> dataConnectors;
71
72
73 private Map<String, AttributeDefinition> definitions;
74
75
76 private Map<String, PrincipalConnector> principalConnectors;
77
78
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
88
89
90
91 public Map<String, AttributeDefinition> getAttributeDefinitions() {
92 return definitions;
93 }
94
95
96
97
98
99
100 public Map<String, DataConnector> getDataConnectors() {
101 return dataConnectors;
102 }
103
104
105
106
107
108
109 public Map<String, PrincipalConnector> getPrincipalConnectors() {
110 return principalConnectors;
111 }
112
113
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
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
164
165
166
167
168
169
170
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
202 resolveDependencies(effectiveConnector, resolutionContext);
203
204 return effectiveConnector.resolve(resolutionContext);
205 }
206
207
208
209
210
211
212
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
234
235
236
237
238
239
240
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
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
269
270
271
272
273
274
275
276
277
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
295 definition = new ContextualAttributeDefinition(definition);
296
297
298 resolutionContext.getResolvedPlugins().put(attributeID, definition);
299 }
300 }
301
302
303 resolveDependencies(definition, resolutionContext);
304
305
306 BaseAttribute attribute = definition.resolve(resolutionContext);
307 log.debug("Resolved attribute {} containing {} values", attributeID, attribute.getValues().size());
308 return attribute;
309 }
310
311
312
313
314
315
316
317
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
334 dataConnector = new ContextualDataConnector(dataConnector);
335
336
337 resolutionContext.getResolvedPlugins().put(connectorID, dataConnector);
338 }
339 }
340
341
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
370
371
372
373
374
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
390
391
392
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
405 if (resolvedAttribute == null) {
406 attributeItr.remove();
407 continue;
408 }
409
410
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
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
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
443
444
445
446
447 protected void addVertex(DirectedGraph<ResolutionPlugIn, DefaultEdge> graph, ResolutionPlugIn<?> plugin) {
448 graph.addVertex(plugin);
449 ResolutionPlugIn<?> dependency = null;
450
451
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
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 }