mirror of https://github.com/apache/druid.git
Fix classNotFoundException when connecting to secure LDAP (#11978)
This PR fixes a problem where the com.sun.jndi.ldap.Connection tries to build BasicSecuritySSLSocketFactory when calling LDAPCredentialsValidator.validateCredentials since BasicSecuritySSLSocketFactory is in extension class loader and not visible to system classloader.
This commit is contained in:
parent
af6541a236
commit
503384569a
|
@ -22,6 +22,7 @@ package org.apache.druid.security.basic.authentication.validator;
|
||||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
import com.fasterxml.jackson.annotation.JsonTypeName;
|
import com.fasterxml.jackson.annotation.JsonTypeName;
|
||||||
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
import org.apache.druid.java.util.common.StringUtils;
|
import org.apache.druid.java.util.common.StringUtils;
|
||||||
import org.apache.druid.java.util.common.logger.Logger;
|
import org.apache.druid.java.util.common.logger.Logger;
|
||||||
import org.apache.druid.metadata.PasswordProvider;
|
import org.apache.druid.metadata.PasswordProvider;
|
||||||
|
@ -43,6 +44,7 @@ import javax.naming.directory.InitialDirContext;
|
||||||
import javax.naming.directory.SearchControls;
|
import javax.naming.directory.SearchControls;
|
||||||
import javax.naming.directory.SearchResult;
|
import javax.naming.directory.SearchResult;
|
||||||
import javax.naming.ldap.LdapName;
|
import javax.naming.ldap.LdapName;
|
||||||
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
@ -58,6 +60,9 @@ public class LDAPCredentialsValidator implements CredentialsValidator
|
||||||
private final LruBlockCache cache;
|
private final LruBlockCache cache;
|
||||||
|
|
||||||
private final BasicAuthLDAPConfig ldapConfig;
|
private final BasicAuthLDAPConfig ldapConfig;
|
||||||
|
// Custom overrides that can be passed via tests
|
||||||
|
@Nullable
|
||||||
|
private final Properties overrideProperties;
|
||||||
|
|
||||||
@JsonCreator
|
@JsonCreator
|
||||||
public LDAPCredentialsValidator(
|
public LDAPCredentialsValidator(
|
||||||
|
@ -91,6 +96,19 @@ public class LDAPCredentialsValidator implements CredentialsValidator
|
||||||
this.ldapConfig.getCredentialVerifyDuration(),
|
this.ldapConfig.getCredentialVerifyDuration(),
|
||||||
this.ldapConfig.getCredentialMaxDuration()
|
this.ldapConfig.getCredentialMaxDuration()
|
||||||
);
|
);
|
||||||
|
this.overrideProperties = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
public LDAPCredentialsValidator(
|
||||||
|
final BasicAuthLDAPConfig ldapConfig,
|
||||||
|
final LruBlockCache cache,
|
||||||
|
final Properties overrideProperties
|
||||||
|
)
|
||||||
|
{
|
||||||
|
this.ldapConfig = ldapConfig;
|
||||||
|
this.cache = cache;
|
||||||
|
this.overrideProperties = overrideProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
Properties bindProperties(BasicAuthLDAPConfig ldapConfig)
|
Properties bindProperties(BasicAuthLDAPConfig ldapConfig)
|
||||||
|
@ -119,6 +137,9 @@ public class LDAPCredentialsValidator implements CredentialsValidator
|
||||||
properties.put(Context.SECURITY_PROTOCOL, "ssl");
|
properties.put(Context.SECURITY_PROTOCOL, "ssl");
|
||||||
properties.put("java.naming.ldap.factory.socket", BasicSecuritySSLSocketFactory.class.getName());
|
properties.put("java.naming.ldap.factory.socket", BasicSecuritySSLSocketFactory.class.getName());
|
||||||
}
|
}
|
||||||
|
if (null != overrideProperties) {
|
||||||
|
properties.putAll(overrideProperties);
|
||||||
|
}
|
||||||
return properties;
|
return properties;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -139,7 +160,11 @@ public class LDAPCredentialsValidator implements CredentialsValidator
|
||||||
contextMap.put(BasicAuthUtils.SEARCH_RESULT_CONTEXT_KEY, principal.getSearchResult());
|
contextMap.put(BasicAuthUtils.SEARCH_RESULT_CONTEXT_KEY, principal.getSearchResult());
|
||||||
return new AuthenticationResult(username, authorizerName, authenticatorName, contextMap);
|
return new AuthenticationResult(username, authorizerName, authenticatorName, contextMap);
|
||||||
} else {
|
} else {
|
||||||
|
ClassLoader currentClassLoader = Thread.currentThread().getContextClassLoader();
|
||||||
try {
|
try {
|
||||||
|
// Set the context classloader same as the loader of this class so that BasicSecuritySSLSocketFactory
|
||||||
|
// class can be found
|
||||||
|
Thread.currentThread().setContextClassLoader(this.getClass().getClassLoader());
|
||||||
InitialDirContext dirContext = new InitialDirContext(bindProperties(this.ldapConfig));
|
InitialDirContext dirContext = new InitialDirContext(bindProperties(this.ldapConfig));
|
||||||
try {
|
try {
|
||||||
userResult = getLdapUserObject(this.ldapConfig, dirContext, username);
|
userResult = getLdapUserObject(this.ldapConfig, dirContext, username);
|
||||||
|
@ -162,6 +187,9 @@ public class LDAPCredentialsValidator implements CredentialsValidator
|
||||||
LOG.error(e, "Exception during user lookup");
|
LOG.error(e, "Exception during user lookup");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
finally {
|
||||||
|
Thread.currentThread().setContextClassLoader(currentClassLoader);
|
||||||
|
}
|
||||||
|
|
||||||
if (!validatePassword(this.ldapConfig, userDn, password)) {
|
if (!validatePassword(this.ldapConfig, userDn, password)) {
|
||||||
LOG.debug("Password incorrect for LDAP user %s", username);
|
LOG.debug("Password incorrect for LDAP user %s", username);
|
||||||
|
@ -213,8 +241,10 @@ public class LDAPCredentialsValidator implements CredentialsValidator
|
||||||
boolean validatePassword(BasicAuthLDAPConfig ldapConfig, LdapName userDn, char[] password)
|
boolean validatePassword(BasicAuthLDAPConfig ldapConfig, LdapName userDn, char[] password)
|
||||||
{
|
{
|
||||||
InitialDirContext context = null;
|
InitialDirContext context = null;
|
||||||
|
ClassLoader currentClassLoader = Thread.currentThread().getContextClassLoader();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
Thread.currentThread().setContextClassLoader(this.getClass().getClassLoader());
|
||||||
context = new InitialDirContext(userProperties(ldapConfig, userDn, password));
|
context = new InitialDirContext(userProperties(ldapConfig, userDn, password));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -235,10 +265,11 @@ public class LDAPCredentialsValidator implements CredentialsValidator
|
||||||
LOG.warn("Exception closing LDAP context");
|
LOG.warn("Exception closing LDAP context");
|
||||||
// ignored
|
// ignored
|
||||||
}
|
}
|
||||||
|
Thread.currentThread().setContextClassLoader(currentClassLoader);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class LruBlockCache extends LinkedHashMap<String, LdapUserPrincipal>
|
public static class LruBlockCache extends LinkedHashMap<String, LdapUserPrincipal>
|
||||||
{
|
{
|
||||||
private static final long serialVersionUID = 7509410739092012261L;
|
private static final long serialVersionUID = 7509410739092012261L;
|
||||||
|
|
||||||
|
|
|
@ -19,12 +19,43 @@
|
||||||
|
|
||||||
package org.apache.druid.security.authentication.validator;
|
package org.apache.druid.security.authentication.validator;
|
||||||
|
|
||||||
|
import org.apache.druid.java.util.common.StringUtils;
|
||||||
|
import org.apache.druid.metadata.DefaultPasswordProvider;
|
||||||
|
import org.apache.druid.security.basic.BasicAuthLDAPConfig;
|
||||||
|
import org.apache.druid.security.basic.BasicAuthUtils;
|
||||||
import org.apache.druid.security.basic.authentication.validator.LDAPCredentialsValidator;
|
import org.apache.druid.security.basic.authentication.validator.LDAPCredentialsValidator;
|
||||||
import org.junit.Assert;
|
import org.junit.Assert;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
import org.mockito.ArgumentMatchers;
|
||||||
|
import org.mockito.Mockito;
|
||||||
|
|
||||||
|
import javax.naming.Context;
|
||||||
|
import javax.naming.NamingEnumeration;
|
||||||
|
import javax.naming.NamingException;
|
||||||
|
import javax.naming.directory.SearchControls;
|
||||||
|
import javax.naming.directory.SearchResult;
|
||||||
|
import javax.naming.ldap.LdapContext;
|
||||||
|
import javax.naming.spi.InitialContextFactory;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Hashtable;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.Properties;
|
||||||
|
|
||||||
public class LDAPCredentialsValidatorTest
|
public class LDAPCredentialsValidatorTest
|
||||||
{
|
{
|
||||||
|
private static final BasicAuthLDAPConfig LDAP_CONFIG = new BasicAuthLDAPConfig(
|
||||||
|
"ldaps://my-ldap-url",
|
||||||
|
"bindUser",
|
||||||
|
new DefaultPasswordProvider("bindPassword"),
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
BasicAuthUtils.DEFAULT_KEY_ITERATIONS,
|
||||||
|
BasicAuthUtils.DEFAULT_CREDENTIAL_VERIFY_DURATION_SECONDS,
|
||||||
|
BasicAuthUtils.DEFAULT_CREDENTIAL_MAX_DURATION_SECONDS,
|
||||||
|
BasicAuthUtils.DEFAULT_CREDENTIAL_CACHE_SIZE);
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testEncodeForLDAP_noSpecialChars()
|
public void testEncodeForLDAP_noSpecialChars()
|
||||||
{
|
{
|
||||||
|
@ -44,4 +75,80 @@ public class LDAPCredentialsValidatorTest
|
||||||
Assert.assertEquals(expectedWildcardTrue, encodedWildcardTrue);
|
Assert.assertEquals(expectedWildcardTrue, encodedWildcardTrue);
|
||||||
Assert.assertEquals(expectedWildcardFalse, encodedWildcardFalse);
|
Assert.assertEquals(expectedWildcardFalse, encodedWildcardFalse);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This doesn't test password validation.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void testValidateCredentials()
|
||||||
|
{
|
||||||
|
Properties properties = new Properties();
|
||||||
|
properties.put(Context.INITIAL_CONTEXT_FACTORY, MockContextFactory.class.getName());
|
||||||
|
LDAPCredentialsValidator validator = new LDAPCredentialsValidator(
|
||||||
|
LDAP_CONFIG,
|
||||||
|
new LDAPCredentialsValidator.LruBlockCache(
|
||||||
|
3600,
|
||||||
|
3600,
|
||||||
|
100
|
||||||
|
),
|
||||||
|
properties
|
||||||
|
);
|
||||||
|
validator.validateCredentials("ldap", "ldap", "validUser", "password".toCharArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class MockContextFactory implements InitialContextFactory
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
public Context getInitialContext(Hashtable<?, ?> environment) throws NamingException
|
||||||
|
{
|
||||||
|
LdapContext context = Mockito.mock(LdapContext.class);
|
||||||
|
|
||||||
|
String encodedUsername = LDAPCredentialsValidator.encodeForLDAP("validUser", true);
|
||||||
|
SearchResult result = Mockito.mock(SearchResult.class);
|
||||||
|
Mockito.when(result.getNameInNamespace()).thenReturn("uid=user,ou=Users,dc=example,dc=org");
|
||||||
|
Iterator<SearchResult> results = Collections.singletonList(result).iterator();
|
||||||
|
|
||||||
|
Mockito.when(
|
||||||
|
context.search(
|
||||||
|
ArgumentMatchers.eq(LDAP_CONFIG.getBaseDn()),
|
||||||
|
ArgumentMatchers.eq(StringUtils.format(LDAP_CONFIG.getUserSearch(), encodedUsername)),
|
||||||
|
ArgumentMatchers.any(SearchControls.class))
|
||||||
|
).thenReturn(new NamingEnumeration<SearchResult>()
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
public SearchResult next()
|
||||||
|
{
|
||||||
|
return results.next();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasMore()
|
||||||
|
{
|
||||||
|
return results.hasNext();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close()
|
||||||
|
{
|
||||||
|
// No-op
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasMoreElements()
|
||||||
|
{
|
||||||
|
return results.hasNext();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SearchResult nextElement()
|
||||||
|
{
|
||||||
|
return results.next();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue