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