/** * 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.activemq.security; import java.util.ArrayList; import java.util.HashSet; import java.util.Hashtable; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadFactory; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.locks.ReentrantReadWriteLock; import javax.naming.Binding; import javax.naming.Context; import javax.naming.InvalidNameException; 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 javax.naming.ldap.LdapName; import javax.naming.ldap.Rdn; import org.apache.activemq.command.ActiveMQDestination; import org.apache.activemq.command.ActiveMQQueue; import org.apache.activemq.command.ActiveMQTopic; import org.apache.activemq.filter.DestinationMapEntry; import org.apache.activemq.jaas.UserPrincipal; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class SimpleCachedLDAPAuthorizationMap implements AuthorizationMap { private static final Logger LOG = LoggerFactory.getLogger(SimpleCachedLDAPAuthorizationMap.class); // Configuration Options private final String initialContextFactory = "com.sun.jndi.ldap.LdapCtxFactory"; private String connectionURL = "ldap://localhost:1024"; private String connectionUsername = "uid=admin,ou=system"; private String connectionPassword = "secret"; private String connectionProtocol = "s"; private String authentication = "simple"; private int queuePrefixLength = 4; private int topicPrefixLength = 4; private int tempPrefixLength = 4; private String queueSearchBase = "ou=Queue,ou=Destination,ou=ActiveMQ,ou=system"; private String topicSearchBase = "ou=Topic,ou=Destination,ou=ActiveMQ,ou=system"; private String tempSearchBase = "ou=Temp,ou=Destination,ou=ActiveMQ,ou=system"; private String permissionGroupMemberAttribute = "member"; private String adminPermissionGroupSearchFilter = "(cn=Admin)"; private String readPermissionGroupSearchFilter = "(cn=Read)"; private String writePermissionGroupSearchFilter = "(cn=Write)"; private boolean legacyGroupMapping = true; private String groupObjectClass = "groupOfNames"; private String userObjectClass = "person"; private String groupNameAttribute = "cn"; private String userNameAttribute = "uid"; private int refreshInterval = -1; private boolean refreshDisabled = false; protected String groupClass = DefaultAuthorizationMap.DEFAULT_GROUP_CLASS; // Internal State private long lastUpdated = -1; private static String ANY_DESCENDANT = "\\$"; protected DirContext context; private EventDirContext eventContext; private final AtomicReference<DefaultAuthorizationMap> map = new AtomicReference<DefaultAuthorizationMap>(new DefaultAuthorizationMap()); private final ThreadPoolExecutor updaterService; protected Map<ActiveMQDestination, AuthorizationEntry> entries = new ConcurrentHashMap<ActiveMQDestination, AuthorizationEntry>(); public SimpleCachedLDAPAuthorizationMap() { // Allow for only a couple outstanding update request, they can be slow so we // don't want a bunch to pile up for no reason. updaterService = new ThreadPoolExecutor(0, 1, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(2), new ThreadFactory() { @Override public Thread newThread(Runnable r) { return new Thread(r, "SimpleCachedLDAPAuthorizationMap update thread"); } }); updaterService.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardPolicy()); } protected DirContext createContext() throws NamingException { Hashtable<String, String> env = new Hashtable<String, String>(); env.put(Context.INITIAL_CONTEXT_FACTORY, initialContextFactory); if (connectionUsername != null && !"".equals(connectionUsername)) { env.put(Context.SECURITY_PRINCIPAL, connectionUsername); } else { throw new NamingException("Empty username is not allowed"); } if (connectionPassword != null && !"".equals(connectionPassword)) { env.put(Context.SECURITY_CREDENTIALS, connectionPassword); } else { throw new NamingException("Empty password is not allowed"); } env.put(Context.SECURITY_PROTOCOL, connectionProtocol); env.put(Context.PROVIDER_URL, connectionURL); env.put(Context.SECURITY_AUTHENTICATION, authentication); return new InitialDirContext(env); } protected boolean isContextAlive() { boolean alive = false; if (context != null) { try { context.getAttributes(""); alive = true; } catch (Exception e) { } } return alive; } /** * Returns the existing open context or creates a new one and registers listeners for push notifications if such an * update style is enabled. This implementation should not be invoked concurrently. * * @return the current context * * @throws NamingException * if there is an error setting things up */ protected DirContext open() throws NamingException { if (isContextAlive()) { return context; } try { context = createContext(); if (refreshInterval == -1 && !refreshDisabled) { eventContext = ((EventDirContext) context.lookup("")); final SearchControls constraints = new SearchControls(); constraints.setSearchScope(SearchControls.SUBTREE_SCOPE); // Listeners for Queue policy // // Listeners for each type of permission for (PermissionType permissionType : PermissionType.values()) { eventContext.addNamingListener(queueSearchBase, getFilterForPermissionType(permissionType), constraints, this.new CachedLDAPAuthorizationMapNamespaceChangeListener(DestinationType.QUEUE, permissionType)); } // Listener for changes to the destination pattern entry itself and not a permission entry. eventContext.addNamingListener(queueSearchBase, "cn=*", new SearchControls(), this.new CachedLDAPAuthorizationMapNamespaceChangeListener( DestinationType.QUEUE, null)); // Listeners for Topic policy // // Listeners for each type of permission for (PermissionType permissionType : PermissionType.values()) { eventContext.addNamingListener(topicSearchBase, getFilterForPermissionType(permissionType), constraints, this.new CachedLDAPAuthorizationMapNamespaceChangeListener(DestinationType.TOPIC, permissionType)); } // Listener for changes to the destination pattern entry itself and not a permission entry. eventContext.addNamingListener(topicSearchBase, "cn=*", new SearchControls(), this.new CachedLDAPAuthorizationMapNamespaceChangeListener( DestinationType.TOPIC, null)); // Listeners for Temp policy // // Listeners for each type of permission for (PermissionType permissionType : PermissionType.values()) { eventContext.addNamingListener(tempSearchBase, getFilterForPermissionType(permissionType), constraints, this.new CachedLDAPAuthorizationMapNamespaceChangeListener(DestinationType.TEMP, permissionType)); } } } catch (NamingException e) { context = null; throw e; } return context; } /** * Queries the directory and initializes the policy based on the data in the directory. This implementation should * not be invoked concurrently. * * @throws Exception * if there is an unrecoverable error processing the directory contents */ @SuppressWarnings("rawtypes") protected synchronized void query() throws Exception { DirContext currentContext = open(); entries.clear(); final SearchControls constraints = new SearchControls(); constraints.setSearchScope(SearchControls.SUBTREE_SCOPE); DefaultAuthorizationMap newMap = new DefaultAuthorizationMap(); for (PermissionType permissionType : PermissionType.values()) { try { processQueryResults(newMap, currentContext.search(queueSearchBase, getFilterForPermissionType(permissionType), constraints), DestinationType.QUEUE, permissionType); } catch (Exception e) { LOG.error("Policy not applied!. Error processing policy under '{}' with filter '{}'", new Object[]{ queueSearchBase, getFilterForPermissionType(permissionType) }, e); } } for (PermissionType permissionType : PermissionType.values()) { try { processQueryResults(newMap, currentContext.search(topicSearchBase, getFilterForPermissionType(permissionType), constraints), DestinationType.TOPIC, permissionType); } catch (Exception e) { LOG.error("Policy not applied!. Error processing policy under '{}' with filter '{}'", new Object[]{ topicSearchBase, getFilterForPermissionType(permissionType) }, e); } } for (PermissionType permissionType : PermissionType.values()) { try { processQueryResults(newMap, currentContext.search(tempSearchBase, getFilterForPermissionType(permissionType), constraints), DestinationType.TEMP, permissionType); } catch (Exception e) { LOG.error("Policy not applied!. Error processing policy under '{}' with filter '{}'", new Object[]{ tempSearchBase, getFilterForPermissionType(permissionType) }, e); } } // Create and swap in the new instance with updated LDAP data. newMap.setAuthorizationEntries(new ArrayList<DestinationMapEntry>(entries.values())); newMap.setGroupClass(groupClass); this.map.set(newMap); updated(); } /** * Processes results from a directory query in the context of a given destination type and permission type. This * implementation should not be invoked concurrently. * * @param results * the results to process * @param destinationType * the type of the destination for which the directory results apply * @param permissionType * the type of the permission for which the directory results apply * * @throws Exception * if there is an error processing the results */ protected void processQueryResults(DefaultAuthorizationMap map, NamingEnumeration<SearchResult> results, DestinationType destinationType, PermissionType permissionType) throws Exception { while (results.hasMore()) { SearchResult result = results.next(); AuthorizationEntry entry = null; try { entry = getEntry(map, new LdapName(result.getNameInNamespace()), destinationType); } catch (Exception e) { LOG.error("Policy not applied! Error parsing authorization policy entry under {}", result.getNameInNamespace(), e); continue; } applyACL(entry, result, permissionType); } } /** * Marks the time at which the authorization state was last refreshed. Relevant for synchronous * policy updates. This implementation should not be invoked concurrently. */ protected void updated() { lastUpdated = System.currentTimeMillis(); } /** * Retrieves or creates the {@link AuthorizationEntry} that corresponds to the DN in {@code dn}. This implementation * should not be invoked concurrently. * * @param map * the DefaultAuthorizationMap to operate on. * @param dn * the DN representing the policy entry in the directory * @param destinationType * the type of the destination to get/create the entry for * * @return the corresponding authorization entry for the DN * * @throws IllegalArgumentException * if destination type is not one of {@link DestinationType#QUEUE}, {@link DestinationType#TOPIC}, * {@link DestinationType#TEMP} or if the policy entry DN is malformed */ protected AuthorizationEntry getEntry(DefaultAuthorizationMap map, LdapName dn, DestinationType destinationType) { AuthorizationEntry entry = null; switch (destinationType) { case TEMP: // handle temp entry if (dn.size() != getPrefixLengthForDestinationType(destinationType) + 1) { // handle unknown entry throw new IllegalArgumentException("Malformed policy structure for a temporary destination " + "policy entry. The permission group entries should be immediately below the " + "temporary policy base DN."); } entry = map.getTempDestinationAuthorizationEntry(); if (entry == null) { entry = new TempDestinationAuthorizationEntry(); map.setTempDestinationAuthorizationEntry((TempDestinationAuthorizationEntry) entry); } break; case QUEUE: case TOPIC: // handle regular destinations if (dn.size() != getPrefixLengthForDestinationType(destinationType) + 2) { throw new IllegalArgumentException("Malformed policy structure for a queue or topic destination " + "policy entry. The destination pattern and permission group entries should be " + "nested below the queue or topic policy base DN."); } ActiveMQDestination dest = formatDestination(dn, destinationType); if (dest != null) { entry = entries.get(dest); if (entry == null) { entry = new AuthorizationEntry(); entry.setDestination(dest); entries.put(dest, entry); } } break; default: // handle unknown entry throw new IllegalArgumentException("Unknown destination type " + destinationType); } return entry; } /** * Applies the policy from the directory to the given entry within the context of the provided permission type. * * @param entry * the policy entry to apply the policy to * @param result * the results from the directory to apply to the policy entry * @param permissionType * the permission type of the data in the directory * * @throws NamingException * if there is an error applying the ACL */ protected void applyACL(AuthorizationEntry entry, SearchResult result, PermissionType permissionType) throws NamingException { // Find members Attribute memberAttribute = result.getAttributes().get(permissionGroupMemberAttribute); NamingEnumeration<?> memberAttributeEnum = memberAttribute.getAll(); HashSet<Object> members = new HashSet<Object>(); while (memberAttributeEnum.hasMoreElements()) { String memberDn = (String) memberAttributeEnum.nextElement(); boolean group = false; boolean user = false; String principalName = null; if (!legacyGroupMapping) { // Lookup of member to determine principal type (group or user) and name. Attributes memberAttributes; try { memberAttributes = context.getAttributes(memberDn, new String[] { "objectClass", groupNameAttribute, userNameAttribute }); } catch (NamingException e) { LOG.error("Policy not applied! Unknown member {} in policy entry {}", new Object[]{ memberDn, result.getNameInNamespace() }, e); continue; } Attribute memberEntryObjectClassAttribute = memberAttributes.get("objectClass"); NamingEnumeration<?> memberEntryObjectClassAttributeEnum = memberEntryObjectClassAttribute.getAll(); while (memberEntryObjectClassAttributeEnum.hasMoreElements()) { String objectClass = (String) memberEntryObjectClassAttributeEnum.nextElement(); if (objectClass.equalsIgnoreCase(groupObjectClass)) { group = true; Attribute name = memberAttributes.get(groupNameAttribute); if (name == null) { LOG.error("Policy not applied! Group {} does not have name attribute {} under entry {}", new Object[]{ memberDn, groupNameAttribute, result.getNameInNamespace() }); break; } principalName = (String) name.get(); } if (objectClass.equalsIgnoreCase(userObjectClass)) { user = true; Attribute name = memberAttributes.get(userNameAttribute); if (name == null) { LOG.error("Policy not applied! User {} does not have name attribute {} under entry {}", new Object[]{ memberDn, userNameAttribute, result.getNameInNamespace() }); break; } principalName = (String) name.get(); } } } else { group = true; principalName = memberDn.replaceAll("(cn|CN)=", ""); } if ((!group && !user) || (group && user)) { LOG.error("Policy not applied! Can't determine type of member {} under entry {}", memberDn, result.getNameInNamespace()); } else if (principalName != null) { DefaultAuthorizationMap map = this.map.get(); if (group && !user) { try { members.add(DefaultAuthorizationMap.createGroupPrincipal(principalName, map.getGroupClass())); } catch (Exception e) { NamingException ne = new NamingException( "Can't create a group " + principalName + " of class " + map.getGroupClass()); ne.initCause(e); throw ne; } } else if (!group && user) { members.add(new UserPrincipal(principalName)); } } } try { applyAcl(entry, permissionType, members); } catch (Exception e) { LOG.error("Policy not applied! Error adding principals to ACL under {}", result.getNameInNamespace(), e); } } /** * Applies policy to the entry given the actual principals that will be applied to the policy entry. * * @param entry * the policy entry to which the policy should be applied * @param permissionType * the type of the permission that the policy will be applied to * @param acls * the principals that represent the actual policy * * @throw IllegalArgumentException if {@code permissionType} is unsupported */ protected void applyAcl(AuthorizationEntry entry, PermissionType permissionType, Set<Object> acls) { switch (permissionType) { case READ: entry.setReadACLs(acls); break; case WRITE: entry.setWriteACLs(acls); break; case ADMIN: entry.setAdminACLs(acls); break; default: throw new IllegalArgumentException("Unknown permission " + permissionType + "."); } } /** * Parses a DN into the equivalent {@link ActiveMQDestination}. The default implementation expects a format of * cn=<PERMISSION_NAME>,ou=<DESTINATION_PATTERN>,.... or ou=<DESTINATION_PATTERN>,.... for permission and * destination entries, respectively. For example {@code cn=admin,ou=$,ou=...} or {@code ou=$,ou=...}. * * @param dn * the DN to parse * @param destinationType * the type of the destination that we are parsing * * @return the destination that the DN represents * * @throws IllegalArgumentException * if {@code destinationType} is {@link DestinationType#TEMP} or if the format of {@code dn} is * incorrect for for a topic or queue * * @see #formatDestination(Rdn, DestinationType) */ protected ActiveMQDestination formatDestination(LdapName dn, DestinationType destinationType) { ActiveMQDestination destination = null; switch (destinationType) { case QUEUE: case TOPIC: // There exists a need to deal with both names representing a permission or simply a // destination. As such, we need to determine the proper RDN to work with based // on the destination type and the DN size. if (dn.size() == (getPrefixLengthForDestinationType(destinationType) + 2)) { destination = formatDestination(dn.getRdn(dn.size() - 2), destinationType); } else if (dn.size() == (getPrefixLengthForDestinationType(destinationType) + 1)) { destination = formatDestination(dn.getRdn(dn.size() - 1), destinationType); } else { throw new IllegalArgumentException("Malformed DN for representing a permission or destination entry."); } break; default: throw new IllegalArgumentException("Cannot format destination for destination type " + destinationType); } return destination; } /** * Parses RDN values representing the destination name/pattern and destination type into the equivalent * {@link ActiveMQDestination}. * * @param destinationName * the RDN representing the name or pattern for the destination * @param destinationType * the type of the destination * * @return the destination that the RDN represent * * @throws IllegalArgumentException * if {@code destinationType} is not one of {@link DestinationType#TOPIC} or * {@link DestinationType#QUEUE}. * * @see #formatDestinationName(Rdn) * @see #formatDestination(LdapName, DestinationType) */ protected ActiveMQDestination formatDestination(Rdn destinationName, DestinationType destinationType) { ActiveMQDestination dest = null; switch (destinationType) { case QUEUE: dest = new ActiveMQQueue(formatDestinationName(destinationName)); break; case TOPIC: dest = new ActiveMQTopic(formatDestinationName(destinationName)); break; default: throw new IllegalArgumentException("Unknown destination type: " + destinationType); } return dest; } /** * Parses the RDN representing a destination name/pattern into the standard string representation of the * name/pattern. This implementation does not care about the type of the RDN such that the RDN could be a CN or OU. * * @param destinationName * the RDN representing the name or pattern for the destination * * @see #formatDestination(Rdn, Rdn) */ protected String formatDestinationName(Rdn destinationName) { return destinationName.getValue().toString().replaceAll(ANY_DESCENDANT, ">"); } /** * Transcribes an existing set into a new set. Used to make defensive copies for concurrent access. * * @param source * the source set or {@code null} * * @return a new set containing the same elements as {@code source} or {@code null} if {@code source} is * {@code null} */ protected <T> Set<T> transcribeSet(Set<T> source) { if (source != null) { return new HashSet<T>(source); } else { return null; } } /** * Returns the filter string for the given permission type. * * @throws IllegalArgumentException * if {@code permissionType} is not supported * * @see #setAdminPermissionGroupSearchFilter(String) * @see #setReadPermissionGroupSearchFilter(String) * @see #setWritePermissionGroupSearchFilter(String) */ protected String getFilterForPermissionType(PermissionType permissionType) { String filter = null; switch (permissionType) { case ADMIN: filter = adminPermissionGroupSearchFilter; break; case READ: filter = readPermissionGroupSearchFilter; break; case WRITE: filter = writePermissionGroupSearchFilter; break; default: throw new IllegalArgumentException("Unknown permission type " + permissionType); } return filter; } /** * Returns the DN prefix size based on the given destination type. * * @throws IllegalArgumentException * if {@code destinationType} is not supported * * @see #setQueueSearchBase(String) * @see #setTopicSearchBase(String) * @see #setTempSearchBase(String) */ protected int getPrefixLengthForDestinationType(DestinationType destinationType) { int filter = 0; switch (destinationType) { case QUEUE: filter = queuePrefixLength; break; case TOPIC: filter = topicPrefixLength; break; case TEMP: filter = tempPrefixLength; break; default: throw new IllegalArgumentException("Unknown permission type " + destinationType); } return filter; } /** * Performs a check for updates from the server in the event that synchronous updates are enabled and are the * refresh interval has elapsed. */ protected void checkForUpdates() { if (lastUpdated == -1) { //ACL's have never been queried, but we need them NOW as we're being asked for them. try { query(); return; } catch (Exception e) { LOG.error("Error updating authorization map. Partial policy may be applied until the next successful update.", e); } } if (context != null && refreshDisabled) { return; } if (context == null || (!refreshDisabled && (refreshInterval != -1 && System.currentTimeMillis() >= lastUpdated + refreshInterval))) { this.updaterService.execute(new Runnable() { @Override public void run() { // Check again in case of stacked update request. if (context == null || (!refreshDisabled && (refreshInterval != -1 && System.currentTimeMillis() >= lastUpdated + refreshInterval))) { if (!isContextAlive()) { try { context = createContext(); } catch (NamingException ne) { // LDAP is down, use already cached values return; } } LOG.debug("Updating authorization map!"); try { query(); } catch (Exception e) { LOG.error("Error updating authorization map. Partial policy may be applied until the next successful update.", e); } } } }); } } // Authorization Map /** * Provides synchronized and defensive access to the admin ACLs for temp destinations as the super implementation * returns live copies of the ACLs and {@link AuthorizationEntry} is not setup for concurrent access. */ @Override public Set<Object> getTempDestinationAdminACLs() { checkForUpdates(); DefaultAuthorizationMap map = this.map.get(); return transcribeSet(map.getTempDestinationAdminACLs()); } /** * Provides synchronized and defensive access to the read ACLs for temp destinations as the super implementation * returns live copies of the ACLs and {@link AuthorizationEntry} is not setup for concurrent access. */ @Override public Set<Object> getTempDestinationReadACLs() { checkForUpdates(); DefaultAuthorizationMap map = this.map.get(); return transcribeSet(map.getTempDestinationReadACLs()); } /** * Provides synchronized and defensive access to the write ACLs for temp destinations as the super implementation * returns live copies of the ACLs and {@link AuthorizationEntry} is not setup for concurrent access. */ @Override public Set<Object> getTempDestinationWriteACLs() { checkForUpdates(); DefaultAuthorizationMap map = this.map.get(); return transcribeSet(map.getTempDestinationWriteACLs()); } /** * Provides synchronized access to the admin ACLs for the destinations as {@link AuthorizationEntry} * is not setup for concurrent access. */ @Override public Set<Object> getAdminACLs(ActiveMQDestination destination) { checkForUpdates(); DefaultAuthorizationMap map = this.map.get(); return map.getAdminACLs(destination); } /** * Provides synchronized access to the read ACLs for the destinations as {@link AuthorizationEntry} is not setup for * concurrent access. */ @Override public Set<Object> getReadACLs(ActiveMQDestination destination) { checkForUpdates(); DefaultAuthorizationMap map = this.map.get(); return map.getReadACLs(destination); } /** * Provides synchronized access to the write ACLs for the destinations as {@link AuthorizationEntry} is not setup * for concurrent access. */ @Override public Set<Object> getWriteACLs(ActiveMQDestination destination) { checkForUpdates(); DefaultAuthorizationMap map = this.map.get(); return map.getWriteACLs(destination); } /** * Handler for new policy entries in the directory. * * @param namingEvent * the new entry event that occurred * @param destinationType * the type of the destination to which the event applies * @param permissionType * the permission type to which the event applies */ public void objectAdded(NamingEvent namingEvent, DestinationType destinationType, PermissionType permissionType) { LOG.debug("Adding object: {}", namingEvent.getNewBinding()); SearchResult result = (SearchResult) namingEvent.getNewBinding(); try { DefaultAuthorizationMap map = this.map.get(); LdapName name = new LdapName(result.getName()); AuthorizationEntry entry = getEntry(map, name, destinationType); applyACL(entry, result, permissionType); if (!(entry instanceof TempDestinationAuthorizationEntry)) { map.put(entry.getDestination(), entry); } } catch (InvalidNameException e) { LOG.error("Policy not applied! Error parsing DN for addition of {}", result.getName(), e); } catch (Exception e) { LOG.error("Policy not applied! Error processing object addition for addition of {}", result.getName(), e); } } /** * Handler for removed policy entries in the directory. * * @param namingEvent * the removed entry event that occurred * @param destinationType * the type of the destination to which the event applies * @param permissionType * the permission type to which the event applies */ public void objectRemoved(NamingEvent namingEvent, DestinationType destinationType, PermissionType permissionType) { LOG.debug("Removing object: {}", namingEvent.getOldBinding()); Binding result = namingEvent.getOldBinding(); try { DefaultAuthorizationMap map = this.map.get(); LdapName name = new LdapName(result.getName()); AuthorizationEntry entry = getEntry(map, name, destinationType); applyAcl(entry, permissionType, new HashSet<Object>()); } catch (InvalidNameException e) { LOG.error("Policy not applied! Error parsing DN for object removal for removal of {}", result.getName(), e); } catch (Exception e) { LOG.error("Policy not applied! Error processing object removal for removal of {}", result.getName(), e); } } /** * Handler for renamed policy entries in the directory. This handler deals with the renaming of destination entries * as well as permission entries. If the permission type is not null, it is assumed that we are dealing with the * renaming of a permission entry. Otherwise, it is assumed that we are dealing with the renaming of a destination * entry. * * @param namingEvent * the renaming entry event that occurred * @param destinationType * the type of the destination to which the event applies * @param permissionType * the permission type to which the event applies */ public void objectRenamed(NamingEvent namingEvent, DestinationType destinationType, PermissionType permissionType) { Binding oldBinding = namingEvent.getOldBinding(); Binding newBinding = namingEvent.getNewBinding(); LOG.debug("Renaming object: {} to {}", oldBinding, newBinding); try { LdapName oldName = new LdapName(oldBinding.getName()); ActiveMQDestination oldDest = formatDestination(oldName, destinationType); LdapName newName = new LdapName(newBinding.getName()); ActiveMQDestination newDest = formatDestination(newName, destinationType); if (permissionType != null) { // Handle the case where a permission entry is being renamed. objectRemoved(namingEvent, destinationType, permissionType); SearchControls controls = new SearchControls(); controls.setSearchScope(SearchControls.OBJECT_SCOPE); boolean matchedToType = false; for (PermissionType newPermissionType : PermissionType.values()) { NamingEnumeration<SearchResult> results = context.search(newName, getFilterForPermissionType(newPermissionType), controls); if (results.hasMore()) { objectAdded(namingEvent, destinationType, newPermissionType); matchedToType = true; break; } } if (!matchedToType) { LOG.error("Policy not applied! Error processing object rename for rename of {} to {}. Could not determine permission type of new object.", oldBinding.getName(), newBinding.getName()); } } else { // Handle the case where a destination entry is being renamed. if (oldDest != null && newDest != null) { AuthorizationEntry entry = entries.remove(oldDest); if (entry != null) { entry.setDestination(newDest); DefaultAuthorizationMap map = this.map.get(); map.put(newDest, entry); map.remove(oldDest, entry); entries.put(newDest, entry); } else { LOG.warn("No authorization entry for {}", oldDest); } } } } catch (InvalidNameException e) { LOG.error("Policy not applied! Error parsing DN for object rename for rename of {} to {}", new Object[]{ oldBinding.getName(), newBinding.getName() }, e); } catch (Exception e) { LOG.error("Policy not applied! Error processing object rename for rename of {} to {}", new Object[]{ oldBinding.getName(), newBinding.getName() }, e); } } /** * Handler for changed policy entries in the directory. * * @param namingEvent * the changed entry event that occurred * @param destinationType * the type of the destination to which the event applies * @param permissionType * the permission type to which the event applies */ public void objectChanged(NamingEvent namingEvent, DestinationType destinationType, PermissionType permissionType) { LOG.debug("Changing object {} to {}", namingEvent.getOldBinding(), namingEvent.getNewBinding()); objectRemoved(namingEvent, destinationType, permissionType); objectAdded(namingEvent, destinationType, permissionType); } /** * Handler for exception events from the registry. * * @param namingExceptionEvent * the exception event */ public void namingExceptionThrown(NamingExceptionEvent namingExceptionEvent) { context = null; LOG.error("Caught unexpected exception.", namingExceptionEvent.getException()); } // Init / Destroy public void afterPropertiesSet() throws Exception { query(); } public void destroy() throws Exception { if (eventContext != null) { eventContext.close(); eventContext = null; } if (context != null) { context.close(); context = null; } } // Getters and Setters public String getConnectionURL() { return connectionURL; } public void setConnectionURL(String connectionURL) { this.connectionURL = connectionURL; } public String getConnectionUsername() { return connectionUsername; } public void setConnectionUsername(String connectionUsername) { this.connectionUsername = connectionUsername; } public String getConnectionPassword() { return connectionPassword; } public void setConnectionPassword(String connectionPassword) { this.connectionPassword = connectionPassword; } public String getConnectionProtocol() { return connectionProtocol; } public void setConnectionProtocol(String connectionProtocol) { this.connectionProtocol = connectionProtocol; } public String getAuthentication() { return authentication; } public void setAuthentication(String authentication) { this.authentication = authentication; } public String getQueueSearchBase() { return queueSearchBase; } public void setQueueSearchBase(String queueSearchBase) { try { LdapName baseName = new LdapName(queueSearchBase); queuePrefixLength = baseName.size(); this.queueSearchBase = queueSearchBase; } catch (InvalidNameException e) { throw new IllegalArgumentException("Invalid base DN value " + queueSearchBase, e); } } public String getTopicSearchBase() { return topicSearchBase; } public void setTopicSearchBase(String topicSearchBase) { try { LdapName baseName = new LdapName(topicSearchBase); topicPrefixLength = baseName.size(); this.topicSearchBase = topicSearchBase; } catch (InvalidNameException e) { throw new IllegalArgumentException("Invalid base DN value " + topicSearchBase, e); } } public String getTempSearchBase() { return tempSearchBase; } public void setTempSearchBase(String tempSearchBase) { try { LdapName baseName = new LdapName(tempSearchBase); tempPrefixLength = baseName.size(); this.tempSearchBase = tempSearchBase; } catch (InvalidNameException e) { throw new IllegalArgumentException("Invalid base DN value " + tempSearchBase, e); } } public String getPermissionGroupMemberAttribute() { return permissionGroupMemberAttribute; } public void setPermissionGroupMemberAttribute(String permissionGroupMemberAttribute) { this.permissionGroupMemberAttribute = permissionGroupMemberAttribute; } public String getAdminPermissionGroupSearchFilter() { return adminPermissionGroupSearchFilter; } public void setAdminPermissionGroupSearchFilter(String adminPermissionGroupSearchFilter) { this.adminPermissionGroupSearchFilter = adminPermissionGroupSearchFilter; } public String getReadPermissionGroupSearchFilter() { return readPermissionGroupSearchFilter; } public void setReadPermissionGroupSearchFilter(String readPermissionGroupSearchFilter) { this.readPermissionGroupSearchFilter = readPermissionGroupSearchFilter; } public String getWritePermissionGroupSearchFilter() { return writePermissionGroupSearchFilter; } public void setWritePermissionGroupSearchFilter(String writePermissionGroupSearchFilter) { this.writePermissionGroupSearchFilter = writePermissionGroupSearchFilter; } public boolean isLegacyGroupMapping() { return legacyGroupMapping; } public void setLegacyGroupMapping(boolean legacyGroupMapping) { this.legacyGroupMapping = legacyGroupMapping; } public String getGroupObjectClass() { return groupObjectClass; } public void setGroupObjectClass(String groupObjectClass) { this.groupObjectClass = groupObjectClass; } public String getUserObjectClass() { return userObjectClass; } public void setUserObjectClass(String userObjectClass) { this.userObjectClass = userObjectClass; } public String getGroupNameAttribute() { return groupNameAttribute; } public void setGroupNameAttribute(String groupNameAttribute) { this.groupNameAttribute = groupNameAttribute; } public String getUserNameAttribute() { return userNameAttribute; } public void setUserNameAttribute(String userNameAttribute) { this.userNameAttribute = userNameAttribute; } public boolean isRefreshDisabled() { return refreshDisabled; } public void setRefreshDisabled(boolean refreshDisabled) { this.refreshDisabled = refreshDisabled; } public int getRefreshInterval() { return refreshInterval; } public void setRefreshInterval(int refreshInterval) { this.refreshInterval = refreshInterval; } public String getGroupClass() { return groupClass; } public void setGroupClass(String groupClass) { this.groupClass = groupClass; map.get().setGroupClass(groupClass); } protected static enum DestinationType { QUEUE, TOPIC, TEMP; } protected static enum PermissionType { READ, WRITE, ADMIN; } /** * Listener implementation for directory changes that maps change events to destination types. */ protected class CachedLDAPAuthorizationMapNamespaceChangeListener implements NamespaceChangeListener, ObjectChangeListener { private final DestinationType destinationType; private final PermissionType permissionType; /** * Creates a new listener. If {@code permissionType} is {@code null}, add and remove events are ignored as they * do not directly affect policy state. This configuration is used when listening for changes on entries that * represent destination patterns and not for entries that represent permissions. * * @param destinationType * the type of the destination being listened for * @param permissionType * the optional permission type being listened for */ public CachedLDAPAuthorizationMapNamespaceChangeListener(DestinationType destinationType, PermissionType permissionType) { this.destinationType = destinationType; this.permissionType = permissionType; } @Override public void namingExceptionThrown(NamingExceptionEvent evt) { SimpleCachedLDAPAuthorizationMap.this.namingExceptionThrown(evt); } @Override public void objectAdded(NamingEvent evt) { // This test is a hack to work around the fact that Apache DS 2.0 seems to trigger notifications // for the entire sub-tree even when one-level is the selected search scope. if (permissionType != null) { SimpleCachedLDAPAuthorizationMap.this.objectAdded(evt, destinationType, permissionType); } } @Override public void objectRemoved(NamingEvent evt) { // This test is a hack to work around the fact that Apache DS 2.0 seems to trigger notifications // for the entire sub-tree even when one-level is the selected search scope. if (permissionType != null) { SimpleCachedLDAPAuthorizationMap.this.objectRemoved(evt, destinationType, permissionType); } } @Override public void objectRenamed(NamingEvent evt) { SimpleCachedLDAPAuthorizationMap.this.objectRenamed(evt, destinationType, permissionType); } @Override public void objectChanged(NamingEvent evt) { // This test is a hack to work around the fact that Apache DS 2.0 seems to trigger notifications // for the entire sub-tree even when one-level is the selected search scope. if (permissionType != null) { SimpleCachedLDAPAuthorizationMap.this.objectChanged(evt, destinationType, permissionType); } } } }