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 org.apache.activemq.command.ActiveMQDestination;
020import org.apache.activemq.command.ActiveMQQueue;
021import org.apache.activemq.command.ActiveMQTopic;
022import org.apache.activemq.filter.DestinationMapEntry;
023import org.apache.activemq.jaas.GroupPrincipal;
024import org.slf4j.Logger;
025import org.slf4j.LoggerFactory;
026import org.springframework.beans.factory.InitializingBean;
027
028import javax.naming.Binding;
029import javax.naming.Context;
030import javax.naming.NamingEnumeration;
031import javax.naming.NamingException;
032import javax.naming.directory.*;
033import javax.naming.event.*;
034import java.util.*;
035
036/**
037 * A {@link DefaultAuthorizationMap} implementation which uses LDAP to initialize and update
038 *
039 * @org.apache.xbean.XBean
040 *
041 */
042public 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