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.text.MessageFormat;
020import java.util.HashSet;
021import java.util.Hashtable;
022import java.util.Iterator;
023import java.util.Map;
024import java.util.Set;
025
026import javax.naming.Context;
027import javax.naming.NamingEnumeration;
028import javax.naming.NamingException;
029import javax.naming.directory.Attribute;
030import javax.naming.directory.Attributes;
031import javax.naming.directory.DirContext;
032import javax.naming.directory.InitialDirContext;
033import javax.naming.directory.SearchControls;
034import javax.naming.directory.SearchResult;
035import javax.naming.ldap.LdapName;
036import javax.naming.ldap.Rdn;
037
038import org.apache.activemq.advisory.AdvisorySupport;
039import org.apache.activemq.command.ActiveMQDestination;
040import org.apache.activemq.filter.DestinationMap;
041import org.apache.activemq.jaas.GroupPrincipal;
042import org.apache.activemq.jaas.LDAPLoginModule;
043import org.slf4j.Logger;
044import org.slf4j.LoggerFactory;
045
046/**
047 * An {@link AuthorizationMap} which uses LDAP
048 *
049 * @org.apache.xbean.XBean
050 * @author ngcutura
051 */
052public class LDAPAuthorizationMap implements AuthorizationMap {
053
054    public static final String INITIAL_CONTEXT_FACTORY = "initialContextFactory";
055    public static final String CONNECTION_URL = "connectionURL";
056    public static final String CONNECTION_USERNAME = "connectionUsername";
057    public static final String CONNECTION_PASSWORD = "connectionPassword";
058    public static final String CONNECTION_PROTOCOL = "connectionProtocol";
059    public static final String AUTHENTICATION = "authentication";
060
061    public static final String TOPIC_SEARCH_MATCHING = "topicSearchMatching";
062    public static final String TOPIC_SEARCH_SUBTREE = "topicSearchSubtree";
063    public static final String QUEUE_SEARCH_MATCHING = "queueSearchMatching";
064    public static final String QUEUE_SEARCH_SUBTREE = "queueSearchSubtree";
065
066    public static final String ADMIN_BASE = "adminBase";
067    public static final String ADMIN_ATTRIBUTE = "adminAttribute";
068    public static final String READ_BASE = "readBase";
069    public static final String READ_ATTRIBUTE = "readAttribute";
070    public static final String WRITE_BASE = "writeBAse";
071    public static final String WRITE_ATTRIBUTE = "writeAttribute";
072
073    private static final Logger LOG = LoggerFactory.getLogger(LDAPLoginModule.class);
074
075    private String initialContextFactory;
076    private String connectionURL;
077    private String connectionUsername;
078    private String connectionPassword;
079    private String connectionProtocol;
080    private String authentication;
081
082    private DirContext context;
083
084    private MessageFormat topicSearchMatchingFormat;
085    private MessageFormat queueSearchMatchingFormat;
086    private String advisorySearchBase = "uid=ActiveMQ.Advisory,ou=topics,ou=destinations,o=ActiveMQ,dc=example,dc=com";
087    private String tempSearchBase = "uid=ActiveMQ.Temp,ou=topics,ou=destinations,o=ActiveMQ,dc=example,dc=com";
088
089    private boolean topicSearchSubtreeBool = true;
090    private boolean queueSearchSubtreeBool = true;
091    private boolean useAdvisorySearchBase = true;
092
093    private String adminBase;
094    private String adminAttribute;
095    private String readBase;
096    private String readAttribute;
097    private String writeBase;
098    private String writeAttribute;
099
100    public LDAPAuthorizationMap() {
101        // lets setup some sensible defaults
102        initialContextFactory = "com.sun.jndi.ldap.LdapCtxFactory";
103        connectionURL = "ldap://localhost:10389";
104        connectionUsername = "uid=admin,ou=system";
105        connectionProtocol = "s";
106        authentication = "simple";
107
108        topicSearchMatchingFormat = new MessageFormat("uid={0},ou=topics,ou=destinations,o=ActiveMQ,dc=example,dc=com");
109        queueSearchMatchingFormat = new MessageFormat("uid={0},ou=queues,ou=destinations,o=ActiveMQ,dc=example,dc=com");
110
111
112        adminBase = "(cn=admin)";
113        adminAttribute = "uniqueMember";
114        readBase = "(cn=read)";
115        readAttribute = "uniqueMember";
116        writeBase = "(cn=write)";
117        writeAttribute = "uniqueMember";
118    }
119
120    public LDAPAuthorizationMap(Map<String,String> options) {
121        initialContextFactory = options.get(INITIAL_CONTEXT_FACTORY);
122        connectionURL = options.get(CONNECTION_URL);
123        connectionUsername = options.get(CONNECTION_USERNAME);
124        connectionPassword = options.get(CONNECTION_PASSWORD);
125        connectionProtocol = options.get(CONNECTION_PROTOCOL);
126        authentication = options.get(AUTHENTICATION);
127
128        adminBase = options.get(ADMIN_BASE);
129        adminAttribute = options.get(ADMIN_ATTRIBUTE);
130        readBase = options.get(READ_BASE);
131        readAttribute = options.get(READ_ATTRIBUTE);
132        writeBase = options.get(WRITE_BASE);
133        writeAttribute = options.get(WRITE_ATTRIBUTE);
134
135        String topicSearchMatching = options.get(TOPIC_SEARCH_MATCHING);
136        String topicSearchSubtree = options.get(TOPIC_SEARCH_SUBTREE);
137        String queueSearchMatching = options.get(QUEUE_SEARCH_MATCHING);
138        String queueSearchSubtree = options.get(QUEUE_SEARCH_SUBTREE);
139        topicSearchMatchingFormat = new MessageFormat(topicSearchMatching);
140        queueSearchMatchingFormat = new MessageFormat(queueSearchMatching);
141        topicSearchSubtreeBool = Boolean.valueOf(topicSearchSubtree).booleanValue();
142        queueSearchSubtreeBool = Boolean.valueOf(queueSearchSubtree).booleanValue();
143    }
144
145    public Set<GroupPrincipal> getTempDestinationAdminACLs() {
146        try {
147            context = open();
148        } catch (NamingException e) {
149            LOG.error(e.toString());
150            return new HashSet<GroupPrincipal>();
151        }
152        SearchControls constraints = new SearchControls();
153        constraints.setReturningAttributes(new String[] {adminAttribute});
154        return getACLs(tempSearchBase, constraints, adminBase, adminAttribute);
155    }
156
157    public Set<GroupPrincipal> getTempDestinationReadACLs() {
158        try {
159            context = open();
160        } catch (NamingException e) {
161            LOG.error(e.toString());
162            return new HashSet<GroupPrincipal>();
163        }
164        SearchControls constraints = new SearchControls();
165        constraints.setReturningAttributes(new String[] {readAttribute});
166        return getACLs(tempSearchBase, constraints, readBase, readAttribute);
167    }
168
169    public Set<GroupPrincipal> getTempDestinationWriteACLs() {
170        try {
171            context = open();
172        } catch (NamingException e) {
173            LOG.error(e.toString());
174            return new HashSet<GroupPrincipal>();
175        }
176        SearchControls constraints = new SearchControls();
177        constraints.setReturningAttributes(new String[] {writeAttribute});
178        return getACLs(tempSearchBase, constraints, writeBase, writeAttribute);
179    }
180
181    public Set<GroupPrincipal> getAdminACLs(ActiveMQDestination destination) {
182        if (destination.isComposite()) {
183            return getCompositeACLs(destination, adminBase, adminAttribute);
184        }
185        return getACLs(destination, adminBase, adminAttribute);
186    }
187
188    public Set<GroupPrincipal> getReadACLs(ActiveMQDestination destination) {
189        if (destination.isComposite()) {
190            return getCompositeACLs(destination, readBase, readAttribute);
191        }
192        return getACLs(destination, readBase, readAttribute);
193    }
194
195    public Set<GroupPrincipal> getWriteACLs(ActiveMQDestination destination) {
196        if (destination.isComposite()) {
197            return getCompositeACLs(destination, writeBase, writeAttribute);
198        }
199        return getACLs(destination, writeBase, writeAttribute);
200    }
201
202    // Properties
203    // -------------------------------------------------------------------------
204
205    public String getAdminAttribute() {
206        return adminAttribute;
207    }
208
209    public void setAdminAttribute(String adminAttribute) {
210        this.adminAttribute = adminAttribute;
211    }
212
213    public String getAdminBase() {
214        return adminBase;
215    }
216
217    public void setAdminBase(String adminBase) {
218        this.adminBase = adminBase;
219    }
220
221    public String getAuthentication() {
222        return authentication;
223    }
224
225    public void setAuthentication(String authentication) {
226        this.authentication = authentication;
227    }
228
229    public String getConnectionPassword() {
230        return connectionPassword;
231    }
232
233    public void setConnectionPassword(String connectionPassword) {
234        this.connectionPassword = connectionPassword;
235    }
236
237    public String getConnectionProtocol() {
238        return connectionProtocol;
239    }
240
241    public void setConnectionProtocol(String connectionProtocol) {
242        this.connectionProtocol = connectionProtocol;
243    }
244
245    public String getConnectionURL() {
246        return connectionURL;
247    }
248
249    public void setConnectionURL(String connectionURL) {
250        this.connectionURL = connectionURL;
251    }
252
253    public String getConnectionUsername() {
254        return connectionUsername;
255    }
256
257    public void setConnectionUsername(String connectionUsername) {
258        this.connectionUsername = connectionUsername;
259    }
260
261    public DirContext getContext() {
262        return context;
263    }
264
265    public void setContext(DirContext context) {
266        this.context = context;
267    }
268
269    public String getInitialContextFactory() {
270        return initialContextFactory;
271    }
272
273    public void setInitialContextFactory(String initialContextFactory) {
274        this.initialContextFactory = initialContextFactory;
275    }
276
277    public MessageFormat getQueueSearchMatchingFormat() {
278        return queueSearchMatchingFormat;
279    }
280
281    public void setQueueSearchMatchingFormat(MessageFormat queueSearchMatchingFormat) {
282        this.queueSearchMatchingFormat = queueSearchMatchingFormat;
283    }
284
285    public boolean isQueueSearchSubtreeBool() {
286        return queueSearchSubtreeBool;
287    }
288
289    public void setQueueSearchSubtreeBool(boolean queueSearchSubtreeBool) {
290        this.queueSearchSubtreeBool = queueSearchSubtreeBool;
291    }
292
293    public String getReadAttribute() {
294        return readAttribute;
295    }
296
297    public void setReadAttribute(String readAttribute) {
298        this.readAttribute = readAttribute;
299    }
300
301    public String getReadBase() {
302        return readBase;
303    }
304
305    public void setReadBase(String readBase) {
306        this.readBase = readBase;
307    }
308
309    public MessageFormat getTopicSearchMatchingFormat() {
310        return topicSearchMatchingFormat;
311    }
312
313    public void setTopicSearchMatchingFormat(MessageFormat topicSearchMatchingFormat) {
314        this.topicSearchMatchingFormat = topicSearchMatchingFormat;
315    }
316
317    public boolean isTopicSearchSubtreeBool() {
318        return topicSearchSubtreeBool;
319    }
320
321    public void setTopicSearchSubtreeBool(boolean topicSearchSubtreeBool) {
322        this.topicSearchSubtreeBool = topicSearchSubtreeBool;
323    }
324
325    public String getWriteAttribute() {
326        return writeAttribute;
327    }
328
329    public void setWriteAttribute(String writeAttribute) {
330        this.writeAttribute = writeAttribute;
331    }
332
333    public String getWriteBase() {
334        return writeBase;
335    }
336
337    public void setWriteBase(String writeBase) {
338        this.writeBase = writeBase;
339    }
340
341    public boolean isUseAdvisorySearchBase() {
342        return useAdvisorySearchBase;
343    }
344
345    public void setUseAdvisorySearchBase(boolean useAdvisorySearchBase) {
346        this.useAdvisorySearchBase = useAdvisorySearchBase;
347    }
348
349    public String getAdvisorySearchBase() {
350        return advisorySearchBase;
351    }
352
353    public void setAdvisorySearchBase(String advisorySearchBase) {
354        this.advisorySearchBase = advisorySearchBase;
355    }
356
357    public String getTempSearchBase() {
358        return tempSearchBase;
359    }
360
361    public void setTempSearchBase(String tempSearchBase) {
362        this.tempSearchBase = tempSearchBase;
363    }
364
365    protected Set<GroupPrincipal> getCompositeACLs(ActiveMQDestination destination, String roleBase, String roleAttribute) {
366        ActiveMQDestination[] dests = destination.getCompositeDestinations();
367        Set<GroupPrincipal> acls = null;
368        for (ActiveMQDestination dest : dests) {
369            acls = DestinationMap.union(acls, getACLs(dest, roleBase, roleAttribute));
370            if (acls == null || acls.isEmpty()) {
371                break;
372            }
373        }
374        return acls;
375    }
376
377    // Implementation methods
378    // -------------------------------------------------------------------------
379    protected Set<GroupPrincipal> getACLs(ActiveMQDestination destination, String roleBase, String roleAttribute) {
380        try {
381            context = open();
382        } catch (NamingException e) {
383            LOG.error(e.toString());
384            return new HashSet<GroupPrincipal>();
385        }
386
387
388
389        String destinationBase = "";
390        SearchControls constraints = new SearchControls();
391        if (AdvisorySupport.isAdvisoryTopic(destination) && useAdvisorySearchBase) {
392            destinationBase = advisorySearchBase;
393        } else {
394            if ((destination.getDestinationType() & ActiveMQDestination.QUEUE_TYPE) == ActiveMQDestination.QUEUE_TYPE) {
395                destinationBase = queueSearchMatchingFormat.format(new String[]{destination.getPhysicalName()});
396                if (queueSearchSubtreeBool) {
397                    constraints.setSearchScope(SearchControls.SUBTREE_SCOPE);
398                } else {
399                    constraints.setSearchScope(SearchControls.ONELEVEL_SCOPE);
400                }
401            }
402            if ((destination.getDestinationType() & ActiveMQDestination.TOPIC_TYPE) == ActiveMQDestination.TOPIC_TYPE) {
403                destinationBase = topicSearchMatchingFormat.format(new String[]{destination.getPhysicalName()});
404                if (topicSearchSubtreeBool) {
405                    constraints.setSearchScope(SearchControls.SUBTREE_SCOPE);
406                } else {
407                    constraints.setSearchScope(SearchControls.ONELEVEL_SCOPE);
408                }
409            }
410        }
411
412        constraints.setReturningAttributes(new String[] {roleAttribute});
413
414        return getACLs(destinationBase, constraints, roleBase, roleAttribute);
415    }
416
417    protected Set<GroupPrincipal> getACLs(String destinationBase, SearchControls constraints, String roleBase, String roleAttribute) {
418        try {
419            Set<GroupPrincipal> roles = new HashSet<GroupPrincipal>();
420            Set<String> acls = new HashSet<String>();
421            NamingEnumeration<?> results = context.search(destinationBase, roleBase, constraints);
422            while (results.hasMore()) {
423                SearchResult result = (SearchResult)results.next();
424                Attributes attrs = result.getAttributes();
425                if (attrs == null) {
426                    continue;
427                }
428                acls = addAttributeValues(roleAttribute, attrs, acls);
429            }
430            for (Iterator<String> iter = acls.iterator(); iter.hasNext();) {
431                String roleName = iter.next();
432                LdapName ldapname = new LdapName(roleName);
433                Rdn rdn = ldapname.getRdn(ldapname.size() - 1);
434                LOG.debug("Found role: [" + rdn.getValue().toString() + "]");
435                roles.add(new GroupPrincipal(rdn.getValue().toString()));
436            }
437            return roles;
438        } catch (NamingException e) {
439            LOG.error(e.toString());
440            return new HashSet<GroupPrincipal>();
441        }
442    }
443
444    protected Set<String> addAttributeValues(String attrId, Attributes attrs, Set<String> values) throws NamingException {
445        if (attrId == null || attrs == null) {
446            return values;
447        }
448        if (values == null) {
449            values = new HashSet<String>();
450        }
451        Attribute attr = attrs.get(attrId);
452        if (attr == null) {
453            return values;
454        }
455        NamingEnumeration<?> e = attr.getAll();
456        while (e.hasMore()) {
457            String value = (String)e.next();
458            values.add(value);
459        }
460        return values;
461    }
462
463    protected DirContext open() throws NamingException {
464        if (context != null) {
465            return context;
466        }
467
468        try {
469            Hashtable<String, String> env = new Hashtable<String, String>();
470            env.put(Context.INITIAL_CONTEXT_FACTORY, initialContextFactory);
471            if (connectionUsername != null && !"".equals(connectionUsername)) {
472                env.put(Context.SECURITY_PRINCIPAL, connectionUsername);
473            } else {
474                throw new NamingException("Empty username is not allowed");
475            }
476            if (connectionPassword != null && !"".equals(connectionPassword)) {
477                env.put(Context.SECURITY_CREDENTIALS, connectionPassword);
478            } else {
479                throw new NamingException("Empty password is not allowed");
480            }
481            env.put(Context.SECURITY_PROTOCOL, connectionProtocol);
482            env.put(Context.PROVIDER_URL, connectionURL);
483            env.put(Context.SECURITY_AUTHENTICATION, authentication);
484            context = new InitialDirContext(env);
485
486        } catch (NamingException e) {
487            LOG.error(e.toString());
488            throw e;
489        }
490        return context;
491    }
492
493}