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