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