blob: 2784a24bb202c1daf063c5c219ac89c66d1f921c [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2010-2016 SAP AG and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* SAP AG - initial API and implementation
*******************************************************************************/
package org.eclipse.skalli.core.user.ldap;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.SizeLimitExceededException;
import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
import javax.naming.ldap.LdapContext;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.math.NumberUtils;
import org.eclipse.skalli.commons.CollectionUtils;
import org.eclipse.skalli.core.cache.Cache;
import org.eclipse.skalli.core.cache.GroundhogCache;
import org.eclipse.skalli.core.user.NormalizeUtil;
import org.eclipse.skalli.model.User;
import org.eclipse.skalli.services.configuration.ConfigurationService;
import org.eclipse.skalli.services.configuration.Configurations;
import org.eclipse.skalli.services.configuration.EventConfigUpdate;
import org.eclipse.skalli.services.event.EventListener;
import org.eclipse.skalli.services.event.EventService;
import org.eclipse.skalli.services.user.EventUserUpdate;
import org.eclipse.skalli.services.user.UserService;
import org.eclipse.skalli.services.user.ldap.LdapContextProvider;
import org.osgi.service.component.ComponentConstants;
import org.osgi.service.component.ComponentContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Implementation of {@link UserService} accessing an LDAP server.
*/
public class LDAPUserComponent implements UserService, EventListener<EventConfigUpdate> {
private static final Logger LOG = LoggerFactory.getLogger(LDAPUserComponent.class);
private static final String DEFAULT_POOL_PROTOCOLS = "plain ssl"; //$NON-NLS-1$
private static final int DEFAULT_POOL_MAXSIZE = 5;
private static final long DEFAULT_POOL_TIMEOUT = 10000L;
private static final int DEFAULT_CACHE_SIZE = 100;
private static final String CONNECT_POOL_PROTOCOLS = "com.sun.jndi.ldap.connect.pool.protocol"; //$NON-NLS-1$
private static final String CONNECT_POOL_DEBUG = "com.sun.jndi.ldap.connect.pool.debug"; //$NON-NLS-1$
private static final String CONNECT_POOL_MAXSIZE = "com.sun.jndi.ldap.connect.pool.maxsize"; //$NON-NLS-1$
private static final String CONNECT_POOL_TIMEOUT = "com.sun.jndi.ldap.connect.pool.timeout"; //$NON-NLS-1$
private ConcurrentHashMap<String,LdapContextProvider> ctxProviders =
new ConcurrentHashMap<String, LdapContextProvider>();
private LdapContextProvider ctxProvider;
private String destination;
private String baseDN;
private String searchScope;
private EventService eventService;
private Cache<String, User> cache;
protected void activate(ComponentContext context) {
// define properties for the LDAP connection pool globally, but let the individual context providers
// decide whether to use the connection pool or not; note, for some weird reason the pool properties
// are system properties and cannot be set per context
System.setProperty(CONNECT_POOL_PROTOCOLS, DEFAULT_POOL_PROTOCOLS);
System.setProperty(CONNECT_POOL_MAXSIZE, Integer.toString(DEFAULT_POOL_MAXSIZE));
System.setProperty(CONNECT_POOL_TIMEOUT, Long.toString(DEFAULT_POOL_TIMEOUT));
if (LOG.isDebugEnabled()) {
System.setProperty(CONNECT_POOL_DEBUG, "fine"); //$NON-NLS-1$
}
LOG.info(MessageFormat.format("[UserService][LDAP] {0} : activated", //$NON-NLS-1$
(String) context.getProperties().get(ComponentConstants.COMPONENT_NAME)));
initialize();
}
protected void deactivate(ComponentContext context) {
LOG.info(MessageFormat.format("[UserService][LDAP] {0} : deactivated", //$NON-NLS-1$
(String) context.getProperties().get(ComponentConstants.COMPONENT_NAME)));
}
protected void bindEventService(EventService eventService) {
eventService.registerListener(EventConfigUpdate.class, this);
this.eventService = eventService;
}
protected void unbindEventService(EventService eventService) {
eventService.unregisterListener(EventConfigUpdate.class, this);
this.eventService = null;
}
protected void bindConfigurationService(ConfigurationService configurationService) {
initialize();
}
protected void unbindConfigurationService(ConfigurationService configurationService) {
}
protected void bindLdapContextProvider(LdapContextProvider ctxProvider) {
ctxProviders.put(ctxProvider.getId(), ctxProvider);
initialize();
}
protected void unbindLdapContextProvider(LdapContextProvider ctxProvider) {
ctxProviders.remove(ctxProvider.getId());
}
protected synchronized void initialize() {
int cacheSize = DEFAULT_CACHE_SIZE;
LDAPConfig ldapConfig = Configurations.getConfiguration(LDAPConfig.class);
if (ldapConfig != null) {
String providerId = StringUtils.isNotBlank(ldapConfig.getProviderId())
? ldapConfig.getProviderId()
: "default"; //$NON-NLS-1$
ctxProvider = ctxProviders.get(providerId);
destination = ldapConfig.getDestination();
baseDN = ldapConfig.getBaseDN();
searchScope = ldapConfig.getSearchScope();
cacheSize = NumberUtils.toInt(ldapConfig.getCacheSize(), DEFAULT_CACHE_SIZE);
}
cache = new GroundhogCache<String, User>(cacheSize, cache);
}
@Override
public String getType() {
return "ldap"; //$NON-NLS-1$
}
@Override
public synchronized List<User> findUser(String searchText) {
if (StringUtils.isBlank(searchText)) {
return Collections.emptyList();
}
if (ctxProvider == null) {
return Collections.emptyList();
}
List<User> users = searchUserByName(searchText);
for (User user : users) {
if (user != null) {
addUserToCache(user);
}
}
return users;
}
/**
* Adds or updates the given user within the cache class data member.
* Also fires an event so other user cache instances can be updated (See UserContainer class).
* @param user
*/
private void addUserToCache(User user){
cache.put(user.getUserId().toLowerCase(Locale.ENGLISH), user);
if (this.eventService != null) {
this.eventService.fireEvent(new EventUserUpdate(user));
}
}
@Override
public synchronized User getUserById(String userId) {
if (StringUtils.isBlank(userId)) {
return null;
}
String lowerCaseUserId = userId.toLowerCase(Locale.ENGLISH);
User user = cache.get(lowerCaseUserId);
if (user == null) {
if (ctxProvider == null) {
return null;
}
user = searchUserById(userId);
if (user != null) {
addUserToCache(user);
}
}
return user;
}
@Override
public synchronized List<User> getUsers() {
return new LinkedList<User>(cache.values());
}
@Override
public synchronized Set<User> getUsersById(Set<String> userIds) {
if (userIds == null || userIds.isEmpty()) {
return Collections.emptySet();
}
Set<User> users = new HashSet<User>(userIds.size());
Set<String> notFoundInCache = new HashSet<String>();
for (String userId : userIds) {
if (StringUtils.isBlank(userId)) {
continue;
}
String lowerCaseUserId = userId.toLowerCase(Locale.ENGLISH);
User user = cache.get(lowerCaseUserId);
if (user != null) {
users.add(user);
} else {
notFoundInCache.add(lowerCaseUserId);
}
}
if (notFoundInCache.size() > 0) {
if (ctxProvider == null) {
return users;
}
Set<User> ldapUsers = searchUsersByIds(notFoundInCache);
for (User user : ldapUsers) {
if (user != null) {
String userId = user.getUserId();
if (StringUtils.isNotBlank(userId)) {
users.add(user);
addUserToCache(user);
}
}
}
}
return users;
}
@Override
public synchronized void onEvent(EventConfigUpdate event) {
if (LDAPConfig.class.equals(event.getConfigClass())) {
initialize();
}
}
private User searchUserById(String userId) {
LdapContext ctx = null;
try {
ctx = ctxProvider.getLdapContext(destination);
return searchUserById(ctx, userId);
} catch (Exception e) {
LOG.debug(MessageFormat.format("Failed to retrieve user ''{0}''", userId), e);
return new User(userId);
} finally {
closeQuietly(ctx);
}
}
private Set<User> searchUsersByIds(Set<String> userIds) {
LdapContext ctx = null;
try {
if (ctxProvider == null) {
return Collections.emptySet();
}
ctx = ctxProvider.getLdapContext(destination);
Set<User> ret = new HashSet<User>();
for (String userId : userIds) {
ret.add(searchUserById(ctx, userId));
}
return ret;
} catch (Exception e) {
LOG.debug(MessageFormat.format("Failed to retrieve users {0}",
CollectionUtils.toString(userIds, ',')), e);
return Collections.emptySet();
} finally {
closeQuietly(ctx);
}
}
private List<User> searchUserByName(String name) {
LdapContext ctx = null;
try {
if (ctxProvider == null) {
return Collections.emptyList();
}
ctx = ctxProvider.getLdapContext(destination);
return searchUserByName(ctx, name);
} catch (Exception e) {
LOG.debug(MessageFormat.format("Failed to search user ''{0}''", name), e);
return Collections.emptyList();
} finally {
closeQuietly(ctx);
}
}
private User searchUserById(LdapContext ctx, String userId) throws NamingException {
SearchControls sc = getSearchControls();
NamingEnumeration<SearchResult> results = null;
try {
results = ctx.search(baseDN,
MessageFormat.format("(&(objectClass=user)(sAMAccountName={0}))", userId), sc); //$NON-NLS-1$
while (results != null && results.hasMore()) {
SearchResult entry = results.next();
User user = processEntry(entry);
if (user != null) {
if (LOG.isDebugEnabled()) {
LOG.debug(MessageFormat.format("Success reading from LDAP: {0}, {1} <{2}>", //$NON-NLS-1$
user.getUserId(), user.getDisplayName(), user.getEmail()));
}
return user;
}
}
} finally {
closeQuietly(results);
}
return new User(userId);
}
private List<User> searchUserByName(LdapContext ctx, String name) throws NamingException {
List<User> ret = new ArrayList<User>(0);
try {
boolean somethingAdded = false;
SearchControls sc = getSearchControls();
String[] parts = StringUtils.split(NormalizeUtil.normalize(name), " ,"); //$NON-NLS-1$
if (parts.length == 1) {
somethingAdded = search(parts[0], ret, ctx, sc);
}
else if (parts.length > 1) {
// givenname surname ('Michael Ochmann'), or surname givenname('Ochmann, Michael')
NamingEnumeration<SearchResult> results = null;
try {
results = ctx.search(baseDN,
MessageFormat.format("(&(objectClass=user)(givenName={0}*)(sn={1}*))", //$NON-NLS-1$
parts[0], parts[1]), sc);
somethingAdded |= addLDAPSearchResult(ret, results);
} finally {
closeQuietly(results);
}
try {
results = ctx.search(baseDN,
MessageFormat.format("(&(objectClass=user)(sn={0}*)(givenName={1}*))", //$NON-NLS-1$
parts[0], parts[1]), sc);
somethingAdded |= addLDAPSearchResult(ret, results);
} finally {
closeQuietly(results);
}
// givenname initial surname, e.g. 'Michael R. Ochmann'
if (parts.length > 2) {
try {
results = ctx.search(baseDN,
MessageFormat.format("(&(objectClass=user)(givenName={0}*)(sn={1}*))", //$NON-NLS-1$
parts[0], parts[2]), sc);
somethingAdded |= addLDAPSearchResult(ret, results);
} finally {
closeQuietly(results);
}
try {
results = ctx.search(baseDN,
MessageFormat.format("(&(objectClass=user)(sn={0}*)(givenName={1}*))", //$NON-NLS-1$
parts[0], parts[2]), sc);
somethingAdded |= addLDAPSearchResult(ret, results);
} finally {
closeQuietly(results);
}
}
if (!somethingAdded) {
// try to match each part individually
for (int i = 0; i < parts.length; ++i) {
somethingAdded = search(parts[i], ret, ctx, sc);
}
}
}
} catch (SizeLimitExceededException e) {
// 1000 is good enough at the moment for this use case...
LOG.warn(MessageFormat.format("LDAP query size limit exceeded while searching for ''{0}''", name), e);
}
return ret;
}
private boolean search(String s, List<User> ret, LdapContext ctx, SearchControls sc) throws NamingException {
// try a match with surname*
boolean somethingAdded = false;
NamingEnumeration<SearchResult> results = null;
try {
results = ctx.search(baseDN,
MessageFormat.format("(&(objectClass=user)(|(sn={0}*)(givenName={1}*)))", s, s), sc); //$NON-NLS-1$
somethingAdded = addLDAPSearchResult(ret, results);
} finally {
closeQuietly(results);
}
if (!somethingAdded) {
try {
// try a match with the account name and mail address
results = ctx.search(baseDN,
MessageFormat.format("(&(objectClass=user)(sAMAccountName={0}*))", s), sc); //$NON-NLS-1$
somethingAdded |= addLDAPSearchResult(ret, results);
} finally {
closeQuietly(results);
}
if (!somethingAdded) {
try {
// try to match surname~= or givenname~=
results = ctx.search(baseDN,
MessageFormat.format("(&(objectClass=user)(|(sn~={0})(givenName~={1})))", s, s), sc); //$NON-NLS-1$
somethingAdded |= addLDAPSearchResult(ret, results);
} finally {
closeQuietly(results);
}
if (!somethingAdded) {
try {
results = ctx.search(baseDN,
MessageFormat.format("(&(objectClass=user)(mail={0}*))", s), sc); //$NON-NLS-1$
somethingAdded |= addLDAPSearchResult(ret, results);
} finally {
closeQuietly(results);
}
}
}
}
return somethingAdded;
}
// Iterate over a batch of search results sent by the server
private boolean addLDAPSearchResult(List<User> users, NamingEnumeration<SearchResult> results)
throws NamingException {
boolean somethingAdded = false;
while (results != null && results.hasMore()) {
// Display an entry
SearchResult entry = results.next();
User user = processEntry(entry);
if (user != null) {
if (LOG.isDebugEnabled()) {
LOG.debug(MessageFormat.format("Success reading from LDAP: {0}, {1} <{2}>",
user.getUserId(), user.getDisplayName(), user.getEmail()));
}
users.add(user);
somethingAdded = true;
}
}
return somethingAdded;
}
private String getStringValue(Attributes attributes, LDAPAttributeNames attributeName)
throws NamingException {
String ret = null;
Attribute attribute = attributes.get(attributeName.getLdapKey());
if (attribute != null) {
for (int i = 0; i < attribute.size(); i++) {
ret = (String) attribute.get(i);
}
}
return ret;
}
private User processEntry(SearchResult entry) throws NamingException {
User user = new User();
Attributes attrs = entry.getAttributes();
Attribute attrBits = attrs.get(LDAPAttributeNames.BITS.getLdapKey());
if (attrBits != null) {
long lng = Long.parseLong(attrBits.get(0).toString());
long secondBit = lng & 2; // get bit 2
if (secondBit != 0) {
// User not enabled
return null;
}
}
user.setUserId(StringUtils.lowerCase(getStringValue(attrs, LDAPAttributeNames.USERID)));
user.setFirstname(getStringValue(attrs, LDAPAttributeNames.FIRSTNAME));
user.setLastname(getStringValue(attrs, LDAPAttributeNames.LASTNAME));
user.setEmail(getStringValue(attrs, LDAPAttributeNames.EMAIL));
user.setTelephone(getStringValue(attrs, LDAPAttributeNames.TELEPHONE));
user.setMobile(getStringValue(attrs, LDAPAttributeNames.MOBILE));
user.setRoom(getStringValue(attrs, LDAPAttributeNames.ROOM));
user.setLocation(getStringValue(attrs, LDAPAttributeNames.LOCATION));
user.setDepartment(getStringValue(attrs, LDAPAttributeNames.DEPARTMENT));
user.setCompany(getStringValue(attrs, LDAPAttributeNames.COMPANY));
user.setSip(getStringValue(attrs, LDAPAttributeNames.SIP));
return user;
}
@SuppressWarnings("nls")
private SearchControls getSearchControls() {
SearchControls sc = new SearchControls();
if ("base".equalsIgnoreCase(searchScope)) {
sc.setSearchScope(SearchControls.OBJECT_SCOPE);
} else if ("onelevel".equalsIgnoreCase(searchScope)) {
sc.setSearchScope(SearchControls.ONELEVEL_SCOPE);
} else if ("subtree".equalsIgnoreCase(searchScope)) {
sc.setSearchScope(SearchControls.SUBTREE_SCOPE);
}
sc.setReturningAttributes(LDAPAttributeNames.getAll());
return sc;
}
private void closeQuietly(LdapContext ctx) {
if (ctx != null) {
try {
ctx .close();
} catch (NamingException e) {
LOG.error("Failed to close LDAP connection", e);
}
}
}
private void closeQuietly(NamingEnumeration<?> result) {
if (result != null) {
try {
result.close();
} catch (NamingException e) {
LOG.error("Failed to close LDAP result set", e);
}
}
}
}