--- /dev/null
+/*\r
+\r
+ Derby - Class org.apache.derby.impl.jdbc.authentication.LDAPAuthenticationSchemeImpl\r
+\r
+ Licensed to the Apache Software Foundation (ASF) under one or more\r
+ contributor license agreements. See the NOTICE file distributed with\r
+ this work for additional information regarding copyright ownership.\r
+ The ASF licenses this file to you under the Apache License, Version 2.0\r
+ (the "License"); you may not use this file except in compliance with\r
+ the License. You may obtain a copy of the License at\r
+\r
+ http://www.apache.org/licenses/LICENSE-2.0\r
+\r
+ Unless required by applicable law or agreed to in writing, software\r
+ distributed under the License is distributed on an "AS IS" BASIS,\r
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
+ See the License for the specific language governing permissions and\r
+ limitations under the License.\r
+\r
+ */\r
+\r
+package org.apache.derby.impl.jdbc.authentication;\r
+\r
+import org.apache.derby.iapi.reference.MessageId;\r
+import org.apache.derby.iapi.services.monitor.Monitor;\r
+import org.apache.derby.iapi.error.StandardException;\r
+import org.apache.derby.iapi.services.i18n.MessageService;\r
+import org.apache.derby.iapi.jdbc.AuthenticationService;\r
+\r
+import org.apache.derby.authentication.UserAuthenticator;\r
+\r
+import org.apache.derby.iapi.services.sanity.SanityManager;\r
+import org.apache.derby.iapi.util.StringUtil;\r
+\r
+import javax.naming.*;\r
+import javax.naming.directory.*;\r
+\r
+\r
+import java.util.Properties;\r
+import java.io.FileOutputStream;\r
+import java.io.IOException;\r
+import java.security.AccessController;\r
+import java.security.PrivilegedActionException;\r
+import java.security.PrivilegedExceptionAction;\r
+import java.sql.SQLException;\r
+\r
+/**\r
+ * This is the Derby LDAP authentication scheme implementation.\r
+ *\r
+ * JNDI system/environment properties can be set at the database\r
+ * level as database properties. They will be picked-up and set in\r
+ * the JNDI initial context if any are found.\r
+ *\r
+ * We do connect first to the LDAP server in order to retrieve the\r
+ * user's distinguished name (DN) and then we reconnect and try to\r
+ * authenticate with the user's DN and passed-in password.\r
+ *\r
+ * In 2.0 release, we first connect to do a search (user full DN lookup).\r
+ * This initial lookup can be done through anonymous bind or using special\r
+ * LDAP search credentials that the user may have configured on the\r
+ * LDAP settings for the database or the system.\r
+ * It is a typical operation with LDAP servers where sometimes it is\r
+ * hard to tell/guess in advance a users' full DN's.\r
+ *\r
+ * NOTE: In a future release, we will cache/maintain the user DN within\r
+ * the the Derby database or system to avoid the initial lookup.\r
+ * Also note that LDAP search/retrieval operations are usually very fast.\r
+ *\r
+ * The default LDAP url is ldap:/// (ldap://localhost:389/)\r
+ *\r
+ * @see org.apache.derby.authentication.UserAuthenticator \r
+ *\r
+ */\r
+\r
+public final class LDAPAuthenticationSchemeImpl\r
+extends JNDIAuthenticationSchemeBase\r
+{\r
+ private static final String dfltLDAPURL = "ldap://";\r
+\r
+ private String searchBaseDN;\r
+\r
+ private String leftSearchFilter; // stick in uid in between\r
+ private String rightSearchFilter;\r
+ private boolean useUserPropertyAsDN;\r
+\r
+ // Search Auth DN & Password if anonymous search not allowed\r
+ private String searchAuthDN;\r
+ private String searchAuthPW;\r
+ // we only want the user's full DN in return\r
+ private static final String[] attrDN = {"dn"}; ;\r
+\r
+ //\r
+ // Derby LDAP Configuration properties\r
+ //\r
+ private static final String LDAP_SEARCH_BASE =\r
+ "derby.authentication.ldap.searchBase";\r
+ private static final String LDAP_SEARCH_FILTER =\r
+ "derby.authentication.ldap.searchFilter";\r
+ private static final String LDAP_SEARCH_AUTH_DN =\r
+ "derby.authentication.ldap.searchAuthDN";\r
+ private static final String LDAP_SEARCH_AUTH_PW =\r
+ "derby.authentication.ldap.searchAuthPW";\r
+ private static final String LDAP_LOCAL_USER_DN =\r
+ "derby.user";\r
+ private static final String LDAP_SEARCH_FILTER_USERNAME =\r
+ "%USERNAME%";\r
+\r
+ public LDAPAuthenticationSchemeImpl(JNDIAuthenticationService as, Properties dbProperties) {\r
+\r
+ super(as, dbProperties);\r
+ }\r
+\r
+ /**\r
+ * Authenticate the passed-in user's credentials.\r
+ *\r
+ * We authenticate against a LDAP Server.\r
+ *\r
+ *\r
+ * @param userName The user's name used to connect to JBMS system\r
+ * @param userPassword The user's password used to connect to JBMS system\r
+ * @param databaseName The database which the user wants to connect to.\r
+ * @param info Additional jdbc connection info.\r
+ */\r
+ public boolean authenticateUser(String userName,\r
+ String userPassword,\r
+ String databaseName,\r
+ Properties info\r
+ )\r
+ throws java.sql.SQLException\r
+ {\r
+ if ( ((userName == null) || (userName.length() == 0)) ||\r
+ ((userPassword == null) || (userPassword.length() == 0)) )\r
+ {\r
+ // We don't tolerate 'guest' user for now as well as\r
+ // null password.\r
+ // If a null password is passed upon authenticating a user\r
+ // through LDAP, then the LDAP server might consider this as\r
+ // anonymous bind and therefore no authentication will be done\r
+ // at all.\r
+ return false;\r
+ }\r
+\r
+\r
+ Exception e;\r
+ try {\r
+ Properties env = (Properties) initDirContextEnv.clone();\r
+ String userDN = null;\r
+ //\r
+ // Retrieve the user's DN (Distinguished Name)\r
+ // If we're asked to look it up locally, do it first\r
+ // and if we don't find it, we go against the LDAP\r
+ // server for a look-up (search)\r
+ //\r
+ if (useUserPropertyAsDN)\r
+ userDN =\r
+ authenticationService.getProperty(\r
+ org.apache.derby.iapi.reference.Property.USER_PROPERTY_PREFIX);\r
+\r
+ if (userDN == (String) null) {\r
+ userDN = getDNFromUID(userName);\r
+ }\r
+ \r
+ if (SanityManager.DEBUG)\r
+ {\r
+ if (SanityManager.DEBUG_ON(\r
+ AuthenticationServiceBase.AuthenticationTrace)) {\r
+ SanityManager.DEBUG(AuthenticationServiceBase.AuthenticationTrace,\r
+ "User DN = ["+ userDN+"]\n");\r
+ }\r
+ }\r
+\r
+ env.put(Context.SECURITY_PRINCIPAL, userDN);\r
+ env.put(Context.SECURITY_CREDENTIALS, userPassword);\r
+ \r
+ // Connect & authenticate (bind) to the LDAP server now\r
+\r
+ // it is happening right here\r
+\r
+ DirContext ctx = privInitialDirContext(env);\r
+ \r
+ \r
+\r
+ // if the above was successfull, then username and\r
+ // password must be correct\r
+ return true;\r
+\r
+ } catch (javax.naming.AuthenticationException jndiae) {\r
+ return false;\r
+\r
+ } catch (javax.naming.NameNotFoundException jndinnfe) {\r
+ return false;\r
+\r
+ } catch (javax.naming.NamingException jndine) {\r
+ e = jndine;\r
+ }\r
+\r
+ throw getLoginSQLException(e);\r
+ }\r
+\r
+ \r
+\r
+ /**\r
+ * Call new InitialDirContext in a privilege block\r
+ * @param env environment used to create the initial DirContext. Null indicates an empty environment.\r
+ * @return an initial DirContext using the supplied environment. \r
+ */\r
+ private DirContext privInitialDirContext(final Properties env) throws NamingException {\r
+ try {\r
+ return ((InitialDirContext)AccessController.doPrivileged(\r
+ new PrivilegedExceptionAction() {\r
+ public Object run() throws SecurityException, NamingException {\r
+ return new InitialDirContext(env);\r
+ }\r
+ }));\r
+ } catch (PrivilegedActionException pae) {\r
+ Exception e = pae.getException();\r
+ \r
+ if (e instanceof NamingException)\r
+ throw (NamingException)e;\r
+ else\r
+ throw (SecurityException)e;\r
+ } \r
+ \r
+ } \r
+\r
+ /**\r
+ * This method basically tests and sets default/expected JNDI properties\r
+ * for the JNDI provider scheme (here it is LDAP).\r
+ *\r
+ **/\r
+ protected void setJNDIProviderProperties()\r
+ {\r
+\r
+ // check if we're told to use a different initial context factory\r
+ if (initDirContextEnv.getProperty(\r
+ Context.INITIAL_CONTEXT_FACTORY) == (String) null)\r
+ {\r
+ initDirContextEnv.put(Context.INITIAL_CONTEXT_FACTORY,\r
+ "com.sun.jndi.ldap.LdapCtxFactory");\r
+ }\r
+\r
+ // retrieve LDAP server name/port# and construct LDAP url\r
+ if (initDirContextEnv.getProperty(\r
+ Context.PROVIDER_URL) == (String) null)\r
+ {\r
+ // Now we construct the LDAP url and expect to find the LDAP Server\r
+ // name.\r
+ //\r
+ String ldapServer = authenticationService.getProperty(\r
+ org.apache.derby.iapi.reference.Property.AUTHENTICATION_SERVER_PARAMETER);\r
+\r
+ if (ldapServer == (String) null) {\r
+\r
+ // we do expect a LDAP Server name to be configured\r
+ Monitor.logTextMessage(\r
+ MessageId.AUTH_NO_LDAP_HOST_MENTIONED,\r
+ org.apache.derby.iapi.reference.Property.AUTHENTICATION_SERVER_PARAMETER);\r
+\r
+ this.providerURL = dfltLDAPURL + "/";\r
+\r
+ } else {\r
+\r
+ if (ldapServer.startsWith(dfltLDAPURL) || ldapServer.startsWith("ldaps://") )\r
+ this.providerURL = ldapServer;\r
+ else if (ldapServer.startsWith("//"))\r
+ this.providerURL = "ldap:" + ldapServer;\r
+ else\r
+ this.providerURL = dfltLDAPURL + ldapServer;\r
+ }\r
+ initDirContextEnv.put(Context.PROVIDER_URL, providerURL);\r
+ }\r
+\r
+ // check if we should we use a particular authentication method\r
+ // we assume the ldap server supports this authentication method\r
+ // (Netscape DS 3.1.1 does not support CRAM-MD5 for instance)\r
+ if (initDirContextEnv.getProperty(\r
+ Context.SECURITY_AUTHENTICATION) == (String) null)\r
+ {\r
+ // set the default to be clear userName/Password as not of all the\r
+ // LDAP server(s) support CRAM-MD5 (especially ldap v2 ones)\r
+ // Netscape Directory Server 3.1.1 does not support CRAM-MD5\r
+ // (told by Sun JNDI engineering). Netscape DS 4.0 allows SASL\r
+ // plug-ins to be installed and that can be used as authentication\r
+ // method.\r
+ //\r
+ initDirContextEnv.put(Context.SECURITY_AUTHENTICATION,\r
+ "simple"\r
+ );\r
+ }\r
+\r
+ // Retrieve and set the search base (root) DN to use on the ldap\r
+ // server.\r
+ String ldapSearchBase =\r
+ authenticationService.getProperty(LDAP_SEARCH_BASE);\r
+ if (ldapSearchBase != (String) null)\r
+ this.searchBaseDN = ldapSearchBase;\r
+ else\r
+ this.searchBaseDN = "";\r
+\r
+ // retrieve principal and credentials for the search bind as the\r
+ // user may not want to allow anonymous binds (for searches)\r
+ this.searchAuthDN =\r
+ authenticationService.getProperty(LDAP_SEARCH_AUTH_DN);\r
+ this.searchAuthPW =\r
+ authenticationService.getProperty(LDAP_SEARCH_AUTH_PW);\r
+\r
+ //\r
+ // Construct the LDAP search filter:\r
+ //\r
+ // If we were told to use a special search filther, we do so;\r
+ // otherwise we use our default search filter.\r
+ // The user may have set the search filter 3 different ways:\r
+ //\r
+ // - if %USERNAME% was found in the search filter, then we\r
+ // will substitute this with the passed-in uid at runtime.\r
+ //\r
+ // - if "derby.user" is the search filter value, then we\r
+ // will assume the user's DN can be found in the system or\r
+ // database property "derby.user.<uid>" . If the property\r
+ // does not exist, then we will do a normal lookup with our\r
+ // default search filter; otherwise we will perform an\r
+ // authenticated bind to the LDAP server using the found DN.\r
+ //\r
+ // - if neither of the 2 previous values were found, then we use\r
+ // our default search filter and we will substitute insert the\r
+ // uid passed at runtime into our default search filter.\r
+ //\r
+ String searchFilterProp =\r
+ authenticationService.getProperty(LDAP_SEARCH_FILTER);\r
+ \r
+ if (searchFilterProp == (String) null)\r
+ {\r
+ // use our default search filter\r
+ this.leftSearchFilter = "(&(objectClass=inetOrgPerson)(uid=";\r
+ this.rightSearchFilter = "))";\r
+\r
+ } else if (StringUtil.SQLEqualsIgnoreCase(searchFilterProp,LDAP_LOCAL_USER_DN)) {\r
+\r
+ // use local user DN in derby.user.<uid>\r
+ this.leftSearchFilter = "(&(objectClass=inetOrgPerson)(uid=";\r
+ this.rightSearchFilter = "))";\r
+ this.useUserPropertyAsDN = true;\r
+\r
+ } else if (searchFilterProp.indexOf(\r
+ LDAP_SEARCH_FILTER_USERNAME) != -1) {\r
+\r
+ // user has set %USERNAME% in the search filter\r
+ this.leftSearchFilter = searchFilterProp.substring(0,\r
+ searchFilterProp.indexOf(LDAP_SEARCH_FILTER_USERNAME));\r
+ this.rightSearchFilter = searchFilterProp.substring(\r
+ searchFilterProp.indexOf(LDAP_SEARCH_FILTER_USERNAME)+\r
+ (int) LDAP_SEARCH_FILTER_USERNAME.length());\r
+\r
+\r
+ } else { // add this search filter to ours\r
+\r
+ // complement this search predicate to ours\r
+ this.leftSearchFilter = "(&("+searchFilterProp+")"+\r
+ "(objectClass=inetOrgPerson)(uid=";\r
+ this.rightSearchFilter = "))";\r
+\r
+ }\r
+\r
+ if (SanityManager.DEBUG)\r
+ {\r
+ if (SanityManager.DEBUG_ON(\r
+ AuthenticationServiceBase.AuthenticationTrace)) {\r
+\r
+ java.io.PrintWriter iDbgStream =\r
+ SanityManager.GET_DEBUG_STREAM();\r
+\r
+ iDbgStream.println(\r
+ "\n\n+ LDAP Authentication Configuration:\n"+\r
+ " - provider URL ["+this.providerURL+"]\n"+\r
+ " - search base ["+this.searchBaseDN+"]\n"+\r
+ " - search filter to be [" +\r
+ this.leftSearchFilter + "<uid>" +\r
+ this.rightSearchFilter + "]\n" +\r
+ " - use local DN [" +\r
+ (useUserPropertyAsDN ? "true" : "false") +\r
+ "]\n"\r
+ );\r
+ }\r
+ }\r
+\r
+ if (SanityManager.DEBUG)\r
+ {\r
+ if (SanityManager.DEBUG_ON(\r
+ AuthenticationServiceBase.AuthenticationTrace)) {\r
+ \r
+ // This tracing needs some investigation and cleanup.\r
+ // 1) It creates the file in user.dir instead of derby.system.home\r
+ // 2) It doesn't seem to work. The file is empty after successful\r
+ // and unsuccessful ldap connects. Perhaps the fileOutputStream\r
+ // is never flushed and closed.\r
+ // I (Kathey Marsden) wrapped this in a priv block and kept the previous\r
+ // behaviour that it will not stop processing if file \r
+ // creation fails. Perhaps that should be investigated as well.\r
+ FileOutputStream fos = null;\r
+ try {\r
+ fos = ((FileOutputStream)AccessController.doPrivileged(\r
+ new PrivilegedExceptionAction() {\r
+ public Object run() throws SecurityException, java.io.IOException {\r
+ return new FileOutputStream("DerbyLDAP.out");\r
+ }\r
+ }));\r
+ } catch (PrivilegedActionException pae) {\r
+ // If trace file creation fails do not stop execution. \r
+ }\r
+ if (fos != null)\r
+ initDirContextEnv.put("com.sun.naming.ldap.trace.ber",fos);\r
+\r
+ \r
+ }\r
+ }\r
+ }\r
+\r
+ \r
+ \r
+ \r
+\r
+ /**\r
+ * Search for the full user's DN in the LDAP server.\r
+ * LDAP server bind may or not be anonymous.\r
+ *\r
+ * If the admin does not want us to do anonymous bind/search, then we\r
+ * must have been given principal/credentials in order to successfully\r
+ * bind to perform the user's DN search.\r
+ *\r
+ * @exception NamingException if could not retrieve the user DN.\r
+ **/\r
+ private String getDNFromUID(String uid)\r
+ throws javax.naming.NamingException\r
+ {\r
+ //\r
+ // We bind to the LDAP server here\r
+ // Note that this bind might be anonymous (if anonymous searches\r
+ // are allowed in the LDAP server, or authenticated if we were\r
+ // told/configured to.\r
+ //\r
+ Properties env = null;\r
+ if (this.searchAuthDN != (String) null) {\r
+ env = (Properties) initDirContextEnv.clone();\r
+ env.put(Context.SECURITY_PRINCIPAL, this.searchAuthDN);\r
+ env.put(Context.SECURITY_CREDENTIALS, this.searchAuthPW);\r
+ }\r
+ else\r
+ env = initDirContextEnv;\r
+\r
+ DirContext ctx = privInitialDirContext(env);\r
+\r
+ // Construct Search Filter\r
+ SearchControls ctls = new SearchControls();\r
+ // Set-up a LDAP subtree search scope\r
+ ctls.setSearchScope(SearchControls.SUBTREE_SCOPE);\r
+\r
+ // Just retrieve the DN\r
+ ctls.setReturningAttributes(attrDN);\r
+\r
+ String searchFilter =\r
+ this.leftSearchFilter + uid + this.rightSearchFilter; \r
+ NamingEnumeration results =\r
+ ctx.search(searchBaseDN, searchFilter, ctls);\r
+ \r
+ // If we did not find anything then login failed\r
+ if (results == null || !results.hasMore())\r
+ throw new NameNotFoundException();\r
+ \r
+ SearchResult result = (SearchResult)results.next();\r
+ \r
+ if (results.hasMore())\r
+ {\r
+ // This is a login failure as we cannot assume the first one\r
+ // is the valid one.\r
+ if (SanityManager.DEBUG)\r
+ {\r
+ if (SanityManager.DEBUG_ON(\r
+ AuthenticationServiceBase.AuthenticationTrace)) {\r
+\r
+ java.io.PrintWriter iDbgStream =\r
+ SanityManager.GET_DEBUG_STREAM();\r
+\r
+ iDbgStream.println(\r
+ " - LDAP Authentication request failure: "+\r
+ "search filter [" + searchFilter + "]"+\r
+ ", retrieve more than one occurence in "+\r
+ "LDAP server [" + this.providerURL + "]");\r
+ }\r
+ }\r
+ throw new NameNotFoundException();\r
+ }\r
+\r
+ NameParser parser = ctx.getNameParser(searchBaseDN);\r
+ Name userDN = parser.parse(searchBaseDN);\r
+\r
+ if (userDN == (Name) null)\r
+ // This should not happen in theory\r
+ throw new NameNotFoundException();\r
+ else\r
+ userDN.addAll(parser.parse(result.getName()));\r
+ \r
+ // Return the full user's DN\r
+ return userDN.toString();\r
+ }\r
+}\r