001: /*
002:
003: Derby - Class org.apache.derby.impl.jdbc.authentication.LDAPAuthenticationSchemeImpl
004:
005: Licensed to the Apache Software Foundation (ASF) under one or more
006: contributor license agreements. See the NOTICE file distributed with
007: this work for additional information regarding copyright ownership.
008: The ASF licenses this file to you under the Apache License, Version 2.0
009: (the "License"); you may not use this file except in compliance with
010: the License. You may obtain a copy of the License at
011:
012: http://www.apache.org/licenses/LICENSE-2.0
013:
014: Unless required by applicable law or agreed to in writing, software
015: distributed under the License is distributed on an "AS IS" BASIS,
016: WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
017: See the License for the specific language governing permissions and
018: limitations under the License.
019:
020: */
021:
022: package org.apache.derby.impl.jdbc.authentication;
023:
024: import org.apache.derby.iapi.reference.MessageId;
025: import org.apache.derby.iapi.services.monitor.Monitor;
026: import org.apache.derby.iapi.error.StandardException;
027: import org.apache.derby.iapi.services.i18n.MessageService;
028: import org.apache.derby.iapi.jdbc.AuthenticationService;
029:
030: import org.apache.derby.authentication.UserAuthenticator;
031:
032: import org.apache.derby.iapi.services.sanity.SanityManager;
033: import org.apache.derby.iapi.util.StringUtil;
034:
035: import javax.naming.*;
036: import javax.naming.directory.*;
037:
038: import java.util.Properties;
039: import java.sql.SQLException;
040:
041: /**
042: * This is the Cloudscape LDAP authentication scheme implementation.
043: *
044: * JNDI system/environment properties can be set at the database
045: * level as database properties. They will be picked-up and set in
046: * the JNDI initial context if any are found.
047: *
048: * We do connect first to the LDAP server in order to retrieve the
049: * user's distinguished name (DN) and then we reconnect and try to
050: * authenticate with the user's DN and passed-in password.
051: *
052: * In 2.0 release, we first connect to do a search (user full DN lookup).
053: * This initial lookup can be done through anonymous bind or using special
054: * LDAP search credentials that the user may have configured on the
055: * LDAP settings for the database or the system.
056: * It is a typical operation with LDAP servers where sometimes it is
057: * hard to tell/guess in advance a users' full DN's.
058: *
059: * NOTE: In a future release, we will cache/maintain the user DN within
060: * the the cloudscape database or system to avoid the initial lookup.
061: * Also note that LDAP search/retrieval operations are usually very fast.
062: *
063: * The default LDAP url is ldap:/// (ldap://localhost:389/)
064: *
065: * @see org.apache.derby.authentication.UserAuthenticator
066: *
067: */
068:
069: public final class LDAPAuthenticationSchemeImpl extends
070: JNDIAuthenticationSchemeBase {
071: private static final String dfltLDAPURL = "ldap://";
072:
073: private String searchBaseDN;
074:
075: private String leftSearchFilter; // stick in uid in between
076: private String rightSearchFilter;
077: private boolean useUserPropertyAsDN;
078:
079: // Search Auth DN & Password if anonymous search not allowed
080: private String searchAuthDN;
081: private String searchAuthPW;
082: // we only want the user's full DN in return
083: private static final String[] attrDN = { "dn" };;
084:
085: //
086: // Cloudscape LDAP Configuration properties
087: //
088: private static final String LDAP_SEARCH_BASE = "derby.authentication.ldap.searchBase";
089: private static final String LDAP_SEARCH_FILTER = "derby.authentication.ldap.searchFilter";
090: private static final String LDAP_SEARCH_AUTH_DN = "derby.authentication.ldap.searchAuthDN";
091: private static final String LDAP_SEARCH_AUTH_PW = "derby.authentication.ldap.searchAuthPW";
092: private static final String LDAP_LOCAL_USER_DN = "derby.user";
093: private static final String LDAP_SEARCH_FILTER_USERNAME = "%USERNAME%";
094:
095: public LDAPAuthenticationSchemeImpl(JNDIAuthenticationService as,
096: Properties dbProperties) {
097:
098: super (as, dbProperties);
099: }
100:
101: /**
102: * Authenticate the passed-in user's credentials.
103: *
104: * We authenticate against a LDAP Server.
105: *
106: *
107: * @param userName The user's name used to connect to JBMS system
108: * @param userPassword The user's password used to connect to JBMS system
109: * @param databaseName The database which the user wants to connect to.
110: * @param info Additional jdbc connection info.
111: */
112: public boolean authenticateUser(String userName,
113: String userPassword, String databaseName, Properties info)
114: throws java.sql.SQLException {
115: if (((userName == null) || (userName.length() == 0))
116: || ((userPassword == null) || (userPassword.length() == 0))) {
117: // We don't tolerate 'guest' user for now as well as
118: // null password.
119: // If a null password is passed upon authenticating a user
120: // through LDAP, then the LDAP server might consider this as
121: // anonymous bind and therefore no authentication will be done
122: // at all.
123: return false;
124: }
125:
126: Exception e;
127: try {
128: Properties env = (Properties) initDirContextEnv.clone();
129: String userDN = null;
130: //
131: // Retrieve the user's DN (Distinguished Name)
132: // If we're asked to look it up locally, do it first
133: // and if we don't find it, we go against the LDAP
134: // server for a look-up (search)
135: //
136: if (useUserPropertyAsDN)
137: userDN = authenticationService
138: .getProperty(org.apache.derby.iapi.reference.Property.USER_PROPERTY_PREFIX);
139:
140: if (userDN == (String) null) {
141: userDN = getDNFromUID(userName);
142: }
143:
144: if (SanityManager.DEBUG) {
145: if (SanityManager
146: .DEBUG_ON(AuthenticationServiceBase.AuthenticationTrace)) {
147: SanityManager
148: .DEBUG(
149: AuthenticationServiceBase.AuthenticationTrace,
150: "User DN = [" + userDN + "]\n");
151: }
152: }
153:
154: env.put(Context.SECURITY_PRINCIPAL, userDN);
155: env.put(Context.SECURITY_CREDENTIALS, userPassword);
156:
157: // Connect & authenticate (bind) to the LDAP server now
158:
159: // it is happening right here
160: DirContext ctx = new InitialDirContext(env);
161:
162: // if the above was successfull, then username and
163: // password must be correct
164: return true;
165:
166: } catch (javax.naming.AuthenticationException jndiae) {
167: return false;
168:
169: } catch (javax.naming.NameNotFoundException jndinnfe) {
170: return false;
171:
172: } catch (javax.naming.NamingException jndine) {
173: e = jndine;
174: }
175:
176: throw getLoginSQLException(e);
177: }
178:
179: /**
180: * This method basically tests and sets default/expected JNDI properties
181: * for the JNDI provider scheme (here it is LDAP).
182: *
183: **/
184: protected void setJNDIProviderProperties() {
185:
186: // check if we're told to use a different initial context factory
187: if (initDirContextEnv
188: .getProperty(Context.INITIAL_CONTEXT_FACTORY) == (String) null) {
189: initDirContextEnv.put(Context.INITIAL_CONTEXT_FACTORY,
190: "com.sun.jndi.ldap.LdapCtxFactory");
191: }
192:
193: // retrieve LDAP server name/port# and construct LDAP url
194: if (initDirContextEnv.getProperty(Context.PROVIDER_URL) == (String) null) {
195: // Now we construct the LDAP url and expect to find the LDAP Server
196: // name.
197: //
198: String ldapServer = authenticationService
199: .getProperty(org.apache.derby.iapi.reference.Property.AUTHENTICATION_SERVER_PARAMETER);
200:
201: if (ldapServer == (String) null) {
202:
203: // we do expect a LDAP Server name to be configured
204: Monitor
205: .logTextMessage(
206: MessageId.AUTH_NO_LDAP_HOST_MENTIONED,
207: org.apache.derby.iapi.reference.Property.AUTHENTICATION_SERVER_PARAMETER);
208:
209: this .providerURL = dfltLDAPURL + "/";
210:
211: } else {
212:
213: if (ldapServer.startsWith(dfltLDAPURL)
214: || ldapServer.startsWith("ldaps://"))
215: this .providerURL = ldapServer;
216: else if (ldapServer.startsWith("//"))
217: this .providerURL = "ldap:" + ldapServer;
218: else
219: this .providerURL = dfltLDAPURL + ldapServer;
220: }
221: initDirContextEnv.put(Context.PROVIDER_URL, providerURL);
222: }
223:
224: // check if we should we use a particular authentication method
225: // we assume the ldap server supports this authentication method
226: // (Netscape DS 3.1.1 does not support CRAM-MD5 for instance)
227: if (initDirContextEnv
228: .getProperty(Context.SECURITY_AUTHENTICATION) == (String) null) {
229: // set the default to be clear userName/Password as not of all the
230: // LDAP server(s) support CRAM-MD5 (especially ldap v2 ones)
231: // Netscape Directory Server 3.1.1 does not support CRAM-MD5
232: // (told by Sun JNDI engineering). Netscape DS 4.0 allows SASL
233: // plug-ins to be installed and that can be used as authentication
234: // method.
235: //
236: initDirContextEnv.put(Context.SECURITY_AUTHENTICATION,
237: "simple");
238: }
239:
240: // Retrieve and set the search base (root) DN to use on the ldap
241: // server.
242: String ldapSearchBase = authenticationService
243: .getProperty(LDAP_SEARCH_BASE);
244: if (ldapSearchBase != (String) null)
245: this .searchBaseDN = ldapSearchBase;
246: else
247: this .searchBaseDN = "";
248:
249: // retrieve principal and credentials for the search bind as the
250: // user may not want to allow anonymous binds (for searches)
251: this .searchAuthDN = authenticationService
252: .getProperty(LDAP_SEARCH_AUTH_DN);
253: this .searchAuthPW = authenticationService
254: .getProperty(LDAP_SEARCH_AUTH_PW);
255:
256: //
257: // Construct the LDAP search filter:
258: //
259: // If we were told to use a special search filther, we do so;
260: // otherwise we use our default search filter.
261: // The user may have set the search filter 3 different ways:
262: //
263: // - if %USERNAME% was found in the search filter, then we
264: // will substitute this with the passed-in uid at runtime.
265: //
266: // - if "derby.user" is the search filter value, then we
267: // will assume the user's DN can be found in the system or
268: // database property "derby.user.<uid>" . If the property
269: // does not exist, then we will do a normal lookup with our
270: // default search filter; otherwise we will perform an
271: // authenticated bind to the LDAP server using the found DN.
272: //
273: // - if neither of the 2 previous values were found, then we use
274: // our default search filter and we will substitute insert the
275: // uid passed at runtime into our default search filter.
276: //
277: String searchFilterProp = authenticationService
278: .getProperty(LDAP_SEARCH_FILTER);
279:
280: if (searchFilterProp == (String) null) {
281: // use our default search filter
282: this .leftSearchFilter = "(&(objectClass=inetOrgPerson)(uid=";
283: this .rightSearchFilter = "))";
284:
285: } else if (StringUtil.SQLEqualsIgnoreCase(searchFilterProp,
286: LDAP_LOCAL_USER_DN)) {
287:
288: // use local user DN in derby.user.<uid>
289: this .leftSearchFilter = "(&(objectClass=inetOrgPerson)(uid=";
290: this .rightSearchFilter = "))";
291: this .useUserPropertyAsDN = true;
292:
293: } else if (searchFilterProp
294: .indexOf(LDAP_SEARCH_FILTER_USERNAME) != -1) {
295:
296: // user has set %USERNAME% in the search filter
297: this .leftSearchFilter = searchFilterProp.substring(0,
298: searchFilterProp
299: .indexOf(LDAP_SEARCH_FILTER_USERNAME));
300: this .rightSearchFilter = searchFilterProp
301: .substring(searchFilterProp
302: .indexOf(LDAP_SEARCH_FILTER_USERNAME)
303: + (int) LDAP_SEARCH_FILTER_USERNAME
304: .length());
305:
306: } else { // add this search filter to ours
307:
308: // complement this search predicate to ours
309: this .leftSearchFilter = "(&(" + searchFilterProp + ")"
310: + "(objectClass=inetOrgPerson)(uid=";
311: this .rightSearchFilter = "))";
312:
313: }
314:
315: if (SanityManager.DEBUG) {
316: if (SanityManager
317: .DEBUG_ON(AuthenticationServiceBase.AuthenticationTrace)) {
318:
319: java.io.PrintWriter iDbgStream = SanityManager
320: .GET_DEBUG_STREAM();
321:
322: iDbgStream
323: .println("\n\n+ LDAP Authentication Configuration:\n"
324: + " - provider URL ["
325: + this .providerURL
326: + "]\n"
327: + " - search base ["
328: + this .searchBaseDN
329: + "]\n"
330: + " - search filter to be ["
331: + this .leftSearchFilter
332: + "<uid>"
333: + this .rightSearchFilter
334: + "]\n"
335: + " - use local DN ["
336: + (useUserPropertyAsDN ? "true"
337: : "false") + "]\n");
338: }
339: }
340:
341: if (SanityManager.DEBUG) {
342: if (SanityManager
343: .DEBUG_ON(AuthenticationServiceBase.AuthenticationTrace)) {
344: try {
345: initDirContextEnv.put(
346: "com.sun.naming.ldap.trace.ber",
347: new java.io.FileOutputStream(
348: "CloudLDAP.out"));
349: } catch (java.io.IOException ie) {
350: }
351: }
352: }
353: }
354:
355: /**
356: * Search for the full user's DN in the LDAP server.
357: * LDAP server bind may or not be anonymous.
358: *
359: * If the admin does not want us to do anonymous bind/search, then we
360: * must have been given principal/credentials in order to successfully
361: * bind to perform the user's DN search.
362: *
363: * @exception NamingException if could not retrieve the user DN.
364: **/
365: private String getDNFromUID(String uid)
366: throws javax.naming.NamingException {
367: //
368: // We bind to the LDAP server here
369: // Note that this bind might be anonymous (if anonymous searches
370: // are allowed in the LDAP server, or authenticated if we were
371: // told/configured to.
372: //
373: Properties env = null;
374: if (this .searchAuthDN != (String) null) {
375: env = (Properties) initDirContextEnv.clone();
376: env.put(Context.SECURITY_PRINCIPAL, this .searchAuthDN);
377: env.put(Context.SECURITY_CREDENTIALS, this .searchAuthPW);
378: } else
379: env = initDirContextEnv;
380:
381: DirContext ctx = new InitialDirContext(env);
382:
383: // Construct Search Filter
384: SearchControls ctls = new SearchControls();
385: // Set-up a LDAP subtree search scope
386: ctls.setSearchScope(SearchControls.SUBTREE_SCOPE);
387:
388: // Just retrieve the DN
389: ctls.setReturningAttributes(attrDN);
390:
391: String searchFilter = this .leftSearchFilter + uid
392: + this .rightSearchFilter;
393: NamingEnumeration results = ctx.search(searchBaseDN,
394: searchFilter, ctls);
395:
396: // If we did not find anything then login failed
397: if (results == null || !results.hasMore())
398: throw new NameNotFoundException();
399:
400: SearchResult result = (SearchResult) results.next();
401:
402: if (results.hasMore()) {
403: // This is a login failure as we cannot assume the first one
404: // is the valid one.
405: if (SanityManager.DEBUG) {
406: if (SanityManager
407: .DEBUG_ON(AuthenticationServiceBase.AuthenticationTrace)) {
408:
409: java.io.PrintWriter iDbgStream = SanityManager
410: .GET_DEBUG_STREAM();
411:
412: iDbgStream
413: .println(" - LDAP Authentication request failure: "
414: + "search filter ["
415: + searchFilter
416: + "]"
417: + ", retrieve more than one occurence in "
418: + "LDAP server ["
419: + this .providerURL + "]");
420: }
421: }
422: throw new NameNotFoundException();
423: }
424:
425: NameParser parser = ctx.getNameParser(searchBaseDN);
426: Name userDN = parser.parse(searchBaseDN);
427:
428: if (userDN == (Name) null)
429: // This should not happen in theory
430: throw new NameNotFoundException();
431: else
432: userDN.addAll(parser.parse(result.getName()));
433:
434: // Return the full user's DN
435: return userDN.toString();
436: }
437: }
|