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