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