org.springframework.ldap.core.support.AbstractContextSource Maven / Gradle / Ivy
/*
* Copyright 2005-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ldap.core.support;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Hashtable;
import java.util.ListIterator;
import java.util.Map;
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.DirContext;
import javax.naming.ldap.LdapName;
import javax.naming.ldap.Rdn;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.ldap.UncategorizedLdapException;
import org.springframework.ldap.core.AuthenticationSource;
import org.springframework.ldap.core.ContextSource;
import org.springframework.ldap.core.DistinguishedName;
import org.springframework.ldap.support.LdapEncoder;
import org.springframework.ldap.support.LdapUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
/**
* Abstract implementation of the {@link ContextSource} interface. By default, returns an
* authenticated DirContext
implementation for both read-only and read-write
* operations. To have an anonymous environment created for read-only operations, set the
* anonymousReadOnly
property to true
.
*
* Implementing classes need to implement {@link #getDirContextInstance(Hashtable)} to
* create a DirContext
instance of the desired type.
*
* If an {@link AuthenticationSource} is set, this will be used for getting user principal
* and password for each new connection, otherwise a default one will be created using the
* specified userDn
and password
.
*
* Note: When using implementations of this class outside of a Spring Context it is
* necessary to call {@link #afterPropertiesSet()} when all properties are set, in order
* to finish up initialization.
*
* @author Mattias Hellborg Arthursson
* @author Adam Skogman
* @author Ulrik Sandberg
* @see org.springframework.ldap.core.LdapTemplate
* @see org.springframework.ldap.core.support.DefaultDirObjectFactory
* @see org.springframework.ldap.core.support.LdapContextSource
* @see org.springframework.ldap.core.support.DirContextSource
*/
public abstract class AbstractContextSource implements BaseLdapPathContextSource, InitializingBean {
private static final String DEFAULT_CONTEXT_FACTORY = "com.sun.jndi.ldap.LdapCtxFactory";
private static final Class DEFAULT_DIR_OBJECT_FACTORY = DefaultDirObjectFactory.class;
private static final boolean DONT_DISABLE_POOLING = false;
private static final boolean EXPLICITLY_DISABLE_POOLING = true;
private static final int DEFAULT_BUFFER_SIZE = 1024;
private Class dirObjectFactory = DEFAULT_DIR_OBJECT_FACTORY;
private Class contextFactory;
private LdapName base = LdapUtils.emptyLdapName();
/**
* @deprecated use {@link #getUserDn()} and {@link #setUserDn(String)} instead
*/
@Deprecated
protected String userDn = "";
/**
* @deprecated use {@link #getPassword()} and {@link #setPassword(String)} instead
*/
@Deprecated
protected String password = "";
private String[] urls;
private boolean pooled = false;
private Hashtable baseEnv = new Hashtable();
private Hashtable anonymousEnv;
private AuthenticationSource authenticationSource;
private boolean cacheEnvironmentProperties = true;
private boolean anonymousReadOnly = false;
private String referral = null;
private static final Logger LOG = LoggerFactory.getLogger(AbstractContextSource.class);
public static final String SUN_LDAP_POOLING_FLAG = "com.sun.jndi.ldap.connect.pool";
private static final String JDK_142 = "1.4.2";
private DirContextAuthenticationStrategy authenticationStrategy = new SimpleDirContextAuthenticationStrategy();
public AbstractContextSource() {
try {
this.contextFactory = Class.forName(DEFAULT_CONTEXT_FACTORY);
}
catch (ClassNotFoundException ex) {
LOG.trace("The default for contextFactory cannot be resolved", ex);
}
}
public DirContext getContext(String principal, String credentials) {
// This method is typically called for authentication purposes, which means that
// we
// should explicitly disable pooling in case passwords are changed (LDAP-183).
return doGetContext(principal, credentials, EXPLICITLY_DISABLE_POOLING);
}
private DirContext doGetContext(String principal, String credentials, boolean explicitlyDisablePooling) {
Hashtable env = getAuthenticatedEnv(principal, credentials);
if (explicitlyDisablePooling) {
env.remove(SUN_LDAP_POOLING_FLAG);
}
DirContext ctx = createContext(env);
try {
DirContext processedDirContext = this.authenticationStrategy.processContextAfterCreation(ctx, principal,
credentials);
return processedDirContext;
}
catch (NamingException ex) {
closeContext(ctx);
throw LdapUtils.convertLdapException(ex);
}
}
/*
* (non-Javadoc)
*
* @see org.springframework.ldap.core.ContextSource#getReadOnlyContext()
*/
public DirContext getReadOnlyContext() {
if (!this.anonymousReadOnly) {
return doGetContext(this.authenticationSource.getPrincipal(), this.authenticationSource.getCredentials(),
DONT_DISABLE_POOLING);
}
else {
return createContext(getAnonymousEnv());
}
}
/*
* (non-Javadoc)
*
* @see org.springframework.ldap.core.ContextSource#getReadWriteContext()
*/
public DirContext getReadWriteContext() {
return doGetContext(this.authenticationSource.getPrincipal(), this.authenticationSource.getCredentials(),
DONT_DISABLE_POOLING);
}
/**
* Default implementation of setting the environment up to be authenticated. This
* method should typically NOT be overridden; any customization to the authentication
* mechanism should be managed by setting a different
* {@link DirContextAuthenticationStrategy} on this instance.
* @param env the environment to modify.
* @param principal the principal to authenticate with.
* @param credentials the credentials to authenticate with.
* @see DirContextAuthenticationStrategy
* @see #setAuthenticationStrategy(DirContextAuthenticationStrategy)
*/
protected void setupAuthenticatedEnvironment(Hashtable env, String principal, String credentials) {
try {
this.authenticationStrategy.setupEnvironment(env, principal, credentials);
}
catch (NamingException ex) {
throw LdapUtils.convertLdapException(ex);
}
}
/**
* Close the context and swallow any exceptions.
* @param ctx the DirContext to close.
*/
private void closeContext(DirContext ctx) {
if (ctx != null) {
try {
ctx.close();
}
catch (Exception ex) {
LOG.debug("Exception closing context", ex);
}
}
}
/**
* Assemble a valid url String from all registered urls to add as
* PROVIDER_URL
to the environment.
* @param ldapUrls all individual url Strings.
* @return the full url String
*/
public String assembleProviderUrlString(String[] ldapUrls) {
StringBuilder providerUrlBuffer = new StringBuilder(DEFAULT_BUFFER_SIZE);
for (String ldapUrl : ldapUrls) {
providerUrlBuffer.append(ldapUrl);
if (!this.base.isEmpty()) {
if (!ldapUrl.endsWith("/")) {
providerUrlBuffer.append("/");
}
}
providerUrlBuffer.append(formatForUrl(this.base));
providerUrlBuffer.append(' ');
}
return providerUrlBuffer.toString().trim();
}
static String formatForUrl(LdapName ldapName) {
StringBuilder sb = new StringBuilder();
ListIterator it = ldapName.getRdns().listIterator(ldapName.size());
while (it.hasPrevious()) {
Rdn component = it.previous();
Attributes attributes = component.toAttributes();
// Loop through all attribute of the rdn (usually just one, but more are
// supported by RFC)
NamingEnumeration allAttributes = attributes.getAll();
while (allAttributes.hasMoreElements()) {
Attribute oneAttribute = allAttributes.nextElement();
String encodedAttributeName = nameEncodeForUrl(oneAttribute.getID());
// Loop through all values of the attribute (usually just one, but more
// are supported by RFC)
NamingEnumeration allValues;
try {
allValues = oneAttribute.getAll();
}
catch (NamingException ex) {
throw new UncategorizedLdapException("Unexpected error occurred formatting base URL", ex);
}
while (allValues.hasMoreElements()) {
sb.append(encodedAttributeName).append('=');
Object oneValue = allValues.nextElement();
if (oneValue instanceof String) {
String oneString = (String) oneValue;
sb.append(nameEncodeForUrl(oneString));
}
else {
throw new IllegalArgumentException("Binary attributes not supported for base URL");
}
if (allValues.hasMoreElements()) {
sb.append('+');
}
}
if (allAttributes.hasMoreElements()) {
sb.append('+');
}
}
if (it.hasPrevious()) {
sb.append(',');
}
}
return sb.toString();
}
static String nameEncodeForUrl(String value) {
try {
String ldapEncoded = LdapEncoder.nameEncode(value);
URI valueUri = new URI(null, null, ldapEncoded, null);
return valueUri.toString();
}
catch (URISyntaxException ex) {
throw new UncategorizedLdapException("This really shouldn't happen - report this", ex);
}
}
/**
* Set the base suffix from which all operations should origin. If a base suffix is
* set, you will not have to (and, indeed, must not) specify the full distinguished
* names in any operations performed.
* @param base the base suffix.
*/
public void setBase(String base) {
if (base != null) {
this.base = LdapUtils.newLdapName(base);
}
else {
this.base = LdapUtils.emptyLdapName();
}
}
/**
* @deprecated {@link DistinguishedName} and associated classes and methods are
* deprecated as of 2.0.
*/
@Override
public DistinguishedName getBaseLdapPath() {
return new DistinguishedName(this.base);
}
@Override
public LdapName getBaseLdapName() {
return (LdapName) this.base.clone();
}
@Override
public String getBaseLdapPathAsString() {
return getBaseLdapName().toString();
}
/**
* Create a DirContext using the supplied environment.
* @param environment the LDAP environment to use when creating the
* DirContext
.
* @return a new DirContext implementation initialized with the supplied environment.
*/
protected DirContext createContext(Hashtable environment) {
DirContext ctx = null;
try {
ctx = getDirContextInstance(environment);
if (LOG.isInfoEnabled()) {
Hashtable ctxEnv = ctx.getEnvironment();
String ldapUrl = (String) ctxEnv.get(Context.PROVIDER_URL);
LOG.debug("Got Ldap context on server '" + ldapUrl + "'");
}
return ctx;
}
catch (NamingException ex) {
closeContext(ctx);
throw LdapUtils.convertLdapException(ex);
}
}
/**
* Set the context factory. Default is com.sun.jndi.ldap.LdapCtxFactory.
* @param contextFactory the context factory used when creating Contexts.
*/
public void setContextFactory(Class contextFactory) {
this.contextFactory = contextFactory;
}
/**
* Get the context factory.
* @return the context factory used when creating Contexts.
*/
public Class getContextFactory() {
return this.contextFactory;
}
/**
* Set the DirObjectFactory to use. Default is {@link DefaultDirObjectFactory}. The
* specified class needs to be an implementation of javax.naming.spi.DirObjectFactory.
* Note: Setting this value to null may have cause connection leaks when using
* ContextMapper methods in LdapTemplate.
* @param dirObjectFactory the DirObjectFactory to be used. Null means that no
* DirObjectFactory will be used.
*/
public void setDirObjectFactory(Class dirObjectFactory) {
this.dirObjectFactory = dirObjectFactory;
}
/**
* Get the DirObjectFactory to use.
* @return the DirObjectFactory to be used. null
means that no
* DirObjectFactory will be used.
*/
public Class getDirObjectFactory() {
return this.dirObjectFactory;
}
/**
* Checks that all necessary data is set and that there is no compatibility issues,
* after which the instance is initialized. Note that you need to call this method
* explicitly after setting all desired properties if using the class outside of a
* Spring Context.
*/
public void afterPropertiesSet() {
if (ObjectUtils.isEmpty(this.urls)) {
throw new IllegalArgumentException("At least one server url must be set");
}
if (this.contextFactory == null) {
throw new IllegalArgumentException("contextFactory must be set");
}
if (this.authenticationSource == null) {
LOG.debug("AuthenticationSource not set - " + "using default implementation");
if (!StringUtils.hasText(this.userDn)) {
LOG.info("Property 'userDn' not set - " + "anonymous context will be used for read-write operations");
this.anonymousReadOnly = true;
}
if (!this.anonymousReadOnly) {
if (this.password == null) {
throw new IllegalArgumentException(
"Property 'password' cannot be null. To use a blank password, please ensure it is set to \"\"");
}
if (!StringUtils.hasText(this.password)) {
LOG.info("Property 'password' not set - " + "blank password will be used");
}
}
this.authenticationSource = new SimpleAuthenticationSource();
}
if (this.cacheEnvironmentProperties) {
this.anonymousEnv = setupAnonymousEnv();
}
}
@SuppressWarnings("deprecation")
private Hashtable setupAnonymousEnv() {
if (this.pooled) {
this.baseEnv.put(SUN_LDAP_POOLING_FLAG, "true");
LOG.debug("Using LDAP pooling.");
}
else {
this.baseEnv.remove(SUN_LDAP_POOLING_FLAG);
LOG.debug("Not using LDAP pooling");
}
Hashtable env = new Hashtable(this.baseEnv);
env.put(Context.INITIAL_CONTEXT_FACTORY, this.contextFactory.getName());
env.put(Context.PROVIDER_URL, assembleProviderUrlString(this.urls));
if (this.dirObjectFactory != null) {
env.put(Context.OBJECT_FACTORIES, this.dirObjectFactory.getName());
}
if (StringUtils.hasText(this.referral)) {
env.put(Context.REFERRAL, this.referral);
}
if (!this.base.isEmpty()) {
// Save the base path for use in the DefaultDirObjectFactory.
env.put(DefaultDirObjectFactory.JNDI_ENV_BASE_PATH_KEY, this.base);
}
LOG.debug("Trying provider Urls: " + assembleProviderUrlString(this.urls));
return env;
}
/**
* Set the password (credentials) to use for getting authenticated contexts.
* @param password the password.
*/
public void setPassword(String password) {
this.password = password;
}
/**
* Gets the password (credentials) to use for getting authenticated contexts.
* @return the password
*/
public String getPassword() {
return this.password;
}
/**
* Set the user distinguished name (principal) to use for getting authenticated
* contexts.
* @param userDn the user distinguished name.
*/
public void setUserDn(String userDn) {
this.userDn = userDn;
}
/**
* Gets the user distinguished name (principal) to use for getting authenticated
* contexts.
* @return the user distinguished name.
*/
public String getUserDn() {
return this.userDn;
}
/**
* Set the urls of the LDAP servers. Use this method if several servers are required.
* @param urls the urls of all servers.
*/
public void setUrls(String[] urls) {
this.urls = urls.clone();
}
/**
* Get the urls of the LDAP servers.
* @return the urls of all servers.
*/
public String[] getUrls() {
return this.urls.clone();
}
/**
* Set the url of the LDAP server. Utility method if only one server is used.
* @param url the url of the LDAP server.
*/
public void setUrl(String url) {
this.urls = new String[] { url };
}
/**
* Set whether the pooling flag should be set, enabling the built-in LDAP connection
* pooling. Default is false
. The built-in LDAP connection pooling
* suffers from a number of deficiencies, e.g. no connection validation. Also,
* enabling this flag when using TLS connections will explicitly not work. Consider
* using the Spring LDAP PoolingContextSource
as an alternative instead
* of enabling this flag.
*
* Note that since LDAP pooling is system wide, full configuration of this needs be
* done using system parameters as specified in the LDAP/JNDI documentation. Also
* note, that pooling is done on user dn basis, i.e. each individually authenticated
* connection will be pooled separately. This means that LDAP pooling will be most
* efficient using anonymous connections or connections authenticated using one single
* system user.
* @param pooled whether Contexts should be pooled.
*/
public void setPooled(boolean pooled) {
this.pooled = pooled;
}
/**
* Get whether the pooling flag should be set.
* @return whether Contexts should be pooled.
*/
public boolean isPooled() {
return this.pooled;
}
/**
* If any custom environment properties are needed, these can be set using this
* method.
* @param baseEnvironmentProperties the base environment properties that should always
* be used when creating new Context instances.
*/
public void setBaseEnvironmentProperties(Map baseEnvironmentProperties) {
this.baseEnv = new Hashtable(baseEnvironmentProperties);
}
protected Hashtable getAnonymousEnv() {
if (this.cacheEnvironmentProperties) {
return this.anonymousEnv;
}
else {
return setupAnonymousEnv();
}
}
protected Hashtable getAuthenticatedEnv(String principal, String credentials) {
// The authenticated environment should always be rebuilt.
Hashtable env = new Hashtable(getAnonymousEnv());
setupAuthenticatedEnvironment(env, principal, credentials);
return env;
}
/**
* Set the authentication source to use when retrieving user principal and
* credentials.
* @param authenticationSource the {@link AuthenticationSource} that will provide user
* info.
*/
public void setAuthenticationSource(AuthenticationSource authenticationSource) {
this.authenticationSource = authenticationSource;
}
/**
* Get the authentication source.
* @return the {@link AuthenticationSource} that will provide user info.
*/
public AuthenticationSource getAuthenticationSource() {
return this.authenticationSource;
}
/**
* Set whether environment properties should be cached between requsts for anonymous
* environment. Default is true
; setting this property to
* false
causes the environment Hashmap to be rebuilt from the current
* property settings of this instance between each request for an anonymous
* environment.
* @param cacheEnvironmentProperties true
causes that the anonymous
* environment properties should be cached, false
causes the Hashmap to
* be rebuilt for each request.
*/
public void setCacheEnvironmentProperties(boolean cacheEnvironmentProperties) {
this.cacheEnvironmentProperties = cacheEnvironmentProperties;
}
/**
* Set whether an anonymous environment should be used for read-only operations.
* Default is false
.
* @param anonymousReadOnly true
if an anonymous environment should be
* used for read-only operations, false
otherwise.
*/
public void setAnonymousReadOnly(boolean anonymousReadOnly) {
this.anonymousReadOnly = anonymousReadOnly;
}
/**
* Get whether an anonymous environment should be used for read-only operations.
* @return true
if an anonymous environment should be used for read-only
* operations, false
otherwise.
*/
public boolean isAnonymousReadOnly() {
return this.anonymousReadOnly;
}
/**
* Set the {@link DirContextAuthenticationStrategy} to use for preparing the
* environment and processing the created DirContext
instances.
* @param authenticationStrategy the {@link DirContextAuthenticationStrategy} to use;
* default is {@link SimpleDirContextAuthenticationStrategy}.
*/
public void setAuthenticationStrategy(DirContextAuthenticationStrategy authenticationStrategy) {
this.authenticationStrategy = authenticationStrategy;
}
/**
* Set the method to handle referrals. Default is 'ignore'; setting this flag to
* 'follow' will enable referrals to be automatically followed. Note that this might
* require particular name server setup in order to work (the referred URLs will need
* to be automatically found using standard DNS resolution).
* @param referral the value to set the system property Context.REFERRAL
* to, customizing the way that referrals are handled.
*/
public void setReferral(String referral) {
this.referral = referral;
}
/**
* Implement in subclass to create a DirContext of the desired type (e.g.
* InitialDirContext or InitialLdapContext).
* @param environment the environment to use when creating the instance.
* @return a new DirContext instance.
* @throws NamingException if one is encountered when creating the instance.
*/
protected abstract DirContext getDirContextInstance(Hashtable environment) throws NamingException;
class SimpleAuthenticationSource implements AuthenticationSource {
@Override
public String getPrincipal() {
return AbstractContextSource.this.userDn;
}
@Override
public String getCredentials() {
return AbstractContextSource.this.password;
}
}
}