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.
26 package edu.internet2.middleware.shibboleth.aa.attrresolv.provider;
28 import java.io.IOException;
29 import java.net.Socket;
30 import java.security.GeneralSecurityException;
31 import java.security.Principal;
32 import java.security.PrivateKey;
33 import java.security.SecureRandom;
34 import java.security.cert.X509Certificate;
35 import java.util.HashMap;
36 import java.util.HashSet;
37 import java.util.Iterator;
38 import java.util.Properties;
41 import javax.naming.CommunicationException;
42 import javax.naming.Context;
43 import javax.naming.NamingEnumeration;
44 import javax.naming.NamingException;
45 import javax.naming.directory.Attribute;
46 import javax.naming.directory.Attributes;
47 import javax.naming.directory.BasicAttribute;
48 import javax.naming.directory.BasicAttributes;
49 import javax.naming.directory.InitialDirContext;
50 import javax.naming.directory.SearchControls;
51 import javax.naming.directory.SearchResult;
52 import javax.naming.ldap.InitialLdapContext;
53 import javax.naming.ldap.LdapContext;
54 import javax.naming.ldap.StartTlsRequest;
55 import javax.naming.ldap.StartTlsResponse;
56 import javax.net.ssl.KeyManager;
57 import javax.net.ssl.SSLContext;
58 import javax.net.ssl.SSLSocketFactory;
59 import javax.net.ssl.X509KeyManager;
61 import org.apache.log4j.Logger;
62 import org.w3c.dom.Element;
63 import org.w3c.dom.NodeList;
65 import edu.internet2.middleware.shibboleth.aa.attrresolv.AttributeResolver;
66 import edu.internet2.middleware.shibboleth.aa.attrresolv.DataConnectorPlugIn;
67 import edu.internet2.middleware.shibboleth.aa.attrresolv.Dependencies;
68 import edu.internet2.middleware.shibboleth.aa.attrresolv.ResolutionPlugInException;
69 import edu.internet2.middleware.shibboleth.common.Credential;
70 import edu.internet2.middleware.shibboleth.common.Credentials;
73 * <code>DataConnectorPlugIn</code> implementation that utilizes a user-specified JNDI <code>DirContext</code> to
74 * retrieve attribute data.
76 * @author Walter Hoehn (wassa@columbia.edu)
78 public class JNDIDirectoryDataConnector extends BaseDataConnector implements DataConnectorPlugIn {
80 private static Logger log = Logger.getLogger(JNDIDirectoryDataConnector.class.getName());
81 protected String searchFilter;
82 protected Properties properties;
83 protected SearchControls controls;
84 protected boolean mergeMultiResults = false;
85 protected boolean startTls = false;
86 boolean useExternalAuth = false;
87 private SSLSocketFactory sslsf;
90 * Constructs a DataConnector based on DOM configuration.
93 * a <JNDIDirectoryDataConnector /> DOM Element as specified by urn:mace:shibboleth:resolver:1.0
94 * @throws ResolutionPlugInException
95 * if the PlugIn cannot be initialized
97 public JNDIDirectoryDataConnector(Element e) throws ResolutionPlugInException {
101 // Decide if we are using starttls
102 String tlsAttribute = e.getAttribute("useStartTls");
103 if (tlsAttribute != null && tlsAttribute.equalsIgnoreCase("TRUE")) {
105 log.debug("Start TLS support enabled for connector.");
108 // Do we merge the attributes in the event of multiple results from a search?
109 String mergeMultiResultsAttrib = e.getAttribute("mergeMultipleResults");
110 if (mergeMultiResultsAttrib != null && mergeMultiResultsAttrib.equalsIgnoreCase("TRUE")) {
111 mergeMultiResults = true;
112 log.debug("Multiple searcg result merging enabled for connector.");
115 // Determine the search filter and controls
116 NodeList searchNodes = e.getElementsByTagNameNS(AttributeResolver.resolverNamespace, "Search");
117 if (searchNodes.getLength() != 1) {
118 log.error("JNDI Directory Data Connector requires a \"Search\" specification.");
119 throw new ResolutionPlugInException("JNDI Directory Data Connector requires a \"Search\" specification.");
122 String searchFilterSpec = ((Element) searchNodes.item(0)).getAttribute("filter");
123 if (searchFilterSpec != null && !searchFilterSpec.equals("")) {
124 searchFilter = searchFilterSpec;
125 log.debug("Search Filter: (" + searchFilter + ").");
127 log.error("Search spec requires a filter attribute.");
128 throw new ResolutionPlugInException("Search spec requires a filter attribute.");
131 defineSearchControls(((Element) searchNodes.item(0)));
133 // Load JNDI properties
134 NodeList propertyNodes = e.getElementsByTagNameNS(AttributeResolver.resolverNamespace, "Property");
135 properties = new Properties(System.getProperties());
136 for (int i = 0; propertyNodes.getLength() > i; i++) {
137 Element property = (Element) propertyNodes.item(i);
138 String propName = property.getAttribute("name");
139 String propValue = property.getAttribute("value");
141 log.debug("Property: (" + propName + ").");
142 log.debug(" Value: (" + propValue + ").");
144 if (propName == null || propName.equals("")) {
145 log.error("Property (" + propName + ") is malformed. Connot accept empty property name.");
146 throw new ResolutionPlugInException("Property is malformed.");
147 } else if (propValue == null || propValue.equals("")) {
148 log.error("Property (" + propName + ") is malformed. Cannot accept empty property value.");
149 throw new ResolutionPlugInException("Property is malformed.");
151 properties.setProperty(propName, propValue);
155 // Fail-fast connection test
156 InitialDirContext context = null;
160 log.debug("Attempting to connect to JNDI directory source as a sanity check.");
161 context = initConnection();
162 } catch (IOException ioe) {
163 log.error("Failed to startup directory context: " + ioe);
164 throw new ResolutionPlugInException("Failed to startup directory context.");
168 // We can't do SASL EXTERNAL auth until we have a TLS session
169 // So, we need to take this out of the environment and then stick it back in later
170 if ("EXTERNAL".equals(properties.getProperty(Context.SECURITY_AUTHENTICATION))) {
171 useExternalAuth = true;
172 properties.remove(Context.SECURITY_AUTHENTICATION);
175 // If TLS credentials were supplied, load them and setup a KeyManager
176 KeyManager keyManager = null;
177 NodeList credNodes = e.getElementsByTagNameNS(Credentials.credentialsNamespace, "Credential");
178 if (credNodes.getLength() > 0) {
179 log.debug("JNDI Directory Data Connector has a \"Credential\" specification. "
180 + "Loading credential...");
181 Credentials credentials = new Credentials((Element) credNodes.item(0));
182 Credential clientCred = credentials.getCredential();
183 if (clientCred == null) {
184 log.error("No credentials were loaded.");
185 throw new ResolutionPlugInException("Error loading credential.");
187 keyManager = new KeyManagerImpl(clientCred.getPrivateKey(), clientCred.getX509CertificateChain());
191 // Setup a customized SSL socket factory that uses our implementation of KeyManager
192 // This factory will be used for all subsequent TLS negotiation
193 SSLContext sslc = SSLContext.getInstance("TLS");
194 sslc.init(new KeyManager[]{keyManager}, null, new SecureRandom());
195 sslsf = sslc.getSocketFactory();
197 log.debug("Attempting to connect to JNDI directory source as a sanity check.");
199 } catch (GeneralSecurityException gse) {
200 log.error("Failed to startup directory context. Error creating SSL socket: " + gse);
201 throw new ResolutionPlugInException("Failed to startup directory context.");
203 } catch (IOException ioe) {
204 log.error("Failed to startup directory context. Error negotiating Start TLS: " + ioe);
205 throw new ResolutionPlugInException("Failed to startup directory context.");
209 log.debug("JNDI Directory context activated.");
211 } catch (NamingException ne) {
212 log.error("Failed to startup directory context: " + ne);
213 throw new ResolutionPlugInException("Failed to startup directory context.");
216 if (context != null) {
219 } catch (NamingException ne) {
220 log.error("An error occured while closing the JNDI context: " + e);
227 * Create JNDI search controls based on DOM configuration
230 * a <Controls /> DOM Element as specified by urn:mace:shibboleth:resolver:1.0
232 protected void defineSearchControls(Element searchNode) {
234 controls = new SearchControls();
236 NodeList controlNodes = searchNode.getElementsByTagNameNS(AttributeResolver.resolverNamespace, "Controls");
237 if (controlNodes.getLength() < 1) {
238 log.debug("No Search Control spec found.");
240 if (controlNodes.getLength() > 1) {
241 log.error("Found multiple Search Control specs for a Connector. Ignoring all but the first.");
244 String searchScopeSpec = ((Element) controlNodes.item(0)).getAttribute("searchScope");
245 if (searchScopeSpec != null && !searchScopeSpec.equals("")) {
246 if (searchScopeSpec.equals("OBJECT_SCOPE")) {
247 controls.setSearchScope(SearchControls.OBJECT_SCOPE);
248 } else if (searchScopeSpec.equals("ONELEVEL_SCOPE")) {
249 controls.setSearchScope(SearchControls.ONELEVEL_SCOPE);
250 } else if (searchScopeSpec.equals("SUBTREE_SCOPE")) {
251 controls.setSearchScope(SearchControls.SUBTREE_SCOPE);
254 controls.setSearchScope(Integer.parseInt(searchScopeSpec));
255 } catch (NumberFormatException nfe) {
256 log.error("Control spec included an invalid (searchScope) attribute value.");
261 String timeLimitSpec = ((Element) controlNodes.item(0)).getAttribute("timeLimit");
262 if (timeLimitSpec != null && !timeLimitSpec.equals("")) {
264 controls.setTimeLimit(Integer.parseInt(timeLimitSpec));
265 } catch (NumberFormatException nfe) {
266 log.error("Control spec included an invalid (timeLimit) attribute value.");
270 String returningObjectsSpec = ((Element) controlNodes.item(0)).getAttribute("returningObjects");
271 if (returningObjectsSpec != null && !returningObjectsSpec.equals("")) {
272 controls.setReturningObjFlag(new Boolean(returningObjectsSpec).booleanValue());
275 String linkDereferencingSpec = ((Element) controlNodes.item(0)).getAttribute("linkDereferencing");
276 if (linkDereferencingSpec != null && !linkDereferencingSpec.equals("")) {
277 if (linkDereferencingSpec != null && !linkDereferencingSpec.equals("")) {
278 controls.setDerefLinkFlag(new Boolean(linkDereferencingSpec).booleanValue());
282 String countLimitSpec = ((Element) controlNodes.item(0)).getAttribute("countLimit");
283 if (countLimitSpec != null && !countLimitSpec.equals("")) {
285 controls.setCountLimit(Long.parseLong(countLimitSpec));
286 } catch (NumberFormatException nfe) {
287 log.error("Control spec included an invalid (countLimit) attribute value.");
292 if (log.isDebugEnabled()) {
293 log.debug("Search Control (searchScope): " + controls.getSearchScope());
294 log.debug("Search Control (timeLimit): " + controls.getTimeLimit());
295 log.debug("Search Control (returningObjects): " + controls.getReturningObjFlag());
296 log.debug("Search Control (linkDereferencing): " + controls.getDerefLinkFlag());
297 log.debug("Search Control (countLimit): " + controls.getCountLimit());
302 * See {@link DataConnectorPlugIn#resolve(Principal, String, Dependencies)}
304 public Attributes resolve(Principal principal, String requester, Dependencies depends)
305 throws ResolutionPlugInException {
307 InitialDirContext context = null;
308 NamingEnumeration nEnumeration = null;
309 String populatedSearch = searchFilter.replaceAll("%PRINCIPAL%", principal.getName());
312 context = initConnection();
313 nEnumeration = context.search("", populatedSearch, controls);
315 // If we get a failure during the init or query, attempt once to re-establish the connection
316 } catch (CommunicationException e) {
318 log.warn("Encountered a connection problem while querying for attributes. Re-initializing "
319 + "JNDI context and retrying...");
320 context = initConnection();
321 nEnumeration = context.search("", populatedSearch, controls);
322 } catch (IOException e) {
324 log.warn("Encountered a connection problem while querying for attributes. Re-initializing "
325 + "JNDI context and retrying...");
326 context = initConnection();
327 nEnumeration = context.search("", populatedSearch, controls);
330 if (nEnumeration == null || !nEnumeration.hasMore()) {
331 log.error("Could not locate a principal with the name (" + principal.getName() + ").");
332 throw new ResolutionPlugInException("No data available for this principal.");
335 SearchResult result = (SearchResult) nEnumeration.next();
336 Attributes attributes = result.getAttributes();
338 if (!mergeMultiResults) {
339 if (nEnumeration.hasMore()) {
340 log.error("Multiple results returned from filter " + searchFilter + " for principal " + principal
341 + ", only one expected.");
342 throw new ResolutionPlugInException("Multiple results returned when only one expected.");
345 log.debug("Multiple results returned by filter " + populatedSearch
346 + " merging attributes from each result");
347 attributes = mergeResults(nEnumeration, attributes);
350 // For Sun's ldap provider only, construct the dn of the returned entry and manually add that as an
352 if (context.getEnvironment().get(Context.INITIAL_CONTEXT_FACTORY)
353 .equals("com.sun.jndi.ldap.LdapCtxFactory")) {
354 BasicAttribute dn = new BasicAttribute("dn", result.getName() + "," + context.getNameInNamespace());
360 } catch (NamingException e) {
361 log.error("An error occurred while retieving data for principal (" + principal.getName() + ") :"
363 throw new ResolutionPlugInException("Error retrieving data for principal.");
364 } catch (IOException e) {
365 log.error("An error occurred while retieving data for principal (" + principal.getName() + ") :"
367 throw new ResolutionPlugInException("Error retrieving data for principal.");
371 if (context != null) {
374 if (nEnumeration != null) {
375 nEnumeration.close();
377 } catch (NamingException e) {
378 log.error("An error occured while closing the JNDI context: " + e);
384 * Merges the attributes found in each result and a base Attributes object. If a named attribute appears in more
385 * than one result it's values are added to any existing values already in the given Attributes object. Duplicate
386 * attribute values are eliminated.
388 * @param searchResults
391 * the container to add the attributes from the search result to (may already contain attributes)
392 * @return all the attributes and values merged from search results and initial attributes set
393 * @throws NamingException
394 * thrown if there is a problem reading result data
396 private Attributes mergeResults(NamingEnumeration searchResults, Attributes attributes) throws NamingException {
398 HashMap attributeMap = new HashMap();
400 mergeAttributes(attributeMap, attributes);
403 while (searchResults.hasMore()) {
404 result = (SearchResult) searchResults.next();
405 mergeAttributes(attributeMap, result.getAttributes());
408 Attributes mergedAttribs = new BasicAttributes(false);
409 Attribute mergedAttrib;
410 Iterator attribNames = attributeMap.keySet().iterator();
411 Iterator attribValues;
413 while (attribNames.hasNext()) {
414 attribName = (String) attribNames.next();
415 mergedAttrib = new BasicAttribute(attribName, false);
416 Set valueSet = (Set) attributeMap.get(attribName);
417 attribValues = valueSet.iterator();
418 while (attribValues.hasNext()) {
419 mergedAttrib.add(attribValues.next());
421 mergedAttribs.put(mergedAttrib);
424 return mergedAttribs;
428 * Merges a given collection of Attributes into an existing collection.
430 * @param attributeMap
431 * existing collection of attribute data
433 * collection of attribute data to be merged in
434 * @throws NamingException
435 * thrown if there is a problem getting attribute information
437 private void mergeAttributes(HashMap attributeMap, Attributes attributes) throws NamingException {
439 if (attributes == null || attributes.size() <= 0) {
440 // In case the search result this came from was empty
445 NamingEnumeration baseAttribs = attributes.getAll();
446 Attribute baseAttrib;
447 while (baseAttribs.hasMore()) {
448 baseAttrib = (Attribute) baseAttribs.next();
449 if (attributeMap.containsKey(baseAttrib.getID())) {
450 valueSet = (HashSet) attributeMap.get(baseAttrib.getID());
452 valueSet = new HashSet();
454 for (int i = 0; i < baseAttrib.size(); i++) {
455 valueSet.add(baseAttrib.get(i));
457 attributeMap.put(baseAttrib.getID(), valueSet);
461 private InitialDirContext initConnection() throws NamingException, IOException, ResolutionPlugInException {
463 InitialDirContext context;
465 context = new InitialDirContext(properties);
468 context = new InitialLdapContext(properties, null);
469 if (!(context instanceof LdapContext)) {
470 log.error("Directory context does not appear to be an implementation of LdapContext. "
471 + "This is required for startTls.");
472 throw new ResolutionPlugInException("Start TLS is only supported for implementations of LdapContext.");
474 StartTlsResponse tls = (StartTlsResponse) ((LdapContext) context).extendedOperation(new StartTlsRequest());
475 tls.negotiate(sslsf);
476 if (useExternalAuth) {
477 context.addToEnvironment(Context.SECURITY_AUTHENTICATION, "EXTERNAL");
485 * Implementation of <code>X509KeyManager</code> that always uses a hard-coded client certificate.
488 class KeyManagerImpl implements X509KeyManager {
490 private PrivateKey key;
491 private X509Certificate[] chain;
493 KeyManagerImpl(PrivateKey key, X509Certificate[] chain) {
499 public String[] getClientAliases(String arg0, Principal[] arg1) {
501 return new String[]{"default"};
504 public String chooseClientAlias(String[] arg0, Principal[] arg1, Socket arg2) {
509 public String[] getServerAliases(String arg0, Principal[] arg1) {
514 public String chooseServerAlias(String arg0, Principal[] arg1, Socket arg2) {
519 public X509Certificate[] getCertificateChain(String arg0) {
524 public PrivateKey getPrivateKey(String arg0) {