733363e803e37f384b2c3e469a126ef85eec0712
[java-idp.git] / src / edu / internet2 / middleware / shibboleth / aa / attrresolv / AttributeResolver.java
1 /*
2  * Copyright [2005] [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.aa.attrresolv;
18
19 import java.io.IOException;
20 import java.security.Principal;
21 import java.util.ArrayList;
22 import java.util.Arrays;
23 import java.util.Collection;
24 import java.util.HashMap;
25 import java.util.HashSet;
26 import java.util.Iterator;
27 import java.util.List;
28 import java.util.Map;
29 import java.util.Set;
30
31 import javax.naming.directory.Attributes;
32 import javax.naming.directory.BasicAttributes;
33
34 import org.apache.log4j.Logger;
35 import org.opensaml.SAMLException;
36 import org.w3c.dom.Document;
37 import org.w3c.dom.Element;
38 import org.w3c.dom.Node;
39 import org.w3c.dom.NodeList;
40 import org.xml.sax.InputSource;
41 import org.xml.sax.SAXException;
42
43 import edu.internet2.middleware.shibboleth.aa.attrresolv.provider.ValueHandler;
44 import edu.internet2.middleware.shibboleth.common.ShibResource;
45 import edu.internet2.middleware.shibboleth.common.ShibResource.ResourceNotAvailableException;
46 import edu.internet2.middleware.shibboleth.idp.IdPConfig;
47 import edu.internet2.middleware.shibboleth.xml.Parser;
48
49 /**
50  * An engine for obtaining attribute values for specified principals. Attributes values are resolved using a directed
51  * graph of pluggable attribute definitions and data connectors.
52  * 
53  * @author Walter Hoehn (wassa@columbia.edu)
54  */
55
56 public class AttributeResolver {
57
58         private static Logger log = Logger.getLogger(AttributeResolver.class.getName());
59         private HashMap<String, ResolutionPlugIn> plugIns = new HashMap<String, ResolutionPlugIn>();
60         private ResolverCache resolverCache = new ResolverCache();
61         public static final String resolverNamespace = "urn:mace:shibboleth:resolver:1.0";
62
63         public AttributeResolver(IdPConfig configuration) throws AttributeResolverException {
64
65                 if (configuration == null || configuration.getResolverConfigLocation() == null) {
66                         log.error("No Attribute Resolver configuration file specified.");
67                         throw new AttributeResolverException("No Attribute Resolver configuration file specified.");
68                 }
69
70                 loadConfig(configuration.getResolverConfigLocation());
71         }
72
73         public AttributeResolver(String configFileLocation) throws AttributeResolverException {
74
75                 loadConfig(configFileLocation);
76         }
77
78         private void loadConfig(String configFile) throws AttributeResolverException {
79
80                 try {
81                         ShibResource config = new ShibResource(configFile, this.getClass());
82                         Parser.DOMParser parser = new Parser.DOMParser(true);
83                         parser.parse(new InputSource(config.getInputStream()));
84                         loadConfig(parser.getDocument());
85
86                 } catch (ResourceNotAvailableException e) {
87                         log.error("No Attribute Resolver configuration could be loaded from (" + configFile + "): " + e);
88                         throw new AttributeResolverException("No Attribute Resolver configuration found.");
89                 } catch (SAXException e) {
90                         log.error("Error parsing Attribute Resolver Configuration file: " + e);
91                         throw new AttributeResolverException("Error parsing Attribute Resolver Configuration file.");
92                 } catch (IOException e) {
93                         log.error("Error reading Attribute Resolver Configuration file: " + e);
94                         throw new AttributeResolverException("Error reading Attribute Resolver Configuration file.");
95                 } catch (SAMLException e) {
96                         log.error("Error parsing Attribute Resolver Configuration file: " + e);
97                         throw new AttributeResolverException("Error parsing Attribute Resolver Configuration file.");
98                 }
99         }
100
101         private void loadConfig(Document document) throws AttributeResolverException {
102
103                 log.info("Configuring Attribute Resolver.");
104                 if (!document.getDocumentElement().getTagName().equals("AttributeResolver")) {
105                         log.error("Configuration must include <AttributeResolver> as the root node.");
106                         throw new AttributeResolverException("Cannot load Attribute Resolver.");
107                 }
108
109                 NodeList plugInNodes = document.getElementsByTagNameNS(resolverNamespace, "AttributeResolver").item(0)
110                                 .getChildNodes();
111                 if (plugInNodes.getLength() <= 0) {
112                         log.error("Configuration inclues no PlugIn definitions.");
113                         throw new AttributeResolverException("Cannot load Attribute Resolver.");
114                 }
115                 for (int i = 0; plugInNodes.getLength() > i; i++) {
116                         if (plugInNodes.item(i).getNodeType() == Node.ELEMENT_NODE) {
117                                 try {
118                                         log.info("Found a PlugIn. Loading...");
119                                         ResolutionPlugIn plugIn = ResolutionPlugInFactory.createPlugIn((Element) plugInNodes.item(i));
120                                         registerPlugIn(plugIn, plugIn.getId());
121                                 } catch (DuplicatePlugInException dpe) {
122                                         log.warn("Skipping PlugIn: " + dpe.getMessage());
123                                 } catch (ClassCastException cce) {
124                                         log.error("Problem realizing PlugIn configuration" + cce.getMessage());
125                                 } catch (AttributeResolverException are) {
126                                         log.warn("Skipping PlugIn: " + ((Element) plugInNodes.item(i)).getAttribute("id"));
127                                 }
128                         }
129                 }
130
131                 verifyPlugIns();
132                 log.info("Configuration complete.");
133         }
134
135         private void verifyPlugIns() throws AttributeResolverException {
136
137                 log.info("Verifying PlugIn graph consitency.");
138                 Set<String> inconsistent = new HashSet<String>();
139                 Iterator registered = plugIns.keySet().iterator();
140
141                 while (registered.hasNext()) {
142                         ResolutionPlugIn plugIn = plugIns.get((String) registered.next());
143                         log.debug("Checking PlugIn (" + plugIn.getId() + ") for consistency.");
144                         verifyPlugIn(plugIn, new HashSet<String>(), inconsistent);
145                 }
146
147                 if (!inconsistent.isEmpty()) {
148                         log.info("Unloading inconsistent PlugIns.");
149                         Iterator inconsistentIt = inconsistent.iterator();
150                         while (inconsistentIt.hasNext()) {
151                                 plugIns.remove(inconsistentIt.next());
152                         }
153                 }
154
155                 if (plugIns.size() < 1) {
156                         log.error("Failed to load any PlugIn definitions.");
157                         throw new AttributeResolverException("Cannot load Attribute Resolver.");
158                 }
159
160         }
161
162         private void verifyPlugIn(ResolutionPlugIn plugIn, Set<String> verifyChain, Set<String> inconsistent) {
163
164                 // Short-circuit if we have already found this PlugIn to be inconsistent
165                 if (inconsistent.contains(plugIn.getId())) { return; }
166
167                 // Make sure that we don't have a circular dependency
168                 if (verifyChain.contains(plugIn.getId())) {
169                         log.error("The PlugIn (" + plugIn.getId()
170                                         + ") is inconsistent.  It is involved in a circular dependency chain.");
171                         inconsistent.add(plugIn.getId());
172                         return;
173                 }
174
175                 // Recursively go through all DataConnector dependencies and make sure all are registered and consistent.
176                 List<String> depends = new ArrayList<String>();
177                 depends.addAll(Arrays.asList(plugIn.getDataConnectorDependencyIds()));
178                 Iterator<String> dependsIt = depends.iterator();
179                 while (dependsIt.hasNext()) {
180                         String key = dependsIt.next();
181                         if (!plugIns.containsKey(key)) {
182                                 log.error("The PlugIn (" + plugIn.getId() + ") is inconsistent.  It depends on a PlugIn (" + key
183                                                 + ") that is not registered.");
184                                 inconsistent.add(plugIn.getId());
185                                 return;
186                         }
187
188                         ResolutionPlugIn dependent = plugIns.get(key);
189                         if (!(dependent instanceof DataConnectorPlugIn)) {
190                                 log.error("The PlugIn (" + plugIn.getId() + ") is inconsistent.  It depends on a PlugIn (" + key
191                                                 + ") that is mislabeled as an DataConnectorPlugIn.");
192                                 inconsistent.add(plugIn.getId());
193                                 return;
194                         }
195
196                         verifyChain.add(plugIn.getId());
197                         verifyPlugIn(dependent, verifyChain, inconsistent);
198
199                         if (inconsistent.contains(key)) {
200                                 log.error("The PlugIn (" + plugIn.getId() + ") is inconsistent.  It depends on a PlugIn (" + key
201                                                 + ") that is inconsistent.");
202                                 inconsistent.add(plugIn.getId());
203                                 return;
204                         }
205                 }
206                 verifyChain.remove(plugIn.getId());
207
208                 // Recursively go through all AttributeDefinition dependencies and make sure all are registered and consistent.
209                 depends.clear();
210                 depends.addAll(Arrays.asList(plugIn.getAttributeDefinitionDependencyIds()));
211                 dependsIt = depends.iterator();
212                 while (dependsIt.hasNext()) {
213                         String key = (String) dependsIt.next();
214
215                         if (!plugIns.containsKey(key)) {
216                                 log.error("The PlugIn (" + plugIn.getId() + ") is inconsistent.  It depends on a PlugIn (" + key
217                                                 + ") that is not registered.");
218                                 inconsistent.add(plugIn.getId());
219                                 return;
220                         }
221
222                         ResolutionPlugIn dependent = plugIns.get(key);
223                         if (!(dependent instanceof AttributeDefinitionPlugIn)) {
224                                 log.error("The PlugIn (" + plugIn.getId() + ") is inconsistent.  It depends on a PlugIn (" + key
225                                                 + ") that is mislabeled as an AttributeDefinitionPlugIn.");
226                                 inconsistent.add(plugIn.getId());
227                                 return;
228                         }
229
230                         verifyChain.add(plugIn.getId());
231                         verifyPlugIn(dependent, verifyChain, inconsistent);
232
233                         if (inconsistent.contains(key)) {
234                                 log.error("The PlugIn (" + plugIn.getId() + ") is inconsistent.  It depends on a PlugIn (" + key
235                                                 + ") that is inconsistent.");
236                                 inconsistent.add(plugIn.getId());
237                                 return;
238                         }
239                 }
240                 verifyChain.remove(plugIn.getId());
241
242                 // Check the failover dependency, if there is one.
243                 if (plugIn instanceof DataConnectorPlugIn) {
244                         String key = ((DataConnectorPlugIn) plugIn).getFailoverDependencyId();
245                         if (key != null) {
246                                 if (!plugIns.containsKey(key)) {
247                                         log.error("The PlugIn (" + plugIn.getId() + ") is inconsistent.  It depends on a PlugIn (" + key
248                                                         + ") that is not registered.");
249                                         inconsistent.add(plugIn.getId());
250                                         return;
251                                 }
252
253                                 ResolutionPlugIn dependent = plugIns.get(key);
254                                 if (!(dependent instanceof DataConnectorPlugIn)) {
255                                         log.error("The PlugIn (" + plugIn.getId()
256                                                         + ") is inconsistent.  It depends on a fail-over PlugIn (" + key
257                                                         + ") that is not a DataConnectorPlugIn.");
258                                         inconsistent.add(plugIn.getId());
259                                         return;
260                                 }
261
262                                 verifyChain.add(plugIn.getId());
263                                 verifyPlugIn(dependent, verifyChain, inconsistent);
264
265                                 if (inconsistent.contains(key)) {
266                                         log.error("The PlugIn (" + plugIn.getId() + ") is inconsistent.  It depends on a PlugIn (" + key
267                                                         + ") that is inconsistent.");
268                                         inconsistent.add(plugIn.getId());
269                                         return;
270                                 }
271                         }
272                 }
273                 verifyChain.remove(plugIn.getId());
274         }
275
276         private void registerPlugIn(ResolutionPlugIn connector, String id) throws DuplicatePlugInException {
277
278                 if (plugIns.containsKey(id)) {
279                         log.error("A PlugIn is already registered with the Id (" + id + ").");
280                         throw new DuplicatePlugInException("Found a duplicate PlugIn Id.");
281                 }
282                 plugIns.put(id, connector);
283                 log.info("Registered PlugIn: (" + id + ")");
284
285         }
286
287         /**
288          * Resolve a set of attributes for a particular principal and requester.
289          * 
290          * @param principal
291          *            the <code>Principal</code> for which the attributes should be resolved
292          * @param requester
293          *            the name of the requesting entity
294          * @param responding
295          *            the name of the entity responding to the request
296          * @param attributes
297          *            the set of attributes to be resolved
298          */
299         public void resolveAttributes(Principal principal, String requester, String responder, Map attributes) {
300
301                 HashMap requestCache = new HashMap<String, ResolverAttribute>();
302                 Iterator<ResolverAttribute> iterator = attributes.values().iterator();
303
304                 while (iterator.hasNext()) {
305                         ResolverAttribute attribute = iterator.next();
306                         try {
307                                 if (plugIns.get(attribute.getName()) == null) {
308                                         log.warn("No PlugIn registered for attribute: (" + attribute.getName() + ")");
309                                         iterator.remove();
310                                 } else {
311                                         log.info("Resolving attribute: (" + attribute.getName() + ")");
312                                         if (attribute.resolved()) {
313                                                 log.debug("Attribute (" + attribute.getName()
314                                                                 + ") already resolved for this request.  No need for further resolution.");
315
316                                         } else {
317                                                 resolveAttribute(attribute, principal, requester, responder, requestCache, attributes);
318                                         }
319
320                                         if (!attribute.hasValues()) {
321                                                 iterator.remove();
322                                         }
323                                 }
324                         } catch (ResolutionPlugInException rpe) {
325                                 log.error("Problem encountered while resolving attribute: (" + attribute.getName() + "): " + rpe);
326                                 iterator.remove();
327                         }
328                 }
329         }
330
331         public Collection<String> listRegisteredAttributeDefinitionPlugIns() {
332
333                 log.debug("Listing available Attribute Definition PlugIns.");
334                 Set<String> found = new HashSet<String>();
335                 Iterator registered = plugIns.keySet().iterator();
336
337                 while (registered.hasNext()) {
338                         ResolutionPlugIn plugIn = plugIns.get((String) registered.next());
339                         if (plugIn instanceof AttributeDefinitionPlugIn) {
340                                 found.add(((AttributeDefinitionPlugIn) plugIn).getId());
341                         }
342                 }
343
344                 if (log.isDebugEnabled()) {
345                         for (Iterator iterator = found.iterator(); iterator.hasNext();) {
346                                 log.debug("Found registered Attribute Definition: " + (String) iterator.next());
347                         }
348                 }
349                 return found;
350         }
351
352         private Attributes resolveConnector(String connector, Principal principal, String requester, String responder,
353                         Map requestCache, Map<String, ResolverAttribute> requestedAttributes) throws ResolutionPlugInException {
354
355                 DataConnectorPlugIn currentDefinition = (DataConnectorPlugIn) plugIns.get(connector);
356
357                 // Check to see if we have already resolved the connector during this request
358                 if (requestCache.containsKey(currentDefinition.getId())) {
359                         log.debug("Connector (" + currentDefinition.getId()
360                                         + ") already resolved for this request, using cached version");
361                         return (Attributes) requestCache.get(currentDefinition.getId());
362                 }
363
364                 // Check to see if we have a cached resolution for this connector
365                 if (currentDefinition.getTTL() > 0) {
366                         Attributes cachedAttributes = resolverCache.getResolvedConnector(principal, currentDefinition.getId());
367                         if (cachedAttributes != null) {
368                                 log.debug("Connector (" + currentDefinition.getId()
369                                                 + ") resolution cached from a previous request, using cached version");
370                                 return cachedAttributes;
371                         }
372                 }
373
374                 // Resolve all attribute dependencies
375                 String[] attributeDependencies = currentDefinition.getAttributeDefinitionDependencyIds();
376                 Dependencies depends = new Dependencies();
377
378                 for (int i = 0; attributeDependencies.length > i; i++) {
379                         log.debug("Connector (" + currentDefinition.getId() + ") depends on attribute (" + attributeDependencies[i]
380                                         + ").");
381                         ResolverAttribute dependant = requestedAttributes.get(attributeDependencies[i]);
382                         if (dependant == null) {
383                                 dependant = new DependentOnlyResolutionAttribute(attributeDependencies[i]);
384                         }
385                         resolveAttribute(dependant, principal, requester, responder, requestCache, requestedAttributes);
386                         depends.addAttributeResolution(attributeDependencies[i], dependant);
387
388                 }
389
390                 // Resolve all connector dependencies
391                 String[] connectorDependencies = currentDefinition.getDataConnectorDependencyIds();
392                 for (int i = 0; connectorDependencies.length > i; i++) {
393                         log.debug("Connector (" + currentDefinition.getId() + ") depends on connector (" + connectorDependencies[i]
394                                         + ").");
395                         depends.addConnectorResolution(connectorDependencies[i], resolveConnector(connectorDependencies[i],
396                                         principal, requester, responder, requestCache, requestedAttributes));
397                 }
398
399                 // Resolve the connector
400                 Attributes resolvedAttributes = null;
401                 try {
402                         resolvedAttributes = currentDefinition.resolve(principal, requester, responder, depends);
403
404                         // Add attribute resolution to cache
405                         if (currentDefinition.getTTL() > 0) {
406                                 resolverCache.cacheConnectorResolution(principal, currentDefinition.getId(),
407                                                 currentDefinition.getTTL(), resolvedAttributes);
408                         }
409                 } catch (ResolutionPlugInException e) {
410                         // Something went wrong, so check for a fail-over...
411                         if (currentDefinition.getFailoverDependencyId() != null) {
412                                 log.warn("Connector (" + currentDefinition.getId() + ") failed, invoking failover dependency");
413                                 resolvedAttributes = resolveConnector(currentDefinition.getFailoverDependencyId(), principal,
414                                                 requester, responder, requestCache, requestedAttributes);
415                         } else if (currentDefinition.getPropagateErrors()) {
416                                 throw e;
417                         } else {
418                                 log.warn("Connector (" + currentDefinition.getId()
419                                                 + ") returning empty attribute set instead of propagating error: " + e);
420                                 resolvedAttributes = new BasicAttributes();
421                         }
422                 }
423
424                 // Cache for this request
425                 requestCache.put(currentDefinition.getId(), resolvedAttributes);
426                 return resolvedAttributes;
427         }
428
429         private void resolveAttribute(ResolverAttribute attribute, Principal principal, String requester, String responder,
430                         Map<String, ResolverAttribute> requestCache, Map<String, ResolverAttribute> requestedAttributes)
431                         throws ResolutionPlugInException {
432
433                 AttributeDefinitionPlugIn currentDefinition = (AttributeDefinitionPlugIn) plugIns.get(attribute.getName());
434
435                 // Check to see if we have already resolved the attribute during this request
436                 // (this checks dependency-only attributes and attributes resolved with no values
437                 if (requestCache.containsKey(currentDefinition.getId())) {
438                         log.debug("Attribute (" + currentDefinition.getId()
439                                         + ") already resolved for this request, using cached version");
440                         attribute.resolveFromCached((ResolverAttribute) requestCache.get(currentDefinition.getId()));
441                         return;
442                 }
443
444                 // Check to see if we have already resolved the attribute during this request
445                 // (this checks attributes that were submitted to the AR for resolution)
446                 ResolverAttribute requestedAttribute = requestedAttributes.get(currentDefinition.getId());
447                 if (requestedAttribute != null && requestedAttribute.resolved()) {
448                         attribute.resolveFromCached(requestedAttribute);
449                         return;
450                 }
451
452                 // Check to see if we have a cached resolution for this attribute
453                 if (currentDefinition.getTTL() > 0) {
454                         ResolverAttribute cachedAttribute = resolverCache
455                                         .getResolvedAttribute(principal, currentDefinition.getId());
456                         if (cachedAttribute != null) {
457                                 log.debug("Attribute (" + currentDefinition.getId()
458                                                 + ") resolution cached from a previous request, using cached version");
459                                 attribute.resolveFromCached(cachedAttribute);
460                                 return;
461                         }
462                 }
463
464                 // Resolve all attribute dependencies
465                 Dependencies depends = new Dependencies();
466                 String[] attributeDependencies = currentDefinition.getAttributeDefinitionDependencyIds();
467
468                 boolean dependancyOnly = false;
469                 for (int i = 0; attributeDependencies.length > i; i++) {
470                         log.debug("Attribute (" + attribute.getName() + ") depends on attribute (" + attributeDependencies[i]
471                                         + ").");
472                         ResolverAttribute dependant = requestedAttributes.get(attributeDependencies[i]);
473                         if (dependant == null) {
474                                 dependancyOnly = true;
475                                 dependant = new DependentOnlyResolutionAttribute(attributeDependencies[i]);
476                         }
477                         resolveAttribute(dependant, principal, requester, responder, requestCache, requestedAttributes);
478                         depends.addAttributeResolution(attributeDependencies[i], dependant);
479
480                 }
481
482                 // Resolve all connector dependencies
483                 String[] connectorDependencies = currentDefinition.getDataConnectorDependencyIds();
484                 for (int i = 0; connectorDependencies.length > i; i++) {
485                         log.debug("Attribute (" + attribute.getName() + ") depends on connector (" + connectorDependencies[i]
486                                         + ").");
487                         depends.addConnectorResolution(connectorDependencies[i], resolveConnector(connectorDependencies[i],
488                                         principal, requester, responder, requestCache, requestedAttributes));
489                 }
490
491                 // Resolve the attribute
492                 try {
493                         currentDefinition.resolve(attribute, principal, requester, responder, depends);
494
495                         // Add attribute resolution to cache
496                         if (currentDefinition.getTTL() > 0) {
497                                 resolverCache.cacheAttributeResolution(principal, attribute.getName(), currentDefinition.getTTL(),
498                                                 attribute);
499                         }
500                 } catch (ResolutionPlugInException e) {
501                         if (currentDefinition.getPropagateErrors()) {
502                                 throw e;
503                         } else {
504                                 log.warn("Attribute (" + currentDefinition.getId()
505                                                 + ") returning no values instead of propagating error: " + e);
506                         }
507                 }
508
509                 // If necessary, cache for this request
510                 if (dependancyOnly || !attribute.hasValues()) {
511                         requestCache.put(currentDefinition.getId(), attribute);
512                 }
513         }
514
515         private class DuplicatePlugInException extends Exception {
516
517                 public DuplicatePlugInException(String message) {
518
519                         super(message);
520                 }
521         }
522
523         class DependentOnlyResolutionAttribute implements ResolverAttribute {
524
525                 String name;
526                 ArrayList values = new ArrayList();
527                 boolean resolved = false;
528
529                 DependentOnlyResolutionAttribute(String name) {
530
531                         this.name = name;
532                 }
533
534                 public String getName() {
535
536                         return name;
537                 }
538
539                 public boolean resolved() {
540
541                         return resolved;
542                 }
543
544                 public void setResolved() {
545
546                         resolved = true;
547                 }
548
549                 public void resolveFromCached(ResolverAttribute attribute) {
550
551                 }
552
553                 public void setLifetime(long lifetime) {
554
555                 }
556
557                 public void setNamespace(String namespace) {
558
559                 }
560
561                 public long getLifetime() {
562
563                         return 0;
564                 }
565
566                 public void addValue(Object value) {
567
568                         values.add(value);
569                 }
570
571                 public Iterator getValues() {
572
573                         return values.iterator();
574                 }
575
576                 public boolean hasValues() {
577
578                         if (values.isEmpty()) { return false; }
579                         return true;
580                 }
581
582                 public void registerValueHandler(ValueHandler handler) {
583
584                 }
585
586                 public ValueHandler getRegisteredValueHandler() {
587
588                         return null;
589                 }
590         }
591
592         /**
593          * Cleanup resources that won't be released when this object is garbage-collected
594          */
595         public void destroy() {
596
597                 resolverCache.destroy();
598         }
599 }