You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@karaf.apache.org by gn...@apache.org on 2014/07/16 16:59:18 UTC
git commit: [KARAF-3125] Add a possibility to cache LDAP credentials
Repository: karaf
Updated Branches:
refs/heads/master ac8f887c7 -> c4bd1fa9c
[KARAF-3125] Add a possibility to cache LDAP credentials
Project: http://git-wip-us.apache.org/repos/asf/karaf/repo
Commit: http://git-wip-us.apache.org/repos/asf/karaf/commit/c4bd1fa9
Tree: http://git-wip-us.apache.org/repos/asf/karaf/tree/c4bd1fa9
Diff: http://git-wip-us.apache.org/repos/asf/karaf/diff/c4bd1fa9
Branch: refs/heads/master
Commit: c4bd1fa9cbaba4f5d68ee2525ecb729f952e6eb7
Parents: ac8f887
Author: Guillaume Nodet <gn...@gmail.com>
Authored: Wed Jul 16 16:59:06 2014 +0200
Committer: Guillaume Nodet <gn...@gmail.com>
Committed: Wed Jul 16 16:59:06 2014 +0200
----------------------------------------------------------------------
.../karaf/jaas/modules/impl/Activator.java | 2 +
.../karaf/jaas/modules/ldap/ExpiringMap.java | 451 +++++++++++++++++++
.../karaf/jaas/modules/ldap/LDAPCache.java | 78 ++++
.../jaas/modules/ldap/LDAPLoginModule.java | 213 +++++----
.../jaas/modules/ldap/LdapLoginModuleTest.java | 6 +
5 files changed, 661 insertions(+), 89 deletions(-)
----------------------------------------------------------------------
http://git-wip-us.apache.org/repos/asf/karaf/blob/c4bd1fa9/jaas/modules/src/main/java/org/apache/karaf/jaas/modules/impl/Activator.java
----------------------------------------------------------------------
diff --git a/jaas/modules/src/main/java/org/apache/karaf/jaas/modules/impl/Activator.java b/jaas/modules/src/main/java/org/apache/karaf/jaas/modules/impl/Activator.java
index 68c084e..db1d6de 100644
--- a/jaas/modules/src/main/java/org/apache/karaf/jaas/modules/impl/Activator.java
+++ b/jaas/modules/src/main/java/org/apache/karaf/jaas/modules/impl/Activator.java
@@ -22,6 +22,7 @@ import org.apache.karaf.jaas.config.JaasRealm;
import org.apache.karaf.jaas.modules.BackingEngineFactory;
import org.apache.karaf.jaas.modules.EncryptionService;
import org.apache.karaf.jaas.modules.encryption.BasicEncryptionService;
+import org.apache.karaf.jaas.modules.ldap.LDAPCache;
import org.apache.karaf.jaas.modules.properties.AutoEncryptionSupport;
import org.apache.karaf.jaas.modules.properties.PropertiesBackingEngineFactory;
import org.apache.karaf.jaas.modules.publickey.PublickeyBackingEngineFactory;
@@ -79,6 +80,7 @@ public class Activator extends BaseActivator implements ManagedService {
autoEncryptionSupport.destroy();
}
super.doStop();
+ LDAPCache.clear();
}
@Override
http://git-wip-us.apache.org/repos/asf/karaf/blob/c4bd1fa9/jaas/modules/src/main/java/org/apache/karaf/jaas/modules/ldap/ExpiringMap.java
----------------------------------------------------------------------
diff --git a/jaas/modules/src/main/java/org/apache/karaf/jaas/modules/ldap/ExpiringMap.java b/jaas/modules/src/main/java/org/apache/karaf/jaas/modules/ldap/ExpiringMap.java
new file mode 100644
index 0000000..4c16012
--- /dev/null
+++ b/jaas/modules/src/main/java/org/apache/karaf/jaas/modules/ldap/ExpiringMap.java
@@ -0,0 +1,451 @@
+/*
+ * 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.karaf.jaas.modules.ldap;
+
+import java.util.Collection;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.locks.ReadWriteLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+
+/**
+ * This map comes from the org.apache.mina.util package with a small
+ * modification to remove the listeners to avoid an additional class.
+ */
+/**
+ * A map with expiration. This class contains a worker thread that will
+ * periodically check this class in order to determine if any objects
+ * should be removed based on the provided time-to-live value.
+ *
+ * @author <a href="http://mina.apache.org">Apache MINA Project</a>
+ */
+public class ExpiringMap<K, V> implements Map<K, V> {
+
+ /**
+ * The default value, 60
+ */
+ public static final int DEFAULT_TIME_TO_LIVE = 60;
+
+ /**
+ * The default value, 1
+ */
+ public static final int DEFAULT_EXPIRATION_INTERVAL = 1;
+
+ private static volatile int expirerCount = 1;
+
+ private final ConcurrentHashMap<K, ExpiringObject> delegate;
+
+ private final Expirer expirer;
+
+ /**
+ * Creates a new instance of ExpiringMap using the default values
+ * DEFAULT_TIME_TO_LIVE and DEFAULT_EXPIRATION_INTERVAL
+ *
+ */
+ public ExpiringMap() {
+ this(DEFAULT_TIME_TO_LIVE, DEFAULT_EXPIRATION_INTERVAL);
+ }
+
+ /**
+ * Creates a new instance of ExpiringMap using the supplied
+ * time-to-live value and the default value for DEFAULT_EXPIRATION_INTERVAL
+ *
+ * @param timeToLive
+ * The time-to-live value (seconds)
+ */
+ public ExpiringMap(int timeToLive) {
+ this(timeToLive, DEFAULT_EXPIRATION_INTERVAL);
+ }
+
+ /**
+ * Creates a new instance of ExpiringMap using the supplied values and
+ * a {@link ConcurrentHashMap} for the internal data structure.
+ *
+ * @param timeToLive
+ * The time-to-live value (seconds)
+ * @param expirationInterval
+ * The time between checks to see if a value should be removed (seconds)
+ */
+ public ExpiringMap(int timeToLive, int expirationInterval) {
+ this(new ConcurrentHashMap<K, ExpiringObject>(), timeToLive,
+ expirationInterval);
+ }
+
+ private ExpiringMap(ConcurrentHashMap<K, ExpiringObject> delegate,
+ int timeToLive, int expirationInterval) {
+ this.delegate = delegate;
+
+ this.expirer = new Expirer();
+ expirer.setTimeToLive(timeToLive);
+ expirer.setExpirationInterval(expirationInterval);
+ }
+
+ public V put(K key, V value) {
+ ExpiringObject answer = delegate.put(key, new ExpiringObject(key, value, System.currentTimeMillis()));
+ if (answer == null) {
+ return null;
+ }
+
+ return answer.getValue();
+ }
+
+ public V get(Object key) {
+ ExpiringObject object = delegate.get(key);
+
+ if (object != null) {
+ object.setLastAccessTime(System.currentTimeMillis());
+
+ return object.getValue();
+ }
+
+ return null;
+ }
+
+ public V remove(Object key) {
+ ExpiringObject answer = delegate.remove(key);
+ if (answer == null) {
+ return null;
+ }
+
+ return answer.getValue();
+ }
+
+ public boolean containsKey(Object key) {
+ return delegate.containsKey(key);
+ }
+
+ public boolean containsValue(Object value) {
+ return delegate.containsValue(value);
+ }
+
+ public int size() {
+ return delegate.size();
+ }
+
+ public boolean isEmpty() {
+ return delegate.isEmpty();
+ }
+
+ public void clear() {
+ delegate.clear();
+ }
+
+ @Override
+ public int hashCode() {
+ return delegate.hashCode();
+ }
+
+ public Set<K> keySet() {
+ return delegate.keySet();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ return delegate.equals(obj);
+ }
+
+ public void putAll(Map<? extends K, ? extends V> inMap) {
+ for (Entry<? extends K, ? extends V> e : inMap.entrySet()) {
+ this.put(e.getKey(), e.getValue());
+ }
+ }
+
+ public Collection<V> values() {
+ throw new UnsupportedOperationException();
+ }
+
+ public Set<Map.Entry<K, V>> entrySet() {
+ throw new UnsupportedOperationException();
+ }
+
+ public Expirer getExpirer() {
+ return expirer;
+ }
+
+ public int getExpirationInterval() {
+ return expirer.getExpirationInterval();
+ }
+
+ public int getTimeToLive() {
+ return expirer.getTimeToLive();
+ }
+
+ public void setExpirationInterval(int expirationInterval) {
+ expirer.setExpirationInterval(expirationInterval);
+ }
+
+ public void setTimeToLive(int timeToLive) {
+ expirer.setTimeToLive(timeToLive);
+ }
+
+ private class ExpiringObject {
+ private K key;
+
+ private V value;
+
+ private long lastAccessTime;
+
+ private final ReadWriteLock lastAccessTimeLock = new ReentrantReadWriteLock();
+
+ ExpiringObject(K key, V value, long lastAccessTime) {
+ if (value == null) {
+ throw new IllegalArgumentException("An expiring object cannot be null.");
+ }
+
+ this.key = key;
+ this.value = value;
+ this.lastAccessTime = lastAccessTime;
+ }
+
+ public long getLastAccessTime() {
+ lastAccessTimeLock.readLock().lock();
+
+ try {
+ return lastAccessTime;
+ } finally {
+ lastAccessTimeLock.readLock().unlock();
+ }
+ }
+
+ public void setLastAccessTime(long lastAccessTime) {
+ lastAccessTimeLock.writeLock().lock();
+
+ try {
+ this.lastAccessTime = lastAccessTime;
+ } finally {
+ lastAccessTimeLock.writeLock().unlock();
+ }
+ }
+
+ public K getKey() {
+ return key;
+ }
+
+ public V getValue() {
+ return value;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ return value.equals(obj);
+ }
+
+ @Override
+ public int hashCode() {
+ return value.hashCode();
+ }
+ }
+
+ /**
+ * A Thread that monitors an {@link ExpiringMap} and will remove
+ * elements that have passed the threshold.
+ *
+ */
+ public class Expirer implements Runnable {
+ private final ReadWriteLock stateLock = new ReentrantReadWriteLock();
+
+ private long timeToLiveMillis;
+
+ private long expirationIntervalMillis;
+
+ private boolean running = false;
+
+ private final Thread expirerThread;
+
+ /**
+ * Creates a new instance of Expirer.
+ *
+ */
+ public Expirer() {
+ expirerThread = new Thread(this, "ExpiringMapExpirer-" + expirerCount++);
+ expirerThread.setDaemon(true);
+ }
+
+ public void run() {
+ while (running) {
+ processExpires();
+
+ try {
+ Thread.sleep(expirationIntervalMillis);
+ } catch (InterruptedException e) {
+ // Do nothing
+ }
+ }
+ }
+
+ private void processExpires() {
+ long timeNow = System.currentTimeMillis();
+
+ for (ExpiringObject o : delegate.values()) {
+
+ if (timeToLiveMillis <= 0) {
+ continue;
+ }
+
+ long timeIdle = timeNow - o.getLastAccessTime();
+
+ if (timeIdle >= timeToLiveMillis) {
+ delegate.remove(o.getKey());
+ }
+ }
+ }
+
+ /**
+ * Kick off this thread which will look for old objects and remove them.
+ *
+ */
+ public void startExpiring() {
+ stateLock.writeLock().lock();
+
+ try {
+ if (!running) {
+ running = true;
+ expirerThread.start();
+ }
+ } finally {
+ stateLock.writeLock().unlock();
+ }
+ }
+
+ /**
+ * If this thread has not started, then start it.
+ * Otherwise just return;
+ */
+ public void startExpiringIfNotStarted() {
+ stateLock.readLock().lock();
+ try {
+ if (running) {
+ return;
+ }
+ } finally {
+ stateLock.readLock().unlock();
+ }
+
+ stateLock.writeLock().lock();
+ try {
+ if (!running) {
+ running = true;
+ expirerThread.start();
+ }
+ } finally {
+ stateLock.writeLock().unlock();
+ }
+ }
+
+ /**
+ * Stop the thread from monitoring the map.
+ */
+ public void stopExpiring() {
+ stateLock.writeLock().lock();
+
+ try {
+ if (running) {
+ running = false;
+ expirerThread.interrupt();
+ }
+ } finally {
+ stateLock.writeLock().unlock();
+ }
+ }
+
+ /**
+ * Checks to see if the thread is running
+ *
+ * @return
+ * If the thread is running, true. Otherwise false.
+ */
+ public boolean isRunning() {
+ stateLock.readLock().lock();
+
+ try {
+ return running;
+ } finally {
+ stateLock.readLock().unlock();
+ }
+ }
+
+ /**
+ * Returns the Time-to-live value.
+ *
+ * @return
+ * The time-to-live (seconds)
+ */
+ public int getTimeToLive() {
+ stateLock.readLock().lock();
+
+ try {
+ return (int) timeToLiveMillis / 1000;
+ } finally {
+ stateLock.readLock().unlock();
+ }
+ }
+
+ /**
+ * Update the value for the time-to-live
+ *
+ * @param timeToLive
+ * The time-to-live (seconds)
+ */
+ public void setTimeToLive(long timeToLive) {
+ stateLock.writeLock().lock();
+
+ try {
+ this.timeToLiveMillis = timeToLive * 1000;
+ } finally {
+ stateLock.writeLock().unlock();
+ }
+ }
+
+ /**
+ * Get the interval in which an object will live in the map before
+ * it is removed.
+ *
+ * @return
+ * The time in seconds.
+ */
+ public int getExpirationInterval() {
+ stateLock.readLock().lock();
+
+ try {
+ return (int) expirationIntervalMillis / 1000;
+ } finally {
+ stateLock.readLock().unlock();
+ }
+ }
+
+ /**
+ * Set the interval in which an object will live in the map before
+ * it is removed.
+ *
+ * @param expirationInterval
+ * The time in seconds
+ */
+ public void setExpirationInterval(long expirationInterval) {
+ stateLock.writeLock().lock();
+
+ try {
+ this.expirationIntervalMillis = expirationInterval * 1000;
+ } finally {
+ stateLock.writeLock().unlock();
+ }
+ }
+ }
+}
http://git-wip-us.apache.org/repos/asf/karaf/blob/c4bd1fa9/jaas/modules/src/main/java/org/apache/karaf/jaas/modules/ldap/LDAPCache.java
----------------------------------------------------------------------
diff --git a/jaas/modules/src/main/java/org/apache/karaf/jaas/modules/ldap/LDAPCache.java b/jaas/modules/src/main/java/org/apache/karaf/jaas/modules/ldap/LDAPCache.java
new file mode 100644
index 0000000..a328a05
--- /dev/null
+++ b/jaas/modules/src/main/java/org/apache/karaf/jaas/modules/ldap/LDAPCache.java
@@ -0,0 +1,78 @@
+/*
+ *
+ * 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
+ *
+ * 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.
+ * under the License.
+ */
+package org.apache.karaf.jaas.modules.ldap;
+
+import java.util.Map;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+public class LDAPCache {
+
+ public static final String CACHE_TIME_TO_LIVE = "cache.timeToLive";
+ public static final int DEFAULT_TIME_TO_LIVE = 60 * 60; // One hour
+
+ private static final ConcurrentMap<Map<String, ?>, LDAPCache> CACHES = new ConcurrentHashMap<>();
+
+ public static void clear() {
+ CACHES.clear();
+ }
+
+ public static LDAPCache getCache(Map<String, ?> options) {
+ LDAPCache cache = CACHES.get(options);
+ if (cache == null) {
+ CACHES.putIfAbsent(options, new LDAPCache(options));
+ cache = CACHES.get(options);
+ }
+ return cache;
+ }
+
+ private final int timeToLive;
+ private final ExpiringMap<String, String[]> userDnAndNamespace;
+ private final ExpiringMap<String, String[]> userRoles;
+
+ public LDAPCache(Map<String, ?> options) {
+ if (options.containsKey(CACHE_TIME_TO_LIVE)) {
+ timeToLive = Integer.parseInt(options.get(CACHE_TIME_TO_LIVE).toString());
+ } else {
+ timeToLive = DEFAULT_TIME_TO_LIVE;
+ }
+ userDnAndNamespace = new ExpiringMap<>(timeToLive);
+ userRoles = new ExpiringMap<>(timeToLive);
+ }
+
+ public String[] getUserDnAndNamespace(String user, Callable<String[]> callable) throws Exception {
+ String[] result = userDnAndNamespace.get(user);
+ if (result == null) {
+ result = callable.call();
+ if (result != null) {
+ userDnAndNamespace.put(user, result);
+ }
+ }
+ return result;
+ }
+
+ public String[] getUserRoles(String userDN, Callable<String[]> callable) throws Exception {
+ String[] result = userRoles.get(userDN);
+ if (result == null) {
+ result = callable.call();
+ if (result != null) {
+ userRoles.put(userDN, result);
+ }
+ }
+ return result;
+ }
+}
http://git-wip-us.apache.org/repos/asf/karaf/blob/c4bd1fa9/jaas/modules/src/main/java/org/apache/karaf/jaas/modules/ldap/LDAPLoginModule.java
----------------------------------------------------------------------
diff --git a/jaas/modules/src/main/java/org/apache/karaf/jaas/modules/ldap/LDAPLoginModule.java b/jaas/modules/src/main/java/org/apache/karaf/jaas/modules/ldap/LDAPLoginModule.java
index 7b2c020..b865bc6 100644
--- a/jaas/modules/src/main/java/org/apache/karaf/jaas/modules/ldap/LDAPLoginModule.java
+++ b/jaas/modules/src/main/java/org/apache/karaf/jaas/modules/ldap/LDAPLoginModule.java
@@ -32,9 +32,12 @@ import javax.security.auth.callback.*;
import javax.security.auth.login.LoginException;
import java.io.IOException;
import java.security.Principal;
+import java.util.ArrayList;
import java.util.HashSet;
import java.util.Hashtable;
+import java.util.List;
import java.util.Map;
+import java.util.concurrent.Callable;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -184,7 +187,7 @@ public class LDAPLoginModule extends AbstractKarafLoginModule {
principals = new HashSet<Principal>();
// step 1: get the user DN
- Hashtable<String, Object> env = new Hashtable<>();
+ final Hashtable<String, Object> env = new Hashtable<>();
logger.debug("Create the LDAP initial context.");
for (String key : options.keySet()) {
if (key.startsWith(CONTEXT_PREFIX)) {
@@ -203,56 +206,76 @@ public class LDAPLoginModule extends AbstractKarafLoginModule {
setupSsl(env);
}
logger.debug("Get the user DN.");
- String userDN;
- String userDNNamespace;
- DirContext context = null;
+ final String userDN;
+ final String userDNNamespace;
try {
- logger.debug("Initialize the JNDI LDAP Dir Context.");
- context = new InitialDirContext(env);
- logger.debug("Define the subtree scope search control.");
- SearchControls controls = new SearchControls();
- if (userSearchSubtree) {
- controls.setSearchScope(SearchControls.SUBTREE_SCOPE);
- } else {
- controls.setSearchScope(SearchControls.ONELEVEL_SCOPE);
- }
- logger.debug("Looking for the user in LDAP with ");
- logger.debug(" base DN: " + userBaseDN);
- userFilter = userFilter.replaceAll(Pattern.quote("%u"), Matcher.quoteReplacement(user));
- userFilter = userFilter.replace("\\", "\\\\");
- logger.debug(" filter: " + userFilter);
- NamingEnumeration namingEnumeration = context.search(userBaseDN, userFilter, controls);
- if (!namingEnumeration.hasMore()) {
- logger.warn("User " + user + " not found in LDAP.");
+ String[] userDnAndNamespace = LDAPCache.getCache(env).getUserDnAndNamespace(user, new Callable<String[]>() {
+ @Override
+ public String[] call() throws Exception {
+ DirContext context = null;
+ NamingEnumeration namingEnumeration = null;
+ try {
+ logger.debug("Initialize the JNDI LDAP Dir Context.");
+ context = new InitialDirContext(env);
+ logger.debug("Define the subtree scope search control.");
+ SearchControls controls = new SearchControls();
+ if (userSearchSubtree) {
+ controls.setSearchScope(SearchControls.SUBTREE_SCOPE);
+ } else {
+ controls.setSearchScope(SearchControls.ONELEVEL_SCOPE);
+ }
+ logger.debug("Looking for the user in LDAP with ");
+ logger.debug(" base DN: " + userBaseDN);
+ userFilter = userFilter.replaceAll(Pattern.quote("%u"), Matcher.quoteReplacement(user));
+ userFilter = userFilter.replace("\\", "\\\\");
+ logger.debug(" filter: " + userFilter);
+ namingEnumeration = context.search(userBaseDN, userFilter, controls);
+ if (!namingEnumeration.hasMore()) {
+ logger.warn("User " + user + " not found in LDAP.");
+ return null;
+ }
+ logger.debug("Get the user DN.");
+ SearchResult result = (SearchResult) namingEnumeration.next();
+
+ // We need to do the following because slashes are handled badly. For example, when searching
+ // for a user with lots of special characters like cn=admin,=+<>#;\
+ // SearchResult contains 2 different results:
+ //
+ // SearchResult.getName = cn=admin\,\=\+\<\>\#\;\\\\
+ // SearchResult.getNameInNamespace = cn=admin\,\=\+\<\>#\;\\,ou=people,dc=example,dc=com
+ //
+ // the second escapes the slashes correctly.
+ String userDN = result.getNameInNamespace().replace("," + userBaseDN, "");
+ String userDNNamespace = (String) result.getNameInNamespace();
+ return new String[] { userDN, userDNNamespace };
+ } finally {
+ if (namingEnumeration != null) {
+ try {
+ namingEnumeration.close();
+ } catch (Exception e) {
+ // ignore
+ }
+ }
+ if (context != null) {
+ try {
+ context.close();
+ } catch (Exception e) {
+ // ignore
+ }
+ }
+ }
+ }
+ });
+ if (userDnAndNamespace == null) {
return false;
}
- logger.debug("Get the user DN.");
- SearchResult result = (SearchResult) namingEnumeration.next();
-
- // We need to do the following because slashes are handled badly. For example, when searching
- // for a user with lots of special characters like cn=admin,=+<>#;\
- // SearchResult contains 2 different results:
- //
- // SearchResult.getName = cn=admin\,\=\+\<\>\#\;\\\\
- // SearchResult.getNameInNamespace = cn=admin\,\=\+\<\>#\;\\,ou=people,dc=example,dc=com
- //
- // the second escapes the slashes correctly.
- userDN = result.getNameInNamespace().replace("," + userBaseDN, "");
- userDNNamespace = (String) result.getNameInNamespace();
- namingEnumeration.close();
+ userDN = userDnAndNamespace[0];
+ userDNNamespace = userDnAndNamespace[1];
} catch (Exception e) {
throw new LoginException("Can't connect to the LDAP server: " + e.getMessage());
- } finally {
- if (context != null) {
- try {
- context.close();
- } catch (Exception e) {
- // ignore
- }
- }
}
// step 2: bind the user using the DN
- context = null;
+ DirContext context = null;
try {
// switch the credentials to the Karaf login user so that we can verify his password is correct
logger.debug("Bind user (authentication).");
@@ -278,58 +301,70 @@ public class LDAPLoginModule extends AbstractKarafLoginModule {
}
principals.add(new UserPrincipal(user));
// step 3: retrieving user roles
- context = null;
try {
- logger.debug("Get user roles.");
- // switch back to the connection credentials for the role search like we did for the user search in step 1
- if (connectionUsername != null && connectionUsername.trim().length() > 0) {
- env.put(Context.SECURITY_AUTHENTICATION, authentication);
- env.put(Context.SECURITY_PRINCIPAL, connectionUsername);
- env.put(Context.SECURITY_CREDENTIALS, connectionPassword);
- }
- context = new InitialDirContext(env);
- SearchControls controls = new SearchControls();
- if (roleSearchSubtree) {
- controls.setSearchScope(SearchControls.SUBTREE_SCOPE);
- } else {
- controls.setSearchScope(SearchControls.ONELEVEL_SCOPE);
- }
- if (roleNameAttribute != null) {
- controls.setReturningAttributes(new String[]{ roleNameAttribute });
- }
- logger.debug("Looking for the user roles in LDAP with ");
- logger.debug(" base DN: " + roleBaseDN);
- roleFilter = roleFilter.replaceAll(Pattern.quote("%u"), Matcher.quoteReplacement(user));
- roleFilter = roleFilter.replaceAll(Pattern.quote("%dn"), Matcher.quoteReplacement(userDN));
- roleFilter = roleFilter.replaceAll(Pattern.quote("%fqdn"), Matcher.quoteReplacement(userDN + "," + userBaseDN));
- roleFilter = roleFilter.replaceAll(Pattern.quote("%nsdn"), Matcher.quoteReplacement(userDNNamespace));
- roleFilter = roleFilter.replace("\\", "\\\\");
- logger.debug(" filter: " + roleFilter);
- NamingEnumeration namingEnumeration = context.search(roleBaseDN, roleFilter, controls);
- while (namingEnumeration.hasMore()) {
- SearchResult result = (SearchResult)namingEnumeration.next();
- Attributes attributes = result.getAttributes();
- Attribute roles = attributes.get(roleNameAttribute);
- if (roles != null) {
- for (int i = 0; i < roles.size(); i++) {
- String role = (String)roles.get(i);
- if (role != null) {
- principals.add(new RolePrincipal(role));
+ String[] roles = LDAPCache.getCache(env).getUserRoles(userDN, new Callable<String[]>() {
+ @Override
+ public String[] call() throws Exception {
+ DirContext context = null;
+ try {
+ logger.debug("Get user roles.");
+ // switch back to the connection credentials for the role search like we did for the user search in step 1
+ if (connectionUsername != null && connectionUsername.trim().length() > 0) {
+ env.put(Context.SECURITY_AUTHENTICATION, authentication);
+ env.put(Context.SECURITY_PRINCIPAL, connectionUsername);
+ env.put(Context.SECURITY_CREDENTIALS, connectionPassword);
+ }
+ context = new InitialDirContext(env);
+ SearchControls controls = new SearchControls();
+ if (roleSearchSubtree) {
+ controls.setSearchScope(SearchControls.SUBTREE_SCOPE);
+ } else {
+ controls.setSearchScope(SearchControls.ONELEVEL_SCOPE);
+ }
+ if (roleNameAttribute != null) {
+ controls.setReturningAttributes(new String[]{roleNameAttribute});
+ }
+ logger.debug("Looking for the user roles in LDAP with ");
+ logger.debug(" base DN: " + roleBaseDN);
+ roleFilter = roleFilter.replaceAll(Pattern.quote("%u"), Matcher.quoteReplacement(user));
+ roleFilter = roleFilter.replaceAll(Pattern.quote("%dn"), Matcher.quoteReplacement(userDN));
+ roleFilter = roleFilter.replaceAll(Pattern.quote("%fqdn"), Matcher.quoteReplacement(userDN + "," + userBaseDN));
+ roleFilter = roleFilter.replaceAll(Pattern.quote("%nsdn"), Matcher.quoteReplacement(userDNNamespace));
+ roleFilter = roleFilter.replace("\\", "\\\\");
+ logger.debug(" filter: " + roleFilter);
+ List<String> rolesList = new ArrayList<>();
+ NamingEnumeration namingEnumeration = context.search(roleBaseDN, roleFilter, controls);
+ while (namingEnumeration.hasMore()) {
+ SearchResult result = (SearchResult) namingEnumeration.next();
+ Attributes attributes = result.getAttributes();
+ Attribute roles = attributes.get(roleNameAttribute);
+ if (roles != null) {
+ for (int i = 0; i < roles.size(); i++) {
+ String role = (String) roles.get(i);
+ if (role != null) {
+ rolesList.add(role);
+ }
+ }
+ }
+
+ }
+ return rolesList.toArray(new String[rolesList.size()]);
+ } finally {
+ if (context != null) {
+ try {
+ context.close();
+ } catch (Exception e) {
+ // ignore
+ }
}
}
}
-
+ });
+ for (String role : roles) {
+ principals.add(new RolePrincipal(role));
}
} catch (Exception e) {
throw new LoginException("Can't get user " + user + " roles: " + e.getMessage());
- } finally {
- if (context != null) {
- try {
- context.close();
- } catch (Exception e) {
- // ignore
- }
- }
}
return true;
}
http://git-wip-us.apache.org/repos/asf/karaf/blob/c4bd1fa9/jaas/modules/src/test/java/org/apache/karaf/jaas/modules/ldap/LdapLoginModuleTest.java
----------------------------------------------------------------------
diff --git a/jaas/modules/src/test/java/org/apache/karaf/jaas/modules/ldap/LdapLoginModuleTest.java b/jaas/modules/src/test/java/org/apache/karaf/jaas/modules/ldap/LdapLoginModuleTest.java
index faf0340..0f24705 100644
--- a/jaas/modules/src/test/java/org/apache/karaf/jaas/modules/ldap/LdapLoginModuleTest.java
+++ b/jaas/modules/src/test/java/org/apache/karaf/jaas/modules/ldap/LdapLoginModuleTest.java
@@ -26,6 +26,7 @@ import org.apache.directory.server.core.annotations.CreatePartition;
import org.apache.felix.utils.properties.Properties;
import org.apache.karaf.jaas.boot.principal.RolePrincipal;
import org.apache.karaf.jaas.boot.principal.UserPrincipal;
+import org.junit.After;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -51,6 +52,11 @@ import static org.junit.Assert.fail;
)
public class LdapLoginModuleTest extends AbstractLdapTestUnit {
+ @After
+ public void tearDown() {
+ LDAPCache.clear();
+ }
+
@Test
public void testAdminLogin() throws Exception {
Properties options = ldapLoginModuleOptions();