ea3f831a34e61fe67d4a1137f9624174a8fb322c
[java-idp.git] / src / edu / internet2 / middleware / shibboleth / aa / attrresolv / provider / JNDIDirectoryDataConnector.java
1 /* 
2  * The Shibboleth License, Version 1. 
3  * Copyright (c) 2002 
4  * University Corporation for Advanced Internet Development, Inc. 
5  * All rights reserved
6  * 
7  * 
8  * Redistribution and use in source and binary forms, with or without 
9  * modification, are permitted provided that the following conditions are met:
10  * 
11  * Redistributions of source code must retain the above copyright notice, this 
12  * list of conditions and the following disclaimer.
13  * 
14  * Redistributions in binary form must reproduce the above copyright notice, 
15  * this list of conditions and the following disclaimer in the documentation 
16  * and/or other materials provided with the distribution, if any, must include 
17  * the following acknowledgment: "This product includes software developed by 
18  * the University Corporation for Advanced Internet Development 
19  * <http://www.ucaid.edu>Internet2 Project. Alternately, this acknowledegement 
20  * may appear in the software itself, if and wherever such third-party 
21  * acknowledgments normally appear.
22  * 
23  * Neither the name of Shibboleth nor the names of its contributors, nor 
24  * Internet2, nor the University Corporation for Advanced Internet Development, 
25  * Inc., nor UCAID may be used to endorse or promote products derived from this 
26  * software without specific prior written permission. For written permission, 
27  * please contact shibboleth@shibboleth.org
28  * 
29  * Products derived from this software may not be called Shibboleth, Internet2, 
30  * UCAID, or the University Corporation for Advanced Internet Development, nor 
31  * may Shibboleth appear in their name, without prior written permission of the 
32  * University Corporation for Advanced Internet Development.
33  * 
34  * 
35  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 
36  * AND WITH ALL FAULTS. ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 
37  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 
38  * PARTICULAR PURPOSE, AND NON-INFRINGEMENT ARE DISCLAIMED AND THE ENTIRE RISK 
39  * OF SATISFACTORY QUALITY, PERFORMANCE, ACCURACY, AND EFFORT IS WITH LICENSEE. 
40  * IN NO EVENT SHALL THE COPYRIGHT OWNER, CONTRIBUTORS OR THE UNIVERSITY 
41  * CORPORATION FOR ADVANCED INTERNET DEVELOPMENT, INC. BE LIABLE FOR ANY DIRECT, 
42  * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 
43  * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 
44  * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 
45  * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 
46  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 
47  * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
48  */
49
50 package edu.internet2.middleware.shibboleth.aa.attrresolv.provider;
51
52 import java.security.Principal;
53 import java.util.Properties;
54
55 import javax.naming.CommunicationException;
56 import javax.naming.NamingEnumeration;
57 import javax.naming.NamingException;
58 import javax.naming.directory.Attributes;
59 import javax.naming.directory.InitialDirContext;
60 import javax.naming.directory.SearchControls;
61 import javax.naming.directory.SearchResult;
62
63 import org.apache.log4j.Logger;
64 import org.w3c.dom.Element;
65 import org.w3c.dom.NodeList;
66
67 import edu.internet2.middleware.shibboleth.aa.attrresolv.AttributeResolver;
68 import edu.internet2.middleware.shibboleth.aa.attrresolv.DataConnectorPlugIn;
69 import edu.internet2.middleware.shibboleth.aa.attrresolv.Dependencies;
70 import edu.internet2.middleware.shibboleth.aa.attrresolv.ResolutionPlugInException;
71
72 /**
73  * <code>DataConnectorPlugIn</code> implementation that utilizes a user-specified JNDI 
74  * <code>DirContext</code> to retrieve attribute data.
75  * 
76  * @author Walter Hoehn (wassa@columbia.edu)
77  *
78  */
79 public class JNDIDirectoryDataConnector extends BaseDataConnector implements DataConnectorPlugIn {
80
81         private static Logger log = Logger.getLogger(JNDIDirectoryDataConnector.class.getName());
82         protected String searchFilter;
83         protected Properties properties;
84         protected SearchControls controls;
85     protected String failover = null;
86
87     /**
88      * Constructs a DataConnector based on DOM configuration.
89      * 
90      * @param e a &lt;JNDIDirectoryDataConnector /&gt; DOM Element as specified by 
91      * urn:mace:shibboleth:resolver:1.0
92      * 
93      * @throws ResolutionPlugInException if the PlugIn cannot be initialized
94      */
95         public JNDIDirectoryDataConnector(Element e) throws ResolutionPlugInException {
96
97                 super(e);
98         
99                 NodeList searchNodes = e.getElementsByTagNameNS(AttributeResolver.resolverNamespace, "Search");
100                 if (searchNodes.getLength() != 1) {
101                         log.error("JNDI Directory Data Connector requires a \"Search\" specification.");
102                         throw new ResolutionPlugInException("JNDI Directory Data Connector requires a \"Search\" specification.");
103                 }
104
105                 String searchFilterSpec = ((Element) searchNodes.item(0)).getAttribute("filter");
106                 if (searchFilterSpec != null && !searchFilterSpec.equals("")) {
107                         searchFilter = searchFilterSpec;
108                         log.debug("Search Filter: (" + searchFilter + ").");
109                 } else {
110                         log.error("Search spec requires a filter attribute.");
111                         throw new ResolutionPlugInException("Search spec requires a filter attribute.");
112                 }
113
114                 defineSearchControls(((Element) searchNodes.item(0)));
115
116                 NodeList propertyNodes = e.getElementsByTagNameNS(AttributeResolver.resolverNamespace, "Property");
117                 properties = new Properties(System.getProperties());
118                 for (int i = 0; propertyNodes.getLength() > i; i++) {
119                         Element property = (Element) propertyNodes.item(i);
120                         String propName = property.getAttribute("name");
121                         String propValue = property.getAttribute("value");
122
123                         log.debug("Property: (" + propName + ").");
124                         log.debug("   Value: (" + propValue + ").");
125
126                         if (propName == null || propName.equals("")) {
127                                 log.error("Property is malformed.");
128                                 throw new ResolutionPlugInException("Property (" + propName
129                                                 + ") is malformed.  Connot accept empty property name.");
130                         } else if (propValue == null || propValue.equals("")) {
131                                 log.error("Property is malformed.");
132                                 throw new ResolutionPlugInException("Property (" + propName
133                                                 + ") is malformed.  Cannot accept empty property value.");
134                         } else {
135                                 properties.setProperty(propName, propValue);
136                         }
137                 }
138
139         //Fail-fast connection test
140                 InitialDirContext context = null;
141                 try {
142                         context = new InitialDirContext(properties);
143                         log.debug("JNDI Directory context activated.");
144                         
145                 } catch (NamingException e1) {
146                         log.error("Failed to startup directory context: " + e1);
147                         throw new ResolutionPlugInException("Failed to startup directory context.");
148                 } finally {
149                         try {
150                                 if (context != null) {
151                                         context.close();
152                                 }
153                         } catch (NamingException ne) {
154                                 log.error("An error occured while closing the JNDI context: " + e);
155                         }
156
157                 }
158         }
159
160     /**
161      * Create JNDI search controls based on DOM configuration
162      * @param searchNode a &lt;Controls /&gt; DOM Element as specified by 
163      * urn:mace:shibboleth:resolver:1.0
164      */
165         protected void defineSearchControls(Element searchNode) {
166
167                 controls = new SearchControls();
168
169                 NodeList controlNodes = searchNode.getElementsByTagNameNS(AttributeResolver.resolverNamespace, "Controls");
170                 if (controlNodes.getLength() < 1) {
171                         log.debug("No Search Control spec found.");
172                 } else {
173                         if (controlNodes.getLength() > 1) {
174                                 log.error("Found multiple Search Control specs for a Connector.  Ignoring all but the first.");
175                         }
176
177                         String searchScopeSpec = ((Element) controlNodes.item(0)).getAttribute("searchScope");
178                         if (searchScopeSpec != null && !searchScopeSpec.equals("")) {
179                                 if (searchScopeSpec.equals("OBJECT_SCOPE")) {
180                                         controls.setSearchScope(SearchControls.OBJECT_SCOPE);
181                                 } else if (searchScopeSpec.equals("ONELEVEL_SCOPE")) {
182                                         controls.setSearchScope(SearchControls.ONELEVEL_SCOPE);
183                                 } else if (searchScopeSpec.equals("SUBTREE_SCOPE")) {
184                                         controls.setSearchScope(SearchControls.SUBTREE_SCOPE);
185                                 } else {
186                                         try {
187                                                 controls.setSearchScope(Integer.parseInt(searchScopeSpec));
188                                         } catch (NumberFormatException nfe) {
189                                                 log.error("Control spec included an invalid (searchScope) attribute value.");
190                                         }
191                                 }
192                         }
193
194                         String timeLimitSpec = ((Element) controlNodes.item(0)).getAttribute("timeLimit");
195                         if (timeLimitSpec != null && !timeLimitSpec.equals("")) {
196                                 try {
197                                         controls.setTimeLimit(Integer.parseInt(timeLimitSpec));
198                                 } catch (NumberFormatException nfe) {
199                                         log.error("Control spec included an invalid (timeLimit) attribute value.");
200                                 }
201                         }
202
203                         String returningObjectsSpec = ((Element) controlNodes.item(0)).getAttribute("returningObjects");
204                         if (returningObjectsSpec != null && !returningObjectsSpec.equals("")) {
205                                 controls.setReturningObjFlag(new Boolean(returningObjectsSpec).booleanValue());
206                         }
207
208                         String linkDereferencingSpec = ((Element) controlNodes.item(0)).getAttribute("linkDereferencing");
209                         if (linkDereferencingSpec != null && !linkDereferencingSpec.equals("")) {
210                                 if (linkDereferencingSpec != null && !linkDereferencingSpec.equals("")) {
211                                         controls.setDerefLinkFlag(new Boolean(linkDereferencingSpec).booleanValue());
212                                 }
213                         }
214
215                         String countLimitSpec = ((Element) controlNodes.item(0)).getAttribute("countLimit");
216                         if (countLimitSpec != null && !countLimitSpec.equals("")) {
217                                 try {
218                                         controls.setCountLimit(Long.parseLong(countLimitSpec));
219                                 } catch (NumberFormatException nfe) {
220                                         log.error("Control spec included an invalid (countLimit) attribute value.");
221                                 }
222                         }
223                 }
224
225                 if (log.isDebugEnabled()) {
226                         log.debug("Search Control (searchScope): " + controls.getSearchScope());
227                         log.debug("Search Control (timeLimit): " + controls.getTimeLimit());
228                         log.debug("Search Control (returningObjects): " + controls.getReturningObjFlag());
229                         log.debug("Search Control (linkDereferencing): " + controls.getDerefLinkFlag());
230                         log.debug("Search Control (countLimit): " + controls.getCountLimit());
231                 }
232         }
233
234         /**
235          * @see edu.internet2.middleware.shibboleth.aa.attrresolv.DataConnectorPlugIn#resolve(java.security.Principal)
236          */
237         public Attributes resolve(Principal principal, String requester, Dependencies depends)
238                 throws ResolutionPlugInException {
239
240                 InitialDirContext context = null;
241                 try {
242                         context = new InitialDirContext(properties);
243                         NamingEnumeration nEnumeration = null;
244
245                         try {
246                                 nEnumeration = context.search("", searchFilter.replaceAll("%PRINCIPAL%", principal.getName()), controls);
247                         } catch (CommunicationException e) {
248                                 log.debug(e);
249                                 log.warn(
250                                         "Encountered a connection problem while querying for attributes.  Re-initializing JNDI context and retrying...");
251                                 context = new InitialDirContext(context.getEnvironment());
252                                 nEnumeration = context.search("", searchFilter.replaceAll("%PRINCIPAL%", principal.getName()), controls);
253                         }
254
255                         if (nEnumeration == null || !nEnumeration.hasMore()) {
256                                 log.error("Could not locate a principal with the name (" + principal.getName() + ").");
257                                 throw new ResolutionPlugInException("No data available for this principal.");
258                         }
259
260                         SearchResult result = (SearchResult) nEnumeration.next();
261                         Attributes attributes = result.getAttributes();
262
263                         if (nEnumeration.hasMore()) {
264                                 log.error("Unable to disambiguate date for principal (" + principal.getName() + ") in search.");
265                                 throw new ResolutionPlugInException("Cannot disambiguate data for this principal.");
266                         }
267
268                         return attributes;
269
270                 } catch (NamingException e) {
271                         log.error(
272                                 "An error occurred while retieving data for principal ("
273                                         + principal.getName()
274                                         + ") :"
275                                         + e.getMessage());
276                         throw new ResolutionPlugInException("Error retrieving data for principal.");
277                 } finally {
278                         try {
279                                 if (context != null) {
280                                         context.close();
281                                 }
282                         } catch (NamingException e) {
283                                 log.error("An error occured while closing the JNDI context: " + e);
284                         }
285                 }
286         }
287 }