/* * 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.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.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 implements Closeable, NamespaceChangeListener, ObjectChangeListener { private static final ConcurrentMap<LDAPOptions, LDAPCache> CACHES = new ConcurrentHashMap<>(); private static Logger LOGGER = LoggerFactory.getLogger(LDAPLoginModule.class); public static void clear() { while (!CACHES.isEmpty()) { LDAPOptions options = CACHES.keySet().iterator().next(); LDAPCache cache = CACHES.remove(options); if (cache != null) { cache.clearCache(); } } } public static LDAPCache getCache(LDAPOptions options) { LDAPCache cache = CACHES.get(options); if (cache == null) { CACHES.putIfAbsent(options, new LDAPCache(options)); cache = CACHES.get(options); } return cache; } private final Map<String, String[]> userDnAndNamespace; private final Map<String, String[]> userRoles; private final LDAPOptions options; private DirContext context; 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; } } } 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); if (!options.getDisableCache()) { 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(); if (filter != null) { 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 = doGetUserDnAndNamespace(user); if (result != null && !options.getDisableCache()) { userDnAndNamespace.put(user, result); } } return 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); if (!options.getDisableCache()) { 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(); if (filter != null) { 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 } } } } else { LOGGER.debug("The user role filter is null so no roles are retrieved"); return new String[] {}; } } @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(); } }