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