/* Copyright (c) 2001-2016, The HSQL Development Group * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright notice, this * list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * * Neither the name of the HSQL Development Group nor the names of its * contributors may be used to endorse or promote products derived from this * software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL HSQL DEVELOPMENT GROUP, HSQLDB.ORG, * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ package org.hsqldb.auth; import java.io.IOException; import java.util.ArrayList; import java.util.Hashtable; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.naming.AuthenticationException; import javax.naming.Context; import javax.naming.NamingEnumeration; import javax.naming.NamingException; import javax.naming.directory.Attribute; import javax.naming.directory.Attributes; import javax.naming.directory.BasicAttributes; import javax.naming.directory.SearchResult; import javax.naming.ldap.InitialLdapContext; import javax.naming.ldap.LdapContext; import javax.naming.ldap.StartTlsRequest; import javax.naming.ldap.StartTlsResponse; import org.hsqldb.lib.FrameworkLogger; /** * Authenticates to a HyperSQL catalog according to entries in a LDAP * database. * If using LDAP StartTLS and your server has a certificate not trusted by * default by your JRE, then set system property 'javax.net.ssl.trustStore' to * the path to a trust store containing the cert (as well as any other certs * that your app needs for other purposes). *
* This class with authenticate login attempts against LDAP entries with RDN of * the HyperSQL account name (the precise attribute name defaults to 'uid', but * you may change that). *
* This class purposefully does not support LDAPS, because LDAPS is deprecated * in favor of StartTLS, which we do support. * If you need to support LDAPS and are using SE 1.6, use our JaasAuthBean with * Sun's LdapLoginModule. *
* This class does not support SASL/External authentication, because the work * involved with securely obtaining user-specific certs would be more complex * than everything else here combined. * Another AuthFunctionBean would have to be written if SASL/External is needed. *
* To use instances of this class, you must use at least the methods * setLdapHost, setParentDn, initialize, plus * rolesSchemaAttribute and/or accessAttribute. *
* For a user to be given HyperSQL catalog access, that user must either have * a value for accessAttribute if that property is set (optionally requiring * a match with accessValuePattern); or, if the accessAttribute is not set then * must have some (any) value for rolesSchemaAttribute (optionally requiring a * match with roleSchemaValuePattern). * Consequently, if you have set both accessAttribute and rolesSchemaAttribute, * the latter attribute will only be consulted if the check of the former * attribute succeeds. *
* If you want roles assigned according to the local HyperSQL database instead * of according to LDAP, then set accessAttribute but not rolesSchemaAttribute. *
* If what is wanted is to grant access but with no roles (overriding local * roles if there are any), then set both accessAttribute and * rolesSchemaAttribute, but do not set any rolesSchemaAttribute attribute * values for these no-role users. * (I hesitate to mention it, but you could accomplish the same thing with only * a rolesSchemaAttribute attribute, by setting only a dummy role/schema value * for non-role users, because HyperSQL will ignore unknown roles or schemas * but still give access since a list was still supplied). *
* * @see AuthFunctionBean * @see #setLdapHost(String) * @see #setParentDn(String) * @see #init() * @author Blaine Simpson (blaine dot simpson at admc dot com) * @since 2.0.1 */ public class LdapAuthBean implements AuthFunctionBean { private static FrameworkLogger logger = FrameworkLogger.getLog(LdapAuthBean.class); private Integer ldapPort; private String ldapHost, principalTemplate, saslRealm, parentDn; private Pattern roleSchemaValuePattern, accessValuePattern; private String initialContextFactory = "com.sun.jndi.ldap.LdapCtxFactory"; private boolean tls; // This is for StartTLS, not tunneled TLS/LDAPS. // Variable named just "tls" only for brevity. private String mechanism = "SIMPLE"; private String rdnAttribute = "uid"; private boolean initialized; private String rolesSchemaAttribute, accessAttribute; protected String[] attributeUnion; public LdapAuthBean() { // Intentionally empty } /** * If this is set, then the entire (brief) transaction with the LDAP server * will be encrypted. */ public void setStartTls(boolean isTls) { this.tls = isTls; } public void setLdapPort(int ldapPort) { this.ldapPort = Integer.valueOf(ldapPort); } /** * @throws IllegalStateException if any required setting has not been set. */ public void init() { if (ldapHost == null) { throw new IllegalStateException( "Required property 'ldapHost' not set"); } if (parentDn == null) { throw new IllegalStateException( "Required property 'parentDn' not set"); } if (initialContextFactory == null) { throw new IllegalStateException( "Required property 'initialContextFactory' not set"); } if (mechanism == null) { throw new IllegalStateException( "Required property 'mechanism' not set"); } if (rdnAttribute == null) { throw new IllegalStateException( "Required property 'rdnAttribute' not set"); } if (rolesSchemaAttribute == null && accessAttribute == null) { throw new IllegalStateException( "You must set property 'rolesSchemaAttribute' " + "and/or property 'accessAttribute'"); } if (roleSchemaValuePattern != null && rolesSchemaAttribute == null) { throw new IllegalStateException( "If property 'roleSchemaValuePattern' is set, then you " + "must also set property 'rolesSchemaAttribute' to " + "indicate which attribute to evaluate"); } if (accessValuePattern != null && accessAttribute == null) { throw new IllegalStateException( "If property 'accessValuePattern' is set, then you " + "must also set property 'accessAttribute' to " + "indicate which attribute to evaluate"); } if (rolesSchemaAttribute != null && accessAttribute != null) { attributeUnion = new String[] { rolesSchemaAttribute, accessAttribute }; } else if (rolesSchemaAttribute != null) { attributeUnion = new String[] { rolesSchemaAttribute }; } else { attributeUnion = new String[] { accessAttribute }; } initialized = true; } /** * Assign a pattern to detect honored accessAttribute values. * If you set accessAttribute but not accessValuePattern, then all that will * be checked for access is if the RDN + parentDN entry has the * accessAttribute attribute. (I.e. the specific value will not matter * whatsoever). ** You may only use this property if you have set property accessAttribute. * If you have set accessAttribute but not this property, then access will * be decided based solely upon existence of this attribute. *
* Capture groups in the pattern will be ignored and serve no purpose. *
* N.b. this Pattern will be used for the matches() operation, therefore it * must match the entire candidate value strings (this is different than * the find operation which does not need to satisfy the entire candidate * value). *
Example1 :
* This will match true values per OpenLDAP's boolean OID.
*
* TRUE
*
* You may only use this property if you have set property * rolesSchemaAttribute. * If rolesSchemaAttribute is set but this property is not set, then * the value will directly determine the user's roles and schema. *
* Unlike the rolesSchemaAttribute, the property at-hand uses the * singular for "role", because whereas rolesSchemaAttribute is the * attribute for listing multiple roles, roleSchemaValuePattern is used * to evaluate single role values. *
* These are two distinct and important purposes for the specified Pattern. *
* Together, these two features work great to extract just the needed role * and schema names from 'memberof' DNs, and will have no problem if you * also use 'memberof' for unrelated purposes. *
* N.b. this Pattern will be used for the matches() operation, therefore it * must match the entire candidate value strings (this is different than * the find operation which does not need to satisfy the entire candidate * value). *
Example1 :
* will extract the CN value from matching attribute values.
*
* cn=([^,]+),ou=dbRole,dc=admc,dc=com
*
Example1 :
* will return the entire
* cn=[^,]+,ou=dbRole,dc=admc,dc=com
*
cn...com
string for matching
* attribute values.
*
* If using StartTLS, then this host name must match the cn of the LDAP * server's certificate. *
* If you need to support LDAPS and are using SE 1.6, use our JaasAuthBean * with Sun's LdapLoginModule instead of this class. *
* * @see JaasAuthBean */ public void setLdapHost(String ldapHost) { this.ldapHost = ldapHost; } /** * A template String containing place-holder token '${username}'. * All occurrences of '${username}' (without the quotes) will be translated * to the username that authentication is being attempted with. ** If you supply a principalTemplate that does not contain '${username}', * then authentication will be user-independent. *
* It is common to authenticate to LDAP servers with the DN of the user's
* LDAP entry. In this situation, set principalTemplate to
* <RDN_ATTR=>${username},<PARENT_DN>
.
* For example if you use parentDn of
* "ou=people,dc=admc,dc=com"
and rdnAttribute of
* uid
, then you would set
*
* "uid=${username},ou=people,dc=admc,dc=com"
*
* By default the user name will be passed exactly as it is, so don't use * this setter if that is what you want. (This works great for OpenLDAP * with DIGEST-MD5 SASL, for example). *
*/ public void setPrincipalTemplate(String principalTemplate) { this.principalTemplate = principalTemplate; } /** * Most users should not call this, and will get the default of * "com.sun.jndi.ldap.LdapCtxFactory". * Use this method if you prefer to use a context factory provided by your * framework or container, for example, or if you are using a non-Sun JRE. */ public void setInitialContextFactory(String initialContextFactory) { this.initialContextFactory = initialContextFactory; } /** * Some LDAP servers using a SASL mechanism require a realm to be specified, * and some mechanisms allow a realm to be specified if you wish to use that * feature. * By default no realm will be sent to the LDAP server. ** Don't use this setter if you are not setting a SASL mechanism. *
*/ public void setSaslRealm(String saslRealm) { this.saslRealm = saslRealm; } /** * Set DN which is parent of the user DNs. * E.g. "ou=people,dc=admc,dc=com" */ public void setParentDn(String parentDn) { this.parentDn = parentDn; } /** * rdnAttribute must hold the user name exactly as the HyperSQL login will * be made with. ** This is the RDN relative to the Parent DN specified with setParentDN. * Defaults to 'uid'. *
* * @see #setParentDn(String) */ public void setRdnAttribute(String rdnAttribute) { this.rdnAttribute = rdnAttribute; } /** * Set the attribute name of the RDN + parentDn entries in which is stored * the list of roles and optional schema for the authenticating user. ** There is no default. You must set this attribute if you want LDAP * instead of the local HyperSQL database to determine the user's roles! * You must set the rolesSchemaAttribute property and/or the * accessAttribute property. * Consequently, if you do no tset this property, then you must set the * accessAttribute property, and this LdapAuthBean will only determine * access not roles. *
* To use the nice reverse group membership feature of LDAP, set * this value to "memberof". *
* If you have set both rolesSchemaAttribute and this value, then the * attribute set here will only be consulted if the accessAttribute check * succeeds. *
*/ public void setRolesSchemaAttribute(String attribute) { rolesSchemaAttribute = attribute; } /** * Set the attribute name of the RDN + parentDn entries which will be * consulted to decide whether the user can access the HyperSQL database. ** There is no default. If you set this attribute, then the attribute will * determine whether the user can access the HyperSQL database, regardless * of whether the rolesSchemaAttribute attribute is set. *
* If you set just this property, then the local HyperSQL database will * decide all roles for the user. If you set this property and property * rolesSchemaAttribute then this attribute will determine access, and if * this attribute grants access then the rolesSchemaAttribute value will * determine the user's roles. *
*/ public void setAccessAttribute(String attribute) { accessAttribute = attribute; } /** * @see AuthFunctionBean#authenticate(String, String) */ public String[] authenticate(String userName, String password) throws DenyException { if (!initialized) { throw new IllegalStateException( "You must invoke the 'init' method to initialize the " + LdapAuthBean.class.getName() + " instance."); } Hashtable env = new Hashtable(5, 0.75f); env.put(Context.INITIAL_CONTEXT_FACTORY, initialContextFactory); env.put(Context.PROVIDER_URL, "ldap://" + ldapHost + ((ldapPort == null) ? "" : (":" + ldapPort))); StartTlsResponse tlsResponse = null; LdapContext ctx = null; try { ctx = new InitialLdapContext(env, null); if (tls) { // Requesting to start TLS on an LDAP association tlsResponse = (StartTlsResponse) ctx.extendedOperation( new StartTlsRequest()); // Starting TLS tlsResponse.negotiate(); } // A TLS/SSL secure channel has been established if you reach here. // Assertion of client's authorization Identity -- Explicit way ctx.addToEnvironment(Context.SECURITY_AUTHENTICATION, mechanism); ctx.addToEnvironment(Context.SECURITY_PRINCIPAL, ((principalTemplate == null) ? userName : principalTemplate.replace("${username}", userName))); ctx.addToEnvironment(Context.SECURITY_CREDENTIALS, password); if (saslRealm != null) { env.put("java.naming.security.sasl.realm", saslRealm); } // The Context.SECURITY_* authorizations are only applied when the // following statement executes. (Or any other remote operations done // while the TLS connection is still open). NamingEnumeration