org.hsqldb.auth.LdapAuthBean Maven / Gradle / Ivy
Show all versions of hsqldb Show documentation
/* Copyright (c) 2001-2019, 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.5.0
*/
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.
*
* @param isTls boolean
*/
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 :
TRUE
This will match true values
* per OpenLDAP's boolean OID.
*
* @see Matcher#matches()
* @param accessValuePattern Pattern
*/
public void setAccessValuePattern(Pattern accessValuePattern) {
this.accessValuePattern = accessValuePattern;
}
/**
* String wrapper for method setAccessValuePattern(Pattern) Use the (x?)
* Pattern constructs to set options.
*
* @see #setAccessValuePattern(Pattern)
* @param patternString String
*/
public void setAccessValuePatternString(String patternString) {
setAccessValuePattern(Pattern.compile(patternString));
}
/**
* Assign a pattern to both detect honored values, and to map from a single
* value of "rolesSchemaAttribute"s to a HyperSQL role or schema string.
* If your rolesSchemaAttribute holds only the String values precisely as
* HyperSQL needs them, then don't use this method at all and all matching
* attribute values will be passed directly.
*
* 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.
*
* -
* Values that do not successfully match the pattern will be ignored.
*
-
* Optionally uses parentheses to specify a single capture group
* (if you use parentheses to specify more than one matching group, we
* will only capture for the first).
* What is captured by this group is exactly the role or schema that
* HyperSQL will attempt to assign.
* If no capture parens are given then the Pattern is only used for the
* acceptance decision, and the LDAP-provided value will be returned
* verbatim.
*
*
*
* 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 :
* cn=([^,]+),ou=dbRole,dc=admc,dc=com
*
* will extract the CN value from matching attribute values.
*
* Example1 :
* cn=[^,]+,ou=dbRole,dc=admc,dc=com
*
* will return the entire cn...com
string for matching
* attribute values.
*
* @see Matcher#matches()
* @param roleSchemaValuePattern pattern
*/
public void setRoleSchemaValuePattern(Pattern roleSchemaValuePattern) {
this.roleSchemaValuePattern = roleSchemaValuePattern;
}
/**
* String wrapper for method setRoleSchemaValuePattern(Pattern)
*
* Use the (x?) Pattern constructs to set options.
*
* @throws java.util.regex.PatternSyntaxException exception
* @see #setRoleSchemaValuePattern(Pattern)
*
* @param patternString pattern
*/
public void setRoleSchemaValuePatternString(String patternString) {
setRoleSchemaValuePattern(Pattern.compile(patternString));
}
/**
* Defaults to "SIMPLE".
*
* @param mechanism Either 'SIMPLE' (the default) for LDAP Simple, or
* one of the LDAP SASL mechanisms, such as 'DIGEST-MD5'.
*/
public void setSecurityMechanism(String mechanism) {
this.mechanism = mechanism;
}
/**
* Do not specify URL scheme ("ldap:") because that is implied.
* (Since we purposefully don't support LDAPS, there would be no reason to
* change that).
*
* 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
* @param ldapHost host
*/
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).
*
* @param principalTemplate template
*/
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.
*
* @param initialContextFactory factory
*/
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.
*
*
* @param saslRealm realm
*/
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"
*
* @param parentDn parent DN
*/
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)
*
* @param rdnAttribute RDN attribute
*/
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.
*
*
* @param attribute attribute
*/
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.
*
*
* @param attribute attribute
*/
public void setAccessAttribute(String attribute) {
accessAttribute = attribute;
}
/**
*
* @see AuthFunctionBean#authenticate(String, String)
* @param userName String
* @param password String
* @throws DenyException on access denial
* @return 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 sRess = null;
try {
sRess = ctx.search(parentDn,
new BasicAttributes(rdnAttribute, userName),
attributeUnion);
} catch (AuthenticationException ae) {
throw new DenyException();
} catch (Exception e) {
throw new RuntimeException(e);
}
if (!sRess.hasMore()) {
throw new DenyException();
}
SearchResult sRes = sRess.next();
if (sRess.hasMore()) {
throw new RuntimeException("> 1 result");
}
Attributes attrs = sRes.getAttributes();
if (accessAttribute != null) {
Attribute attribute = attrs.get(accessAttribute);
if (attribute == null) {
throw new DenyException();
}
if (attribute.size() != 1) {
throw new RuntimeException("Access attribute '"
+ accessAttribute + "' has unexpected value count: "
+ attribute.size());
}
if (accessValuePattern != null) {
Object accessValue = attribute.get(0);
if (accessValue == null) {
throw new RuntimeException(
"Access Attr. value is null");
}
if (!(accessValue instanceof String)) {
throw new RuntimeException("Access Attr. value "
+ "not a String: "
+ accessValue.getClass().getName());
}
if (!accessValuePattern.matcher(
(String) accessValue).matches()) {
throw new DenyException();
}
}
}
if (rolesSchemaAttribute == null) {
return null;
}
// If we reach here, then we definitely need to try to return a
// list of roles + schema.
List returns = new ArrayList();
Attribute attribute = attrs.get(rolesSchemaAttribute);
if (attribute != null) {
int valCount = attribute.size();
Matcher matcher;
Object oneVal;
for (int i = 0; i < valCount; i++) {
oneVal = attribute.get(i);
if (oneVal == null) {
throw new RuntimeException(
"R/S Attr value #" + i + " is null");
}
if (!(oneVal instanceof String)) {
throw new RuntimeException(
"R/S Attr value #" + i + " not a String: "
+ oneVal.getClass().getName());
}
if (roleSchemaValuePattern == null) {
returns.add((String) oneVal);
} else {
matcher = roleSchemaValuePattern.matcher(
(String) oneVal);
if (matcher.matches()) {
returns.add((matcher.groupCount() > 0)
? matcher.group(1)
: (String) oneVal);
}
}
}
}
if (returns.size() < 1) {
if (accessAttribute == null) {
throw new DenyException();
}
return new String[0];
}
return returns.toArray(new String[0]);
} catch (DenyException de) {
// This throws a non-runtime Exception, which is handled as an
// access denial instead of a system problem.
throw de;
} catch (RuntimeException re) {
throw re;
} catch (IOException ioe) {
throw new RuntimeException(ioe);
} catch (NamingException ne) {
throw new RuntimeException(ne);
} finally {
if (tlsResponse != null) try {
tlsResponse.close();
} catch (IOException ioe) {
logger.error("Failed to close TLS Response", ioe);
}
if (ctx != null) try {
ctx.close();
} catch (NamingException ne) {
logger.error("Failed to close LDAP Context", ne);
}
}
}
}