blob: 863f42171e2ca6ac98e159ff0b70f5b4873eef43 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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
*
* http://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.apache.qpid.server.security.auth.manager;
import static java.util.Collections.disjoint;
import static java.util.Collections.singletonList;
import static java.util.Collections.unmodifiableList;
import java.security.GeneralSecurityException;
import java.security.Principal;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
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.DirContext;
import javax.naming.directory.InitialDirContext;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
import javax.net.SocketFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import com.google.common.util.concurrent.ListenableFuture;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.qpid.server.configuration.CommonProperties;
import org.apache.qpid.server.configuration.IllegalConfigurationException;
import org.apache.qpid.server.model.ConfiguredObject;
import org.apache.qpid.server.model.Container;
import org.apache.qpid.server.model.ManagedAttributeField;
import org.apache.qpid.server.model.ManagedObjectFactoryConstructor;
import org.apache.qpid.server.model.NamedAddressSpace;
import org.apache.qpid.server.model.TrustStore;
import org.apache.qpid.server.security.auth.AuthenticationResult;
import org.apache.qpid.server.security.auth.AuthenticationResult.AuthenticationStatus;
import org.apache.qpid.server.security.auth.UsernamePrincipal;
import org.apache.qpid.server.security.auth.manager.ldap.AbstractLDAPSSLSocketFactory;
import org.apache.qpid.server.security.auth.manager.ldap.LDAPSSLSocketFactoryGenerator;
import org.apache.qpid.server.security.auth.sasl.SaslNegotiator;
import org.apache.qpid.server.security.auth.sasl.SaslSettings;
import org.apache.qpid.server.security.auth.sasl.plain.PlainNegotiator;
import org.apache.qpid.server.security.group.GroupPrincipal;
import org.apache.qpid.server.util.CipherSuiteAndProtocolRestrictingSSLSocketFactory;
import org.apache.qpid.server.util.ParameterizedTypes;
import org.apache.qpid.server.util.StringUtil;
import org.apache.qpid.server.transport.network.security.ssl.SSLUtil;
public class SimpleLDAPAuthenticationManagerImpl extends AbstractAuthenticationManager<SimpleLDAPAuthenticationManagerImpl>
implements SimpleLDAPAuthenticationManager<SimpleLDAPAuthenticationManagerImpl>
{
private static final Logger LOGGER = LoggerFactory.getLogger(SimpleLDAPAuthenticationManagerImpl.class);
private static final List<String> CONNECTIVITY_ATTRS = unmodifiableList(Arrays.asList(PROVIDER_URL,
PROVIDER_AUTH_URL,
SEARCH_CONTEXT,
LDAP_CONTEXT_FACTORY,
SEARCH_USERNAME,
SEARCH_PASSWORD,
TRUST_STORE));
/**
* Environment key to instruct {@link InitialDirContext} to override the socket factory.
*/
private static final String JAVA_NAMING_LDAP_FACTORY_SOCKET = "java.naming.ldap.factory.socket";
@ManagedAttributeField
private String _providerUrl;
@ManagedAttributeField
private String _providerAuthUrl;
@ManagedAttributeField
private String _searchContext;
@ManagedAttributeField
private String _searchFilter;
@ManagedAttributeField
private String _ldapContextFactory;
/**
* Trust store - typically used when the Directory has been secured with a certificate signed by a
* private CA (or self-signed certificate).
*/
@ManagedAttributeField
private TrustStore _trustStore;
@ManagedAttributeField
private boolean _bindWithoutSearch;
@ManagedAttributeField
private String _searchUsername;
@ManagedAttributeField
private String _searchPassword;
@ManagedAttributeField
private String _groupAttributeName;
@ManagedAttributeField
private String _groupSearchContext;
@ManagedAttributeField
private String _groupSearchFilter;
@ManagedAttributeField
private boolean _groupSubtreeSearchScope;
private List<String> _tlsProtocolWhiteList;
private List<String> _tlsProtocolBlackList;
private List<String> _tlsCipherSuiteWhiteList;
private List<String> _tlsCipherSuiteBlackList;
private AuthenticationResultCacher _authenticationResultCacher;
/**
* Dynamically created SSL Socket Factory implementation.
*/
private Class<? extends SocketFactory> _sslSocketFactoryOverrideClass;
@ManagedObjectFactoryConstructor
protected SimpleLDAPAuthenticationManagerImpl(final Map<String, Object> attributes, final Container<?> container)
{
super(attributes, container);
}
@Override
protected void validateOnCreate()
{
super.validateOnCreate();
Class<? extends SocketFactory> sslSocketFactoryOverrideClass = createSslSocketFactoryOverrideClass(_trustStore);
validateInitialDirContext(sslSocketFactoryOverrideClass, _providerUrl, _searchUsername, _searchPassword);
}
@Override
protected void validateChange(final ConfiguredObject<?> proxyForValidation, final Set<String> changedAttributes)
{
super.validateChange(proxyForValidation, changedAttributes);
if (!disjoint(changedAttributes, CONNECTIVITY_ATTRS))
{
SimpleLDAPAuthenticationManager changed = (SimpleLDAPAuthenticationManager)proxyForValidation;
TrustStore changedTruststore = changed.getTrustStore();
Class<? extends SocketFactory> sslSocketFactoryOverrideClass = createSslSocketFactoryOverrideClass(changedTruststore);
validateInitialDirContext(sslSocketFactoryOverrideClass, changed.getProviderUrl(), changed.getSearchUsername(),
changed.getSearchPassword());
}
}
@Override
protected void onOpen()
{
super.onOpen();
_tlsProtocolWhiteList = getContextValue(List.class, ParameterizedTypes.LIST_OF_STRINGS, CommonProperties.QPID_SECURITY_TLS_PROTOCOL_WHITE_LIST);
_tlsProtocolBlackList = getContextValue(List.class, ParameterizedTypes.LIST_OF_STRINGS, CommonProperties.QPID_SECURITY_TLS_PROTOCOL_BLACK_LIST);
_tlsCipherSuiteWhiteList = getContextValue(List.class, ParameterizedTypes.LIST_OF_STRINGS, CommonProperties.QPID_SECURITY_TLS_CIPHER_SUITE_WHITE_LIST);
_tlsCipherSuiteBlackList = getContextValue(List.class, ParameterizedTypes.LIST_OF_STRINGS, CommonProperties.QPID_SECURITY_TLS_CIPHER_SUITE_BLACK_LIST);
Integer cacheMaxSize = getContextValue(Integer.class, AUTHENTICATION_CACHE_MAX_SIZE);
Long cacheExpirationTime = getContextValue(Long.class, AUTHENTICATION_CACHE_EXPIRATION_TIME);
Integer cacheIterationCount = getContextValue(Integer.class, AUTHENTICATION_CACHE_ITERATION_COUNT);
if (cacheMaxSize == null || cacheMaxSize <= 0
|| cacheExpirationTime == null || cacheExpirationTime <= 0
|| cacheIterationCount == null || cacheIterationCount < 0)
{
LOGGER.debug("disabling authentication result caching");
cacheMaxSize = 0;
cacheExpirationTime = 1L;
cacheIterationCount = 0;
}
_authenticationResultCacher = new AuthenticationResultCacher(cacheMaxSize, cacheExpirationTime, cacheIterationCount);
}
@Override
protected ListenableFuture<Void> activate()
{
_sslSocketFactoryOverrideClass = createSslSocketFactoryOverrideClass(_trustStore);
return super.activate();
}
@Override
public String getProviderUrl()
{
return _providerUrl;
}
@Override
public String getProviderAuthUrl()
{
return _providerAuthUrl;
}
@Override
public String getSearchContext()
{
return _searchContext;
}
@Override
public String getSearchFilter()
{
return _searchFilter;
}
@Override
public String getLdapContextFactory()
{
return _ldapContextFactory;
}
@Override
public TrustStore getTrustStore()
{
return _trustStore;
}
@Override
public String getSearchUsername()
{
return _searchUsername;
}
@Override
public String getSearchPassword()
{
return _searchPassword;
}
@Override
public String getGroupAttributeName()
{
return _groupAttributeName;
}
@Override
public String getGroupSearchContext()
{
return _groupSearchContext;
}
@Override
public String getGroupSearchFilter()
{
return _groupSearchFilter;
}
@Override
public boolean isGroupSubtreeSearchScope()
{
return _groupSubtreeSearchScope;
}
@Override
public List<String> getMechanisms()
{
return singletonList(PlainNegotiator.MECHANISM);
}
@Override
public SaslNegotiator createSaslNegotiator(final String mechanism,
final SaslSettings saslSettings,
final NamedAddressSpace addressSpace)
{
if(PlainNegotiator.MECHANISM.equals(mechanism))
{
return new PlainNegotiator(this);
}
else
{
return null;
}
}
@Override
public AuthenticationResult authenticate(String username, String password)
{
return getOrLoadAuthenticationResult(username, password);
}
private AuthenticationResult doLDAPNameAuthentication(String userId, String password)
{
final String name;
try
{
name = getNameFromId(userId);
}
catch (NamingException e)
{
LOGGER.warn("Retrieving LDAP name for user '{}' resulted in error.", userId, e);
return new AuthenticationResult(AuthenticationResult.AuthenticationStatus.ERROR, e);
}
if(name == null)
{
//The search didn't return anything, class as not-authenticated before it NPEs below
return new AuthenticationResult(AuthenticationStatus.ERROR);
}
String providerAuthUrl = isSpecified(getProviderAuthUrl()) ? getProviderAuthUrl() : getProviderUrl();
Hashtable<String, Object> env = createInitialDirContextEnvironment(providerAuthUrl);
env.put(Context.SECURITY_AUTHENTICATION, "simple");
env.put(Context.SECURITY_PRINCIPAL, name);
env.put(Context.SECURITY_CREDENTIALS, password);
InitialDirContext ctx = null;
try
{
ctx = createInitialDirContext(env, _sslSocketFactoryOverrideClass);
Set<Principal> groups = Collections.emptySet();
if (isGroupSearchRequired())
{
if (!providerAuthUrl.equals(getProviderUrl()))
{
closeSafely(ctx);
ctx = createSearchInitialDirContext();
}
groups = findGroups(ctx, name);
}
//Authentication succeeded
return new AuthenticationResult(new UsernamePrincipal(name, this), groups, null);
}
catch(AuthenticationException ae)
{
//Authentication failed
return new AuthenticationResult(AuthenticationStatus.ERROR);
}
catch (NamingException e)
{
//Some other failure
LOGGER.warn("LDAP authentication attempt for username '{}' resulted in error.", name, e);
return new AuthenticationResult(AuthenticationResult.AuthenticationStatus.ERROR, e);
}
finally
{
if(ctx != null)
{
closeSafely(ctx);
}
}
}
private AuthenticationResult getOrLoadAuthenticationResult(final String userId, final String password)
{
return _authenticationResultCacher.getOrLoad(new String[]{userId, password}, new Callable<AuthenticationResult>()
{
@Override
public AuthenticationResult call()
{
return doLDAPNameAuthentication(userId, password);
}
});
}
private boolean isGroupSearchRequired()
{
if (isSpecified(getGroupAttributeName()))
{
return true;
}
if (isSpecified(getGroupSearchContext()) && isSpecified(getGroupSearchFilter()))
{
return true;
}
return false;
}
private boolean isSpecified(String value)
{
return value != null && !"".equals(value);
}
private Set<Principal> findGroups(DirContext context, String userDN) throws NamingException
{
Set<Principal> groupPrincipals = new HashSet<>();
if (getGroupAttributeName() != null && !"".equals(getGroupAttributeName()))
{
Attributes attributes = context.getAttributes(userDN, new String[]{getGroupAttributeName()});
NamingEnumeration<? extends Attribute> namingEnum = attributes.getAll();
while (namingEnum.hasMore())
{
Attribute attribute = namingEnum.next();
if (attribute != null)
{
NamingEnumeration<?> attributeValues = attribute.getAll();
while (attributeValues.hasMore())
{
Object attributeValue = attributeValues.next();
if (attributeValue != null)
{
String groupDN = String.valueOf(attributeValue);
groupPrincipals.add(new GroupPrincipal(groupDN, this));
}
}
}
}
}
if (getGroupSearchContext() != null && !"".equals(getGroupSearchContext()) &&
getGroupSearchFilter() != null && !"".equals(getGroupSearchFilter()))
{
SearchControls searchControls = new SearchControls();
searchControls.setReturningAttributes(new String[]{});
searchControls.setSearchScope(isGroupSubtreeSearchScope()
? SearchControls.SUBTREE_SCOPE
: SearchControls.ONELEVEL_SCOPE);
NamingEnumeration<?> groupEnumeration = context.search(getGroupSearchContext(),
getGroupSearchFilter(),
new String[]{encode(userDN)},
searchControls);
while (groupEnumeration.hasMore())
{
SearchResult result = (SearchResult) groupEnumeration.next();
String groupDN = result.getNameInNamespace();
groupPrincipals.add(new GroupPrincipal(groupDN, this));
}
}
return groupPrincipals;
}
private String encode(String value)
{
StringBuilder encoded = new StringBuilder(value.length());
char[] chars = value.toCharArray();
for (int i = 0; i < chars.length; i++)
{
char ch = chars[i];
switch (ch)
{
case '\0':
encoded.append("\\00");
break;
case '(':
encoded.append("\\28");
break;
case ')':
encoded.append("\\29");
break;
case '*':
encoded.append("\\2a");
break;
case '\\':
encoded.append("\\5c");
break;
default:
encoded.append(ch);
break;
}
}
return encoded.toString();
}
private Hashtable<String, Object> createInitialDirContextEnvironment(String providerUrl)
{
Hashtable<String,Object> env = new Hashtable<>();
env.put(Context.INITIAL_CONTEXT_FACTORY, _ldapContextFactory);
env.put(Context.PROVIDER_URL, providerUrl);
return env;
}
private InitialDirContext createInitialDirContext(Hashtable<String, Object> env,
Class<? extends SocketFactory> sslSocketFactoryOverrideClass) throws NamingException
{
ClassLoader existingContextClassLoader = null;
boolean isLdaps = String.valueOf(env.get(Context.PROVIDER_URL)).trim().toLowerCase().startsWith("ldaps:");
boolean revertContentClassLoader = false;
try
{
if (isLdaps)
{
existingContextClassLoader = Thread.currentThread().getContextClassLoader();
env.put(JAVA_NAMING_LDAP_FACTORY_SOCKET, sslSocketFactoryOverrideClass.getName());
Thread.currentThread().setContextClassLoader(sslSocketFactoryOverrideClass.getClassLoader());
revertContentClassLoader = true;
}
return new InitialDirContext(env);
}
finally
{
if (revertContentClassLoader)
{
Thread.currentThread().setContextClassLoader(existingContextClassLoader);
}
}
}
private Class<? extends SocketFactory> createSslSocketFactoryOverrideClass(final TrustStore trustStore)
{
String managerName = String.format("%s_%s_%s", getName(), getId(), trustStore == null ? "none" : trustStore.getName());
String clazzName = new StringUtil().createUniqueJavaName(managerName);
SSLContext sslContext = null;
try
{
sslContext = SSLUtil.tryGetSSLContext();
sslContext.init(null,
trustStore == null ? null : trustStore.getTrustManagers(),
null);
}
catch (GeneralSecurityException e)
{
LOGGER.error("Exception creating SSLContext", e);
if (trustStore != null)
{
throw new IllegalConfigurationException("Error creating SSLContext with trust store : " +
trustStore.getName() , e);
}
else
{
throw new IllegalConfigurationException("Error creating SSLContext (no trust store)", e);
}
}
SSLSocketFactory sslSocketFactory = new CipherSuiteAndProtocolRestrictingSSLSocketFactory(sslContext.getSocketFactory(),
_tlsCipherSuiteWhiteList,
_tlsCipherSuiteBlackList,
_tlsProtocolWhiteList,
_tlsProtocolBlackList);
Class<? extends AbstractLDAPSSLSocketFactory> clazz = LDAPSSLSocketFactoryGenerator.createSubClass(clazzName,
sslSocketFactory);
LOGGER.debug("Connection to Directory will use custom SSL socket factory : {}", clazz);
return clazz;
}
@Override
public String toString()
{
return "SimpleLDAPAuthenticationManagerImpl [id=" + getId() + ", name=" + getName() +
", providerUrl=" + _providerUrl + ", providerAuthUrl=" + _providerAuthUrl +
", searchContext=" + _searchContext + ", state=" + getState() +
", searchFilter=" + _searchFilter + ", ldapContextFactory=" + _ldapContextFactory +
", bindWithoutSearch=" + _bindWithoutSearch + ", trustStore=" + _trustStore +
", searchUsername=" + _searchUsername + "]";
}
private void validateInitialDirContext(Class<? extends SocketFactory> sslSocketFactoryOverrideClass,
final String providerUrl,
final String searchUsername, final String searchPassword)
{
Hashtable<String,Object> env = createInitialDirContextEnvironment(providerUrl);
setupSearchContext(env, searchUsername, searchPassword);
InitialDirContext ctx = null;
try
{
ctx = createInitialDirContext(env, sslSocketFactoryOverrideClass);
}
catch (NamingException e)
{
LOGGER.error("Failed to establish connectivity to the ldap server for '{}'", providerUrl, e);
throw new IllegalConfigurationException("Failed to establish connectivity to the ldap server." , e);
}
finally
{
closeSafely(ctx);
}
}
private void setupSearchContext(final Hashtable<String, Object> env,
final String searchUsername, final String searchPassword)
{
if(_searchUsername != null && _searchUsername.trim().length()>0)
{
env.put(Context.SECURITY_AUTHENTICATION, "simple");
env.put(Context.SECURITY_PRINCIPAL, searchUsername);
env.put(Context.SECURITY_CREDENTIALS, searchPassword);
}
else
{
env.put(Context.SECURITY_AUTHENTICATION, "none");
}
}
private String getNameFromId(String id) throws NamingException
{
if(!isBindWithoutSearch())
{
InitialDirContext ctx = createSearchInitialDirContext();
try
{
SearchControls searchControls = new SearchControls();
searchControls.setReturningAttributes(new String[]{});
searchControls.setCountLimit(1l);
searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE);
NamingEnumeration<?> namingEnum = null;
LOGGER.debug("Searching for '{}'", id);
namingEnum = ctx.search(_searchContext, _searchFilter, new String[]{id}, searchControls);
if (namingEnum.hasMore())
{
SearchResult result = (SearchResult) namingEnum.next();
String name = result.getNameInNamespace();
LOGGER.debug("Found '{}' DN '{}'", id, name);
return name;
}
else
{
LOGGER.debug("Not found '{}'", id);
return null;
}
}
finally
{
closeSafely(ctx);
}
}
else
{
return id;
}
}
private InitialDirContext createSearchInitialDirContext() throws NamingException
{
Hashtable<String, Object> env = createInitialDirContextEnvironment(_providerUrl);
setupSearchContext(env, _searchUsername, _searchPassword);
return createInitialDirContext(env, _sslSocketFactoryOverrideClass);
}
@Override
public boolean isBindWithoutSearch()
{
return _bindWithoutSearch;
}
@Override
public List<String> getTlsProtocolWhiteList()
{
return _tlsProtocolWhiteList;
}
@Override
public List<String> getTlsProtocolBlackList()
{
return _tlsProtocolBlackList;
}
@Override
public List<String> getTlsCipherSuiteWhiteList()
{
return _tlsCipherSuiteWhiteList;
}
@Override
public List<String> getTlsCipherSuiteBlackList()
{
return _tlsCipherSuiteBlackList;
}
private void closeSafely(InitialDirContext ctx)
{
try
{
if (ctx != null)
{
ctx.close();
ctx = null;
}
}
catch (Exception e)
{
LOGGER.warn("Exception closing InitialDirContext", e);
}
}
}