001    /**
002     * Licensed to the Apache Software Foundation (ASF) under one or more
003     * contributor license agreements.  See the NOTICE file distributed with
004     * this work for additional information regarding copyright ownership.
005     * The ASF licenses this file to You under the Apache License, Version 2.0
006     * (the "License"); you may not use this file except in compliance with
007     * the License.  You may obtain a copy of the License at
008     *
009     *      http://www.apache.org/licenses/LICENSE-2.0
010     *
011     * Unless required by applicable law or agreed to in writing, software
012     * distributed under the License is distributed on an "AS IS" BASIS,
013     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014     * See the License for the specific language governing permissions and
015     * limitations under the License.
016     */
017    package org.apache.activemq.security;
018    
019    import org.apache.activemq.command.ActiveMQDestination;
020    import org.apache.activemq.command.ActiveMQQueue;
021    import org.apache.activemq.command.ActiveMQTopic;
022    import org.apache.activemq.filter.DestinationMapEntry;
023    import org.apache.activemq.jaas.GroupPrincipal;
024    import org.slf4j.Logger;
025    import org.slf4j.LoggerFactory;
026    import org.springframework.beans.factory.InitializingBean;
027    
028    import javax.naming.Binding;
029    import javax.naming.Context;
030    import javax.naming.NamingEnumeration;
031    import javax.naming.NamingException;
032    import javax.naming.directory.*;
033    import javax.naming.event.*;
034    import java.util.*;
035    
036    /**
037     * A {@link DefaultAuthorizationMap} implementation which uses LDAP to initialize and update
038     *
039     * @org.apache.xbean.XBean
040     *
041     */
042    public class CachedLDAPAuthorizationMap extends DefaultAuthorizationMap implements NamespaceChangeListener,
043            ObjectChangeListener, InitializingBean {
044    
045        private static final Logger LOG = LoggerFactory.getLogger(CachedLDAPAuthorizationMap.class);
046    
047    
048        private String initialContextFactory = "com.sun.jndi.ldap.LdapCtxFactory";
049        private String connectionURL = "ldap://localhost:1024";
050        private String connectionUsername = "uid=admin,ou=system";
051        private String connectionPassword = "secret";
052        private String connectionProtocol = "s";
053        private String authentication = "simple";
054    
055        private String baseDn = "ou=system";
056        private int cnsLength = 5;
057    
058        private int refreshInterval = -1;
059        private long lastUpdated;
060    
061        private static String ANY_DESCENDANT = "\\$";
062    
063        private DirContext context;
064        private EventDirContext eventContext;
065    
066        protected DirContext open() throws NamingException {
067            if (context != null) {
068                return context;
069            }
070    
071            try {
072                Hashtable<String, String> env = new Hashtable<String, String>();
073                env.put(Context.INITIAL_CONTEXT_FACTORY, initialContextFactory);
074                if (connectionUsername != null || !"".equals(connectionUsername)) {
075                    env.put(Context.SECURITY_PRINCIPAL, connectionUsername);
076                }
077                if (connectionPassword != null || !"".equals(connectionPassword)) {
078                    env.put(Context.SECURITY_CREDENTIALS, connectionPassword);
079                }
080                env.put(Context.SECURITY_PROTOCOL, connectionProtocol);
081                env.put(Context.PROVIDER_URL, connectionURL);
082                env.put(Context.SECURITY_AUTHENTICATION, authentication);
083                context = new InitialDirContext(env);
084    
085    
086                if (refreshInterval == -1) {
087                    eventContext = ((EventDirContext)context.lookup(""));
088                    final SearchControls constraints = new SearchControls();
089                    constraints.setSearchScope(SearchControls.SUBTREE_SCOPE);
090                    LOG.debug("Listening for: " + "'ou=Destination,ou=ActiveMQ," + baseDn + "'");
091                    eventContext.addNamingListener("ou=Destination,ou=ActiveMQ," + baseDn, "cn=*", constraints, this);
092                }
093            } catch (NamingException e) {
094                LOG.error(e.toString());
095                throw e;
096            }
097            return context;
098        }
099    
100        HashMap<ActiveMQDestination, AuthorizationEntry> entries = new HashMap<ActiveMQDestination, AuthorizationEntry>();
101    
102        @SuppressWarnings("rawtypes")
103        public void query() throws Exception {
104            try {
105                context = open();
106            } catch (NamingException e) {
107                LOG.error(e.toString());
108            }
109    
110            final SearchControls constraints = new SearchControls();
111            constraints.setSearchScope(SearchControls.SUBTREE_SCOPE);
112    
113            NamingEnumeration<?> results = context.search("ou=Destination,ou=ActiveMQ," + baseDn, "(|(cn=admin)(cn=write)(cn=read))", constraints);
114            while (results.hasMore()) {
115                SearchResult result = (SearchResult) results.next();
116                AuthorizationEntry entry = getEntry(result.getNameInNamespace());
117                applyACL(entry, result);
118            }
119    
120            setEntries(new ArrayList<DestinationMapEntry>(entries.values()));
121            updated();
122        }
123    
124        protected void updated() {
125            lastUpdated = System.currentTimeMillis();
126        }
127    
128        protected AuthorizationEntry getEntry(String name) {;
129                String[] cns = name.split(",");
130    
131                // handle temp entry
132                if (cns.length == cnsLength && cns[1].equals("ou=Temp")) {
133                    TempDestinationAuthorizationEntry tempEntry = getTempDestinationAuthorizationEntry();
134                    if (tempEntry == null) {
135                        tempEntry = new TempDestinationAuthorizationEntry();
136                        setTempDestinationAuthorizationEntry(tempEntry);
137                    }
138                    return tempEntry;
139                }
140    
141                // handle regular destinations
142                if (cns.length != (cnsLength + 1)) {
143                    LOG.warn("Policy not applied! Wrong cn for authorization entry " + name);
144                }
145    
146                ActiveMQDestination dest = formatDestination(cns[1], cns[2]);
147    
148                if (dest != null) {
149                    AuthorizationEntry entry = entries.get(dest);
150                    if (entry == null) {
151                        entry = new AuthorizationEntry();
152                        entry.setDestination(dest);
153                        entries.put(dest, entry);
154                    }
155                    return entry;
156                } else {
157                    return null;
158                }
159        }
160    
161        protected ActiveMQDestination formatDestination(String destinationName, String destinationType) {
162                ActiveMQDestination dest = null;
163                if (destinationType.equalsIgnoreCase("ou=queue")) {
164                   dest = new ActiveMQQueue(formatDestinationName(destinationName));
165                } else if (destinationType.equalsIgnoreCase("ou=topic")) {
166                   dest = new ActiveMQTopic(formatDestinationName(destinationName));
167                } else {
168                    LOG.warn("Policy not applied! Unknown destination type " + destinationType);
169                }
170                return dest;
171        }
172    
173        protected void applyACL(AuthorizationEntry entry, SearchResult result) throws NamingException {
174            // find members
175            Attribute cn = result.getAttributes().get("cn");
176            Attribute member = result.getAttributes().get("member");
177            NamingEnumeration<?> memberEnum = member.getAll();
178            HashSet<Object> members = new HashSet<Object>();
179            while (memberEnum.hasMoreElements()) {
180                String elem = (String) memberEnum.nextElement();
181                members.add(new GroupPrincipal(elem.replaceAll("cn=", "")));
182            }
183    
184            // apply privilege
185            if (cn.get().equals("admin")) {
186                entry.setAdminACLs(members);
187            } else if (cn.get().equals("write")) {
188                entry.setWriteACLs(members);
189            } else if (cn.get().equals("read")) {
190                entry.setReadACLs(members);
191            } else {
192                LOG.warn("Policy not applied! Unknown privilege " + result.getName());
193            }
194        }
195    
196        protected String formatDestinationName(String cn) {
197            return cn.replaceFirst("cn=", "").replaceAll(ANY_DESCENDANT, ">");
198        }
199    
200        protected boolean isPriviledge(Binding binding) {
201            String name = binding.getName();
202            if (name.startsWith("cn=admin") || name.startsWith("cn=write") || name.startsWith("cn=read")) {
203                return true;
204            } else {
205                return false;
206            }
207        }
208    
209        @Override
210        protected Set<AuthorizationEntry> getAllEntries(ActiveMQDestination destination) {
211            if (refreshInterval != -1 && System.currentTimeMillis() >= lastUpdated + refreshInterval) {
212    
213                reset();
214                entries.clear();
215    
216                LOG.debug("Updating authorization map!");
217                try {
218                    query();
219                } catch (Exception e) {
220                    LOG.error("Error updating authorization map", e);
221                }
222            }
223    
224            return super.getAllEntries(destination);
225        }
226    
227        @Override
228        public void objectAdded(NamingEvent namingEvent) {
229            LOG.debug("Adding object: " + namingEvent.getNewBinding());
230            SearchResult result = (SearchResult)namingEvent.getNewBinding();
231            if (!isPriviledge(result)) return;
232            AuthorizationEntry entry = getEntry(result.getName());
233            if (entry != null) {
234                try {
235                    applyACL(entry, result);
236                    if (!(entry instanceof TempDestinationAuthorizationEntry)) {
237                        put(entry.getDestination(), entry);
238                    }
239                } catch (NamingException ne) {
240                    LOG.warn("Unable to add entry", ne);
241                }
242            }
243        }
244    
245        @Override
246        public void objectRemoved(NamingEvent namingEvent) {
247            LOG.debug("Removing object: " + namingEvent.getOldBinding());
248            Binding result = namingEvent.getOldBinding();
249            if (!isPriviledge(result)) return;
250            AuthorizationEntry entry = getEntry(result.getName());
251            String[] cns = result.getName().split(",");
252            if (!isPriviledge(result)) return;
253            if (cns[0].equalsIgnoreCase("cn=admin")) {
254                entry.setAdminACLs(new HashSet<Object>());
255            } else if (cns[0].equalsIgnoreCase("cn=write")) {
256                entry.setWriteACLs(new HashSet<Object>());
257            } else if (cns[0].equalsIgnoreCase("cn=read")) {
258                entry.setReadACLs(new HashSet<Object>());
259            } else {
260                LOG.warn("Policy not removed! Unknown privilege " + result.getName());
261            }
262        }
263    
264        @Override
265        public void objectRenamed(NamingEvent namingEvent) {
266            Binding oldBinding = namingEvent.getOldBinding();
267            Binding newBinding = namingEvent.getNewBinding();
268            LOG.debug("Renaming object: " + oldBinding + " to " + newBinding);
269    
270            String[] oldCns = oldBinding.getName().split(",");
271            ActiveMQDestination oldDest = formatDestination(oldCns[0], oldCns[1]);
272    
273            String[] newCns = newBinding.getName().split(",");
274            ActiveMQDestination newDest = formatDestination(newCns[0], newCns[1]);
275    
276            if (oldDest != null && newDest != null) {
277                AuthorizationEntry entry = entries.remove(oldDest);
278                if (entry != null) {
279                    entry.setDestination(newDest);
280                    put(newDest, entry);
281                    remove(oldDest, entry);
282                } else {
283                    LOG.warn("No authorization entry for " + oldDest);
284                }
285            }
286        }
287    
288        @Override
289        public void objectChanged(NamingEvent namingEvent) {
290            LOG.debug("Changing object " + namingEvent.getOldBinding() + " to " + namingEvent.getNewBinding());
291            objectRemoved(namingEvent);
292            objectAdded(namingEvent);
293        }
294    
295        @Override
296        public void namingExceptionThrown(NamingExceptionEvent namingExceptionEvent) {
297            LOG.error("Caught Unexpected Exception", namingExceptionEvent.getException());
298        }
299    
300        // init
301    
302        @Override
303        public void afterPropertiesSet() throws Exception {
304            query();
305        }
306    
307        // getters and setters
308    
309        public String getConnectionURL() {
310            return connectionURL;
311        }
312    
313        public void setConnectionURL(String connectionURL) {
314            this.connectionURL = connectionURL;
315        }
316    
317        public String getConnectionUsername() {
318            return connectionUsername;
319        }
320    
321        public void setConnectionUsername(String connectionUsername) {
322            this.connectionUsername = connectionUsername;
323        }
324    
325        public String getConnectionPassword() {
326            return connectionPassword;
327        }
328    
329        public void setConnectionPassword(String connectionPassword) {
330            this.connectionPassword = connectionPassword;
331        }
332    
333        public String getConnectionProtocol() {
334            return connectionProtocol;
335        }
336    
337        public void setConnectionProtocol(String connectionProtocol) {
338            this.connectionProtocol = connectionProtocol;
339        }
340    
341        public String getAuthentication() {
342            return authentication;
343        }
344    
345        public void setAuthentication(String authentication) {
346            this.authentication = authentication;
347        }
348    
349        public String getBaseDn() {
350            return baseDn;
351        }
352    
353        public void setBaseDn(String baseDn) {
354            this.baseDn = baseDn;
355            cnsLength = baseDn.split(",").length + 4;
356        }
357    
358        public int getRefreshInterval() {
359            return refreshInterval;
360        }
361    
362        public void setRefreshInterval(int refreshInterval) {
363            this.refreshInterval = refreshInterval;
364        }
365    }
366