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();