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 if (DatatypeHelper.isEmpty(failoverDataConnectorId)) {
349 throw e;
350 }
351
352 log.error("Received the following error from data connector " + dataConnector.getId()
353 + ", trying its failover connector " + failoverDataConnectorId, e);
354 resolveDataConnector(failoverDataConnectorId, resolutionContext);
355
356 DataConnector failoverConnector = resolutionContext.getResolvedDataConnectors()
357 .get(failoverDataConnectorId);
358 log.debug("Using failover connector {} in place of {} for the remainder of this resolution",
359 failoverConnector.getId(), connectorID);
360 resolutionContext.getResolvedPlugins().put(connectorID, failoverConnector);
361 }
362 }
363
364
365
366
367
368
369
370
371
372 protected void resolveDependencies(ResolutionPlugIn<?> plugin, ShibbolethResolutionContext resolutionContext)
373 throws AttributeResolutionException {
374
375 for (String dependency : plugin.getDependencyIds()) {
376 if (dataConnectors.containsKey(dependency)) {
377 resolveDataConnector(dependency, resolutionContext);
378 } else if (definitions.containsKey(dependency)) {
379 resolveAttribute(dependency, resolutionContext);
380 }
381 }
382 }
383
384
385
386
387
388
389
390 protected void cleanResolvedAttributes(Map<String, BaseAttribute> resolvedAttributes,
391 ShibbolethResolutionContext resolutionContext) {
392 AttributeDefinition attributeDefinition;
393
394 Iterator<Entry<String, BaseAttribute>> attributeItr = resolvedAttributes.entrySet().iterator();
395 BaseAttribute<?> resolvedAttribute;
396 Set<Object> values;
397 while (attributeItr.hasNext()) {
398 resolvedAttribute = attributeItr.next().getValue();
399
400
401 if (resolvedAttribute == null) {
402 attributeItr.remove();
403 continue;
404 }
405
406
407 attributeDefinition = getAttributeDefinitions().get(resolvedAttribute.getId());
408 if (attributeDefinition.isDependencyOnly()) {
409 log.debug("Removing dependency-only attribute {} from resolution result for principal {}.",
410 resolvedAttribute.getId(), resolutionContext.getAttributeRequestContext().getPrincipalName());
411 attributeItr.remove();
412 continue;
413 }
414
415
416 if (resolvedAttribute.getValues().size() == 0) {
417 log.debug("Removing attribute {} from resolution result for principal {}. It contains no values.",
418 resolvedAttribute.getId(), resolutionContext.getAttributeRequestContext().getPrincipalName());
419 attributeItr.remove();
420 continue;
421 }
422
423
424 Iterator<?> valueItr = resolvedAttribute.getValues().iterator();
425 values = new HashSet<Object>();
426 while (valueItr.hasNext()) {
427 Object value = valueItr.next();
428 if (!values.add(value)) {
429 log.debug("Removing duplicate value {} of attribute {} from resolution result", value,
430 resolvedAttribute.getId());
431 valueItr.remove();
432 }
433 }
434 }
435 }
436
437
438
439
440
441
442
443 protected void addVertex(DirectedGraph<ResolutionPlugIn, DefaultEdge> graph, ResolutionPlugIn<?> plugin) {
444 graph.addVertex(plugin);
445 ResolutionPlugIn<?> dependency = null;
446
447
448 for (String id : plugin.getDependencyIds()) {
449 if (dataConnectors.containsKey(id)) {
450 dependency = dataConnectors.get(id);
451 } else if (definitions.containsKey(id)) {
452 dependency = definitions.get(id);
453 }
454
455 if (dependency != null) {
456 graph.addVertex(dependency);
457 graph.addEdge(plugin, dependency);
458 }
459 }
460 }
461
462
463 protected void onNewContextCreated(ApplicationContext newServiceContext) throws ServiceException {
464 String[] beanNames;
465
466 Map<String, DataConnector> oldDataConnectors = dataConnectors;
467 Map<String, DataConnector> newDataConnectors = new HashMap<String, DataConnector>();
468 DataConnector dConnector;
469 beanNames = newServiceContext.getBeanNamesForType(DataConnector.class);
470 log.debug("Loading {} data connectors", beanNames.length);
471 for (String beanName : beanNames) {
472 dConnector = (DataConnector) newServiceContext.getBean(beanName);
473 newDataConnectors.put(dConnector.getId(), dConnector);
474 }
475
476 Map<String, AttributeDefinition> oldAttributeDefinitions = definitions;
477 Map<String, AttributeDefinition> newAttributeDefinitions = new HashMap<String, AttributeDefinition>();
478 AttributeDefinition aDefinition;
479 beanNames = newServiceContext.getBeanNamesForType(AttributeDefinition.class);
480 log.debug("Loading {} attribute definitions", beanNames.length);
481 for (String beanName : beanNames) {
482 aDefinition = (AttributeDefinition) newServiceContext.getBean(beanName);
483 newAttributeDefinitions.put(aDefinition.getId(), aDefinition);
484 }
485
486 Map<String, PrincipalConnector> oldPrincipalConnectors = principalConnectors;
487 Map<String, PrincipalConnector> newPrincipalConnectors = new HashMap<String, PrincipalConnector>();
488 PrincipalConnector pConnector;
489 beanNames = newServiceContext.getBeanNamesForType(PrincipalConnector.class);
490 log.debug("Loading {} principal connectors", beanNames.length);
491 for (String beanName : beanNames) {
492 pConnector = (PrincipalConnector) newServiceContext.getBean(beanName);
493 newPrincipalConnectors.put(pConnector.getId(), pConnector);
494 }
495
496 try {
497 dataConnectors = newDataConnectors;
498 definitions = newAttributeDefinitions;
499 principalConnectors = newPrincipalConnectors;
500 validate();
501 } catch (AttributeResolutionException e) {
502 dataConnectors = oldDataConnectors;
503 definitions = oldAttributeDefinitions;
504 principalConnectors = oldPrincipalConnectors;
505 throw new ServiceException(getId() + " configuration is not valid, retaining old configuration", e);
506 }
507 }
508 }