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