001    /**
002     * Licensed to the Apache Software Foundation (ASF) under one or more
003     * contributor license agreements.  See the NOTICE file distributed with
004     * this work for additional information regarding copyright ownership.
005     * The ASF licenses this file to You under the Apache License, Version 2.0
006     * (the "License"); you may not use this file except in compliance with
007     * the License.  You may obtain a copy of the License at
008     *
009     *      http://www.apache.org/licenses/LICENSE-2.0
010     *
011     * Unless required by applicable law or agreed to in writing, software
012     * distributed under the License is distributed on an "AS IS" BASIS,
013     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014     * See the License for the specific language governing permissions and
015     * limitations under the License.
016     */
017    package org.apache.activemq.security;
018    
019    import java.util.ArrayList;
020    import java.util.HashSet;
021    import java.util.Hashtable;
022    import java.util.Map;
023    import java.util.Set;
024    import java.util.concurrent.ConcurrentHashMap;
025    import java.util.concurrent.LinkedBlockingQueue;
026    import java.util.concurrent.ThreadFactory;
027    import java.util.concurrent.ThreadPoolExecutor;
028    import java.util.concurrent.TimeUnit;
029    import java.util.concurrent.atomic.AtomicReference;
030    
031    import javax.naming.Binding;
032    import javax.naming.Context;
033    import javax.naming.InvalidNameException;
034    import javax.naming.NamingEnumeration;
035    import javax.naming.NamingException;
036    import javax.naming.directory.Attribute;
037    import javax.naming.directory.Attributes;
038    import javax.naming.directory.DirContext;
039    import javax.naming.directory.InitialDirContext;
040    import javax.naming.directory.SearchControls;
041    import javax.naming.directory.SearchResult;
042    import javax.naming.event.EventDirContext;
043    import javax.naming.event.NamespaceChangeListener;
044    import javax.naming.event.NamingEvent;
045    import javax.naming.event.NamingExceptionEvent;
046    import javax.naming.event.ObjectChangeListener;
047    import javax.naming.ldap.LdapName;
048    import javax.naming.ldap.Rdn;
049    
050    import org.apache.activemq.command.ActiveMQDestination;
051    import org.apache.activemq.command.ActiveMQQueue;
052    import org.apache.activemq.command.ActiveMQTopic;
053    import org.apache.activemq.filter.DestinationMapEntry;
054    import org.apache.activemq.jaas.UserPrincipal;
055    import org.slf4j.Logger;
056    import org.slf4j.LoggerFactory;
057    
058    public class SimpleCachedLDAPAuthorizationMap implements AuthorizationMap {
059    
060        private static final Logger LOG = LoggerFactory.getLogger(SimpleCachedLDAPAuthorizationMap.class);
061    
062        // Configuration Options
063        private final String initialContextFactory = "com.sun.jndi.ldap.LdapCtxFactory";
064        private String connectionURL = "ldap://localhost:1024";
065        private String connectionUsername = "uid=admin,ou=system";
066        private String connectionPassword = "secret";
067        private String connectionProtocol = "s";
068        private String authentication = "simple";
069    
070        private int queuePrefixLength = 4;
071        private int topicPrefixLength = 4;
072        private int tempPrefixLength = 4;
073    
074        private String queueSearchBase = "ou=Queue,ou=Destination,ou=ActiveMQ,ou=system";
075        private String topicSearchBase = "ou=Topic,ou=Destination,ou=ActiveMQ,ou=system";
076        private String tempSearchBase = "ou=Temp,ou=Destination,ou=ActiveMQ,ou=system";
077    
078        private String permissionGroupMemberAttribute = "member";
079    
080        private String adminPermissionGroupSearchFilter = "(cn=Admin)";
081        private String readPermissionGroupSearchFilter = "(cn=Read)";
082        private String writePermissionGroupSearchFilter = "(cn=Write)";
083    
084        private boolean legacyGroupMapping = true;
085        private String groupObjectClass = "groupOfNames";
086        private String userObjectClass = "person";
087        private String groupNameAttribute = "cn";
088        private String userNameAttribute = "uid";
089    
090        private int refreshInterval = -1;
091        private boolean refreshDisabled = false;
092    
093        // Internal State
094        private long lastUpdated;
095    
096        private static String ANY_DESCENDANT = "\\$";
097    
098        protected DirContext context;
099        private EventDirContext eventContext;
100    
101        private final AtomicReference<DefaultAuthorizationMap> map =
102            new AtomicReference<DefaultAuthorizationMap>(new DefaultAuthorizationMap());
103        private final ThreadPoolExecutor updaterService;
104    
105        protected Map<ActiveMQDestination, AuthorizationEntry> entries =
106            new ConcurrentHashMap<ActiveMQDestination, AuthorizationEntry>();
107    
108        public SimpleCachedLDAPAuthorizationMap() {
109            // Allow for only a couple outstanding update request, they can be slow so we
110            // don't want a bunch to pile up for no reason.
111            updaterService = new ThreadPoolExecutor(0, 1, 60, TimeUnit.SECONDS,
112                new LinkedBlockingQueue<Runnable>(2),
113                new ThreadFactory() {
114    
115                    @Override
116                    public Thread newThread(Runnable r) {
117                        return new Thread(r, "SimpleCachedLDAPAuthorizationMap update thread");
118                    }
119                });
120            updaterService.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardPolicy());
121        }
122    
123        protected DirContext createContext() throws NamingException {
124            Hashtable<String, String> env = new Hashtable<String, String>();
125            env.put(Context.INITIAL_CONTEXT_FACTORY, initialContextFactory);
126            if (connectionUsername != null || !"".equals(connectionUsername)) {
127                env.put(Context.SECURITY_PRINCIPAL, connectionUsername);
128            }
129            if (connectionPassword != null || !"".equals(connectionPassword)) {
130                env.put(Context.SECURITY_CREDENTIALS, connectionPassword);
131            }
132            env.put(Context.SECURITY_PROTOCOL, connectionProtocol);
133            env.put(Context.PROVIDER_URL, connectionURL);
134            env.put(Context.SECURITY_AUTHENTICATION, authentication);
135            return new InitialDirContext(env);
136        }
137    
138        protected boolean isContextAlive() {
139            boolean alive = false;
140            if (context != null) {
141                try {
142                    context.getAttributes("");
143                    alive = true;
144                } catch (Exception e) {
145                }
146            }
147            return alive;
148        }
149    
150        /**
151         * Returns the existing open context or creates a new one and registers listeners for push notifications if such an
152         * update style is enabled. This implementation should not be invoked concurrently.
153         *
154         * @return the current context
155         *
156         * @throws NamingException
157         *             if there is an error setting things up
158         */
159        protected DirContext open() throws NamingException {
160            if (isContextAlive()) {
161                return context;
162            }
163    
164            try {
165                context = createContext();
166                if (refreshInterval == -1 && !refreshDisabled) {
167                    eventContext = ((EventDirContext) context.lookup(""));
168    
169                    final SearchControls constraints = new SearchControls();
170                    constraints.setSearchScope(SearchControls.SUBTREE_SCOPE);
171    
172                    // Listeners for Queue policy //
173    
174                    // Listeners for each type of permission
175                    for (PermissionType permissionType : PermissionType.values()) {
176                        eventContext.addNamingListener(queueSearchBase, getFilterForPermissionType(permissionType), constraints,
177                            this.new CachedLDAPAuthorizationMapNamespaceChangeListener(DestinationType.QUEUE, permissionType));
178                    }
179                    // Listener for changes to the destination pattern entry itself and not a permission entry.
180                    eventContext.addNamingListener(queueSearchBase, "cn=*", new SearchControls(), this.new CachedLDAPAuthorizationMapNamespaceChangeListener(
181                        DestinationType.QUEUE, null));
182    
183                    // Listeners for Topic policy //
184    
185                    // Listeners for each type of permission
186                    for (PermissionType permissionType : PermissionType.values()) {
187                        eventContext.addNamingListener(topicSearchBase, getFilterForPermissionType(permissionType), constraints,
188                            this.new CachedLDAPAuthorizationMapNamespaceChangeListener(DestinationType.TOPIC, permissionType));
189                    }
190                    // Listener for changes to the destination pattern entry itself and not a permission entry.
191                    eventContext.addNamingListener(topicSearchBase, "cn=*", new SearchControls(), this.new CachedLDAPAuthorizationMapNamespaceChangeListener(
192                        DestinationType.TOPIC, null));
193    
194                    // Listeners for Temp policy //
195    
196                    // Listeners for each type of permission
197                    for (PermissionType permissionType : PermissionType.values()) {
198                        eventContext.addNamingListener(tempSearchBase, getFilterForPermissionType(permissionType), constraints,
199                            this.new CachedLDAPAuthorizationMapNamespaceChangeListener(DestinationType.TEMP, permissionType));
200                    }
201    
202                }
203            } catch (NamingException e) {
204                context = null;
205                throw e;
206            }
207    
208            return context;
209        }
210    
211        /**
212         * Queries the directory and initializes the policy based on the data in the directory. This implementation should
213         * not be invoked concurrently.
214         *
215         * @throws Exception
216         *             if there is an unrecoverable error processing the directory contents
217         */
218        @SuppressWarnings("rawtypes")
219        protected void query() throws Exception {
220            DirContext currentContext = open();
221    
222            final SearchControls constraints = new SearchControls();
223            constraints.setSearchScope(SearchControls.SUBTREE_SCOPE);
224    
225            DefaultAuthorizationMap newMap = new DefaultAuthorizationMap();
226            for (PermissionType permissionType : PermissionType.values()) {
227                try {
228                    processQueryResults(newMap,
229                        currentContext.search(queueSearchBase, getFilterForPermissionType(permissionType),
230                        constraints), DestinationType.QUEUE, permissionType);
231                } catch (Exception e) {
232                    LOG.error("Policy not applied!.  Error processing policy under '{}' with filter '{}'", new Object[]{ queueSearchBase, getFilterForPermissionType(permissionType) }, e);
233                }
234            }
235    
236            for (PermissionType permissionType : PermissionType.values()) {
237                try {
238                    processQueryResults(newMap,
239                        currentContext.search(topicSearchBase, getFilterForPermissionType(permissionType),
240                        constraints), DestinationType.TOPIC, permissionType);
241                } catch (Exception e) {
242                    LOG.error("Policy not applied!.  Error processing policy under '{}' with filter '{}'", new Object[]{ topicSearchBase, getFilterForPermissionType(permissionType) }, e);
243                }
244            }
245    
246            for (PermissionType permissionType : PermissionType.values()) {
247                try {
248                    processQueryResults(newMap,
249                        currentContext.search(tempSearchBase, getFilterForPermissionType(permissionType),
250                        constraints), DestinationType.TEMP, permissionType);
251                } catch (Exception e) {
252                    LOG.error("Policy not applied!.  Error processing policy under '{}' with filter '{}'", new Object[]{ tempSearchBase, getFilterForPermissionType(permissionType) }, e);
253                }
254            }
255    
256            // Create and swap in the new instance with updated LDAP data.
257            newMap.setAuthorizationEntries(new ArrayList<DestinationMapEntry>(entries.values()));
258            this.map.set(newMap);
259    
260            updated();
261        }
262    
263        /**
264         * Processes results from a directory query in the context of a given destination type and permission type. This
265         * implementation should not be invoked concurrently.
266         *
267         * @param results
268         *            the results to process
269         * @param destinationType
270         *            the type of the destination for which the directory results apply
271         * @param permissionType
272         *            the type of the permission for which the directory results apply
273         *
274         * @throws Exception
275         *             if there is an error processing the results
276         */
277        protected void processQueryResults(DefaultAuthorizationMap map, NamingEnumeration<SearchResult> results, DestinationType destinationType, PermissionType permissionType)
278            throws Exception {
279    
280            while (results.hasMore()) {
281                SearchResult result = results.next();
282                AuthorizationEntry entry = null;
283    
284                try {
285                    entry = getEntry(map, new LdapName(result.getNameInNamespace()), destinationType);
286                } catch (Exception e) {
287                    LOG.error("Policy not applied!  Error parsing authorization policy entry under {}", result.getNameInNamespace(), e);
288                    continue;
289                }
290    
291                applyACL(entry, result, permissionType);
292            }
293        }
294    
295        /**
296         * Marks the time at which the authorization state was last refreshed. Relevant for synchronous
297         * policy updates. This implementation should not be invoked concurrently.
298         */
299        protected void updated() {
300            lastUpdated = System.currentTimeMillis();
301        }
302    
303        /**
304         * Retrieves or creates the {@link AuthorizationEntry} that corresponds to the DN in {@code dn}. This implementation
305         * should not be invoked concurrently.
306         *
307         * @param map
308         *            the DefaultAuthorizationMap to operate on.
309         * @param dn
310         *            the DN representing the policy entry in the directory
311         * @param destinationType
312         *            the type of the destination to get/create the entry for
313         *
314         * @return the corresponding authorization entry for the DN
315         *
316         * @throws IllegalArgumentException
317         *             if destination type is not one of {@link DestinationType#QUEUE}, {@link DestinationType#TOPIC},
318         *             {@link DestinationType#TEMP} or if the policy entry DN is malformed
319         */
320        protected AuthorizationEntry getEntry(DefaultAuthorizationMap map, LdapName dn, DestinationType destinationType) {
321            AuthorizationEntry entry = null;
322            switch (destinationType) {
323                case TEMP:
324                    // handle temp entry
325                    if (dn.size() != getPrefixLengthForDestinationType(destinationType) + 1) {
326                        // handle unknown entry
327                        throw new IllegalArgumentException("Malformed policy structure for a temporary destination "
328                            + "policy entry.  The permission group entries should be immediately below the " + "temporary policy base DN.");
329                    }
330                    entry = map.getTempDestinationAuthorizationEntry();
331                    if (entry == null) {
332                        entry = new TempDestinationAuthorizationEntry();
333                        map.setTempDestinationAuthorizationEntry((TempDestinationAuthorizationEntry) entry);
334                    }
335    
336                    break;
337    
338                case QUEUE:
339                case TOPIC:
340                    // handle regular destinations
341                    if (dn.size() != getPrefixLengthForDestinationType(destinationType) + 2) {
342                        throw new IllegalArgumentException("Malformed policy structure for a queue or topic destination "
343                            + "policy entry.  The destination pattern and permission group entries should be " + "nested below the queue or topic policy base DN.");
344                    }
345    
346                    ActiveMQDestination dest = formatDestination(dn, destinationType);
347    
348                    if (dest != null) {
349                        entry = entries.get(dest);
350                        if (entry == null) {
351                            entry = new AuthorizationEntry();
352                            entry.setDestination(dest);
353                            entries.put(dest, entry);
354                        }
355                    }
356    
357                    break;
358                default:
359                    // handle unknown entry
360                    throw new IllegalArgumentException("Unknown destination type " + destinationType);
361            }
362    
363            return entry;
364        }
365    
366        /**
367         * Applies the policy from the directory to the given entry within the context of the provided permission type.
368         *
369         * @param entry
370         *            the policy entry to apply the policy to
371         * @param result
372         *            the results from the directory to apply to the policy entry
373         * @param permissionType
374         *            the permission type of the data in the directory
375         *
376         * @throws NamingException
377         *             if there is an error applying the ACL
378         */
379        protected void applyACL(AuthorizationEntry entry, SearchResult result, PermissionType permissionType) throws NamingException {
380    
381            // Find members
382            Attribute memberAttribute = result.getAttributes().get(permissionGroupMemberAttribute);
383            NamingEnumeration<?> memberAttributeEnum = memberAttribute.getAll();
384    
385            HashSet<Object> members = new HashSet<Object>();
386    
387            while (memberAttributeEnum.hasMoreElements()) {
388                String memberDn = (String) memberAttributeEnum.nextElement();
389                boolean group = false;
390                boolean user = false;
391                String principalName = null;
392    
393                if (!legacyGroupMapping) {
394                    // Lookup of member to determine principal type (group or user) and name.
395                    Attributes memberAttributes;
396                    try {
397                        memberAttributes = context.getAttributes(memberDn, new String[] { "objectClass", groupNameAttribute, userNameAttribute });
398                    } catch (NamingException e) {
399                        LOG.error("Policy not applied! Unknown member {} in policy entry {}", new Object[]{ memberDn, result.getNameInNamespace() }, e);
400                        continue;
401                    }
402    
403                    Attribute memberEntryObjectClassAttribute = memberAttributes.get("objectClass");
404                    NamingEnumeration<?> memberEntryObjectClassAttributeEnum = memberEntryObjectClassAttribute.getAll();
405    
406                    while (memberEntryObjectClassAttributeEnum.hasMoreElements()) {
407                        String objectClass = (String) memberEntryObjectClassAttributeEnum.nextElement();
408    
409                        if (objectClass.equalsIgnoreCase(groupObjectClass)) {
410                            group = true;
411                            Attribute name = memberAttributes.get(groupNameAttribute);
412                            if (name == null) {
413                                LOG.error("Policy not applied! Group {} does not have name attribute {} under entry {}", new Object[]{ memberDn, groupNameAttribute, result.getNameInNamespace() });
414                                break;
415                            }
416    
417                            principalName = (String) name.get();
418                        }
419    
420                        if (objectClass.equalsIgnoreCase(userObjectClass)) {
421                            user = true;
422                            Attribute name = memberAttributes.get(userNameAttribute);
423                            if (name == null) {
424                                LOG.error("Policy not applied! User {} does not have name attribute {} under entry {}", new Object[]{ memberDn, userNameAttribute, result.getNameInNamespace() });
425                                break;
426                            }
427    
428                            principalName = (String) name.get();
429                        }
430                    }
431    
432                } else {
433                    group = true;
434                    principalName = memberDn.replaceAll("(cn|CN)=", "");
435                }
436    
437                if ((!group && !user) || (group && user)) {
438                    LOG.error("Policy not applied! Can't determine type of member {} under entry {}", memberDn, result.getNameInNamespace());
439                } else if (principalName != null) {
440                    DefaultAuthorizationMap map = this.map.get();
441                    if (group && !user) {
442                        try {
443                            members.add(DefaultAuthorizationMap.createGroupPrincipal(principalName, map.getGroupClass()));
444                        } catch (Exception e) {
445                            NamingException ne = new NamingException(
446                                "Can't create a group " + principalName + " of class " + map.getGroupClass());
447                            ne.initCause(e);
448                            throw ne;
449                        }
450                    } else if (!group && user) {
451                        members.add(new UserPrincipal(principalName));
452                    }
453                }
454            }
455    
456            try {
457                applyAcl(entry, permissionType, members);
458            } catch (Exception e) {
459                LOG.error("Policy not applied! Error adding principals to ACL under {}", result.getNameInNamespace(), e);
460            }
461        }
462    
463        /**
464         * Applies policy to the entry given the actual principals that will be applied to the policy entry.
465         *
466         * @param entry
467         *            the policy entry to which the policy should be applied
468         * @param permissionType
469         *            the type of the permission that the policy will be applied to
470         * @param acls
471         *            the principals that represent the actual policy
472         *
473         * @throw IllegalArgumentException if {@code permissionType} is unsupported
474         */
475        protected void applyAcl(AuthorizationEntry entry, PermissionType permissionType, Set<Object> acls) {
476    
477            switch (permissionType) {
478                case READ:
479                    entry.setReadACLs(acls);
480                    break;
481                case WRITE:
482                    entry.setWriteACLs(acls);
483                    break;
484                case ADMIN:
485                    entry.setAdminACLs(acls);
486                    break;
487                default:
488                    throw new IllegalArgumentException("Unknown permission " + permissionType + ".");
489            }
490        }
491    
492        /**
493         * Parses a DN into the equivalent {@link ActiveMQDestination}. The default implementation expects a format of
494         * cn=<PERMISSION_NAME>,ou=<DESTINATION_PATTERN>,.... or ou=<DESTINATION_PATTERN>,.... for permission and
495         * destination entries, respectively. For example {@code cn=admin,ou=$,ou=...} or {@code ou=$,ou=...}.
496         *
497         * @param dn
498         *            the DN to parse
499         * @param destinationType
500         *            the type of the destination that we are parsing
501         *
502         * @return the destination that the DN represents
503         *
504         * @throws IllegalArgumentException
505         *             if {@code destinationType} is {@link DestinationType#TEMP} or if the format of {@code dn} is
506         *             incorrect for for a topic or queue
507         *
508         * @see #formatDestination(Rdn, DestinationType)
509         */
510        protected ActiveMQDestination formatDestination(LdapName dn, DestinationType destinationType) {
511            ActiveMQDestination destination = null;
512    
513            switch (destinationType) {
514                case QUEUE:
515                case TOPIC:
516                    // There exists a need to deal with both names representing a permission or simply a
517                    // destination. As such, we need to determine the proper RDN to work with based
518                    // on the destination type and the DN size.
519                    if (dn.size() == (getPrefixLengthForDestinationType(destinationType) + 2)) {
520                        destination = formatDestination(dn.getRdn(dn.size() - 2), destinationType);
521                    } else if (dn.size() == (getPrefixLengthForDestinationType(destinationType) + 1)) {
522                        destination = formatDestination(dn.getRdn(dn.size() - 1), destinationType);
523                    } else {
524                        throw new IllegalArgumentException("Malformed DN for representing a permission or destination entry.");
525                    }
526                    break;
527                default:
528                    throw new IllegalArgumentException("Cannot format destination for destination type " + destinationType);
529            }
530    
531            return destination;
532        }
533    
534        /**
535         * Parses RDN values representing the destination name/pattern and destination type into the equivalent
536         * {@link ActiveMQDestination}.
537         *
538         * @param destinationName
539         *            the RDN representing the name or pattern for the destination
540         * @param destinationType
541         *            the type of the destination
542         *
543         * @return the destination that the RDN represent
544         *
545         * @throws IllegalArgumentException
546         *             if {@code destinationType} is not one of {@link DestinationType#TOPIC} or
547         *             {@link DestinationType#QUEUE}.
548         *
549         * @see #formatDestinationName(Rdn)
550         * @see #formatDestination(LdapName, DestinationType)
551         */
552        protected ActiveMQDestination formatDestination(Rdn destinationName, DestinationType destinationType) {
553            ActiveMQDestination dest = null;
554    
555            switch (destinationType) {
556                case QUEUE:
557                    dest = new ActiveMQQueue(formatDestinationName(destinationName));
558                    break;
559                case TOPIC:
560                    dest = new ActiveMQTopic(formatDestinationName(destinationName));
561                    break;
562                default:
563                    throw new IllegalArgumentException("Unknown destination type: " + destinationType);
564            }
565    
566            return dest;
567        }
568    
569        /**
570         * Parses the RDN representing a destination name/pattern into the standard string representation of the
571         * name/pattern. This implementation does not care about the type of the RDN such that the RDN could be a CN or OU.
572         *
573         * @param destinationName
574         *            the RDN representing the name or pattern for the destination
575         *
576         * @see #formatDestination(Rdn, Rdn)
577         */
578        protected String formatDestinationName(Rdn destinationName) {
579            return destinationName.getValue().toString().replaceAll(ANY_DESCENDANT, ">");
580        }
581    
582        /**
583         * Transcribes an existing set into a new set. Used to make defensive copies for concurrent access.
584         *
585         * @param source
586         *            the source set or {@code null}
587         *
588         * @return a new set containing the same elements as {@code source} or {@code null} if {@code source} is
589         *         {@code null}
590         */
591        protected <T> Set<T> transcribeSet(Set<T> source) {
592            if (source != null) {
593                return new HashSet<T>(source);
594            } else {
595                return null;
596            }
597        }
598    
599        /**
600         * Returns the filter string for the given permission type.
601         *
602         * @throws IllegalArgumentException
603         *             if {@code permissionType} is not supported
604         *
605         * @see #setAdminPermissionGroupSearchFilter(String)
606         * @see #setReadPermissionGroupSearchFilter(String)
607         * @see #setWritePermissionGroupSearchFilter(String)
608         */
609        protected String getFilterForPermissionType(PermissionType permissionType) {
610            String filter = null;
611    
612            switch (permissionType) {
613                case ADMIN:
614                    filter = adminPermissionGroupSearchFilter;
615                    break;
616                case READ:
617                    filter = readPermissionGroupSearchFilter;
618                    break;
619                case WRITE:
620                    filter = writePermissionGroupSearchFilter;
621                    break;
622                default:
623                    throw new IllegalArgumentException("Unknown permission type " + permissionType);
624            }
625    
626            return filter;
627        }
628    
629        /**
630         * Returns the DN prefix size based on the given destination type.
631         *
632         * @throws IllegalArgumentException
633         *             if {@code destinationType} is not supported
634         *
635         * @see #setQueueSearchBase(String)
636         * @see #setTopicSearchBase(String)
637         * @see #setTempSearchBase(String)
638         */
639        protected int getPrefixLengthForDestinationType(DestinationType destinationType) {
640            int filter = 0;
641    
642            switch (destinationType) {
643                case QUEUE:
644                    filter = queuePrefixLength;
645                    break;
646                case TOPIC:
647                    filter = topicPrefixLength;
648                    break;
649                case TEMP:
650                    filter = tempPrefixLength;
651                    break;
652                default:
653                    throw new IllegalArgumentException("Unknown permission type " + destinationType);
654            }
655    
656            return filter;
657        }
658    
659        /**
660         * Performs a check for updates from the server in the event that synchronous updates are enabled and are the
661         * refresh interval has elapsed.
662         */
663        protected void checkForUpdates() {
664    
665            if (context != null && refreshDisabled) {
666                return;
667            }
668    
669            if (context == null || (!refreshDisabled && (refreshInterval != -1 && System.currentTimeMillis() >= lastUpdated + refreshInterval))) {
670                this.updaterService.execute(new Runnable() {
671                    @Override
672                    public void run() {
673    
674                        // Check again in case of stacked update request.
675                        if (context == null || (!refreshDisabled &&
676                            (refreshInterval != -1 && System.currentTimeMillis() >= lastUpdated + refreshInterval))) {
677    
678                            if (!isContextAlive()) {
679                                try {
680                                    context = createContext();
681                                } catch (NamingException ne) {
682                                    // LDAP is down, use already cached values
683                                    return;
684                                }
685                            }
686    
687                            entries.clear();
688    
689                            LOG.debug("Updating authorization map!");
690                            try {
691                                query();
692                            } catch (Exception e) {
693                                LOG.error("Error updating authorization map.  Partial policy may be applied until the next successful update.", e);
694                            }
695                        }
696                    }
697                });
698            }
699        }
700    
701        // Authorization Map
702    
703        /**
704         * Provides synchronized and defensive access to the admin ACLs for temp destinations as the super implementation
705         * returns live copies of the ACLs and {@link AuthorizationEntry} is not setup for concurrent access.
706         */
707        @Override
708        public Set<Object> getTempDestinationAdminACLs() {
709            checkForUpdates();
710            DefaultAuthorizationMap map = this.map.get();
711            return transcribeSet(map.getTempDestinationAdminACLs());
712        }
713    
714        /**
715         * Provides synchronized and defensive access to the read ACLs for temp destinations as the super implementation
716         * returns live copies of the ACLs and {@link AuthorizationEntry} is not setup for concurrent access.
717         */
718        @Override
719        public Set<Object> getTempDestinationReadACLs() {
720            checkForUpdates();
721            DefaultAuthorizationMap map = this.map.get();
722            return transcribeSet(map.getTempDestinationReadACLs());
723        }
724    
725        /**
726         * Provides synchronized and defensive access to the write ACLs for temp destinations as the super implementation
727         * returns live copies of the ACLs and {@link AuthorizationEntry} is not setup for concurrent access.
728         */
729        @Override
730        public Set<Object> getTempDestinationWriteACLs() {
731            checkForUpdates();
732            DefaultAuthorizationMap map = this.map.get();
733            return transcribeSet(map.getTempDestinationWriteACLs());
734        }
735    
736        /**
737         * Provides synchronized access to the admin ACLs for the destinations as {@link AuthorizationEntry}
738         * is not setup for concurrent access.
739         */
740        @Override
741        public Set<Object> getAdminACLs(ActiveMQDestination destination) {
742            checkForUpdates();
743            DefaultAuthorizationMap map = this.map.get();
744            return map.getAdminACLs(destination);
745        }
746    
747        /**
748         * Provides synchronized access to the read ACLs for the destinations as {@link AuthorizationEntry} is not setup for
749         * concurrent access.
750         */
751        @Override
752        public Set<Object> getReadACLs(ActiveMQDestination destination) {
753            checkForUpdates();
754            DefaultAuthorizationMap map = this.map.get();
755            return map.getReadACLs(destination);
756        }
757    
758        /**
759         * Provides synchronized access to the write ACLs for the destinations as {@link AuthorizationEntry} is not setup
760         * for concurrent access.
761         */
762        @Override
763        public Set<Object> getWriteACLs(ActiveMQDestination destination) {
764            checkForUpdates();
765            DefaultAuthorizationMap map = this.map.get();
766            return map.getWriteACLs(destination);
767        }
768    
769        /**
770         * Handler for new policy entries in the directory.
771         *
772         * @param namingEvent
773         *            the new entry event that occurred
774         * @param destinationType
775         *            the type of the destination to which the event applies
776         * @param permissionType
777         *            the permission type to which the event applies
778         */
779        public void objectAdded(NamingEvent namingEvent, DestinationType destinationType, PermissionType permissionType) {
780            LOG.debug("Adding object: {}", namingEvent.getNewBinding());
781            SearchResult result = (SearchResult) namingEvent.getNewBinding();
782    
783            try {
784                DefaultAuthorizationMap map = this.map.get();
785                LdapName name = new LdapName(result.getName());
786                AuthorizationEntry entry = getEntry(map, name, destinationType);
787    
788                applyACL(entry, result, permissionType);
789                if (!(entry instanceof TempDestinationAuthorizationEntry)) {
790                    map.put(entry.getDestination(), entry);
791                }
792            } catch (InvalidNameException e) {
793                LOG.error("Policy not applied!  Error parsing DN for addition of {}", result.getName(), e);
794            } catch (Exception e) {
795                LOG.error("Policy not applied!  Error processing object addition for addition of {}", result.getName(), e);
796            }
797        }
798    
799        /**
800         * Handler for removed policy entries in the directory.
801         *
802         * @param namingEvent
803         *            the removed entry event that occurred
804         * @param destinationType
805         *            the type of the destination to which the event applies
806         * @param permissionType
807         *            the permission type to which the event applies
808         */
809        public void objectRemoved(NamingEvent namingEvent, DestinationType destinationType, PermissionType permissionType) {
810            LOG.debug("Removing object: {}", namingEvent.getOldBinding());
811            Binding result = namingEvent.getOldBinding();
812    
813            try {
814                DefaultAuthorizationMap map = this.map.get();
815                LdapName name = new LdapName(result.getName());
816                AuthorizationEntry entry = getEntry(map, name, destinationType);
817                applyAcl(entry, permissionType, new HashSet<Object>());
818            } catch (InvalidNameException e) {
819                LOG.error("Policy not applied!  Error parsing DN for object removal for removal of {}", result.getName(), e);
820            } catch (Exception e) {
821                LOG.error("Policy not applied!  Error processing object removal for removal of {}", result.getName(), e);
822            }
823        }
824    
825        /**
826         * Handler for renamed policy entries in the directory. This handler deals with the renaming of destination entries
827         * as well as permission entries. If the permission type is not null, it is assumed that we are dealing with the
828         * renaming of a permission entry. Otherwise, it is assumed that we are dealing with the renaming of a destination
829         * entry.
830         *
831         * @param namingEvent
832         *            the renaming entry event that occurred
833         * @param destinationType
834         *            the type of the destination to which the event applies
835         * @param permissionType
836         *            the permission type to which the event applies
837         */
838        public void objectRenamed(NamingEvent namingEvent, DestinationType destinationType, PermissionType permissionType) {
839            Binding oldBinding = namingEvent.getOldBinding();
840            Binding newBinding = namingEvent.getNewBinding();
841            LOG.debug("Renaming object: {} to {}", oldBinding, newBinding);
842    
843            try {
844                LdapName oldName = new LdapName(oldBinding.getName());
845                ActiveMQDestination oldDest = formatDestination(oldName, destinationType);
846    
847                LdapName newName = new LdapName(newBinding.getName());
848                ActiveMQDestination newDest = formatDestination(newName, destinationType);
849    
850                if (permissionType != null) {
851                    // Handle the case where a permission entry is being renamed.
852                    objectRemoved(namingEvent, destinationType, permissionType);
853    
854                    SearchControls controls = new SearchControls();
855                    controls.setSearchScope(SearchControls.OBJECT_SCOPE);
856    
857                    boolean matchedToType = false;
858    
859                    for (PermissionType newPermissionType : PermissionType.values()) {
860                        NamingEnumeration<SearchResult> results = context.search(newName, getFilterForPermissionType(newPermissionType), controls);
861    
862                        if (results.hasMore()) {
863                            objectAdded(namingEvent, destinationType, newPermissionType);
864                            matchedToType = true;
865                            break;
866                        }
867                    }
868    
869                    if (!matchedToType) {
870                        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());
871                    }
872                } else {
873                    // Handle the case where a destination entry is being renamed.
874                    if (oldDest != null && newDest != null) {
875                        AuthorizationEntry entry = entries.remove(oldDest);
876                        if (entry != null) {
877                            entry.setDestination(newDest);
878                            DefaultAuthorizationMap map = this.map.get();
879                            map.put(newDest, entry);
880                            map.remove(oldDest, entry);
881                            entries.put(newDest, entry);
882                        } else {
883                            LOG.warn("No authorization entry for {}", oldDest);
884                        }
885                    }
886                }
887            } catch (InvalidNameException e) {
888                LOG.error("Policy not applied!  Error parsing DN for object rename for rename of {} to {}", new Object[]{ oldBinding.getName(), newBinding.getName() }, e);
889            } catch (Exception e) {
890                LOG.error("Policy not applied!  Error processing object rename for rename of {} to {}", new Object[]{ oldBinding.getName(), newBinding.getName() }, e);
891            }
892        }
893    
894        /**
895         * Handler for changed policy entries in the directory.
896         *
897         * @param namingEvent
898         *            the changed entry event that occurred
899         * @param destinationType
900         *            the type of the destination to which the event applies
901         * @param permissionType
902         *            the permission type to which the event applies
903         */
904        public void objectChanged(NamingEvent namingEvent, DestinationType destinationType, PermissionType permissionType) {
905            LOG.debug("Changing object {} to {}", namingEvent.getOldBinding(), namingEvent.getNewBinding());
906            objectRemoved(namingEvent, destinationType, permissionType);
907            objectAdded(namingEvent, destinationType, permissionType);
908        }
909    
910        /**
911         * Handler for exception events from the registry.
912         *
913         * @param namingExceptionEvent
914         *            the exception event
915         */
916        public void namingExceptionThrown(NamingExceptionEvent namingExceptionEvent) {
917            context = null;
918            LOG.error("Caught unexpected exception.", namingExceptionEvent.getException());
919        }
920    
921        // Init / Destroy
922        public void afterPropertiesSet() throws Exception {
923            query();
924        }
925    
926        public void destroy() throws Exception {
927            if (eventContext != null) {
928                eventContext.close();
929                eventContext = null;
930            }
931    
932            if (context != null) {
933                context.close();
934                context = null;
935            }
936        }
937    
938        // Getters and Setters
939    
940        public String getConnectionURL() {
941            return connectionURL;
942        }
943    
944        public void setConnectionURL(String connectionURL) {
945            this.connectionURL = connectionURL;
946        }
947    
948        public String getConnectionUsername() {
949            return connectionUsername;
950        }
951    
952        public void setConnectionUsername(String connectionUsername) {
953            this.connectionUsername = connectionUsername;
954        }
955    
956        public String getConnectionPassword() {
957            return connectionPassword;
958        }
959    
960        public void setConnectionPassword(String connectionPassword) {
961            this.connectionPassword = connectionPassword;
962        }
963    
964        public String getConnectionProtocol() {
965            return connectionProtocol;
966        }
967    
968        public void setConnectionProtocol(String connectionProtocol) {
969            this.connectionProtocol = connectionProtocol;
970        }
971    
972        public String getAuthentication() {
973            return authentication;
974        }
975    
976        public void setAuthentication(String authentication) {
977            this.authentication = authentication;
978        }
979    
980        public String getQueueSearchBase() {
981            return queueSearchBase;
982        }
983    
984        public void setQueueSearchBase(String queueSearchBase) {
985            try {
986                LdapName baseName = new LdapName(queueSearchBase);
987                queuePrefixLength = baseName.size();
988                this.queueSearchBase = queueSearchBase;
989            } catch (InvalidNameException e) {
990                throw new IllegalArgumentException("Invalid base DN value " + queueSearchBase, e);
991            }
992        }
993    
994        public String getTopicSearchBase() {
995            return topicSearchBase;
996        }
997    
998        public void setTopicSearchBase(String topicSearchBase) {
999            try {
1000                LdapName baseName = new LdapName(topicSearchBase);
1001                topicPrefixLength = baseName.size();
1002                this.topicSearchBase = topicSearchBase;
1003            } catch (InvalidNameException e) {
1004                throw new IllegalArgumentException("Invalid base DN value " + topicSearchBase, e);
1005            }
1006        }
1007    
1008        public String getTempSearchBase() {
1009            return tempSearchBase;
1010        }
1011    
1012        public void setTempSearchBase(String tempSearchBase) {
1013            try {
1014                LdapName baseName = new LdapName(tempSearchBase);
1015                tempPrefixLength = baseName.size();
1016                this.tempSearchBase = tempSearchBase;
1017            } catch (InvalidNameException e) {
1018                throw new IllegalArgumentException("Invalid base DN value " + tempSearchBase, e);
1019            }
1020        }
1021    
1022        public String getPermissionGroupMemberAttribute() {
1023            return permissionGroupMemberAttribute;
1024        }
1025    
1026        public void setPermissionGroupMemberAttribute(String permissionGroupMemberAttribute) {
1027            this.permissionGroupMemberAttribute = permissionGroupMemberAttribute;
1028        }
1029    
1030        public String getAdminPermissionGroupSearchFilter() {
1031            return adminPermissionGroupSearchFilter;
1032        }
1033    
1034        public void setAdminPermissionGroupSearchFilter(String adminPermissionGroupSearchFilter) {
1035            this.adminPermissionGroupSearchFilter = adminPermissionGroupSearchFilter;
1036        }
1037    
1038        public String getReadPermissionGroupSearchFilter() {
1039            return readPermissionGroupSearchFilter;
1040        }
1041    
1042        public void setReadPermissionGroupSearchFilter(String readPermissionGroupSearchFilter) {
1043            this.readPermissionGroupSearchFilter = readPermissionGroupSearchFilter;
1044        }
1045    
1046        public String getWritePermissionGroupSearchFilter() {
1047            return writePermissionGroupSearchFilter;
1048        }
1049    
1050        public void setWritePermissionGroupSearchFilter(String writePermissionGroupSearchFilter) {
1051            this.writePermissionGroupSearchFilter = writePermissionGroupSearchFilter;
1052        }
1053    
1054        public boolean isLegacyGroupMapping() {
1055            return legacyGroupMapping;
1056        }
1057    
1058        public void setLegacyGroupMapping(boolean legacyGroupMapping) {
1059            this.legacyGroupMapping = legacyGroupMapping;
1060        }
1061    
1062        public String getGroupObjectClass() {
1063            return groupObjectClass;
1064        }
1065    
1066        public void setGroupObjectClass(String groupObjectClass) {
1067            this.groupObjectClass = groupObjectClass;
1068        }
1069    
1070        public String getUserObjectClass() {
1071            return userObjectClass;
1072        }
1073    
1074        public void setUserObjectClass(String userObjectClass) {
1075            this.userObjectClass = userObjectClass;
1076        }
1077    
1078        public String getGroupNameAttribute() {
1079            return groupNameAttribute;
1080        }
1081    
1082        public void setGroupNameAttribute(String groupNameAttribute) {
1083            this.groupNameAttribute = groupNameAttribute;
1084        }
1085    
1086        public String getUserNameAttribute() {
1087            return userNameAttribute;
1088        }
1089    
1090        public void setUserNameAttribute(String userNameAttribute) {
1091            this.userNameAttribute = userNameAttribute;
1092        }
1093    
1094        public boolean isRefreshDisabled() {
1095            return refreshDisabled;
1096        }
1097    
1098        public void setRefreshDisabled(boolean refreshDisabled) {
1099            this.refreshDisabled = refreshDisabled;
1100        }
1101    
1102        public int getRefreshInterval() {
1103            return refreshInterval;
1104        }
1105    
1106        public void setRefreshInterval(int refreshInterval) {
1107            this.refreshInterval = refreshInterval;
1108        }
1109    
1110        protected static enum DestinationType {
1111            QUEUE, TOPIC, TEMP;
1112        }
1113    
1114        protected static enum PermissionType {
1115            READ, WRITE, ADMIN;
1116        }
1117    
1118        /**
1119         * Listener implementation for directory changes that maps change events to destination types.
1120         */
1121        protected class CachedLDAPAuthorizationMapNamespaceChangeListener implements NamespaceChangeListener, ObjectChangeListener {
1122    
1123            private final DestinationType destinationType;
1124            private final PermissionType permissionType;
1125    
1126            /**
1127             * Creates a new listener. If {@code permissionType} is {@code null}, add and remove events are ignored as they
1128             * do not directly affect policy state. This configuration is used when listening for changes on entries that
1129             * represent destination patterns and not for entries that represent permissions.
1130             *
1131             * @param destinationType
1132             *            the type of the destination being listened for
1133             * @param permissionType
1134             *            the optional permission type being listened for
1135             */
1136            public CachedLDAPAuthorizationMapNamespaceChangeListener(DestinationType destinationType, PermissionType permissionType) {
1137                this.destinationType = destinationType;
1138                this.permissionType = permissionType;
1139            }
1140    
1141            @Override
1142            public void namingExceptionThrown(NamingExceptionEvent evt) {
1143                SimpleCachedLDAPAuthorizationMap.this.namingExceptionThrown(evt);
1144            }
1145    
1146            @Override
1147            public void objectAdded(NamingEvent evt) {
1148                // This test is a hack to work around the fact that Apache DS 2.0 seems to trigger notifications
1149                // for the entire sub-tree even when one-level is the selected search scope.
1150                if (permissionType != null) {
1151                    SimpleCachedLDAPAuthorizationMap.this.objectAdded(evt, destinationType, permissionType);
1152                }
1153            }
1154    
1155            @Override
1156            public void objectRemoved(NamingEvent evt) {
1157                // This test is a hack to work around the fact that Apache DS 2.0 seems to trigger notifications
1158                // for the entire sub-tree even when one-level is the selected search scope.
1159                if (permissionType != null) {
1160                    SimpleCachedLDAPAuthorizationMap.this.objectRemoved(evt, destinationType, permissionType);
1161                }
1162            }
1163    
1164            @Override
1165            public void objectRenamed(NamingEvent evt) {
1166                SimpleCachedLDAPAuthorizationMap.this.objectRenamed(evt, destinationType, permissionType);
1167            }
1168    
1169            @Override
1170            public void objectChanged(NamingEvent evt) {
1171                // This test is a hack to work around the fact that Apache DS 2.0 seems to trigger notifications
1172                // for the entire sub-tree even when one-level is the selected search scope.
1173                if (permissionType != null) {
1174                    SimpleCachedLDAPAuthorizationMap.this.objectChanged(evt, destinationType, permissionType);
1175                }
1176            }
1177        }
1178    }