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