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.util.ArrayList; 020import java.util.HashSet; 021import java.util.Hashtable; 022import java.util.Map; 023import java.util.Set; 024import java.util.concurrent.ConcurrentHashMap; 025import java.util.concurrent.LinkedBlockingQueue; 026import java.util.concurrent.ThreadFactory; 027import java.util.concurrent.ThreadPoolExecutor; 028import java.util.concurrent.TimeUnit; 029import java.util.concurrent.atomic.AtomicReference; 030import java.util.concurrent.locks.ReentrantReadWriteLock; 031 032import javax.naming.Binding; 033import javax.naming.Context; 034import javax.naming.InvalidNameException; 035import javax.naming.NamingEnumeration; 036import javax.naming.NamingException; 037import javax.naming.directory.Attribute; 038import javax.naming.directory.Attributes; 039import javax.naming.directory.DirContext; 040import javax.naming.directory.InitialDirContext; 041import javax.naming.directory.SearchControls; 042import javax.naming.directory.SearchResult; 043import javax.naming.event.EventDirContext; 044import javax.naming.event.NamespaceChangeListener; 045import javax.naming.event.NamingEvent; 046import javax.naming.event.NamingExceptionEvent; 047import javax.naming.event.ObjectChangeListener; 048import javax.naming.ldap.LdapName; 049import javax.naming.ldap.Rdn; 050 051import org.apache.activemq.command.ActiveMQDestination; 052import org.apache.activemq.command.ActiveMQQueue; 053import org.apache.activemq.command.ActiveMQTopic; 054import org.apache.activemq.filter.DestinationMapEntry; 055import org.apache.activemq.jaas.UserPrincipal; 056import org.slf4j.Logger; 057import org.slf4j.LoggerFactory; 058 059public class SimpleCachedLDAPAuthorizationMap implements AuthorizationMap { 060 061 private static final Logger LOG = LoggerFactory.getLogger(SimpleCachedLDAPAuthorizationMap.class); 062 063 // Configuration Options 064 private final String initialContextFactory = "com.sun.jndi.ldap.LdapCtxFactory"; 065 private String connectionURL = "ldap://localhost:1024"; 066 private String connectionUsername = "uid=admin,ou=system"; 067 private String connectionPassword = "secret"; 068 private String connectionProtocol = "s"; 069 private String authentication = "simple"; 070 071 private int queuePrefixLength = 4; 072 private int topicPrefixLength = 4; 073 private int tempPrefixLength = 4; 074 075 private String queueSearchBase = "ou=Queue,ou=Destination,ou=ActiveMQ,ou=system"; 076 private String topicSearchBase = "ou=Topic,ou=Destination,ou=ActiveMQ,ou=system"; 077 private String tempSearchBase = "ou=Temp,ou=Destination,ou=ActiveMQ,ou=system"; 078 079 private String permissionGroupMemberAttribute = "member"; 080 081 private String adminPermissionGroupSearchFilter = "(cn=Admin)"; 082 private String readPermissionGroupSearchFilter = "(cn=Read)"; 083 private String writePermissionGroupSearchFilter = "(cn=Write)"; 084 085 private boolean legacyGroupMapping = true; 086 private String groupObjectClass = "groupOfNames"; 087 private String userObjectClass = "person"; 088 private String groupNameAttribute = "cn"; 089 private String userNameAttribute = "uid"; 090 091 private int refreshInterval = -1; 092 private boolean refreshDisabled = false; 093 094 protected String groupClass = DefaultAuthorizationMap.DEFAULT_GROUP_CLASS; 095 096 // Internal State 097 private long lastUpdated = -1; 098 099 private static String ANY_DESCENDANT = "\\$"; 100 101 protected DirContext context; 102 private EventDirContext eventContext; 103 104 private final AtomicReference<DefaultAuthorizationMap> map = 105 new AtomicReference<DefaultAuthorizationMap>(new DefaultAuthorizationMap()); 106 private final ThreadPoolExecutor updaterService; 107 108 protected Map<ActiveMQDestination, AuthorizationEntry> entries = 109 new ConcurrentHashMap<ActiveMQDestination, AuthorizationEntry>(); 110 111 public SimpleCachedLDAPAuthorizationMap() { 112 // Allow for only a couple outstanding update request, they can be slow so we 113 // don't want a bunch to pile up for no reason. 114 updaterService = new ThreadPoolExecutor(0, 1, 60, TimeUnit.SECONDS, 115 new LinkedBlockingQueue<Runnable>(2), 116 new ThreadFactory() { 117 118 @Override 119 public Thread newThread(Runnable r) { 120 return new Thread(r, "SimpleCachedLDAPAuthorizationMap update thread"); 121 } 122 }); 123 updaterService.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardPolicy()); 124 } 125 126 protected DirContext createContext() throws NamingException { 127 Hashtable<String, String> env = new Hashtable<String, String>(); 128 env.put(Context.INITIAL_CONTEXT_FACTORY, initialContextFactory); 129 if (connectionUsername != null && !"".equals(connectionUsername)) { 130 env.put(Context.SECURITY_PRINCIPAL, connectionUsername); 131 } else { 132 throw new NamingException("Empty username is not allowed"); 133 } 134 if (connectionPassword != null && !"".equals(connectionPassword)) { 135 env.put(Context.SECURITY_CREDENTIALS, connectionPassword); 136 } else { 137 throw new NamingException("Empty password is not allowed"); 138 } 139 env.put(Context.SECURITY_PROTOCOL, connectionProtocol); 140 env.put(Context.PROVIDER_URL, connectionURL); 141 env.put(Context.SECURITY_AUTHENTICATION, authentication); 142 return new InitialDirContext(env); 143 } 144 145 protected boolean isContextAlive() { 146 boolean alive = false; 147 if (context != null) { 148 try { 149 context.getAttributes(""); 150 alive = true; 151 } catch (Exception e) { 152 } 153 } 154 return alive; 155 } 156 157 /** 158 * Returns the existing open context or creates a new one and registers listeners for push notifications if such an 159 * update style is enabled. This implementation should not be invoked concurrently. 160 * 161 * @return the current context 162 * 163 * @throws NamingException 164 * if there is an error setting things up 165 */ 166 protected DirContext open() throws NamingException { 167 if (isContextAlive()) { 168 return context; 169 } 170 171 try { 172 context = createContext(); 173 if (refreshInterval == -1 && !refreshDisabled) { 174 eventContext = ((EventDirContext) context.lookup("")); 175 176 final SearchControls constraints = new SearchControls(); 177 constraints.setSearchScope(SearchControls.SUBTREE_SCOPE); 178 179 // Listeners for Queue policy // 180 181 // Listeners for each type of permission 182 for (PermissionType permissionType : PermissionType.values()) { 183 eventContext.addNamingListener(queueSearchBase, getFilterForPermissionType(permissionType), constraints, 184 this.new CachedLDAPAuthorizationMapNamespaceChangeListener(DestinationType.QUEUE, permissionType)); 185 } 186 // Listener for changes to the destination pattern entry itself and not a permission entry. 187 eventContext.addNamingListener(queueSearchBase, "cn=*", new SearchControls(), this.new CachedLDAPAuthorizationMapNamespaceChangeListener( 188 DestinationType.QUEUE, null)); 189 190 // Listeners for Topic policy // 191 192 // Listeners for each type of permission 193 for (PermissionType permissionType : PermissionType.values()) { 194 eventContext.addNamingListener(topicSearchBase, getFilterForPermissionType(permissionType), constraints, 195 this.new CachedLDAPAuthorizationMapNamespaceChangeListener(DestinationType.TOPIC, permissionType)); 196 } 197 // Listener for changes to the destination pattern entry itself and not a permission entry. 198 eventContext.addNamingListener(topicSearchBase, "cn=*", new SearchControls(), this.new CachedLDAPAuthorizationMapNamespaceChangeListener( 199 DestinationType.TOPIC, null)); 200 201 // Listeners for Temp policy // 202 203 // Listeners for each type of permission 204 for (PermissionType permissionType : PermissionType.values()) { 205 eventContext.addNamingListener(tempSearchBase, getFilterForPermissionType(permissionType), constraints, 206 this.new CachedLDAPAuthorizationMapNamespaceChangeListener(DestinationType.TEMP, permissionType)); 207 } 208 209 } 210 } catch (NamingException e) { 211 context = null; 212 throw e; 213 } 214 215 return context; 216 } 217 218 /** 219 * Queries the directory and initializes the policy based on the data in the directory. This implementation should 220 * not be invoked concurrently. 221 * 222 * @throws Exception 223 * if there is an unrecoverable error processing the directory contents 224 */ 225 @SuppressWarnings("rawtypes") 226 protected synchronized void query() throws Exception { 227 DirContext currentContext = open(); 228 entries.clear(); 229 230 final SearchControls constraints = new SearchControls(); 231 constraints.setSearchScope(SearchControls.SUBTREE_SCOPE); 232 233 DefaultAuthorizationMap newMap = new DefaultAuthorizationMap(); 234 for (PermissionType permissionType : PermissionType.values()) { 235 try { 236 processQueryResults(newMap, 237 currentContext.search(queueSearchBase, getFilterForPermissionType(permissionType), 238 constraints), DestinationType.QUEUE, permissionType); 239 } catch (Exception e) { 240 LOG.error("Policy not applied!. Error processing policy under '{}' with filter '{}'", new Object[]{ queueSearchBase, getFilterForPermissionType(permissionType) }, e); 241 } 242 } 243 244 for (PermissionType permissionType : PermissionType.values()) { 245 try { 246 processQueryResults(newMap, 247 currentContext.search(topicSearchBase, getFilterForPermissionType(permissionType), 248 constraints), DestinationType.TOPIC, permissionType); 249 } catch (Exception e) { 250 LOG.error("Policy not applied!. Error processing policy under '{}' with filter '{}'", new Object[]{ topicSearchBase, getFilterForPermissionType(permissionType) }, e); 251 } 252 } 253 254 for (PermissionType permissionType : PermissionType.values()) { 255 try { 256 processQueryResults(newMap, 257 currentContext.search(tempSearchBase, getFilterForPermissionType(permissionType), 258 constraints), DestinationType.TEMP, permissionType); 259 } catch (Exception e) { 260 LOG.error("Policy not applied!. Error processing policy under '{}' with filter '{}'", new Object[]{ tempSearchBase, getFilterForPermissionType(permissionType) }, e); 261 } 262 } 263 264 // Create and swap in the new instance with updated LDAP data. 265 newMap.setAuthorizationEntries(new ArrayList<DestinationMapEntry>(entries.values())); 266 newMap.setGroupClass(groupClass); 267 this.map.set(newMap); 268 269 updated(); 270 } 271 272 /** 273 * Processes results from a directory query in the context of a given destination type and permission type. This 274 * implementation should not be invoked concurrently. 275 * 276 * @param results 277 * the results to process 278 * @param destinationType 279 * the type of the destination for which the directory results apply 280 * @param permissionType 281 * the type of the permission for which the directory results apply 282 * 283 * @throws Exception 284 * if there is an error processing the results 285 */ 286 protected void processQueryResults(DefaultAuthorizationMap map, NamingEnumeration<SearchResult> results, DestinationType destinationType, PermissionType permissionType) 287 throws Exception { 288 289 while (results.hasMore()) { 290 SearchResult result = results.next(); 291 AuthorizationEntry entry = null; 292 293 try { 294 entry = getEntry(map, new LdapName(result.getNameInNamespace()), destinationType); 295 } catch (Exception e) { 296 LOG.error("Policy not applied! Error parsing authorization policy entry under {}", result.getNameInNamespace(), e); 297 continue; 298 } 299 300 applyACL(entry, result, permissionType); 301 } 302 } 303 304 /** 305 * Marks the time at which the authorization state was last refreshed. Relevant for synchronous 306 * policy updates. This implementation should not be invoked concurrently. 307 */ 308 protected void updated() { 309 lastUpdated = System.currentTimeMillis(); 310 } 311 312 /** 313 * Retrieves or creates the {@link AuthorizationEntry} that corresponds to the DN in {@code dn}. This implementation 314 * should not be invoked concurrently. 315 * 316 * @param map 317 * the DefaultAuthorizationMap to operate on. 318 * @param dn 319 * the DN representing the policy entry in the directory 320 * @param destinationType 321 * the type of the destination to get/create the entry for 322 * 323 * @return the corresponding authorization entry for the DN 324 * 325 * @throws IllegalArgumentException 326 * if destination type is not one of {@link DestinationType#QUEUE}, {@link DestinationType#TOPIC}, 327 * {@link DestinationType#TEMP} or if the policy entry DN is malformed 328 */ 329 protected AuthorizationEntry getEntry(DefaultAuthorizationMap map, LdapName dn, DestinationType destinationType) { 330 AuthorizationEntry entry = null; 331 switch (destinationType) { 332 case TEMP: 333 // handle temp entry 334 if (dn.size() != getPrefixLengthForDestinationType(destinationType) + 1) { 335 // handle unknown entry 336 throw new IllegalArgumentException("Malformed policy structure for a temporary destination " 337 + "policy entry. The permission group entries should be immediately below the " + "temporary policy base DN."); 338 } 339 entry = map.getTempDestinationAuthorizationEntry(); 340 if (entry == null) { 341 entry = new TempDestinationAuthorizationEntry(); 342 map.setTempDestinationAuthorizationEntry((TempDestinationAuthorizationEntry) entry); 343 } 344 345 break; 346 347 case QUEUE: 348 case TOPIC: 349 // handle regular destinations 350 if (dn.size() != getPrefixLengthForDestinationType(destinationType) + 2) { 351 throw new IllegalArgumentException("Malformed policy structure for a queue or topic destination " 352 + "policy entry. The destination pattern and permission group entries should be " + "nested below the queue or topic policy base DN."); 353 } 354 355 ActiveMQDestination dest = formatDestination(dn, destinationType); 356 357 if (dest != null) { 358 entry = entries.get(dest); 359 if (entry == null) { 360 entry = new AuthorizationEntry(); 361 entry.setDestination(dest); 362 entries.put(dest, entry); 363 } 364 } 365 366 break; 367 default: 368 // handle unknown entry 369 throw new IllegalArgumentException("Unknown destination type " + destinationType); 370 } 371 372 return entry; 373 } 374 375 /** 376 * Applies the policy from the directory to the given entry within the context of the provided permission type. 377 * 378 * @param entry 379 * the policy entry to apply the policy to 380 * @param result 381 * the results from the directory to apply to the policy entry 382 * @param permissionType 383 * the permission type of the data in the directory 384 * 385 * @throws NamingException 386 * if there is an error applying the ACL 387 */ 388 protected void applyACL(AuthorizationEntry entry, SearchResult result, PermissionType permissionType) throws NamingException { 389 390 // Find members 391 Attribute memberAttribute = result.getAttributes().get(permissionGroupMemberAttribute); 392 NamingEnumeration<?> memberAttributeEnum = memberAttribute.getAll(); 393 394 HashSet<Object> members = new HashSet<Object>(); 395 396 while (memberAttributeEnum.hasMoreElements()) { 397 String memberDn = (String) memberAttributeEnum.nextElement(); 398 boolean group = false; 399 boolean user = false; 400 String principalName = null; 401 402 if (!legacyGroupMapping) { 403 // Lookup of member to determine principal type (group or user) and name. 404 Attributes memberAttributes; 405 try { 406 memberAttributes = context.getAttributes(memberDn, new String[] { "objectClass", groupNameAttribute, userNameAttribute }); 407 } catch (NamingException e) { 408 LOG.error("Policy not applied! Unknown member {} in policy entry {}", new Object[]{ memberDn, result.getNameInNamespace() }, e); 409 continue; 410 } 411 412 Attribute memberEntryObjectClassAttribute = memberAttributes.get("objectClass"); 413 NamingEnumeration<?> memberEntryObjectClassAttributeEnum = memberEntryObjectClassAttribute.getAll(); 414 415 while (memberEntryObjectClassAttributeEnum.hasMoreElements()) { 416 String objectClass = (String) memberEntryObjectClassAttributeEnum.nextElement(); 417 418 if (objectClass.equalsIgnoreCase(groupObjectClass)) { 419 group = true; 420 Attribute name = memberAttributes.get(groupNameAttribute); 421 if (name == null) { 422 LOG.error("Policy not applied! Group {} does not have name attribute {} under entry {}", new Object[]{ memberDn, groupNameAttribute, result.getNameInNamespace() }); 423 break; 424 } 425 426 principalName = (String) name.get(); 427 } 428 429 if (objectClass.equalsIgnoreCase(userObjectClass)) { 430 user = true; 431 Attribute name = memberAttributes.get(userNameAttribute); 432 if (name == null) { 433 LOG.error("Policy not applied! User {} does not have name attribute {} under entry {}", new Object[]{ memberDn, userNameAttribute, result.getNameInNamespace() }); 434 break; 435 } 436 437 principalName = (String) name.get(); 438 } 439 } 440 441 } else { 442 group = true; 443 principalName = memberDn.replaceAll("(cn|CN)=", ""); 444 } 445 446 if ((!group && !user) || (group && user)) { 447 LOG.error("Policy not applied! Can't determine type of member {} under entry {}", memberDn, result.getNameInNamespace()); 448 } else if (principalName != null) { 449 DefaultAuthorizationMap map = this.map.get(); 450 if (group && !user) { 451 try { 452 members.add(DefaultAuthorizationMap.createGroupPrincipal(principalName, map.getGroupClass())); 453 } catch (Exception e) { 454 NamingException ne = new NamingException( 455 "Can't create a group " + principalName + " of class " + map.getGroupClass()); 456 ne.initCause(e); 457 throw ne; 458 } 459 } else if (!group && user) { 460 members.add(new UserPrincipal(principalName)); 461 } 462 } 463 } 464 465 try { 466 applyAcl(entry, permissionType, members); 467 } catch (Exception e) { 468 LOG.error("Policy not applied! Error adding principals to ACL under {}", result.getNameInNamespace(), e); 469 } 470 } 471 472 /** 473 * Applies policy to the entry given the actual principals that will be applied to the policy entry. 474 * 475 * @param entry 476 * the policy entry to which the policy should be applied 477 * @param permissionType 478 * the type of the permission that the policy will be applied to 479 * @param acls 480 * the principals that represent the actual policy 481 * 482 * @throw IllegalArgumentException if {@code permissionType} is unsupported 483 */ 484 protected void applyAcl(AuthorizationEntry entry, PermissionType permissionType, Set<Object> acls) { 485 486 switch (permissionType) { 487 case READ: 488 entry.setReadACLs(acls); 489 break; 490 case WRITE: 491 entry.setWriteACLs(acls); 492 break; 493 case ADMIN: 494 entry.setAdminACLs(acls); 495 break; 496 default: 497 throw new IllegalArgumentException("Unknown permission " + permissionType + "."); 498 } 499 } 500 501 /** 502 * Parses a DN into the equivalent {@link ActiveMQDestination}. The default implementation expects a format of 503 * cn=<PERMISSION_NAME>,ou=<DESTINATION_PATTERN>,.... or ou=<DESTINATION_PATTERN>,.... for permission and 504 * destination entries, respectively. For example {@code cn=admin,ou=$,ou=...} or {@code ou=$,ou=...}. 505 * 506 * @param dn 507 * the DN to parse 508 * @param destinationType 509 * the type of the destination that we are parsing 510 * 511 * @return the destination that the DN represents 512 * 513 * @throws IllegalArgumentException 514 * if {@code destinationType} is {@link DestinationType#TEMP} or if the format of {@code dn} is 515 * incorrect for for a topic or queue 516 * 517 * @see #formatDestination(Rdn, DestinationType) 518 */ 519 protected ActiveMQDestination formatDestination(LdapName dn, DestinationType destinationType) { 520 ActiveMQDestination destination = null; 521 522 switch (destinationType) { 523 case QUEUE: 524 case TOPIC: 525 // There exists a need to deal with both names representing a permission or simply a 526 // destination. As such, we need to determine the proper RDN to work with based 527 // on the destination type and the DN size. 528 if (dn.size() == (getPrefixLengthForDestinationType(destinationType) + 2)) { 529 destination = formatDestination(dn.getRdn(dn.size() - 2), destinationType); 530 } else if (dn.size() == (getPrefixLengthForDestinationType(destinationType) + 1)) { 531 destination = formatDestination(dn.getRdn(dn.size() - 1), destinationType); 532 } else { 533 throw new IllegalArgumentException("Malformed DN for representing a permission or destination entry."); 534 } 535 break; 536 default: 537 throw new IllegalArgumentException("Cannot format destination for destination type " + destinationType); 538 } 539 540 return destination; 541 } 542 543 /** 544 * Parses RDN values representing the destination name/pattern and destination type into the equivalent 545 * {@link ActiveMQDestination}. 546 * 547 * @param destinationName 548 * the RDN representing the name or pattern for the destination 549 * @param destinationType 550 * the type of the destination 551 * 552 * @return the destination that the RDN represent 553 * 554 * @throws IllegalArgumentException 555 * if {@code destinationType} is not one of {@link DestinationType#TOPIC} or 556 * {@link DestinationType#QUEUE}. 557 * 558 * @see #formatDestinationName(Rdn) 559 * @see #formatDestination(LdapName, DestinationType) 560 */ 561 protected ActiveMQDestination formatDestination(Rdn destinationName, DestinationType destinationType) { 562 ActiveMQDestination dest = null; 563 564 switch (destinationType) { 565 case QUEUE: 566 dest = new ActiveMQQueue(formatDestinationName(destinationName)); 567 break; 568 case TOPIC: 569 dest = new ActiveMQTopic(formatDestinationName(destinationName)); 570 break; 571 default: 572 throw new IllegalArgumentException("Unknown destination type: " + destinationType); 573 } 574 575 return dest; 576 } 577 578 /** 579 * Parses the RDN representing a destination name/pattern into the standard string representation of the 580 * name/pattern. This implementation does not care about the type of the RDN such that the RDN could be a CN or OU. 581 * 582 * @param destinationName 583 * the RDN representing the name or pattern for the destination 584 * 585 * @see #formatDestination(Rdn, Rdn) 586 */ 587 protected String formatDestinationName(Rdn destinationName) { 588 return destinationName.getValue().toString().replaceAll(ANY_DESCENDANT, ">"); 589 } 590 591 /** 592 * Transcribes an existing set into a new set. Used to make defensive copies for concurrent access. 593 * 594 * @param source 595 * the source set or {@code null} 596 * 597 * @return a new set containing the same elements as {@code source} or {@code null} if {@code source} is 598 * {@code null} 599 */ 600 protected <T> Set<T> transcribeSet(Set<T> source) { 601 if (source != null) { 602 return new HashSet<T>(source); 603 } else { 604 return null; 605 } 606 } 607 608 /** 609 * Returns the filter string for the given permission type. 610 * 611 * @throws IllegalArgumentException 612 * if {@code permissionType} is not supported 613 * 614 * @see #setAdminPermissionGroupSearchFilter(String) 615 * @see #setReadPermissionGroupSearchFilter(String) 616 * @see #setWritePermissionGroupSearchFilter(String) 617 */ 618 protected String getFilterForPermissionType(PermissionType permissionType) { 619 String filter = null; 620 621 switch (permissionType) { 622 case ADMIN: 623 filter = adminPermissionGroupSearchFilter; 624 break; 625 case READ: 626 filter = readPermissionGroupSearchFilter; 627 break; 628 case WRITE: 629 filter = writePermissionGroupSearchFilter; 630 break; 631 default: 632 throw new IllegalArgumentException("Unknown permission type " + permissionType); 633 } 634 635 return filter; 636 } 637 638 /** 639 * Returns the DN prefix size based on the given destination type. 640 * 641 * @throws IllegalArgumentException 642 * if {@code destinationType} is not supported 643 * 644 * @see #setQueueSearchBase(String) 645 * @see #setTopicSearchBase(String) 646 * @see #setTempSearchBase(String) 647 */ 648 protected int getPrefixLengthForDestinationType(DestinationType destinationType) { 649 int filter = 0; 650 651 switch (destinationType) { 652 case QUEUE: 653 filter = queuePrefixLength; 654 break; 655 case TOPIC: 656 filter = topicPrefixLength; 657 break; 658 case TEMP: 659 filter = tempPrefixLength; 660 break; 661 default: 662 throw new IllegalArgumentException("Unknown permission type " + destinationType); 663 } 664 665 return filter; 666 } 667 668 /** 669 * Performs a check for updates from the server in the event that synchronous updates are enabled and are the 670 * refresh interval has elapsed. 671 */ 672 protected void checkForUpdates() { 673 if (lastUpdated == -1) { 674 //ACL's have never been queried, but we need them NOW as we're being asked for them. 675 try { 676 query(); 677 return; 678 } catch (Exception e) { 679 LOG.error("Error updating authorization map. Partial policy may be applied until the next successful update.", e); 680 } 681 } 682 683 if (context != null && refreshDisabled) { 684 return; 685 } 686 687 if (context == null || (!refreshDisabled && (refreshInterval != -1 && System.currentTimeMillis() >= lastUpdated + refreshInterval))) { 688 this.updaterService.execute(new Runnable() { 689 @Override 690 public void run() { 691 692 // Check again in case of stacked update request. 693 if (context == null || (!refreshDisabled && 694 (refreshInterval != -1 && System.currentTimeMillis() >= lastUpdated + refreshInterval))) { 695 696 if (!isContextAlive()) { 697 try { 698 context = createContext(); 699 } catch (NamingException ne) { 700 // LDAP is down, use already cached values 701 return; 702 } 703 } 704 705 LOG.debug("Updating authorization map!"); 706 try { 707 query(); 708 } catch (Exception e) { 709 LOG.error("Error updating authorization map. Partial policy may be applied until the next successful update.", e); 710 } 711 } 712 } 713 }); 714 } 715 } 716 717 // Authorization Map 718 719 /** 720 * Provides synchronized and defensive access to the admin ACLs for temp destinations as the super implementation 721 * returns live copies of the ACLs and {@link AuthorizationEntry} is not setup for concurrent access. 722 */ 723 @Override 724 public Set<Object> getTempDestinationAdminACLs() { 725 checkForUpdates(); 726 DefaultAuthorizationMap map = this.map.get(); 727 return transcribeSet(map.getTempDestinationAdminACLs()); 728 } 729 730 /** 731 * Provides synchronized and defensive access to the read ACLs for temp destinations as the super implementation 732 * returns live copies of the ACLs and {@link AuthorizationEntry} is not setup for concurrent access. 733 */ 734 @Override 735 public Set<Object> getTempDestinationReadACLs() { 736 checkForUpdates(); 737 DefaultAuthorizationMap map = this.map.get(); 738 return transcribeSet(map.getTempDestinationReadACLs()); 739 } 740 741 /** 742 * Provides synchronized and defensive access to the write ACLs for temp destinations as the super implementation 743 * returns live copies of the ACLs and {@link AuthorizationEntry} is not setup for concurrent access. 744 */ 745 @Override 746 public Set<Object> getTempDestinationWriteACLs() { 747 checkForUpdates(); 748 DefaultAuthorizationMap map = this.map.get(); 749 return transcribeSet(map.getTempDestinationWriteACLs()); 750 } 751 752 /** 753 * Provides synchronized access to the admin ACLs for the destinations as {@link AuthorizationEntry} 754 * is not setup for concurrent access. 755 */ 756 @Override 757 public Set<Object> getAdminACLs(ActiveMQDestination destination) { 758 checkForUpdates(); 759 DefaultAuthorizationMap map = this.map.get(); 760 return map.getAdminACLs(destination); 761 } 762 763 /** 764 * Provides synchronized access to the read ACLs for the destinations as {@link AuthorizationEntry} is not setup for 765 * concurrent access. 766 */ 767 @Override 768 public Set<Object> getReadACLs(ActiveMQDestination destination) { 769 checkForUpdates(); 770 DefaultAuthorizationMap map = this.map.get(); 771 return map.getReadACLs(destination); 772 } 773 774 /** 775 * Provides synchronized access to the write ACLs for the destinations as {@link AuthorizationEntry} is not setup 776 * for concurrent access. 777 */ 778 @Override 779 public Set<Object> getWriteACLs(ActiveMQDestination destination) { 780 checkForUpdates(); 781 DefaultAuthorizationMap map = this.map.get(); 782 return map.getWriteACLs(destination); 783 } 784 785 /** 786 * Handler for new policy entries in the directory. 787 * 788 * @param namingEvent 789 * the new entry event that occurred 790 * @param destinationType 791 * the type of the destination to which the event applies 792 * @param permissionType 793 * the permission type to which the event applies 794 */ 795 public void objectAdded(NamingEvent namingEvent, DestinationType destinationType, PermissionType permissionType) { 796 LOG.debug("Adding object: {}", namingEvent.getNewBinding()); 797 SearchResult result = (SearchResult) namingEvent.getNewBinding(); 798 799 try { 800 DefaultAuthorizationMap map = this.map.get(); 801 LdapName name = new LdapName(result.getName()); 802 AuthorizationEntry entry = getEntry(map, name, destinationType); 803 804 applyACL(entry, result, permissionType); 805 if (!(entry instanceof TempDestinationAuthorizationEntry)) { 806 map.put(entry.getDestination(), entry); 807 } 808 } catch (InvalidNameException e) { 809 LOG.error("Policy not applied! Error parsing DN for addition of {}", result.getName(), e); 810 } catch (Exception e) { 811 LOG.error("Policy not applied! Error processing object addition for addition of {}", result.getName(), e); 812 } 813 } 814 815 /** 816 * Handler for removed policy entries in the directory. 817 * 818 * @param namingEvent 819 * the removed entry event that occurred 820 * @param destinationType 821 * the type of the destination to which the event applies 822 * @param permissionType 823 * the permission type to which the event applies 824 */ 825 public void objectRemoved(NamingEvent namingEvent, DestinationType destinationType, PermissionType permissionType) { 826 LOG.debug("Removing object: {}", namingEvent.getOldBinding()); 827 Binding result = namingEvent.getOldBinding(); 828 829 try { 830 DefaultAuthorizationMap map = this.map.get(); 831 LdapName name = new LdapName(result.getName()); 832 AuthorizationEntry entry = getEntry(map, name, destinationType); 833 applyAcl(entry, permissionType, new HashSet<Object>()); 834 } catch (InvalidNameException e) { 835 LOG.error("Policy not applied! Error parsing DN for object removal for removal of {}", result.getName(), e); 836 } catch (Exception e) { 837 LOG.error("Policy not applied! Error processing object removal for removal of {}", result.getName(), e); 838 } 839 } 840 841 /** 842 * Handler for renamed policy entries in the directory. This handler deals with the renaming of destination entries 843 * as well as permission entries. If the permission type is not null, it is assumed that we are dealing with the 844 * renaming of a permission entry. Otherwise, it is assumed that we are dealing with the renaming of a destination 845 * entry. 846 * 847 * @param namingEvent 848 * the renaming entry event that occurred 849 * @param destinationType 850 * the type of the destination to which the event applies 851 * @param permissionType 852 * the permission type to which the event applies 853 */ 854 public void objectRenamed(NamingEvent namingEvent, DestinationType destinationType, PermissionType permissionType) { 855 Binding oldBinding = namingEvent.getOldBinding(); 856 Binding newBinding = namingEvent.getNewBinding(); 857 LOG.debug("Renaming object: {} to {}", oldBinding, newBinding); 858 859 try { 860 LdapName oldName = new LdapName(oldBinding.getName()); 861 ActiveMQDestination oldDest = formatDestination(oldName, destinationType); 862 863 LdapName newName = new LdapName(newBinding.getName()); 864 ActiveMQDestination newDest = formatDestination(newName, destinationType); 865 866 if (permissionType != null) { 867 // Handle the case where a permission entry is being renamed. 868 objectRemoved(namingEvent, destinationType, permissionType); 869 870 SearchControls controls = new SearchControls(); 871 controls.setSearchScope(SearchControls.OBJECT_SCOPE); 872 873 boolean matchedToType = false; 874 875 for (PermissionType newPermissionType : PermissionType.values()) { 876 NamingEnumeration<SearchResult> results = context.search(newName, getFilterForPermissionType(newPermissionType), controls); 877 878 if (results.hasMore()) { 879 objectAdded(namingEvent, destinationType, newPermissionType); 880 matchedToType = true; 881 break; 882 } 883 } 884 885 if (!matchedToType) { 886 LOG.error("Policy not applied! Error processing object rename for rename of {} to {}. Could not determine permission type of new object.", oldBinding.getName(), newBinding.getName()); 887 } 888 } else { 889 // Handle the case where a destination entry is being renamed. 890 if (oldDest != null && newDest != null) { 891 AuthorizationEntry entry = entries.remove(oldDest); 892 if (entry != null) { 893 entry.setDestination(newDest); 894 DefaultAuthorizationMap map = this.map.get(); 895 map.put(newDest, entry); 896 map.remove(oldDest, entry); 897 entries.put(newDest, entry); 898 } else { 899 LOG.warn("No authorization entry for {}", oldDest); 900 } 901 } 902 } 903 } catch (InvalidNameException e) { 904 LOG.error("Policy not applied! Error parsing DN for object rename for rename of {} to {}", new Object[]{ oldBinding.getName(), newBinding.getName() }, e); 905 } catch (Exception e) { 906 LOG.error("Policy not applied! Error processing object rename for rename of {} to {}", new Object[]{ oldBinding.getName(), newBinding.getName() }, e); 907 } 908 } 909 910 /** 911 * Handler for changed policy entries in the directory. 912 * 913 * @param namingEvent 914 * the changed entry event that occurred 915 * @param destinationType 916 * the type of the destination to which the event applies 917 * @param permissionType 918 * the permission type to which the event applies 919 */ 920 public void objectChanged(NamingEvent namingEvent, DestinationType destinationType, PermissionType permissionType) { 921 LOG.debug("Changing object {} to {}", namingEvent.getOldBinding(), namingEvent.getNewBinding()); 922 objectRemoved(namingEvent, destinationType, permissionType); 923 objectAdded(namingEvent, destinationType, permissionType); 924 } 925 926 /** 927 * Handler for exception events from the registry. 928 * 929 * @param namingExceptionEvent 930 * the exception event 931 */ 932 public void namingExceptionThrown(NamingExceptionEvent namingExceptionEvent) { 933 context = null; 934 LOG.error("Caught unexpected exception.", namingExceptionEvent.getException()); 935 } 936 937 // Init / Destroy 938 public void afterPropertiesSet() throws Exception { 939 query(); 940 } 941 942 public void destroy() throws Exception { 943 if (eventContext != null) { 944 eventContext.close(); 945 eventContext = null; 946 } 947 948 if (context != null) { 949 context.close(); 950 context = null; 951 } 952 } 953 954 // Getters and Setters 955 956 public String getConnectionURL() { 957 return connectionURL; 958 } 959 960 public void setConnectionURL(String connectionURL) { 961 this.connectionURL = connectionURL; 962 } 963 964 public String getConnectionUsername() { 965 return connectionUsername; 966 } 967 968 public void setConnectionUsername(String connectionUsername) { 969 this.connectionUsername = connectionUsername; 970 } 971 972 public String getConnectionPassword() { 973 return connectionPassword; 974 } 975 976 public void setConnectionPassword(String connectionPassword) { 977 this.connectionPassword = connectionPassword; 978 } 979 980 public String getConnectionProtocol() { 981 return connectionProtocol; 982 } 983 984 public void setConnectionProtocol(String connectionProtocol) { 985 this.connectionProtocol = connectionProtocol; 986 } 987 988 public String getAuthentication() { 989 return authentication; 990 } 991 992 public void setAuthentication(String authentication) { 993 this.authentication = authentication; 994 } 995 996 public String getQueueSearchBase() { 997 return queueSearchBase; 998 } 999 1000 public void setQueueSearchBase(String queueSearchBase) { 1001 try { 1002 LdapName baseName = new LdapName(queueSearchBase); 1003 queuePrefixLength = baseName.size(); 1004 this.queueSearchBase = queueSearchBase; 1005 } catch (InvalidNameException e) { 1006 throw new IllegalArgumentException("Invalid base DN value " + queueSearchBase, e); 1007 } 1008 } 1009 1010 public String getTopicSearchBase() { 1011 return topicSearchBase; 1012 } 1013 1014 public void setTopicSearchBase(String topicSearchBase) { 1015 try { 1016 LdapName baseName = new LdapName(topicSearchBase); 1017 topicPrefixLength = baseName.size(); 1018 this.topicSearchBase = topicSearchBase; 1019 } catch (InvalidNameException e) { 1020 throw new IllegalArgumentException("Invalid base DN value " + topicSearchBase, e); 1021 } 1022 } 1023 1024 public String getTempSearchBase() { 1025 return tempSearchBase; 1026 } 1027 1028 public void setTempSearchBase(String tempSearchBase) { 1029 try { 1030 LdapName baseName = new LdapName(tempSearchBase); 1031 tempPrefixLength = baseName.size(); 1032 this.tempSearchBase = tempSearchBase; 1033 } catch (InvalidNameException e) { 1034 throw new IllegalArgumentException("Invalid base DN value " + tempSearchBase, e); 1035 } 1036 } 1037 1038 public String getPermissionGroupMemberAttribute() { 1039 return permissionGroupMemberAttribute; 1040 } 1041 1042 public void setPermissionGroupMemberAttribute(String permissionGroupMemberAttribute) { 1043 this.permissionGroupMemberAttribute = permissionGroupMemberAttribute; 1044 } 1045 1046 public String getAdminPermissionGroupSearchFilter() { 1047 return adminPermissionGroupSearchFilter; 1048 } 1049 1050 public void setAdminPermissionGroupSearchFilter(String adminPermissionGroupSearchFilter) { 1051 this.adminPermissionGroupSearchFilter = adminPermissionGroupSearchFilter; 1052 } 1053 1054 public String getReadPermissionGroupSearchFilter() { 1055 return readPermissionGroupSearchFilter; 1056 } 1057 1058 public void setReadPermissionGroupSearchFilter(String readPermissionGroupSearchFilter) { 1059 this.readPermissionGroupSearchFilter = readPermissionGroupSearchFilter; 1060 } 1061 1062 public String getWritePermissionGroupSearchFilter() { 1063 return writePermissionGroupSearchFilter; 1064 } 1065 1066 public void setWritePermissionGroupSearchFilter(String writePermissionGroupSearchFilter) { 1067 this.writePermissionGroupSearchFilter = writePermissionGroupSearchFilter; 1068 } 1069 1070 public boolean isLegacyGroupMapping() { 1071 return legacyGroupMapping; 1072 } 1073 1074 public void setLegacyGroupMapping(boolean legacyGroupMapping) { 1075 this.legacyGroupMapping = legacyGroupMapping; 1076 } 1077 1078 public String getGroupObjectClass() { 1079 return groupObjectClass; 1080 } 1081 1082 public void setGroupObjectClass(String groupObjectClass) { 1083 this.groupObjectClass = groupObjectClass; 1084 } 1085 1086 public String getUserObjectClass() { 1087 return userObjectClass; 1088 } 1089 1090 public void setUserObjectClass(String userObjectClass) { 1091 this.userObjectClass = userObjectClass; 1092 } 1093 1094 public String getGroupNameAttribute() { 1095 return groupNameAttribute; 1096 } 1097 1098 public void setGroupNameAttribute(String groupNameAttribute) { 1099 this.groupNameAttribute = groupNameAttribute; 1100 } 1101 1102 public String getUserNameAttribute() { 1103 return userNameAttribute; 1104 } 1105 1106 public void setUserNameAttribute(String userNameAttribute) { 1107 this.userNameAttribute = userNameAttribute; 1108 } 1109 1110 public boolean isRefreshDisabled() { 1111 return refreshDisabled; 1112 } 1113 1114 public void setRefreshDisabled(boolean refreshDisabled) { 1115 this.refreshDisabled = refreshDisabled; 1116 } 1117 1118 public int getRefreshInterval() { 1119 return refreshInterval; 1120 } 1121 1122 public void setRefreshInterval(int refreshInterval) { 1123 this.refreshInterval = refreshInterval; 1124 } 1125 1126 public String getGroupClass() { 1127 return groupClass; 1128 } 1129 1130 public void setGroupClass(String groupClass) { 1131 this.groupClass = groupClass; 1132 map.get().setGroupClass(groupClass); 1133 } 1134 1135 protected static enum DestinationType { 1136 QUEUE, TOPIC, TEMP; 1137 } 1138 1139 protected static enum PermissionType { 1140 READ, WRITE, ADMIN; 1141 } 1142 1143 /** 1144 * Listener implementation for directory changes that maps change events to destination types. 1145 */ 1146 protected class CachedLDAPAuthorizationMapNamespaceChangeListener implements NamespaceChangeListener, ObjectChangeListener { 1147 1148 private final DestinationType destinationType; 1149 private final PermissionType permissionType; 1150 1151 /** 1152 * Creates a new listener. If {@code permissionType} is {@code null}, add and remove events are ignored as they 1153 * do not directly affect policy state. This configuration is used when listening for changes on entries that 1154 * represent destination patterns and not for entries that represent permissions. 1155 * 1156 * @param destinationType 1157 * the type of the destination being listened for 1158 * @param permissionType 1159 * the optional permission type being listened for 1160 */ 1161 public CachedLDAPAuthorizationMapNamespaceChangeListener(DestinationType destinationType, PermissionType permissionType) { 1162 this.destinationType = destinationType; 1163 this.permissionType = permissionType; 1164 } 1165 1166 @Override 1167 public void namingExceptionThrown(NamingExceptionEvent evt) { 1168 SimpleCachedLDAPAuthorizationMap.this.namingExceptionThrown(evt); 1169 } 1170 1171 @Override 1172 public void objectAdded(NamingEvent evt) { 1173 // This test is a hack to work around the fact that Apache DS 2.0 seems to trigger notifications 1174 // for the entire sub-tree even when one-level is the selected search scope. 1175 if (permissionType != null) { 1176 SimpleCachedLDAPAuthorizationMap.this.objectAdded(evt, destinationType, permissionType); 1177 } 1178 } 1179 1180 @Override 1181 public void objectRemoved(NamingEvent evt) { 1182 // This test is a hack to work around the fact that Apache DS 2.0 seems to trigger notifications 1183 // for the entire sub-tree even when one-level is the selected search scope. 1184 if (permissionType != null) { 1185 SimpleCachedLDAPAuthorizationMap.this.objectRemoved(evt, destinationType, permissionType); 1186 } 1187 } 1188 1189 @Override 1190 public void objectRenamed(NamingEvent evt) { 1191 SimpleCachedLDAPAuthorizationMap.this.objectRenamed(evt, destinationType, permissionType); 1192 } 1193 1194 @Override 1195 public void objectChanged(NamingEvent evt) { 1196 // This test is a hack to work around the fact that Apache DS 2.0 seems to trigger notifications 1197 // for the entire sub-tree even when one-level is the selected search scope. 1198 if (permissionType != null) { 1199 SimpleCachedLDAPAuthorizationMap.this.objectChanged(evt, destinationType, permissionType); 1200 } 1201 } 1202 } 1203}