Replace DOMParser with calls to pool or helper class.
[java-idp.git] / src / edu / internet2 / middleware / shibboleth / common / NameMapper.java
1 /*
2  * The Shibboleth License, Version 1. Copyright (c) 2002 University Corporation for Advanced Internet Development, Inc.
3  * All rights reserved Redistribution and use in source and binary forms, with or without modification, are permitted
4  * provided that the following conditions are met: Redistributions of source code must retain the above copyright
5  * notice, this list of conditions and the following disclaimer. Redistributions in binary form must reproduce the above
6  * copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials
7  * provided with the distribution, if any, must include the following acknowledgment: "This product includes software
8  * developed by the University Corporation for Advanced Internet Development <http://www.ucaid.edu> Internet2 Project.
9  * Alternately, this acknowledegement may appear in the software itself, if and wherever such third-party
10  * acknowledgments normally appear. Neither the name of Shibboleth nor the names of its contributors, nor Internet2, nor
11  * the University Corporation for Advanced Internet Development, Inc., nor UCAID may be used to endorse or promote
12  * products derived from this software without specific prior written permission. For written permission, please contact
13  * shibboleth@shibboleth.org Products derived from this software may not be called Shibboleth, Internet2, UCAID, or the
14  * University Corporation for Advanced Internet Development, nor may Shibboleth appear in their name, without prior
15  * written permission of the University Corporation for Advanced Internet Development. THIS SOFTWARE IS PROVIDED BY THE
16  * COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND WITH ALL FAULTS. ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
17  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT ARE
18  * DISCLAIMED AND THE ENTIRE RISK OF SATISFACTORY QUALITY, PERFORMANCE, ACCURACY, AND EFFORT IS WITH LICENSEE. IN NO
19  * EVENT SHALL THE COPYRIGHT OWNER, CONTRIBUTORS OR THE UNIVERSITY CORPORATION FOR ADVANCED INTERNET DEVELOPMENT, INC.
20  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
21  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
22  * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
23  * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
24  */
25
26 package edu.internet2.middleware.shibboleth.common;
27
28 import java.io.StringReader;
29 import java.lang.reflect.Constructor;
30 import java.net.URI;
31 import java.net.URISyntaxException;
32 import java.util.Collections;
33 import java.util.HashMap;
34 import java.util.Iterator;
35 import java.util.Map;
36
37 import org.apache.log4j.Logger;
38 import org.opensaml.SAMLException;
39 import org.opensaml.SAMLNameIdentifier;
40 import org.w3c.dom.Element;
41 import org.xml.sax.InputSource;
42
43 import edu.internet2.middleware.shibboleth.hs.provider.SharedMemoryShibHandle;
44 import edu.internet2.middleware.shibboleth.xml.Parser;
45
46 /**
47  * Facility for managing mappings from SAML Name Identifiers to local {@link AuthNPrincipal}objects. Mappings are
48  * registered by Name Identifier format and can be associated with a <code>String</code> id and recovered based on the
49  * same.
50  * 
51  * @author Walter Hoehn
52  * @see NameIdentifierMapping
53  */
54 public class NameMapper {
55
56         private static Logger log = Logger.getLogger(NameMapper.class.getName());
57         private Map byFormat = new HashMap();
58         private Map byId = new HashMap();
59         private static Map registeredMappingTypes = Collections.synchronizedMap(new HashMap());
60         /** true if mappings have been added */
61         protected boolean initialized = false;
62         /** Mapping to use if no other mappings have been added */
63         protected SharedMemoryShibHandle defaultMapping;
64
65         //Preload aliases for bundled mappings
66         static {
67                 try {
68                         registeredMappingTypes.put("CryptoHandleGenerator", Class
69                                         .forName("edu.internet2.middleware.shibboleth.hs.provider.CryptoShibHandle"));
70
71                         registeredMappingTypes.put("SharedMemoryShibHandle", Class
72                                         .forName("edu.internet2.middleware.shibboleth.hs.provider.SharedMemoryShibHandle"));
73
74                         registeredMappingTypes.put("Principal", Class
75                                         .forName("edu.internet2.middleware.shibboleth.hs.provider.PrincipalNameIdentifier"));
76
77                 } catch (ClassNotFoundException e) {
78                         log.error("Unable to pre-register Name mapping implementation types.");
79                 }
80         }
81
82         /**
83          * Constructs the name mapper and loads a default name mapping.
84          */
85         public NameMapper() {
86
87                 try {
88                         //Load the default mapping
89                         String rawConfig = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
90                                         + "<NameMapping format=\"urn:mace:shibboleth:1.0:nameIdentifier\"" + "          handleTTL=\"1800\"/>";
91                         Parser.DOMParser parser = new Parser.DOMParser(false);
92                         parser.parse(new InputSource(new StringReader(rawConfig)));
93                         defaultMapping = new SharedMemoryShibHandle(parser.getDocument().getDocumentElement());
94
95                 } catch (Exception e) {
96                         log.error("Unable to register default Name Identifier Mapping.");
97                         initialize();
98                 }
99         }
100
101         protected void initialize() {
102
103                 initialized = true;
104                 defaultMapping = null;
105         }
106
107         /**
108          * Constructs a {@link NameIdentifierMapping}based on XML configuration data and adds it to this {@link NameMapper},
109          * registering it according to its format.
110          * 
111          * @param e
112          *            An XML representation of a {@link NameIdentifierMapping}
113          * @throws NameIdentifierMappingException
114          *             If the mapping could not be constructed according to the supplied configuration
115          */
116         public void addNameMapping(Element e) throws NameIdentifierMappingException {
117
118                 if (!e.getLocalName().equals("NameMapping")) { throw new IllegalArgumentException(); }
119
120                 log.info("Found Name Mapping. Loading...");
121
122                 String type = ((Element) e).getAttribute("type");
123                 String implementation = ((Element) e).getAttribute("class");
124                 if (type != null && (!type.equals("")) && implementation != null && (!implementation.equals(""))) {
125                         log.error("Name Mapping has both a \"type\" and a \"class\" attribute. Only \"type\" will take effect.");
126                 }
127
128                 if (type != null && (!type.equals(""))) {
129
130                         Class registeredImplementation = (Class) registeredMappingTypes.get(type);
131                         if (registeredImplementation == null) {
132                                 log.error("Name Mapping refers to an unregistered implementation type.");
133                                 throw new NameIdentifierMappingException("Invalid mapping implementation specified.");
134                         }
135
136                         log.debug("Found type (" + type + ") registered with an implementation class of ("
137                                         + registeredImplementation.getName() + ").");
138                         addNameMapping(loadNameIdentifierMapping(registeredImplementation, e));
139
140                 } else if (implementation != null && (!implementation.equals(""))) {
141
142                         try {
143                                 Class implementorClass = Class.forName(implementation);
144                                 addNameMapping(loadNameIdentifierMapping(implementorClass, e));
145
146                         } catch (ClassNotFoundException cnfe) {
147                                 log.error("Name Mapping refers to an implementation class that cannot be loaded: " + cnfe);
148                                 throw new NameIdentifierMappingException("Invalid mapping implementation specified.");
149                         }
150
151                 } else {
152                         log.error("Name Mapping requires either a \"type\" or a \"class\" attribute.");
153                         throw new NameIdentifierMappingException("No mapping implementation specified.");
154                 }
155
156         }
157
158         /**
159          * Adds a {@link NameIdentifierMapping}to this name mapper, registering it according to its format.
160          * 
161          * @param mapping
162          *            the mapping to add
163          */
164         public void addNameMapping(NameIdentifierMapping mapping) {
165
166                 initialize();
167
168                 if (byFormat.containsKey(mapping.getNameIdentifierFormat())) {
169                         log.error("Attempted to register multiple Name Mappings with the same format.  Skipping duplicates...");
170                         return;
171                 }
172                 byFormat.put(mapping.getNameIdentifierFormat(), mapping);
173
174                 if (mapping.getId() != null && !mapping.getId().equals("")) {
175                         byId.put(mapping.getId(), mapping);
176                 }
177
178         }
179
180         /**
181          * Returns the {@link NameIdentifierMapping}registered for a given format.
182          * 
183          * @param format
184          *            the registered format
185          * @return the mapping or <code>null</code> if no mapping is registered for the given format
186          */
187         public NameIdentifierMapping getNameIdentifierMapping(URI format) {
188
189                 if (format.toString().equals("urn:mace:shibboleth:test:nameIdentifier")) { return new TestNameIdentifierMapping(); }
190
191                 if (!initialized) { return defaultMapping; }
192
193                 return (NameIdentifierMapping) byFormat.get(format);
194         }
195
196         /**
197          * Returns the <code>NameIdentifierMapping</code> registered for a given id
198          * 
199          * @param id
200          *            the registered id
201          * @return the mapping or <tt>null</tt> if no mapping is registered for the given id
202          */
203         public NameIdentifierMapping getNameIdentifierMappingById(String id) {
204
205                 if (id == null || id.equals("")) {
206                         if (!initialized) { return defaultMapping; }
207
208                         if (byFormat.size() == 1) {
209                                 Iterator values = byFormat.values().iterator();
210                                 Object mapping = values.next();
211                                 return (NameIdentifierMapping) mapping;
212                         }
213                 }
214
215                 return (NameIdentifierMapping) byId.get(id);
216         }
217
218         protected NameIdentifierMapping loadNameIdentifierMapping(Class implementation, Element config)
219                         throws NameIdentifierMappingException {
220
221                 try {
222                         Class[] params = new Class[]{Element.class};
223                         Constructor implementorConstructor = implementation.getConstructor(params);
224                         Object[] args = new Object[]{config};
225                         log.debug("Initializing Name Identifier Mapping of type (" + implementation.getName() + ").");
226                         return (NameIdentifierMapping) implementorConstructor.newInstance(args);
227
228                 } catch (NoSuchMethodException nsme) {
229                         log.error("Failed to instantiate a Name Identifier Mapping: NameIdentifierMapping "
230                                         + "implementation must contain a constructor that accepts an Element object for "
231                                         + "configuration data.");
232                         throw new NameIdentifierMappingException("Failed to instantiate a Name Identifier Mapping.");
233
234                 } catch (Exception e) {
235                         log.error("Failed to instantiate a Name Identifier Mapping: " + e + ":" + e.getCause());
236                         throw new NameIdentifierMappingException("Failed to instantiate a Name Identifier Mapping: " + e);
237
238                 }
239
240         }
241
242         /**
243          * Maps a SAML Name Identifier to a local principal using the appropriate registered mapping.
244          * 
245          * @param nameId
246          *            the SAML Name Identifier that should be converted
247          * @param sProv
248          *            the provider initiating the request
249          * @param idProv
250          *            the provider handling the request
251          * @return the local principal
252          * @throws NameIdentifierMappingException
253          *             If the {@link NameMapper}encounters an internal error
254          * @throws InvalidNameIdentifierException
255          *             If the {@link SAMLNameIdentifier}contains invalid data
256          */
257         public AuthNPrincipal getPrincipal(SAMLNameIdentifier nameId, ServiceProvider sProv, IdentityProvider idProv)
258                         throws NameIdentifierMappingException, InvalidNameIdentifierException {
259
260                 NameIdentifierMapping mapping = null;
261                 log.debug("Name Identifier format: (" + nameId.getFormat() + ").");
262                 try {
263                         mapping = getNameIdentifierMapping(new URI(nameId.getFormat()));
264                 } catch (URISyntaxException e) {
265                         log.error("Invalid Name Identifier format.");
266                 }
267                 if (mapping == null) { throw new NameIdentifierMappingException("Name Identifier format not registered."); }
268                 return mapping.getPrincipal(nameId, sProv, idProv);
269         }
270
271         /**
272          * Maps a local principal to a SAML Name Identifier using the mapping registered under a given id.
273          * 
274          * @param id
275          *            the id under which the effective <code>NameIdentifierMapping</code> is registered
276          * @param principal
277          *            the principal to map
278          * @param sProv
279          *            the provider initiating the request
280          * @param idProv
281          *            the provider handling the request
282          * @return
283          * @throws NameIdentifierMappingException
284          *             If the <code>NameMapper</code> encounters an internal error
285          */
286         public SAMLNameIdentifier getNameIdentifierName(String id, AuthNPrincipal principal, ServiceProvider sProv,
287                         IdentityProvider idProv) throws NameIdentifierMappingException {
288
289                 NameIdentifierMapping mapping = getNameIdentifierMappingById(id);
290
291                 if (mapping == null) { throw new NameIdentifierMappingException("Name Identifier id not registered."); }
292                 return mapping.getNameIdentifierName(principal, sProv, idProv);
293         }
294
295         /**
296          * <code>NameIdentifierMapping</code> implement that always maps to the same principal name. Used for testing.
297          */
298         public class TestNameIdentifierMapping implements NameIdentifierMapping {
299
300                 private TestNameIdentifierMapping() {
301
302                 //Constructor to prevent others from creating this class
303                 }
304
305                 /*
306                  * (non-Javadoc)
307                  * 
308                  * @see edu.internet2.middleware.shibboleth.common.NameIdentifierMapping#getNameIdentifierFormat()
309                  */
310                 public URI getNameIdentifierFormat() {
311
312                         try {
313                                 return new URI("urn:mace:shibboleth:test:nameIdentifier");
314                         } catch (URISyntaxException e) {
315                                 log.error("Name Mapping \"format\" is not a valid URI: " + e);
316                                 throw new RuntimeException("Internal error: Encountered an error generating a standard URI.");
317                         }
318                 }
319
320                 /*
321                  * (non-Javadoc)
322                  * 
323                  * @see edu.internet2.middleware.shibboleth.common.NameIdentifierMapping#getPrincipal(org.opensaml.SAMLNameIdentifier,
324                  *      edu.internet2.middleware.shibboleth.common.ServiceProvider,
325                  *      edu.internet2.middleware.shibboleth.common.IdentityProvider)
326                  */
327                 public AuthNPrincipal getPrincipal(SAMLNameIdentifier nameId, ServiceProvider sProv, IdentityProvider idProv)
328                                 throws NameIdentifierMappingException, InvalidNameIdentifierException {
329
330                         log.info("Request references built-in test principal.");
331
332                         if (idProv.getProviderId() == null || !idProv.getProviderId().equals(nameId.getNameQualifier())) {
333                                 log.error("The name qualifier (" + nameId.getNameQualifier()
334                                                 + ") for the referenced subject is not valid for this identity provider.");
335                                 throw new NameIdentifierMappingException("The name qualifier (" + nameId.getNameQualifier()
336                                                 + ") for the referenced subject is not valid for this identity provider.");
337                         }
338
339                         return new AuthNPrincipal("test-handle");
340                 }
341
342                 /*
343                  * (non-Javadoc)
344                  * 
345                  * @see edu.internet2.middleware.shibboleth.common.NameIdentifierMapping#destroy()
346                  */
347                 public void destroy() {
348
349                 //Nothing to do
350                 }
351
352                 /*
353                  * (non-Javadoc)
354                  * 
355                  * @see edu.internet2.middleware.shibboleth.common.NameIdentifierMapping#getId()
356                  */
357                 public String getId() {
358
359                         return null;
360                 }
361
362                 /*
363                  * (non-Javadoc)
364                  * 
365                  * @see edu.internet2.middleware.shibboleth.common.NameIdentifierMapping#getNameIdentifierName(edu.internet2.middleware.shibboleth.common.AuthNPrincipal,
366                  *      edu.internet2.middleware.shibboleth.common.ServiceProvider,
367                  *      edu.internet2.middleware.shibboleth.common.IdentityProvider)
368                  */
369                 public SAMLNameIdentifier getNameIdentifierName(AuthNPrincipal principal, ServiceProvider sProv,
370                                 IdentityProvider idProv) throws NameIdentifierMappingException {
371
372                         try {
373                                 return new SAMLNameIdentifier("test-handle", idProv.getProviderId(), getNameIdentifierFormat()
374                                                 .toString());
375                         } catch (SAMLException e) {
376                                 throw new NameIdentifierMappingException("Unable to generate Name Identifier: " + e);
377                         }
378                 }
379         }
380
381         /**
382          * Cleanup resources that won't be released when this object is garbage-collected
383          */
384         public void destroy() {
385
386                 Iterator mappingIterator = byFormat.values().iterator();
387                 while (mappingIterator.hasNext()) {
388                         ((NameIdentifierMapping) mappingIterator.next()).destroy();
389                 }
390         }
391 }