karaf-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From gno...@apache.org
Subject karaf git commit: [KARAF-4247] Issues with LDAP caching
Date Thu, 07 Jan 2016 07:22:40 GMT
Repository: karaf
Updated Branches:
  refs/heads/master d309d62be -> 86822b1d4


[KARAF-4247] Issues with LDAP caching


Project: http://git-wip-us.apache.org/repos/asf/karaf/repo
Commit: http://git-wip-us.apache.org/repos/asf/karaf/commit/86822b1d
Tree: http://git-wip-us.apache.org/repos/asf/karaf/tree/86822b1d
Diff: http://git-wip-us.apache.org/repos/asf/karaf/diff/86822b1d

Branch: refs/heads/master
Commit: 86822b1d44b9c261b4dd4b70f79f1f75c29e754b
Parents: d309d62
Author: Guillaume Nodet <gnodet@apache.org>
Authored: Wed Jan 6 18:44:30 2016 +0100
Committer: Guillaume Nodet <gnodet@apache.org>
Committed: Thu Jan 7 08:22:21 2016 +0100

----------------------------------------------------------------------
 .../karaf/jaas/modules/ldap/ExpiringMap.java    | 451 -------------------
 .../karaf/jaas/modules/ldap/LDAPCache.java      | 287 ++++++++++--
 .../jaas/modules/ldap/LDAPLoginModule.java      | 365 ++-------------
 .../karaf/jaas/modules/ldap/LDAPOptions.java    | 261 +++++++++++
 .../modules/ldap/ManagedSSLSocketFactory.java   |  35 ++
 .../karaf/jaas/modules/ldap/LdapCacheTest.java  | 175 +++++++
 .../jaas/modules/ldap/LdapLoginModuleTest.java  |   6 +-
 7 files changed, 767 insertions(+), 813 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/karaf/blob/86822b1d/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
deleted file mode 100644
index 4c16012..0000000
--- a/jaas/modules/src/main/java/org/apache/karaf/jaas/modules/ldap/ExpiringMap.java
+++ /dev/null
@@ -1,451 +0,0 @@
-/*
- *  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/86822b1d/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
index a328a05..00dafac 100644
--- 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
@@ -1,5 +1,4 @@
 /*
- *
  *  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
@@ -15,23 +14,51 @@
  */
 package org.apache.karaf.jaas.modules.ldap;
 
+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.naming.event.EventDirContext;
+import javax.naming.event.NamespaceChangeListener;
+import javax.naming.event.NamingEvent;
+import javax.naming.event.NamingExceptionEvent;
+import javax.naming.event.ObjectChangeListener;
+import java.io.Closeable;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
 import java.util.Map;
-import java.util.concurrent.Callable;
+import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ConcurrentMap;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
-public class LDAPCache {
+public class LDAPCache implements Closeable, NamespaceChangeListener, ObjectChangeListener {
 
-    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<LDAPOptions, LDAPCache> CACHES = new ConcurrentHashMap<>();
 
-    private static final ConcurrentMap<Map<String, ?>, LDAPCache> CACHES = new ConcurrentHashMap<>();
+    private static Logger LOGGER = LoggerFactory.getLogger(LDAPLoginModule.class);
 
     public static void clear() {
-        CACHES.clear();
+        while (!CACHES.isEmpty()) {
+            LDAPOptions options = CACHES.keySet().iterator().next();
+            LDAPCache cache = CACHES.remove(options);
+            if (cache != null) {
+                cache.clearCache();
+            }
+        }
     }
 
-    public static LDAPCache getCache(Map<String, ?> options) {
+    public static LDAPCache getCache(LDAPOptions options) {
         LDAPCache cache = CACHES.get(options);
         if (cache == null) {
             CACHES.putIfAbsent(options, new LDAPCache(options));
@@ -40,24 +67,75 @@ public class LDAPCache {
         return cache;
     }
 
-    private final int timeToLive;
-    private final ExpiringMap<String, String[]> userDnAndNamespace;
-    private final ExpiringMap<String, String[]> userRoles;
+    private final Map<String, String[]> userDnAndNamespace;
+    private final Map<String, String[]> userRoles;
+    private final LDAPOptions options;
+    private DirContext context;
 
-    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;
+    public LDAPCache(LDAPOptions options) {
+        this.options = options;
+        userDnAndNamespace = new HashMap<>();
+        userRoles = new HashMap<>();
+    }
+
+    @Override
+    public synchronized void close() {
+        clearCache();
+        if (context != null) {
+            try {
+                context.close();
+            } catch (NamingException e) {
+                // Ignore
+            } finally {
+                context = null;
+            }
         }
-        userDnAndNamespace = new ExpiringMap<>(timeToLive);
-        userRoles = new ExpiringMap<>(timeToLive);
     }
 
-    public String[] getUserDnAndNamespace(String user, Callable<String[]> callable) throws Exception {
+    private boolean isContextAlive() {
+        boolean alive = false;
+        if (context != null) {
+            try {
+                context.getAttributes("");
+                alive = true;
+            } catch (Exception e) {
+                // Ignore
+            }
+        }
+        return alive;
+    }
+
+    public synchronized DirContext open() throws NamingException {
+        if (isContextAlive()) {
+            return context;
+        }
+        clearCache();
+        context = new InitialDirContext(options.getEnv());
+
+        EventDirContext eventContext = ((EventDirContext) context.lookup(""));
+
+        final SearchControls constraints = new SearchControls();
+        constraints.setSearchScope(SearchControls.SUBTREE_SCOPE);
+
+        String filter = options.getUserFilter();
+        filter = filter.replaceAll(Pattern.quote("%u"), Matcher.quoteReplacement("*"));
+        filter = filter.replace("\\", "\\\\");
+        eventContext.addNamingListener(options.getUserBaseDn(), filter, constraints, this);
+
+        filter = options.getRoleFilter();
+        filter = filter.replaceAll(Pattern.quote("%u"), Matcher.quoteReplacement("*"));
+        filter = filter.replaceAll(Pattern.quote("%dn"), Matcher.quoteReplacement("*"));
+        filter = filter.replaceAll(Pattern.quote("%fqdn"), Matcher.quoteReplacement("*"));
+        filter = filter.replace("\\", "\\\\");
+        eventContext.addNamingListener(options.getRoleBaseDn(), filter, constraints, this);
+
+        return context;
+    }
+
+    public synchronized String[] getUserDnAndNamespace(String user) throws Exception {
         String[] result = userDnAndNamespace.get(user);
         if (result == null) {
-            result = callable.call();
+            result = doGetUserDnAndNamespace(user);
             if (result != null) {
                 userDnAndNamespace.put(user, result);
             }
@@ -65,14 +143,171 @@ public class LDAPCache {
         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);
+    protected String[] doGetUserDnAndNamespace(String user) throws NamingException {
+        DirContext context = open();
+
+        SearchControls controls = new SearchControls();
+        if (options.getUserSearchSubtree()) {
+            controls.setSearchScope(SearchControls.SUBTREE_SCOPE);
+        } else {
+            controls.setSearchScope(SearchControls.ONELEVEL_SCOPE);
+        }
+
+        String filter = options.getUserFilter();
+        filter = filter.replaceAll(Pattern.quote("%u"), Matcher.quoteReplacement(user));
+        filter = filter.replace("\\", "\\\\");
+
+        LOGGER.debug("Looking for the user in LDAP with ");
+        LOGGER.debug("  base DN: " + options.getUserBaseDn());
+        LOGGER.debug("  filter: " + filter);
+
+        NamingEnumeration namingEnumeration = context.search(options.getUserBaseDn(), filter, controls);
+        try {
+            if (!namingEnumeration.hasMore()) {
+                LOGGER.warn("User " + user + " not found in LDAP.");
+                return null;
+            }
+            LOGGER.debug("Found 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 userDNNamespace = result.getNameInNamespace();
+            // handle case where cn, ou, dc case doesn't match
+            int indexOfUserBaseDN = userDNNamespace.toLowerCase().indexOf("," + options.getUserBaseDn().toLowerCase());
+            String userDN = (indexOfUserBaseDN > 0) ?
+                    userDNNamespace.substring(0, indexOfUserBaseDN) :
+                    result.getName();
+
+            return new String[]{userDN, userDNNamespace};
+        } finally {
+            if (namingEnumeration != null) {
+                try {
+                    namingEnumeration.close();
+                } catch (NamingException e) {
+                    // Ignore
+                }
             }
         }
+    }
+
+    public synchronized String[] getUserRoles(String user, String userDn, String userDnNamespace) throws Exception {
+        String[] result = userRoles.get(userDn);
+        if (result == null) {
+            result = doGetUserRoles(user, userDn, userDnNamespace);
+            userRoles.put(userDn, result);
+        }
         return result;
     }
+
+    protected Set<String> tryMappingRole(String role) {
+        Set<String> roles = new HashSet<>();
+        if (options.getRoleMapping().isEmpty()) {
+            return roles;
+        }
+        Set<String> karafRoles = options.getRoleMapping().get(role);
+        if (karafRoles != null) {
+            // add all mapped roles
+            for (String karafRole : karafRoles) {
+                LOGGER.debug("LDAP role {} is mapped to Karaf role {}", role, karafRole);
+                roles.add(karafRole);
+            }
+        }
+        return roles;
+    }
+
+
+    private String[] doGetUserRoles(String user, String userDn, String userDnNamespace) throws NamingException {
+        DirContext context = open();
+
+        SearchControls controls = new SearchControls();
+        if (options.getRoleSearchSubtree()) {
+            controls.setSearchScope(SearchControls.SUBTREE_SCOPE);
+        } else {
+            controls.setSearchScope(SearchControls.ONELEVEL_SCOPE);
+        }
+
+        String filter = options.getRoleFilter();
+        filter = filter.replaceAll(Pattern.quote("%u"), Matcher.quoteReplacement(user));
+        filter = filter.replaceAll(Pattern.quote("%dn"), Matcher.quoteReplacement(userDn));
+        filter = filter.replaceAll(Pattern.quote("%fqdn"), Matcher.quoteReplacement(userDnNamespace));
+        filter = filter.replace("\\", "\\\\");
+
+        LOGGER.debug("Looking for the user roles in LDAP with ");
+        LOGGER.debug("  base DN: " + options.getRoleBaseDn());
+        LOGGER.debug("  filter: " + filter);
+
+        NamingEnumeration namingEnumeration = context.search(options.getRoleBaseDn(), filter, controls);
+        try {
+            List<String> rolesList = new ArrayList<>();
+            while (namingEnumeration.hasMore()) {
+                SearchResult result = (SearchResult) namingEnumeration.next();
+                Attributes attributes = result.getAttributes();
+                Attribute roles1 = attributes.get(options.getRoleNameAttribute());
+                if (roles1 != null) {
+                    for (int i = 0; i < roles1.size(); i++) {
+                        String role = (String) roles1.get(i);
+                        if (role != null) {
+                            LOGGER.debug("User {} is a member of role {}", user, role);
+                            // handle role mapping
+                            Set<String> roleMappings = tryMappingRole(role);
+                            if (roleMappings.isEmpty()) {
+                                rolesList.add(role);
+                            } else {
+                                for (String roleMapped : roleMappings) {
+                                    rolesList.add(roleMapped);
+                                }
+                            }
+                        }
+                    }
+                }
+
+            }
+            return rolesList.toArray(new String[rolesList.size()]);
+        } finally {
+            if (namingEnumeration != null) {
+                try {
+                    namingEnumeration.close();
+                } catch (NamingException e) {
+                    // Ignore
+                }
+            }
+        }
+    }
+
+    @Override
+    public void objectAdded(NamingEvent evt) {
+        clearCache();
+    }
+
+    @Override
+    public void objectRemoved(NamingEvent evt) {
+        clearCache();
+    }
+
+    @Override
+    public void objectRenamed(NamingEvent evt) {
+        clearCache();
+    }
+
+    @Override
+    public void objectChanged(NamingEvent evt) {
+        clearCache();
+    }
+
+    @Override
+    public void namingExceptionThrown(NamingExceptionEvent evt) {
+        clearCache();
+    }
+
+    protected synchronized void clearCache() {
+        userDnAndNamespace.clear();
+        userRoles.clear();
+    }
 }

http://git-wip-us.apache.org/repos/asf/karaf/blob/86822b1d/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 e115684..f8743c6 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
@@ -1,5 +1,4 @@
 /*
- *
  *  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
@@ -15,148 +14,38 @@
  */
 package org.apache.karaf.jaas.modules.ldap;
 
-import org.apache.karaf.jaas.boot.principal.RolePrincipal;
-import org.apache.karaf.jaas.boot.principal.UserPrincipal;
-import org.apache.karaf.jaas.config.KeystoreManager;
-import org.apache.karaf.jaas.modules.AbstractKarafLoginModule;
-import org.osgi.framework.ServiceReference;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
 import javax.naming.Context;
-import javax.naming.NamingEnumeration;
-import javax.naming.directory.*;
-import javax.net.ssl.SSLSocketFactory;
+import javax.naming.directory.DirContext;
+import javax.naming.directory.InitialDirContext;
 import javax.security.auth.Subject;
-import javax.security.auth.callback.*;
+import javax.security.auth.callback.Callback;
+import javax.security.auth.callback.CallbackHandler;
+import javax.security.auth.callback.NameCallback;
+import javax.security.auth.callback.PasswordCallback;
+import javax.security.auth.callback.UnsupportedCallbackException;
 import javax.security.auth.login.LoginException;
 import java.io.IOException;
 import java.security.Principal;
-import java.util.*;
-import java.util.concurrent.Callable;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Hashtable;
+import java.util.Map;
+
+import org.apache.karaf.jaas.boot.principal.RolePrincipal;
+import org.apache.karaf.jaas.boot.principal.UserPrincipal;
+import org.apache.karaf.jaas.modules.AbstractKarafLoginModule;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 /**
  * Karaf JAAS login module which uses a LDAP backend.
  */
 public class LDAPLoginModule extends AbstractKarafLoginModule {
 
-    private static final String DEFAULT_AUTHENTICATION = "simple";
-
     private static Logger logger = LoggerFactory.getLogger(LDAPLoginModule.class);
 
-    public final static String CONNECTION_URL = "connection.url";
-    public final static String CONNECTION_USERNAME = "connection.username";
-    public final static String CONNECTION_PASSWORD = "connection.password";
-    public final static String USER_BASE_DN = "user.base.dn";
-    public final static String USER_FILTER = "user.filter";
-    public final static String USER_SEARCH_SUBTREE = "user.search.subtree";
-    public final static String ROLE_BASE_DN = "role.base.dn";
-    public final static String ROLE_FILTER = "role.filter";
-    public final static String ROLE_NAME_ATTRIBUTE = "role.name.attribute";
-    public final static String ROLE_SEARCH_SUBTREE = "role.search.subtree";
-    public final static String ROLE_MAPPING = "role.mapping";
-    public final static String AUTHENTICATION = "authentication";
-    public final static String ALLOW_EMPTY_PASSWORDS = "allowEmptyPasswords";
-    public final static String INITIAL_CONTEXT_FACTORY = "initial.context.factory";
-    public static final String CONTEXT_PREFIX = "context.";
-    public final static String SSL = "ssl";
-    public final static String SSL_PROVIDER = "ssl.provider";
-    public final static String SSL_PROTOCOL = "ssl.protocol";
-    public final static String SSL_ALGORITHM = "ssl.algorithm";
-    public final static String SSL_KEYSTORE = "ssl.keystore";
-    public final static String SSL_KEYALIAS = "ssl.keyalias";
-    public final static String SSL_TRUSTSTORE = "ssl.truststore";
-    public final static String SSL_TIMEOUT = "ssl.timeout";
-
-    public final static String DEFAULT_INITIAL_CONTEXT_FACTORY = "com.sun.jndi.ldap.LdapCtxFactory";
-
-    private String connectionURL;
-    private String connectionUsername;
-    private String connectionPassword;
-    private String userBaseDN;
-    private String userFilter;
-    private boolean userSearchSubtree = true;
-    private String roleBaseDN;
-    private String roleFilter;
-    private String roleNameAttribute;
-    private boolean roleSearchSubtree = true;
-    private Map<String, Set<String>> roleMapping;
-    private String authentication = DEFAULT_AUTHENTICATION;
-    private boolean allowEmptyPasswords = false;
-    private String initialContextFactory = null;
-    private boolean ssl;
-    private String sslProvider;
-    private String sslProtocol;
-    private String sslAlgorithm;
-    private String sslKeystore;
-    private String sslKeyAlias;
-    private String sslTrustStore;
-    private int sslTimeout = 10;
-
     public void initialize(Subject subject, CallbackHandler callbackHandler, Map<String, ?> sharedState, Map<String, ?> options) {
         super.initialize(subject, callbackHandler, options);
-        connectionURL = (String) options.get(CONNECTION_URL);
-        connectionUsername = (String) options.get(CONNECTION_USERNAME);
-        connectionPassword = (String) options.get(CONNECTION_PASSWORD);
-        userBaseDN = (String) options.get(USER_BASE_DN);
-        userFilter = (String) options.get(USER_FILTER);
-        userSearchSubtree = Boolean.parseBoolean((String) options.get(USER_SEARCH_SUBTREE));
-        roleBaseDN = (String) options.get(ROLE_BASE_DN);
-        roleFilter = (String) options.get(ROLE_FILTER);
-        roleNameAttribute = (String) options.get(ROLE_NAME_ATTRIBUTE);
-        roleSearchSubtree = Boolean.parseBoolean((String) options.get(ROLE_SEARCH_SUBTREE));
-        roleMapping = parseRoleMapping((String) options.get(ROLE_MAPPING));
-        initialContextFactory = (String) options.get(INITIAL_CONTEXT_FACTORY);
-        if (initialContextFactory == null) {
-            initialContextFactory = DEFAULT_INITIAL_CONTEXT_FACTORY;
-        }
-        authentication = (String) options.get(AUTHENTICATION);
-        if (authentication == null) {
-            authentication = DEFAULT_AUTHENTICATION;
-        }
-        allowEmptyPasswords = Boolean.parseBoolean((String) options.get(ALLOW_EMPTY_PASSWORDS));
-        if (connectionURL == null || connectionURL.trim().length() == 0) {
-            logger.error("No LDAP URL specified.");
-        } else if (!connectionURL.startsWith("ldap:") && !connectionURL.startsWith("ldaps:")) {
-            logger.error("Invalid LDAP URL: " + connectionURL);
-        }
-        if (options.get(SSL) != null) {
-            ssl = Boolean.parseBoolean((String) options.get(SSL));
-        } else {
-            ssl = connectionURL.startsWith("ldaps:");
-        }
-        sslProvider = (String) options.get(SSL_PROVIDER);
-        sslProtocol = (String) options.get(SSL_PROTOCOL);
-        sslAlgorithm = (String) options.get(SSL_ALGORITHM);
-        sslKeystore = (String) options.get(SSL_KEYSTORE);
-        sslKeyAlias = (String) options.get(SSL_KEYALIAS);
-        sslTrustStore = (String) options.get(SSL_TRUSTSTORE);
-        if (options.get(SSL_TIMEOUT) != null) {
-            sslTimeout = (Integer) options.get(SSL_TIMEOUT);
-        }
-    }
-
-    private Map<String, Set<String>> parseRoleMapping(String option) {
-        Map<String, Set<String>> roleMapping = new HashMap<String, Set<String>>();
-        if (option != null) {
-            logger.debug("Parse role mapping {}", option);
-            String[] mappings = option.split(";");
-            for (String mapping : mappings) {
-                String[] map = mapping.split("=", 2);
-                String ldapRole = map[0].trim();
-                String[] karafRoles = map[1].split(",");
-                if (roleMapping.get(ldapRole) == null) {
-                    roleMapping.put(ldapRole, new HashSet<String>());
-                }
-                final Set<String> karafRolesSet = roleMapping.get(ldapRole);
-                for (String karafRole : karafRoles) {
-                    karafRolesSet.add(karafRole.trim());
-                }
-            }
-        }
-        return roleMapping;
     }
 
     public boolean login() throws LoginException {
@@ -190,11 +79,17 @@ public class LDAPLoginModule extends AbstractKarafLoginModule {
         // This is to prevent someone from logging into Karaf as any user without providing a 
         // valid password (because if authentication = none, the password could be any 
         // value - it is ignored).
+        LDAPOptions options = new LDAPOptions(this.options);
+        String authentication = options.getAuthentication();
         if ("none".equals(authentication) && (user != null || tmpPassword != null)) {
             logger.debug("Changing from authentication = none to simple since user or password was specified.");
             // default to simple so that the provided user/password will get checked
             authentication = "simple";
+            Map<String, Object> opts = new HashMap<>(this.options);
+            opts.put(LDAPOptions.AUTHENTICATION, authentication);
+            options = new LDAPOptions(opts);
         }
+        boolean allowEmptyPasswords = options.getAllowEmptyPasswords();
         if (!"none".equals(authentication) && !allowEmptyPasswords
                 && (tmpPassword == null || tmpPassword.length == 0)) {
             throw new LoginException("Empty passwords not allowed");
@@ -206,96 +101,16 @@ public class LDAPLoginModule extends AbstractKarafLoginModule {
         String password = new String(tmpPassword);
         principals = new HashSet<Principal>();
 
+        LDAPCache cache = LDAPCache.getCache(options);
+
         // step 1: get the user DN
-        final Hashtable<String, Object> env = new Hashtable<>();
-        logger.debug("Create the LDAP initial context.");
-        for (String key : options.keySet()) {
-            if (key.startsWith(CONTEXT_PREFIX)) {
-                env.put(key.substring(CONTEXT_PREFIX.length()), options.get(key));
-            }
-        }
-        env.put(Context.INITIAL_CONTEXT_FACTORY, initialContextFactory);
-        env.put(Context.PROVIDER_URL, connectionURL);
-        if (connectionUsername != null && connectionUsername.trim().length() > 0) {
-            logger.debug("Bound access requested.");
-            env.put(Context.SECURITY_AUTHENTICATION, authentication);
-            env.put(Context.SECURITY_PRINCIPAL, connectionUsername);
-            env.put(Context.SECURITY_CREDENTIALS, connectionPassword);
-        }
-        if (ssl) {
-            setupSsl(env);
-        }
-        logger.debug("Get the user DN.");
-        final String userDN;
-        final String userDNNamespace;
+        final String[] userDnAndNamespace;
         try {
-            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 userDNNamespace = (String) result.getNameInNamespace();
-                        // handle case where cn, ou, dc case doesn't match
-                        int indexOfUserBaseDN = userDNNamespace.toLowerCase().indexOf("," + userBaseDN.toLowerCase());
-                        String userDN = (indexOfUserBaseDN > 0) ?
-                            userDNNamespace.substring(0, indexOfUserBaseDN) :
-                            result.getName();
-            
-                        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
-                            }
-                        }
-                    }
-                }
-            });
+            logger.debug("Get the user DN.");
+            userDnAndNamespace = cache.getUserDnAndNamespace(user);
             if (userDnAndNamespace == null) {
                 return false;
             }
-            userDN = userDnAndNamespace[0];
-            userDNNamespace = userDnAndNamespace[1];
         } catch (Exception e) {
             logger.warn("Can't connect to the LDAP server: {}", e.getMessage(), e);
             throw new LoginException("Can't connect to the LDAP server: " + e.getMessage());
@@ -305,9 +120,10 @@ public class LDAPLoginModule extends AbstractKarafLoginModule {
         try {
             // switch the credentials to the Karaf login user so that we can verify his password is correct
             logger.debug("Bind user (authentication).");
+            Hashtable<String, Object> env = options.getEnv();
             env.put(Context.SECURITY_AUTHENTICATION, authentication);
-            logger.debug("Set the security principal for " + userDN + "," + userBaseDN);
-            env.put(Context.SECURITY_PRINCIPAL, userDN + "," + userBaseDN);
+            logger.debug("Set the security principal for " + userDnAndNamespace[0] + "," + options.getUserBaseDn());
+            env.put(Context.SECURITY_PRINCIPAL, userDnAndNamespace[0] + "," + options.getUserBaseDn());
             env.put(Context.SECURITY_CREDENTIALS, password);
             logger.debug("Binding the user.");
             context = new InitialDirContext(env);
@@ -328,72 +144,7 @@ public class LDAPLoginModule extends AbstractKarafLoginModule {
         principals.add(new UserPrincipal(user));
         // step 3: retrieving user roles
         try {
-            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(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) {
-                                        logger.debug("User {} is a member of role {}", user, role);
-                                        // handle role mapping
-                                        Set<String> roleMappings = tryMappingRole(role);
-                                        if (roleMappings.isEmpty()) {
-                                            rolesList.add(role);
-                                        } else {
-                                            for (String roleMapped : roleMappings) {
-                                                rolesList.add(roleMapped);
-                                            }
-                                        }
-                                    }
-                                }
-                            }
-
-                        }
-                        return rolesList.toArray(new String[rolesList.size()]);
-                    } finally {
-                        if (context != null) {
-                            try {
-                                context.close();
-                            } catch (Exception e) {
-                                // ignore
-                            }
-                        }
-                    }
-                }
-            });
+            String[] roles = cache.getUserRoles(user, userDnAndNamespace[0], userDnAndNamespace[1]);
             for (String role : roles) {
                 principals.add(new RolePrincipal(role));
             }
@@ -403,40 +154,6 @@ public class LDAPLoginModule extends AbstractKarafLoginModule {
         return true;
     }
 
-    protected Set<String> tryMappingRole(String role) {
-        Set<String> roles = new HashSet<String>();
-        if (roleMapping == null || roleMapping.isEmpty()) {
-            return roles;
-        }
-        Set<String> karafRoles = roleMapping.get(role);
-        if (karafRoles != null) {
-            // add all mapped roles
-            for (String karafRole : karafRoles) {
-                logger.debug("LDAP role {} is mapped to Karaf role {}", role, karafRole);
-                roles.add(karafRole);
-            }
-        }
-        return roles;
-    }
-
-    protected void setupSsl(Hashtable env) throws LoginException {
-        ServiceReference ref = null;
-        try {
-            logger.debug("Setting up SSL");
-            env.put(Context.SECURITY_PROTOCOL, "ssl");
-            env.put("java.naming.ldap.factory.socket", ManagedSSLSocketFactory.class.getName());
-            ref = bundleContext.getServiceReference(KeystoreManager.class.getName());
-            KeystoreManager manager = (KeystoreManager) bundleContext.getService(ref);
-            SSLSocketFactory factory = manager.createSSLFactory(sslProvider, sslProtocol, sslAlgorithm, sslKeystore, sslKeyAlias, sslTrustStore, sslTimeout);
-            ManagedSSLSocketFactory.setSocketFactory(factory);
-            Thread.currentThread().setContextClassLoader(ManagedSSLSocketFactory.class.getClassLoader());
-        } catch (Exception e) {
-            throw new LoginException("Unable to setup SSL support for LDAP: " + e.getMessage());
-        } finally {
-            bundleContext.ungetService(ref);
-        }
-    }
-
     public boolean abort() throws LoginException {
         return true;
     }
@@ -447,22 +164,4 @@ public class LDAPLoginModule extends AbstractKarafLoginModule {
         return true;
     }
 
-    public static abstract class ManagedSSLSocketFactory extends SSLSocketFactory {
-
-        private static final ThreadLocal<SSLSocketFactory> factories = new ThreadLocal<SSLSocketFactory>();
-
-        public static void setSocketFactory(SSLSocketFactory factory) {
-            factories.set(factory);
-        }
-
-        public static SSLSocketFactory getDefault() {
-            SSLSocketFactory factory = factories.get();
-            if (factory == null) {
-                throw new IllegalStateException("No SSLSocketFactory parameters have been set!");
-            }
-            return factory;
-        }
-
-    }
-
 }

http://git-wip-us.apache.org/repos/asf/karaf/blob/86822b1d/jaas/modules/src/main/java/org/apache/karaf/jaas/modules/ldap/LDAPOptions.java
----------------------------------------------------------------------
diff --git a/jaas/modules/src/main/java/org/apache/karaf/jaas/modules/ldap/LDAPOptions.java b/jaas/modules/src/main/java/org/apache/karaf/jaas/modules/ldap/LDAPOptions.java
new file mode 100644
index 0000000..a173b3e
--- /dev/null
+++ b/jaas/modules/src/main/java/org/apache/karaf/jaas/modules/ldap/LDAPOptions.java
@@ -0,0 +1,261 @@
+/*
+ *  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 javax.naming.Context;
+import javax.naming.NamingException;
+import javax.net.ssl.SSLSocketFactory;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Hashtable;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.karaf.jaas.config.KeystoreManager;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.FrameworkUtil;
+import org.osgi.framework.ServiceReference;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class LDAPOptions {
+
+    public static final String CONNECTION_URL = "connection.url";
+    public static final String CONNECTION_USERNAME = "connection.username";
+    public static final String CONNECTION_PASSWORD = "connection.password";
+    public static final String USER_BASE_DN = "user.base.dn";
+    public static final String USER_FILTER = "user.filter";
+    public static final String USER_SEARCH_SUBTREE = "user.search.subtree";
+    public static final String ROLE_BASE_DN = "role.base.dn";
+    public static final String ROLE_FILTER = "role.filter";
+    public static final String ROLE_NAME_ATTRIBUTE = "role.name.attribute";
+    public static final String ROLE_SEARCH_SUBTREE = "role.search.subtree";
+    public static final String ROLE_MAPPING = "role.mapping";
+    public static final String AUTHENTICATION = "authentication";
+    public static final String ALLOW_EMPTY_PASSWORDS = "allowEmptyPasswords";
+    public static final String INITIAL_CONTEXT_FACTORY = "initial.context.factory";
+    public static final String CONTEXT_PREFIX = "context.";
+    public static final String SSL = "ssl";
+    public static final String SSL_PROVIDER = "ssl.provider";
+    public static final String SSL_PROTOCOL = "ssl.protocol";
+    public static final String SSL_ALGORITHM = "ssl.algorithm";
+    public static final String SSL_KEYSTORE = "ssl.keystore";
+    public static final String SSL_KEYALIAS = "ssl.keyalias";
+    public static final String SSL_TRUSTSTORE = "ssl.truststore";
+    public static final String SSL_TIMEOUT = "ssl.timeout";
+    public static final String DEFAULT_INITIAL_CONTEXT_FACTORY = "com.sun.jndi.ldap.LdapCtxFactory";
+    public static final String DEFAULT_AUTHENTICATION = "simple";
+    public static final int DEFAULT_SSL_TIMEOUT = 10;
+
+    private static Logger LOGGER = LoggerFactory.getLogger(LDAPLoginModule.class);
+
+    private final Map<String, ?> options;
+
+    public LDAPOptions(Map<String, ?> options) {
+        this.options = new HashMap<>(options);
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        LDAPOptions that = (LDAPOptions) o;
+        return options.equals(that.options);
+
+    }
+
+    @Override
+    public int hashCode() {
+        return options.hashCode();
+    }
+
+    public String getUserFilter() {
+        return (String) options.get(USER_FILTER);
+    }
+
+    public String getUserBaseDn() {
+        return (String) options.get(USER_BASE_DN);
+    }
+
+    public boolean getUserSearchSubtree() {
+        return Boolean.parseBoolean((String) options.get(USER_SEARCH_SUBTREE));
+    }
+
+    public String getRoleFilter() {
+        return (String) options.get(ROLE_FILTER);
+    }
+
+    public String getRoleBaseDn() {
+        return (String) options.get(ROLE_BASE_DN);
+    }
+
+    public boolean getRoleSearchSubtree() {
+        return Boolean.parseBoolean((String) options.get(ROLE_SEARCH_SUBTREE));
+    }
+
+    public String getRoleNameAttribute() {
+        return (String) options.get(ROLE_NAME_ATTRIBUTE);
+    }
+
+    public Map<String, Set<String>> getRoleMapping() {
+        return parseRoleMapping((String) options.get(ROLE_MAPPING));
+    }
+
+    private Map<String, Set<String>> parseRoleMapping(String option) {
+        Map<String, Set<String>> roleMapping = new HashMap<String, Set<String>>();
+        if (option != null) {
+            LOGGER.debug("Parse role mapping {}", option);
+            String[] mappings = option.split(";");
+            for (String mapping : mappings) {
+                String[] map = mapping.split("=", 2);
+                String ldapRole = map[0].trim();
+                String[] karafRoles = map[1].split(",");
+                if (roleMapping.get(ldapRole) == null) {
+                    roleMapping.put(ldapRole, new HashSet<String>());
+                }
+                final Set<String> karafRolesSet = roleMapping.get(ldapRole);
+                for (String karafRole : karafRoles) {
+                    karafRolesSet.add(karafRole.trim());
+                }
+            }
+        }
+        return roleMapping;
+    }
+
+    public Hashtable<String, Object> getEnv() throws NamingException {
+        final Hashtable<String, Object> env = new Hashtable<>();
+        for (String key : options.keySet()) {
+            if (key.startsWith(CONTEXT_PREFIX)) {
+                env.put(key.substring(CONTEXT_PREFIX.length()), options.get(key));
+            }
+        }
+        env.put(Context.INITIAL_CONTEXT_FACTORY, getInitialContextFactory());
+        env.put(Context.PROVIDER_URL, getConnectionURL());
+        if (getConnectionUsername() != null && getConnectionUsername().trim().length() > 0) {
+            env.put(Context.SECURITY_AUTHENTICATION, getAuthentication());
+            env.put(Context.SECURITY_PRINCIPAL, getConnectionUsername());
+            env.put(Context.SECURITY_CREDENTIALS, getConnectionPassword());
+        }
+        if (getSsl()) {
+            setupSsl(env);
+        }
+        return env;
+    }
+
+    protected void setupSsl(Hashtable<String, Object> env) throws NamingException {
+        BundleContext bundleContext = FrameworkUtil.getBundle(LDAPOptions.class).getBundleContext();
+        ServiceReference ref = null;
+        try {
+            LOGGER.debug("Setting up SSL");
+            env.put(Context.SECURITY_PROTOCOL, "ssl");
+            env.put("java.naming.ldap.factory.socket", ManagedSSLSocketFactory.class.getName());
+            ref = bundleContext.getServiceReference(KeystoreManager.class.getName());
+            KeystoreManager manager = (KeystoreManager) bundleContext.getService(ref);
+            SSLSocketFactory factory = manager.createSSLFactory(
+                    getSslProvider(), getSslProtocol(), getSslAlgorithm(), getSslKeystore(),
+                    getSslKeyAlias(), getSslTrustStore(), getSslTimeout());
+            ManagedSSLSocketFactory.setSocketFactory(factory);
+            Thread.currentThread().setContextClassLoader(ManagedSSLSocketFactory.class.getClassLoader());
+        } catch (Exception e) {
+            throw new NamingException("Unable to setup SSL support for LDAP: " + e.getMessage());
+        } finally {
+            bundleContext.ungetService(ref);
+        }
+    }
+
+    public Object getInitialContextFactory() {
+        String initialContextFactory = (String) options.get(INITIAL_CONTEXT_FACTORY);
+        if (initialContextFactory == null) {
+            initialContextFactory = DEFAULT_INITIAL_CONTEXT_FACTORY;
+        }
+        return initialContextFactory;
+    }
+
+    public String getConnectionURL() {
+        String connectionURL = (String) options.get(CONNECTION_URL);
+        if (connectionURL == null || connectionURL.trim().length() == 0) {
+            LOGGER.error("No LDAP URL specified.");
+        } else if (!connectionURL.startsWith("ldap:") && !connectionURL.startsWith("ldaps:")) {
+            LOGGER.error("Invalid LDAP URL: " + connectionURL);
+        }
+        return connectionURL;
+    }
+
+    public String getConnectionUsername() {
+        return (String) options.get(CONNECTION_USERNAME);
+    }
+
+    public String getConnectionPassword() {
+        return (String) options.get(CONNECTION_PASSWORD);
+    }
+
+    public String getAuthentication() {
+        String authentication = (String) options.get(AUTHENTICATION);
+        if (authentication == null) {
+            authentication = DEFAULT_AUTHENTICATION;
+        }
+        return authentication;
+    }
+
+    public boolean getSsl() {
+        Object val = options.get(SSL);
+        if (val instanceof Boolean) {
+            return (Boolean) val;
+        } else if (val != null) {
+            return Boolean.parseBoolean(val.toString());
+        } else {
+            return getConnectionURL().startsWith("ldaps:");
+        }
+    }
+
+    public String getSslProvider() {
+        return (String) options.get(SSL_PROVIDER);
+    }
+
+    public String getSslProtocol() {
+        return (String) options.get(SSL_PROTOCOL);
+    }
+
+    public String getSslAlgorithm() {
+        return (String) options.get(SSL_ALGORITHM);
+    }
+
+    public String getSslKeystore() {
+        return (String) options.get(SSL_KEYSTORE);
+    }
+
+    public String getSslKeyAlias() {
+        return (String) options.get(SSL_KEYALIAS);
+    }
+
+    public String getSslTrustStore() {
+        return (String) options.get(SSL_TRUSTSTORE);
+    }
+
+    public int getSslTimeout() {
+        Object val = options.get(SSL_TIMEOUT);
+        if (val instanceof Number) {
+            return ((Number) val).intValue();
+        } else if (val != null) {
+            return Integer.parseInt(val.toString());
+        } else {
+            return DEFAULT_SSL_TIMEOUT;
+        }
+    }
+
+    public boolean getAllowEmptyPasswords() {
+        return Boolean.parseBoolean((String) options.get(ALLOW_EMPTY_PASSWORDS));
+    }
+}

http://git-wip-us.apache.org/repos/asf/karaf/blob/86822b1d/jaas/modules/src/main/java/org/apache/karaf/jaas/modules/ldap/ManagedSSLSocketFactory.java
----------------------------------------------------------------------
diff --git a/jaas/modules/src/main/java/org/apache/karaf/jaas/modules/ldap/ManagedSSLSocketFactory.java b/jaas/modules/src/main/java/org/apache/karaf/jaas/modules/ldap/ManagedSSLSocketFactory.java
new file mode 100644
index 0000000..c7593ce
--- /dev/null
+++ b/jaas/modules/src/main/java/org/apache/karaf/jaas/modules/ldap/ManagedSSLSocketFactory.java
@@ -0,0 +1,35 @@
+/*
+ *  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 javax.net.ssl.SSLSocketFactory;
+
+public abstract class ManagedSSLSocketFactory extends SSLSocketFactory {
+
+    private static final ThreadLocal<SSLSocketFactory> factories = new ThreadLocal<SSLSocketFactory>();
+
+    public static void setSocketFactory(SSLSocketFactory factory) {
+        factories.set(factory);
+    }
+
+    public static SSLSocketFactory getDefault() {
+        SSLSocketFactory factory = factories.get();
+        if (factory == null) {
+            throw new IllegalStateException("No SSLSocketFactory parameters have been set!");
+        }
+        return factory;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/karaf/blob/86822b1d/jaas/modules/src/test/java/org/apache/karaf/jaas/modules/ldap/LdapCacheTest.java
----------------------------------------------------------------------
diff --git a/jaas/modules/src/test/java/org/apache/karaf/jaas/modules/ldap/LdapCacheTest.java b/jaas/modules/src/test/java/org/apache/karaf/jaas/modules/ldap/LdapCacheTest.java
new file mode 100644
index 0000000..c4184d5
--- /dev/null
+++ b/jaas/modules/src/test/java/org/apache/karaf/jaas/modules/ldap/LdapCacheTest.java
@@ -0,0 +1,175 @@
+/*
+ *
+ *  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 javax.naming.directory.Attribute;
+import javax.naming.directory.Attributes;
+import javax.naming.directory.BasicAttribute;
+import javax.naming.directory.BasicAttributes;
+import javax.naming.directory.DirContext;
+import javax.security.auth.Subject;
+import javax.security.auth.callback.Callback;
+import javax.security.auth.callback.CallbackHandler;
+import javax.security.auth.callback.NameCallback;
+import javax.security.auth.callback.PasswordCallback;
+import javax.security.auth.callback.UnsupportedCallbackException;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.security.Principal;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.directory.server.annotations.CreateLdapServer;
+import org.apache.directory.server.annotations.CreateTransport;
+import org.apache.directory.server.core.annotations.ApplyLdifFiles;
+import org.apache.directory.server.core.annotations.CreateDS;
+import org.apache.directory.server.core.annotations.CreatePartition;
+import org.apache.directory.server.core.integ.AbstractLdapTestUnit;
+import org.apache.directory.server.core.integ.FrameworkRunner;
+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.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+@RunWith(FrameworkRunner.class)
+@CreateLdapServer(transports = {@CreateTransport(protocol = "LDAP")})
+@CreateDS(name = "LdapLoginModuleTest-class",
+        partitions = {@CreatePartition(name = "example", suffix = "dc=example,dc=com")})
+@ApplyLdifFiles(
+        "org/apache/karaf/jaas/modules/ldap/example.com.ldif"
+)
+public class LdapCacheTest extends AbstractLdapTestUnit {
+
+    private static boolean portUpdated;
+
+    @Before
+    public void updatePort() throws Exception {
+        if (!portUpdated) {
+            String basedir = System.getProperty("basedir");
+            if (basedir == null) {
+                basedir = new File(".").getCanonicalPath();
+            }
+
+            // Read in ldap.properties and substitute in the correct port
+            File f = new File(basedir + "/src/test/resources/org/apache/karaf/jaas/modules/ldap/ldap.properties");
+
+            FileInputStream inputStream = new FileInputStream(f);
+            String content = IOUtils.toString(inputStream, "UTF-8");
+            inputStream.close();
+            content = content.replaceAll("portno", "" + super.getLdapServer().getPort());
+
+            File f2 = new File(basedir + "/target/test-classes/org/apache/karaf/jaas/modules/ldap/ldap.properties");
+            FileOutputStream outputStream = new FileOutputStream(f2);
+            IOUtils.write(content, outputStream, "UTF-8");
+            outputStream.close();
+            portUpdated = true;
+        }
+    }
+
+    @After
+    public void tearDown() {
+        LDAPCache.clear();
+    }
+
+    @Test
+    public void testAdminLogin() throws Exception {
+        Properties options = ldapLoginModuleOptions();
+        LDAPLoginModule module = new LDAPLoginModule();
+        CallbackHandler cb = new CallbackHandler() {
+            public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
+                for (Callback cb : callbacks) {
+                    if (cb instanceof NameCallback) {
+                        ((NameCallback) cb).setName("admin");
+                    } else if (cb instanceof PasswordCallback) {
+                        ((PasswordCallback) cb).setPassword("admin123".toCharArray());
+                    }
+                }
+            }
+        };
+        Subject subject = new Subject();
+        module.initialize(subject, cb, null, options);
+
+        assertEquals("Precondition", 0, subject.getPrincipals().size());
+        assertTrue(module.login());
+        assertTrue(module.commit());
+
+        assertEquals(2, subject.getPrincipals().size());
+
+        boolean foundUser = false;
+        boolean foundRole = false;
+        for (Principal pr : subject.getPrincipals()) {
+            if (pr instanceof UserPrincipal) {
+                assertEquals("admin", pr.getName());
+                foundUser = true;
+            } else if (pr instanceof RolePrincipal) {
+                assertEquals("admin", pr.getName());
+                foundRole = true;
+            }
+        }
+        assertTrue(foundUser);
+        assertTrue(foundRole);
+
+        assertTrue(module.logout());
+        assertEquals("Principals should be gone as the user has logged out", 0, subject.getPrincipals().size());
+
+        DirContext context = new LDAPCache(new LDAPOptions(options)).open();
+
+        // Make "admin" user a member of a new "another" group
+
+//        dn: cn=admin,ou=groups,dc=example,dc=com
+//        objectClass: top
+//        objectClass: groupOfNames
+//        cn: admin
+//        member: cn=admin,ou=people,dc=example,dc=com
+        Attributes entry = new BasicAttributes();
+        entry.put(new BasicAttribute("cn", "another"));
+        Attribute oc = new BasicAttribute("objectClass");
+        oc.add("top");
+        oc.add("groupOfNames");
+        entry.put(oc);
+        Attribute mb = new BasicAttribute("member");
+        mb.add("cn=admin,ou=people,dc=example,dc=com");
+        entry.put(mb);
+        context.createSubcontext("cn=another,ou=groups,dc=example,dc=com", entry);
+
+        Thread.sleep(100);
+
+        module = new LDAPLoginModule();
+        subject = new Subject();
+        module.initialize(subject, cb, null, options);
+        assertEquals("Precondition", 0, subject.getPrincipals().size());
+        assertTrue(module.login());
+        assertTrue(module.commit());
+        assertEquals("Postcondition", 3, subject.getPrincipals().size());
+    }
+
+    protected Properties ldapLoginModuleOptions() throws IOException {
+        String basedir = System.getProperty("basedir");
+        if (basedir == null) {
+            basedir = new File(".").getCanonicalPath();
+        }
+        File file = new File(basedir + "/target/test-classes/org/apache/karaf/jaas/modules/ldap/ldap.properties");
+        return new Properties(file);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/karaf/blob/86822b1d/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 3207a95..307aae5 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
@@ -257,7 +257,7 @@ public class LdapLoginModuleTest extends AbstractLdapTestUnit {
     @Test
     public void testRoleMappingSimple() throws Exception {
         Properties options = ldapLoginModuleOptions();
-        options.put(LDAPLoginModule.ROLE_MAPPING, "admin=karaf");
+        options.put(LDAPOptions.ROLE_MAPPING, "admin=karaf");
         LDAPLoginModule module = new LDAPLoginModule();
         CallbackHandler cb = new CallbackHandler() {
             @Override
@@ -301,7 +301,7 @@ public class LdapLoginModuleTest extends AbstractLdapTestUnit {
     @Test
     public void testRoleMappingAdvanced() throws Exception {
         Properties options = ldapLoginModuleOptions();
-        options.put(LDAPLoginModule.ROLE_MAPPING, "admin=karaf,test;admin=another");
+        options.put(LDAPOptions.ROLE_MAPPING, "admin=karaf,test;admin=another");
         LDAPLoginModule module = new LDAPLoginModule();
         CallbackHandler cb = new CallbackHandler() {
             public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
@@ -347,7 +347,7 @@ public class LdapLoginModuleTest extends AbstractLdapTestUnit {
     @Test
     public void testRoleMappingParsing() throws Exception {
         Properties options = ldapLoginModuleOptions();
-        options.put(LDAPLoginModule.ROLE_MAPPING, "admin = karaf, test; admin = another");
+        options.put(LDAPOptions.ROLE_MAPPING, "admin = karaf, test; admin = another");
         LDAPLoginModule module = new LDAPLoginModule();
         CallbackHandler cb = new CallbackHandler() {
             public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {


Mime
View raw message