001/* 002 * The contents of this file are subject to the terms of the Common Development and 003 * Distribution License (the License). You may not use this file except in compliance with the 004 * License. 005 * 006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the 007 * specific language governing permission and limitations under the License. 008 * 009 * When distributing Covered Software, include this CDDL Header Notice in each file and include 010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL 011 * Header, with the fields enclosed by brackets [] replaced by your own identifying 012 * information: "Portions Copyright [year] [name of copyright owner]". 013 * 014 * Copyright 2008 Sun Microsystems, Inc. 015 * Portions Copyright 2014-2016 ForgeRock AS. 016 */ 017package org.opends.server.extensions; 018 019import static org.opends.server.util.CollectionUtils.*; 020 021import java.util.Iterator; 022import java.util.LinkedHashMap; 023import java.util.LinkedHashSet; 024import java.util.LinkedList; 025import java.util.Set; 026import java.util.concurrent.LinkedBlockingQueue; 027import java.util.concurrent.TimeUnit; 028 029import org.forgerock.opendj.ldap.SearchScope; 030import org.forgerock.opendj.ldap.DN; 031import org.opends.server.types.DirectoryException; 032import org.opends.server.types.Entry; 033import org.opends.server.types.LDAPURL; 034import org.opends.server.types.MemberList; 035import org.opends.server.types.MembershipException; 036import org.opends.server.types.SearchFilter; 037 038/** 039 * This class defines a mechanism that may be used to iterate over the 040 * members of a dynamic group, optionally using an additional set of 041 * criteria to further filter the results. 042 */ 043public class DynamicGroupMemberList 044 extends MemberList 045{ 046 /** Indicates whether the search thread has completed its processing. */ 047 private boolean searchesCompleted; 048 049 /** The base DN to use when filtering the set of group members. */ 050 private final DN baseDN; 051 052 /** The DN of the entry containing the group definition. */ 053 private final DN groupDN; 054 055 /** 056 * The queue into which results will be placed while they are waiting to be 057 * returned. The types of objects that may be placed in this queue are Entry 058 * objects to return or MembershipException objects to throw. 059 */ 060 private final LinkedBlockingQueue<Object> resultQueue; 061 062 /** The search filter to use when filtering the set of group members. */ 063 private final SearchFilter filter; 064 065 /** The search scope to use when filtering the set of group members. */ 066 private final SearchScope scope; 067 068 /** The set of LDAP URLs that define the membership criteria. */ 069 private final Set<LDAPURL> memberURLs; 070 071 /** 072 * Creates a new dynamic group member list with the provided information. 073 * 074 * @param groupDN The DN of the entry containing the group definition. 075 * @param memberURLs The set of LDAP URLs that define the membership 076 * criteria for the associated group. 077 * 078 * @throws DirectoryException If a problem occurs while creating the member 079 * list. 080 */ 081 public DynamicGroupMemberList(DN groupDN, Set<LDAPURL> memberURLs) 082 throws DirectoryException 083 { 084 this(groupDN, memberURLs, null, null, null); 085 } 086 087 /** 088 * Creates a new dynamic group member list with the provided information. 089 * 090 * @param groupDN The DN of the entry containing the group definition. 091 * @param memberURLs The set of LDAP URLs that define the membership 092 * criteria for the associated group. 093 * @param baseDN The base DN that should be enforced for all entries to 094 * return. 095 * @param scope The scope that should be enforced for all entries to 096 * return. 097 * @param filter The filter that should be enforced for all entries to 098 * return. 099 * 100 * @throws DirectoryException If a problem occurs while creating the member 101 * list. 102 */ 103 public DynamicGroupMemberList(DN groupDN, Set<LDAPURL> memberURLs, 104 DN baseDN, SearchScope scope, 105 SearchFilter filter) 106 throws DirectoryException 107 { 108 this.groupDN = groupDN; 109 this.memberURLs = memberURLs; 110 this.baseDN = baseDN; 111 this.filter = filter; 112 113 if (scope == null) 114 { 115 this.scope = SearchScope.WHOLE_SUBTREE; 116 } 117 else 118 { 119 this.scope = scope; 120 } 121 122 searchesCompleted = false; 123 resultQueue = new LinkedBlockingQueue<>(10); 124 125 // We're going to have to perform one or more internal searches in order to 126 // get the results. We need to be careful about the way that we construct 127 // them in order to avoid the possibility of getting duplicate results, so 128 // searches with overlapping bases will need to be combined. 129 LinkedHashMap<DN,LinkedList<LDAPURL>> baseDNs = new LinkedHashMap<>(); 130 for (LDAPURL memberURL : memberURLs) 131 { 132 // First, determine the base DN for the search. It needs to be evaluated 133 // as relative to both the overall base DN specified in the set of 134 // criteria, as well as any other existing base DNs in the same hierarchy. 135 DN urlBaseDN = memberURL.getBaseDN(); 136 if (baseDN != null) 137 { 138 if (baseDN.isSubordinateOrEqualTo(urlBaseDN)) 139 { 140 // The base DN requested by the user is below the base DN for this 141 // URL, so we'll use the base DN requested by the user. 142 urlBaseDN = baseDN; 143 } 144 else if (! urlBaseDN.isSubordinateOrEqualTo(baseDN)) 145 { 146 // The base DN from the URL is outside the base requested by the user, 147 // so we can skip this URL altogether. 148 continue; 149 } 150 } 151 152 // If this is the first URL, then we can just add it with the base DN. 153 // Otherwise, we need to see if it needs to be merged with other URLs in 154 // the same hierarchy. 155 if (baseDNs.isEmpty()) 156 { 157 baseDNs.put(urlBaseDN, newLinkedList(memberURL)); 158 } 159 else 160 { 161 // See if the specified base DN is already in the map. If so, then 162 // just add the new URL to the existing list. 163 LinkedList<LDAPURL> urlList = baseDNs.get(urlBaseDN); 164 if (urlList == null) 165 { 166 // There's no existing list for the same base DN, but there might be 167 // DNs in an overlapping hierarchy. If so, then use the base DN that 168 // is closest to the naming context. If not, then add a new list with 169 // the current base DN. 170 boolean found = false; 171 Iterator<DN> iterator = baseDNs.keySet().iterator(); 172 while (iterator.hasNext()) 173 { 174 DN existingBaseDN = iterator.next(); 175 if (urlBaseDN.isSubordinateOrEqualTo(existingBaseDN)) 176 { 177 // The base DN for the current URL is below an existing base DN, 178 // so we can just add this URL to the existing list and be done. 179 urlList = baseDNs.get(existingBaseDN); 180 urlList.add(memberURL); 181 found = true; 182 break; 183 } 184 else if (existingBaseDN.isSubordinateOrEqualTo(urlBaseDN)) 185 { 186 // The base DN for the current URL is above the existing base DN, 187 // so we should use the base DN for the current URL instead of the 188 // existing one. 189 urlList = baseDNs.get(existingBaseDN); 190 urlList.add(memberURL); 191 iterator.remove(); 192 baseDNs.put(urlBaseDN, urlList); 193 found = true; 194 break; 195 } 196 } 197 198 if (! found) 199 { 200 baseDNs.put(urlBaseDN, newLinkedList(memberURL)); 201 } 202 } 203 else 204 { 205 // There was already a list with the same base DN, so just add the URL. 206 urlList.add(memberURL); 207 } 208 } 209 } 210 211 // At this point, we should know what base DN(s) we need to use, so we can 212 // create the filter to use with that base DN. There are some special-case 213 // optimizations that we can do here, but in general the filter will look 214 // like "(&(filter)(|(urlFilters)))". 215 LinkedHashMap<DN,SearchFilter> searchMap = new LinkedHashMap<>(); 216 for (DN urlBaseDN : baseDNs.keySet()) 217 { 218 LinkedList<LDAPURL> urlList = baseDNs.get(urlBaseDN); 219 LinkedHashSet<SearchFilter> urlFilters = new LinkedHashSet<>(); 220 for (LDAPURL url : urlList) 221 { 222 urlFilters.add(url.getFilter()); 223 } 224 225 SearchFilter combinedFilter; 226 if (filter == null) 227 { 228 if (urlFilters.size() == 1) 229 { 230 combinedFilter = urlFilters.iterator().next(); 231 } 232 else 233 { 234 combinedFilter = SearchFilter.createORFilter(urlFilters); 235 } 236 } 237 else 238 { 239 if (urlFilters.size() == 1) 240 { 241 SearchFilter urlFilter = urlFilters.iterator().next(); 242 if (urlFilter.equals(filter)) 243 { 244 combinedFilter = filter; 245 } 246 else 247 { 248 LinkedHashSet<SearchFilter> filterSet = new LinkedHashSet<>(); 249 filterSet.add(filter); 250 filterSet.add(urlFilter); 251 combinedFilter = SearchFilter.createANDFilter(filterSet); 252 } 253 } 254 else 255 { 256 if (urlFilters.contains(filter)) 257 { 258 combinedFilter = filter; 259 } 260 else 261 { 262 LinkedHashSet<SearchFilter> filterSet = new LinkedHashSet<>(); 263 filterSet.add(filter); 264 filterSet.add(SearchFilter.createORFilter(urlFilters)); 265 combinedFilter = SearchFilter.createANDFilter(filterSet); 266 } 267 } 268 } 269 270 searchMap.put(urlBaseDN, combinedFilter); 271 } 272 273 // At this point, we should have all the information we need to perform the 274 // searches. Create arrays of the elements for each. 275 DN[] baseDNArray = new DN[baseDNs.size()]; 276 SearchFilter[] filterArray = new SearchFilter[baseDNArray.length]; 277 LDAPURL[][] urlArray = new LDAPURL[baseDNArray.length][]; 278 Iterator<DN> iterator = baseDNs.keySet().iterator(); 279 for (int i=0; i < baseDNArray.length; i++) 280 { 281 baseDNArray[i] = iterator.next(); 282 filterArray[i] = searchMap.get(baseDNArray[i]); 283 284 LinkedList<LDAPURL> urlList = baseDNs.get(baseDNArray[i]); 285 urlArray[i] = new LDAPURL[urlList.size()]; 286 int j=0; 287 for (LDAPURL url : urlList) 288 { 289 urlArray[i][j++] = url; 290 } 291 } 292 293 DynamicGroupSearchThread searchThread = 294 new DynamicGroupSearchThread(this, baseDNArray, filterArray, urlArray); 295 searchThread.start(); 296 } 297 298 /** 299 * Retrieves the DN of the dynamic group with which this dynamic group member 300 * list is associated. 301 * 302 * @return The DN of the dynamic group with which this dynamic group member 303 * list is associated. 304 */ 305 public final DN getDynamicGroupDN() 306 { 307 return groupDN; 308 } 309 310 /** 311 * Indicates that all of the searches needed to iterate across the member list 312 * have completed and there will not be any more results provided. 313 */ 314 final void setSearchesCompleted() 315 { 316 searchesCompleted = true; 317 } 318 319 /** 320 * Adds the provided entry to the set of results that should be returned for 321 * this member list. 322 * 323 * @param entry The entry to add to the set of results that should be 324 * returned for this member list. 325 * 326 * @return {@code true} if the entry was added to the result set, or 327 * {@code false} if it was not (either because a timeout expired or 328 * the attempt was interrupted). If this method returns 329 * {@code false}, then the search thread should terminate 330 * immediately. 331 */ 332 final boolean addResult(Entry entry) 333 { 334 try 335 { 336 return resultQueue.offer(entry, 10, TimeUnit.SECONDS); 337 } 338 catch (InterruptedException ie) 339 { 340 return false; 341 } 342 } 343 344 /** 345 * Adds the provided membership exception so that it will be thrown along with 346 * the set of results for this member list. 347 * 348 * @param membershipException The membership exception to be thrown. 349 * 350 * @return {@code true} if the exception was added to the result set, or 351 * {@code false} if it was not (either because a timeout expired or 352 * the attempt was interrupted). If this method returns 353 * {@code false}, then the search thread should terminate 354 * immediately. 355 */ 356 final boolean addResult(MembershipException membershipException) 357 { 358 try 359 { 360 return resultQueue.offer(membershipException, 10, TimeUnit.SECONDS); 361 } 362 catch (InterruptedException ie) 363 { 364 return false; 365 } 366 } 367 368 @Override 369 public boolean hasMoreMembers() 370 { 371 while (! searchesCompleted) 372 { 373 if (resultQueue.peek() != null) 374 { 375 return true; 376 } 377 378 try 379 { 380 Thread.sleep(0, 1000); 381 } catch (Exception e) {} 382 } 383 384 return resultQueue.peek() != null; 385 } 386 387 @Override 388 public Entry nextMemberEntry() 389 throws MembershipException 390 { 391 if (! hasMoreMembers()) 392 { 393 return null; 394 } 395 396 Object result = resultQueue.poll(); 397 if (result == null) 398 { 399 close(); 400 return null; 401 } 402 else if (result instanceof Entry) 403 { 404 return (Entry) result; 405 } 406 else if (result instanceof MembershipException) 407 { 408 MembershipException me = (MembershipException) result; 409 if (! me.continueIterating()) 410 { 411 close(); 412 } 413 414 throw me; 415 } 416 417 // We should never get here. 418 close(); 419 return null; 420 } 421 422 @Override 423 public void close() 424 { 425 searchesCompleted = true; 426 resultQueue.clear(); 427 } 428}