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