View Javadoc
1   package edu.internet2.middleware.grouper.pspng;
2   
3   /*******************************************************************************
4    * Copyright 2015 Internet2
5    * 
6    * Licensed under the Apache License, Version 2.0 (the "License");
7    * you may not use this file except in compliance with the License.
8    * You may obtain a copy of the License at
9    * 
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   * 
12   * Unless required by applicable law or agreed to in writing, software
13   * distributed under the License is distributed on an "AS IS" BASIS,
14   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15   * See the License for the specific language governing permissions and
16   * limitations under the License.
17   ******************************************************************************/
18  
19  import java.io.IOException;
20  import java.io.Reader;
21  import java.io.StringReader;
22  import java.util.*;
23  
24  import com.unboundid.ldap.sdk.DN;
25  import com.unboundid.ldap.sdk.Filter;
26  import com.unboundid.ldap.sdk.LDAPException;
27  import com.unboundid.ldap.sdk.RDN;
28  import org.apache.commons.collections.MultiMap;
29  import org.apache.commons.collections.map.MultiValueMap;
30  import org.apache.commons.lang.StringUtils;
31  import org.apache.log4j.MDC;
32  import org.ldaptive.*;
33  import org.ldaptive.io.LdifReader;
34  
35  import edu.internet2.middleware.grouper.cache.GrouperCache;
36  import edu.internet2.middleware.grouper.util.GrouperUtil;
37  import edu.internet2.middleware.subject.Subject;
38  
39  
40  
41  /**
42   * This (abstract) class consolidates the common aspects of provisioning to LDAP-based targets.
43   * This includes
44   *   -Configuring and building (ldaptive) LDAP connection pools
45   *   -PersonSubject-to-LdapObject searching and caching.
46   *   -Consolidating/Batching ldap modifications into as few modifications as possible.
47   *   
48   * @author Bert Bee-Lindgren
49   *
50   */
51  public abstract class LdapProvisioner <ConfigurationClass extends LdapProvisionerConfiguration> 
52  extends Provisioner<ConfigurationClass, LdapUser, LdapGroup>
53  {
54    // Used to save a list of LDAP MODIFICATIONS in a ProvisioningWorkItem
55    private static final String LDAP_MOD_LIST = "LDAP_MODS";
56  
57    // This is used to know what strings have already been dn-escaped or ldap-filter escaped
58    private final Set<String> dnEscapedStrings = new HashSet<>();
59    private final Set<String> ldapFilterEscapedStrings = new HashSet<>();
60  
61    private Set<DN> existingOUs = new HashSet<DN>();
62    protected LdapSystem ldapSystem;
63    
64    /**
65     * LDAP ResultCodes that might occur from a schema-related violation, for example when
66     * the last member is removed from an LdapGroup that requires a member
67     */
68    public static Set<ResultCode> schemaRelatedLdapErrors = new HashSet<>();
69    static {
70      schemaRelatedLdapErrors.add(ResultCode.CONSTRAINT_VIOLATION);
71      schemaRelatedLdapErrors.add(ResultCode.LDAP_NOT_SUPPORTED);
72      schemaRelatedLdapErrors.add(ResultCode.UNWILLING_TO_PERFORM);
73      schemaRelatedLdapErrors.add(ResultCode.OBJECT_CLASS_VIOLATION);
74    }
75    
76    public LdapProvisioner(String provisionerName, ConfigurationClass config, boolean fullSyncMode)
77    {
78      super(provisionerName, config, fullSyncMode);
79      
80      LOG.debug("Constructing LdapProvisioner: {}", provisionerName);
81  
82      // Make sure we can connect
83      try {
84        if (!getLdapSystem().test()) {
85          throw new RuntimeException("Unable to make ldap connection");
86        }
87      } catch (PspException e) {
88        LOG.error("{}: Unable to make ldap connection", getDisplayName(), e);
89        throw new RuntimeException("Unable to make ldap connection");
90      }
91    }
92  
93    /**
94     * Note that the given dn string has already been escaped, in particular
95     * any commas or equal signs in the components of the dn have been escaped.
96     *
97     * These strings can be later checked with isStringDnEscaped
98     *
99     * See RDN.toMinimallyEncodedString(), PspJexlUtils.bushyDn
100    * @param dnString
101    */
102   public static void stringHasBeenDnEscaped(String dnString) {
103     if ( activeProvisioner.get() == null || !(activeProvisioner.get() instanceof LdapProvisioner) ) {
104       // This could throw an IllegalStateException, but that would make the PspJexlUtilities
105       // not work outside of a formal provisioner context
106       return;
107     }
108 
109     ((LdapProvisioner) activeProvisioner.get()).dnEscapedStrings.add(dnString);
110   }
111 
112   /**
113    * Has this string already been dn-escaped as determined by whether
114    * stringHasBeenDnEscaped(...) was called for it.
115    * @param dnString
116    * @return
117    */
118   public boolean isStringDnEscaped(String dnString) {
119     return dnEscapedStrings.contains(dnString);
120   }
121 
122   /**
123    * Note that the given string has already been escaped as an ldap filter, in particular
124    * any (,),* have been escaped.
125    *
126    * These strings can be later checked with isStringLdapFilterEscaped
127    *
128    * See  PspJexlUtils.escapeLdapFilter
129    * @param ldapFilterValue
130    */
131 
132   public static void stringHasBeenLdapFilterEscaped(String ldapFilterValue) {
133     if ( activeProvisioner.get() == null || !(activeProvisioner.get() instanceof LdapProvisioner) ) {
134       // This could throw an IllegalStateException, but that would make the PspJexlUtilities
135       // not work outside of a formal provisioner context
136       return;
137     }
138 
139     ((LdapProvisioner) activeProvisioner.get()).ldapFilterEscapedStrings.add(ldapFilterValue);
140   }
141 
142 
143   /**
144    * Has this string already been escaped as an ldap filter, as determined by whether
145    * stringHasBeenLdapFilterEscaped(...) was called for it.
146    * @param filterString
147    * @return
148    */
149 
150   public boolean isStringEscapedForLdapFilter(String filterString) {
151     if ( ldapFilterEscapedStrings.contains(filterString) ) {
152       return true;
153     }
154 
155     // Check to see if string is a attribute=value, in which case check the value
156 
157     // We know the provided String hasn't been escaped and there's nothing else to check
158     // if there is no equals sign
159     if ( !filterString.contains("=") ) {
160       return false;
161     }
162 
163     // Check to see if this was a attribute=<escaped value>
164     String ldapFilterValue = StringUtils.substringAfter(filterString, "=");
165     return ldapFilterEscapedStrings.contains(ldapFilterValue);
166   }
167 
168   // We're overriding this to clean-up our caches
169   @Override
170   public void finishCoordination(List<ProvisioningWorkItem> workItems, boolean wasSuccessful) {
171     // Flush our caches when our current provisioning finishes
172     ldapFilterEscapedStrings.clear();
173     dnEscapedStrings.clear();
174 
175 
176     super.finishCoordination(workItems, wasSuccessful);
177   }
178 
179 
180   /**
181    * Find the subjects in the ldap server.
182    * 
183    * If account-creation is enabled with createMissingAccounts, this will create missing entries.
184    * @param subjectsToFetch
185    * @return
186    */
187   protected Map<Subject, LdapUser> fetchTargetSystemUsers( Collection<Subject> subjectsToFetch) 
188       throws PspException {
189     LOG.debug("Fetching {} users from target system", subjectsToFetch.size());
190     
191     if ( subjectsToFetch.size() > config.getUserSearch_batchSize() )
192       throw new IllegalArgumentException("LdapProvisioner.fetchTargetSystemUsers: invoked with too many subjects to fetch");
193     
194     StringBuilder combinedLdapFilter = new StringBuilder();
195     
196     // Start the combined ldap filter as an OR-query
197     combinedLdapFilter.append("(|");
198     
199     for ( Subject subject : subjectsToFetch ) {
200       SearchFilter f = getUserLdapFilter(subject);
201       
202       String filterString = f.format();
203       
204       // Wrap the subject's filter in (...) if it doesn't start with (
205       if ( filterString.startsWith("(") )
206         combinedLdapFilter.append(filterString);
207       else
208         combinedLdapFilter.append('(').append(filterString).append(')');
209     }
210     combinedLdapFilter.append(')');
211 
212     // Actually do the search
213     List<LdapObject> searchResult;
214     
215     try {
216       searchResult = getLdapSystem().performLdapSearchRequest(
217               subjectsToFetch.size(), config.getUserSearchBaseDn(),
218               SearchScope.SUBTREE,
219               Arrays.asList(config.getUserSearchAttributes()),
220               combinedLdapFilter.toString());
221 
222       LOG.debug("Read {} user objects from directory", searchResult.size());
223 
224       if (shouldLogAboutMissingSubjects(subjectsToFetch, searchResult)) {
225         LOG.warn("Several subjects were not found: only {} subjects found with filter {}", searchResult.size(), combinedLdapFilter);
226       }
227     }
228     catch (PspException e) {
229       LOG.error("Problem searching for subjects with filter {} on base {}", 
230           new Object[] {combinedLdapFilter, config.getUserSearchBaseDn(), e} );
231       throw e;
232     }
233     
234     // Now we have a bag of LdapObjects, but we don't know which goes with which subject.
235     // Generally, we're going to go through the Subjects and their filters and compare
236     // them to the Ldap data we've fetched into memory.
237     // 
238     // This is complicated a bit because Ldaptive doesn't have a way to run a filter in memory
239     // against an LdapObject. Therefore, we're going to use unboundid classes to do
240     // some of this work.
241     Map<Subject, LdapUser> result = new HashMap<Subject, LdapUser>();
242 
243     Set<LdapObject> matchedFetchResults = new HashSet<LdapObject>();
244     
245     // For every subject we tried to bulk fetch, find the matching LdapObject that came back
246     for ( Subject subjectToFetch : subjectsToFetch ) {
247       SearchFilter f = getUserLdapFilter(subjectToFetch);
248           
249       for ( LdapObject aFetchedLdapObject : searchResult ) {
250         if ( aFetchedLdapObject.matchesLdapFilter(f)) {
251           result.put(subjectToFetch, new LdapUser(aFetchedLdapObject));
252           matchedFetchResults.add(aFetchedLdapObject);
253           break;
254         }
255       }
256     }
257 
258     Set<LdapObject> unmatchedFetchResults = new HashSet<LdapObject>(searchResult);
259     unmatchedFetchResults.removeAll(matchedFetchResults);
260     
261     for ( LdapObject unmatchedFetchResult : unmatchedFetchResults )
262       LOG.error("{}: User data from ldap server was not matched with a grouper subject "
263           + "(perhaps attributes are used in userSearchFilter ({}) that are not included "
264           + "in userSearchAttributes ({})?): {}",
265           new Object[] {getDisplayName(), config.getUserSearchFilter(), config.getUserSearchAttributes(),
266           unmatchedFetchResult.getDn()});
267     
268     return result;
269   }
270 
271   protected SearchFilter getUserLdapFilter(Subject subject) throws PspException  {
272     String result = evaluateJexlExpression("UserSearchFilter", config.getUserSearchFilter(), subject, null, null, null);
273     if ( StringUtils.isEmpty(result) )
274       throw new RuntimeException("User searching requires userSearchFilter to be configured correctly");
275     
276     // If the filter contains '||', then this filter is requesting parameter substitution
277     String filterPieces[] = result.split("\\|\\|");
278 
279     // The first piece is either the entire filter or the filter template
280     SearchFilter filter = new SearchFilter(filterPieces[0]);
281 
282     // If the filter is not using ldap-filter parameters, check its syntax
283     if ( filterPieces.length == 1 ) {
284       try {
285         // Use unboundid to sanity-check/parse filter
286         Filter.create(result);
287       }
288       catch (LDAPException e) {
289         LOG.warn("{}: User ldap filter was invalid. " +
290                 "Perhaps its filter clauses needed to be escaped with utils.escapeLdapFilter or use ldap-filter positional parameters. " +
291                 "Subject={}. Bad filter={}. ",
292                 new Object[]{getDisplayName(), subject, result});
293 
294         // We're going to proceed here just in case the filter-checking logic is too
295         // sensitive. The ldap server will eventually see the filter and make its own decision
296       }
297     } else {
298       // Set the positional parameters
299 
300       for (int i = 1; i < filterPieces.length; i++)
301         filter.setParameter(i - 1, filterPieces[i].trim());
302     }
303 
304     LOG.debug("{}: User LDAP filter for subject {}: {}",
305         new Object[]{getDisplayName(), subject.getId(), filter});
306     return filter;
307   }
308   
309   @Override
310   protected LdapUser createUser(Subject personSubject) throws PspException {
311     GrouperUtil.assertion(config.isCreatingMissingUsersEnabled(), "Can't create users unless createMissingUsers is enabled");
312     GrouperUtil.assertion(StringUtils.isNotEmpty(config.getUserCreationLdifTemplate()), "Can't create users unless userCreationLdifTemplate is defined");
313     GrouperUtil.assertion(StringUtils.isNotEmpty(config.getUserCreationBaseDn()), "Can't create users unless userCreationBaseDn is defined");
314     
315     LOG.info("Creating LDAP account for Subject: {} ", personSubject);
316     String ldif = config.getUserCreationLdifTemplate();
317     ldif = ldif.replaceAll("\\|\\|", "\n");
318     ldif = evaluateJexlExpression("UserTemplate", ldif, personSubject, null, null, null);
319 
320     ldif = sanityCheckDnAttributesOfLdif(ldif, "User-creation ldif for %s", personSubject);
321 
322     Connection conn = getLdapSystem().getLdapConnection();
323     try {
324       Reader reader = new StringReader(ldif);
325       LdifReader ldifReader = new LdifReader(reader);
326       SearchResult ldifResult = ldifReader.read();
327       LdapEntry ldifEntry = ldifResult.getEntry();
328       
329       // Update DN to be relative to userCreationBaseDn
330       String actualDn = String.format("%s,%s", ldifEntry.getDn(),config.getUserCreationBaseDn());
331       ldifEntry.setDn(actualDn);
332 
333       performLdapAdd(ldifEntry);
334       
335       // Read the acount that was just created
336       LOG.debug("Reading account that was just added to ldap server: {}", personSubject);
337       return fetchTargetSystemUser(personSubject);
338     } catch (PspException e) {
339       LOG.error("Problem while creating new user: {}: {}", ldif, e);
340       throw e;
341     } catch ( IOException e ) {
342       LOG.error("Problem while processing ldif to create new user: {}", ldif, e);
343       throw new PspException("LDIF problem creating user: %s", e.getMessage());
344     }
345     finally {
346       conn.close();
347     }
348   }
349 
350   /**
351    * Look at attributes that are supposed to store DNs and make sure they
352    * are escaped and/or parsable
353    * @param ldif
354    * @return Presently this just returns the input ldif. Hopefully this
355    * can someday help cleanup dn-escaping problems
356    */
357   protected String sanityCheckDnAttributesOfLdif(String ldif, String ldifSourceFormat, Object... ldifSourceArgs)
358     throws PspException
359   {
360     String ldifSource = String.format(ldifSourceFormat, ldifSourceArgs);
361 
362     // Loop through the lines...
363     String ldifLines[] = ldif.split("\\r?\\n");
364     for ( String ldifLine : ldifLines ) {
365       ldifLine = ldifLine.trim();
366 
367       // Loop through the attributes configured to require DN syntax
368       for ( String dnAttribute : getConfig().getAttributesNeededingDnEscaping() ) {
369         if ( ldifLine.toLowerCase().matches(String.format("^%s *:.*", dnAttribute)) ) {
370           String value = StringUtils.substringAfter(ldifLine, ":");
371 
372           if (! DN.isValidDN(value) ) {
373             if (isStringDnEscaped(value)) {
374               LOG.error("{}: attribute '{}' is an invalid DN even though it was escaped: {}",
375                       new Object[]{getDisplayName(), dnAttribute, value});
376             } else {
377               LOG.error("{}: attribute '{}' is an invalid DN. " +
378                         "Perhaps its components need to be escaped with utils.escapeLdapRdn(rdn): {}",
379                         new Object[]{getDisplayName(), dnAttribute, value});
380             }
381 
382             throw new PspException("Attribute '%s' is an invalid DN in %s (utils.escapeLdapRdn is probably necessary): %s",
383                     dnAttribute, ldifSource, ldifLine);
384           }
385         }
386 
387       }
388     }
389     return ldif;
390   }
391 
392   @Override
393   protected void populateJexlMap(Map<String, Object> variableMap, Subject subject,
394       LdapUser ldapUser, GrouperGroupInfo grouperGroupInfo, LdapGroup ldapGroup) {
395     
396     super.populateJexlMap(variableMap, subject, ldapUser, grouperGroupInfo, ldapGroup);
397     
398     if ( ldapGroup != null )
399       variableMap.put("ldapGroup", ldapGroup.getLdapObject());
400     if ( ldapUser != null )
401       variableMap.put("ldapUser", ldapUser.getLdapObject());
402   }
403   /**
404    * Note that the given {@link ProvisioningWorkItem} needs the given {@link ModifyRequest} done.
405    * 
406    * These are not done right away so that multiple modifications can be implemented together
407    * in batches. For example, LDAP servers can generally process a single ldap modification that
408    * adds 10 values to an attribute MUCH faster than processing 10 single-value Modify-Add operations.
409    * @param operation
410    */
411   protected void scheduleLdapModification(ModifyRequest operation) {
412     ProvisioningWorkItem workItem = getCurrentWorkItem();
413     LOG.info("{}: Scheduling ldap modification: {}", getDisplayName(), operation);
414     
415     workItem.addValueToProvisioningData(LDAP_MOD_LIST, operation);
416   }
417   
418   /**
419    * This implements the LDAP Modifications that were scheduled with schedulLdapModification.
420    * Those scheduled changes are stored within the ProvisioningWorkItems that are passed around.
421    * 
422    * In order to be fast, we first try to coalesce the changes across ProvisioningWorkItems.
423    * 
424    * If all the fancy, coalescing LDAP-implementation fails, each workItem's LDAP operations
425    *  will be done individually so problems will be tracked down to specific workItem(s).
426    */
427   @Override
428   public void finishProvisioningBatch(List<ProvisioningWorkItem> workItems) throws PspException {
429     try {
430       MDC.put("step", "coalesced");
431       makeCoalescedLdapChanges(workItems);
432 
433       // They all worked, so mark them all as successful
434       for ( ProvisioningWorkItem workItem : workItems )
435         workItem.markAsSuccess("Modification complete");
436       
437     } catch (PspException e1) {
438       LOG.warn("RETRYING: Performing slower, unoptimized ldap provisioning after optimized provisioning failed");
439       
440         for ( ProvisioningWorkItem workItem : workItems ) {
441           try {
442             MDC.put("step", "ldap_retry:"+workItem.getMdcLabel());
443             makeIndividualLdapChanges(workItem);
444             workItem.markAsSuccess("Modification complete");
445           } catch (PspException e2) {
446             LOG.error("Simple ldap provisioning failed for {}", workItem, e2);
447             workItem.markAsFailure("Modification failed: %s", e2.getMessage());
448           }
449       }
450     }
451     finally {
452       MDC.remove("step");
453     }
454     
455     super.finishProvisioningBatch(workItems);
456   }
457 
458   /**
459    * This ldap implementation is made complicated by strong desires to be fast, 
460    * specifically:
461    * + Pull changes made to common objects changed by several WorkItems together so they
462    * can be made in single ldap operations.
463    * + Chunk these changes into reasonable-sized pieces
464    * 
465    * Note, this involves the following steps:
466    * 1) Find all the ModifyRequests for a dn
467    * 2) Pull apart the ModifyRequests apart and find the AttributeModifications they contain
468    * 3) For Modify-Add and Modify-Delete operations, pull out all the added and deleted values
469    * and put them together in as few Modify-Add and Modify-Delete operations as is reasonable
470    * 4) Combine each attribute's changes into a list of values to ADD and a list of values to DELETE
471    * 5) Break long lists of attribute values into bite-sized chunks
472    * 6) Make a new Modification request for each dn that contains all the AttributeModifications
473    * for that dn
474 
475  * @param workItems
476  * @throws PspException
477  */
478   private void makeCoalescedLdapChanges(List<ProvisioningWorkItem> workItems) throws PspException {
479     LOG.debug("{}: Making coalescedLdapChanges", getDisplayName());
480 
481     // Assemble and execute all the LDAP_MOD_LIST values saved up in workItems
482     MultiMap dn2Mods = new MultiValueMap();
483     
484     // Sort all the necessary operations by the DN that they modify
485     for ( ProvisioningWorkItem workItem : workItems ) {
486       List<ModifyRequest> mods = (List) workItem.getProvisioningDataValues(LDAP_MOD_LIST);
487       
488       // Obviously there is nothing to coalesce if no mods were necessary for this work item
489       if ( mods == null ) 
490         continue;
491       
492       LOG.info("{}: WorkItem {} needs {} ldap modifications", 
493           new Object[]{getDisplayName(), workItem, mods.size()} );
494       for ( ModifyRequest mod : mods ) {
495         LOG.debug("{}: Mod for WorkItem: {}", getDisplayName(), getLoggingSummary(mod));
496         dn2Mods.put(mod.getDn(), mod);
497       }
498     }
499     
500     // Now loop through the DNs that need to be modified
501     for ( String dn : (Collection<String>) dn2Mods.keySet() ) {
502       // These are all the modifications that were assembled across our provisioning batch
503       Collection<ModifyRequest> modsForDn = (Collection<ModifyRequest>) dn2Mods.get(dn);
504 
505       // This will hold the actual operations that are necessary.
506       // This is a List of List<LDAP Modifications that should be done together>
507       //
508       // There will be more than one element in coalescedOperations when a DN has operations
509       // that are so large that they need to be broken into bite-sized chunks
510       List<List<AttributeModification>> coalescedOperations = new ArrayList<List<AttributeModification>>();
511       
512       // We know we're going to need at least one operation (since this DN was in dn2Mods), 
513       // so put an empty list of mods here to keep things simpler below
514       coalescedOperations.add(new ArrayList<AttributeModification>());
515       
516       // Sort all the attribute values modified by these modifications into one DEL & ADD
517       // list for each attribute. 
518       MultiMap attribute2ValuesToAdd = new MultiValueMap();
519       MultiMap attribute2ValuesToDel = new MultiValueMap();
520       
521       for ( ModifyRequest mod : modsForDn ) {
522         for ( AttributeModification attributeMod : mod.getAttributeModifications() ) {
523           LdapAttribute attribute = attributeMod.getAttribute();
524           
525           switch (attributeMod.getAttributeModificationType() ) {
526             case ADD: 
527               for ( String value : attribute.getStringValues() )
528                 attribute2ValuesToAdd.put(attribute.getName(), value);
529               break;
530             case REMOVE: 
531               for ( String value : attribute.getStringValues() )
532                 attribute2ValuesToDel.put(attribute.getName(), value);
533               break;
534             case REPLACE: 
535             default:
536               // We don't know how to combine these, so just do the operation as is
537               // in our first eventual operation
538               coalescedOperations.get(0).add(attributeMod);  
539           }
540         }
541       }
542       
543       int maxValues = config.getMaxValuesToChangePerOperation();
544       
545       // Create a single value-removal for each attribute that needs values removed
546       // Loop through the attributes that had values removed from them
547       //
548       // NOTE: We're doing removals first so that if an value is both added and removed
549       // (because there were 2+ workItems that conflicted with each other), the ADD will be
550       // done last and will 'stick.' If the removal is supposed to be the one that sticks, then
551       // it will be taken care of at full-sync time.
552       
553       // TODO: Figure out what workItems conflict and make sure those groups are full-sync'ed
554       // first
555 
556       for ( String attributeName : (Collection<String>) attribute2ValuesToDel.keySet() ) {
557         Collection<String> valuesToRemove = (Collection<String>) attribute2ValuesToDel.get(attributeName);
558         if (valuesToRemove == null ) {
559           valuesToRemove = Collections.EMPTY_LIST;
560         }
561 
562         Collection<String> valuesToAdd = (Collection<String>) attribute2ValuesToAdd.get(attributeName);
563         if ( valuesToAdd == null ) {
564           valuesToAdd = Collections.EMPTY_LIST;
565         }
566         
567         // Find the intersection between the values to add and remove
568         Set<String> valuesWithConflictingOperations = new HashSet<String>(valuesToRemove);
569         valuesWithConflictingOperations.retainAll(valuesToAdd);
570         
571         if ( valuesWithConflictingOperations.size() > 0 ) {
572           LOG.warn("Found {} conflicting ldap operations in event batch. Scheduling a full sync on affected groups", valuesWithConflictingOperations.size());
573 
574           Set<GrouperGroupInfo> groupsNeedingFullSync = new HashSet<>();
575           
576           // Go through all the conflicting values and find the groups involved in the conflicts
577           for ( String conflictingProvisioningAttributeValue : valuesWithConflictingOperations ) {
578             // Look for the workItem that needed these values to be provisioned
579             for ( ProvisioningWorkItem workItem : workItems ) {
580               if ( isWorkItemMakingChange(workItem, dn, attributeName, conflictingProvisioningAttributeValue) ) {
581                 groupsNeedingFullSync.add(workItem.getGroupInfo(this));
582               }
583             }
584           }
585         }
586       }
587         
588 
589       for ( String attributeName : (Collection<String>) attribute2ValuesToDel.keySet() ) {
590         Collection<String> values = (Collection<String>) attribute2ValuesToDel.get(attributeName);
591         List<List<String>> valueChunks = PspUtils.chopped(values, maxValues);
592         
593         for (int i=0; i<valueChunks.size(); i++) {
594           List<String> valueChunk = valueChunks.get(i);
595           
596           LdapAttribute attribute = new LdapAttribute(attributeName, GrouperUtil.toArray(valueChunk, String.class));
597           AttributeModification mod = new AttributeModification(AttributeModificationType.REMOVE, attribute);
598           
599           // Grow our list of operations if necessary
600           if ( coalescedOperations.size() <= i )
601             coalescedOperations.add(new ArrayList<AttributeModification>());
602           
603           coalescedOperations.get(i).add(mod);
604         }
605       }
606       
607 
608       
609       // Create a single value-add for each attribute that needs values added
610       // Loop through the attributes that had values added to them
611       for ( String attributeName : (Collection<String>) attribute2ValuesToAdd.keySet() ) {
612         Collection<String> values = (Collection<String>) attribute2ValuesToAdd.get(attributeName);
613         List<List<String>> valueChunks = PspUtils.chopped(values, maxValues);
614         
615         for (int i=0; i<valueChunks.size(); i++) {
616           List<String> valueChunk = valueChunks.get(i);
617           
618           LdapAttribute attribute = new LdapAttribute(attributeName, GrouperUtil.toArray(valueChunk, String.class));
619           AttributeModification mod = new AttributeModification(AttributeModificationType.ADD, attribute);
620           
621           // Grow our list of operations if necessary
622           if ( coalescedOperations.size() <= i )
623             coalescedOperations.add(new ArrayList<AttributeModification>());
624           
625           coalescedOperations.get(i).add(mod);
626         }
627       }
628       
629       Connection conn = getLdapSystem().getLdapConnection();
630       try {
631         for ( List<AttributeModification> operation : coalescedOperations ) {
632           ModifyRequest mod = new ModifyRequest(dn, GrouperUtil.toArray(operation, AttributeModification.class));
633           try {
634             conn.open();
635             
636             LOG.info("Performing LDAP modification: {}", getLoggingSummary(mod) );
637             conn.getProviderConnection().modify(mod);
638           } catch (LdapException e) {
639             LOG.info("(THIS WILL BE RETRIED) Problem doing coalesced ldap modification: {} / {}: {}",
640                 new Object[]{dn, mod, e.getMessage()});
641             throw new PspException("Coalesced LDAP Modification failed: %s",e.getMessage());
642           } 
643         }
644       } finally {
645         conn.close();
646       }
647     }
648   }
649 
650 
651   protected boolean isWorkItemMakingChange(
652       ProvisioningWorkItem workItem,
653       String dn, String attributeName, String provisioningAttributeValue) {
654     
655     @SuppressWarnings("unchecked")
656     List<ModifyRequest> modRequests = (List) workItem.getProvisioningDataValues(LDAP_MOD_LIST);
657     
658     // This is complicated and nested because of the data structures involved, but it boils down to looking 
659     // through all the ldap changes and compare the following: DN, AttributeName, AttributeValue
660     for ( ModifyRequest modRequest : modRequests ) {
661       // Does the DN match?
662       if ( dn.equalsIgnoreCase(modRequest.getDn()) ) {
663         // Go through the attribute changes within the modRequest...
664         for ( AttributeModification attributeMod : modRequest.getAttributeModifications()) {
665           if ( attributeMod.getAttribute().getName().equalsIgnoreCase(attributeName) ) {
666             for ( String modValue : attributeMod.getAttribute().getStringValues() ) {
667               if ( modValue.equalsIgnoreCase(provisioningAttributeValue) ) {
668                 
669                 // Everything matches, so this is a match
670                 
671                 return true;
672               }
673             }
674           }
675         }
676       }
677     }
678     
679     return false;
680   }
681 
682   
683   /**
684    * This method is a backup plan to makeCoalescedLdapChanges and takes a simple approach 
685    * to ldap provisioning. This is useful for two reasons: 1) It might work around a bug
686    * buried in the complexity of coalescing changes; 2) It tells us which workItems 
687    * have a problem because each workItem is done separately. 
688    * @param workItem
689    */
690   private void makeIndividualLdapChanges(ProvisioningWorkItem workItem) throws PspException {
691     List<ModifyRequest> mods = (List) workItem.getProvisioningDataValues(LDAP_MOD_LIST);
692     
693     if ( mods == null ) {
694       LOG.debug("{}: No ldap changes are necessary for work item {}", getDisplayName(), workItem);
695       return;
696     }
697     
698     LOG.debug("{}: Implementing changes for work item {}", getDisplayName(), workItem);
699     for ( ModifyRequest mod : mods ) {
700       try {
701         getLdapSystem().performLdapModify(mod, false);
702       } catch (PspException e) {
703         LOG.error("{}: Ldap provisioning failed for {} / {}", new Object[]{getDisplayName(), workItem, mod, e});
704 
705         throw e;
706       }
707     }
708   }
709 
710   protected LdapSystem getLdapSystem() throws PspException {
711     if ( ldapSystem != null )
712       return ldapSystem;
713     
714     // Make sure we only build a single LdapSystem
715     synchronized (this) {
716       // See if another thread build the LdapSystem while we were waiting
717       // for the mutex
718       if ( ldapSystem != null )
719         return ldapSystem;
720       
721       ldapSystem = new LdapSystem(config.getLdapPoolName(), config.isActiveDirectory());
722       return ldapSystem;
723     }
724   }
725 
726   private String getLoggingSummary(ModifyRequest modForDn) {
727     if ( modForDn == null )
728       return "no changes";
729     
730     StringBuilder sb = new StringBuilder();
731     // Put the first two DN components into buffer
732     sb.append(LdapObject.getDnSummary(modForDn.getDn(), 2));
733 
734     for ( AttributeModification attribute : modForDn.getAttributeModifications()) {
735       switch (attribute.getAttributeModificationType()) {
736         case ADD: sb.append(String.format("[%s: +%d value(s)]",
737                       attribute.getAttribute().getName(),
738                       attribute.getAttribute().getStringValues().size()));
739         break;
740         
741         case REMOVE: sb.append(String.format("[%s: -%d value(s)]",
742             attribute.getAttribute().getName(),
743             attribute.getAttribute().getStringValues().size()));
744         break;
745         
746         case REPLACE: sb.append(String.format("[%s: =%d value(s)]",
747             attribute.getAttribute().getName(),
748             attribute.getAttribute().getStringValues().size()));
749         break;
750       }
751     }
752   
753     return sb.toString();
754   }
755 
756 
757   /**
758    * Public way to create any missing OUs.
759    *
760    * @param dnString
761    * @param wholeDnIsTheOu false: The top of the DN is not an OU (eg, cn=group,ou=folder1,ou=folder2,dc=example).
762    *                       true: The top of the DN is an OU (eg, ou=folder1, ou=folder2, dc=example).
763    * @throws PspException
764    */
765   public void ensureLdapOusExist(String dnString, boolean wholeDnIsTheOu) throws PspException {
766     LOG.info("{}: Checking for (and creating) missing OUs in DN: {} (wholeDnIsOu={})",
767             new Object[]{getDisplayName(), dnString, wholeDnIsTheOu});
768 
769     DN startingDn;
770     try {
771       startingDn = new DN(dnString);
772 
773       if ( wholeDnIsTheOu ) {
774         ensureLdapOusExist(startingDn);
775       } else {
776         ensureLdapOusExist(startingDn.getParent());
777       }
778     } catch (LDAPException e) {
779       LOG.error("Problem parsing DN {}", dnString, e);
780       throw new PspException("Problem parsing DN: %s", dnString);
781     }
782 
783   }
784 
785 
786   /**
787    * Internal worker function called by ensureLdapOusExist(dnString, wholeDnIsTheOu).
788    *
789    * This function reads a dn and if it doesn't already exist, then it makes sure the
790    * parent dn exists (with a recursive call) and then creates an ou at the dn location
791    * by calling createOuInExistingLocation(dn).
792    *
793    * @param dn
794    * @throws PspException
795    */
796   protected void ensureLdapOusExist(DN dn) throws PspException {
797     if ( dn.isNullDN() ) {
798       throw new PspException("Never found an existing DN component when creating OUs");
799     }
800 
801 
802     if ( existingOUs.contains(dn) ) {
803       LOG.debug("{}: OU is known to exist: {}", getDisplayName(), dn.toMinimallyEncodedString());
804       return;
805     }
806 
807     LOG.debug("{}: Checking to see if ou exists: {}", getDisplayName(), dn);
808     try {
809         if ( getLdapSystem().performLdapRead(dn) != null ) {
810           // OU already exists
811           existingOUs.add(dn);
812           return;
813         } else {
814           // OU doesn't already exist. Make sure parent exists and then create new OU
815           ensureLdapOusExist(dn.getParent());
816           createOuInExistingLocation(dn);
817           existingOUs.add(dn);
818         }
819     }
820     catch (PspException e) {
821         LOG.error("{}: Creating OU failed: {}", new Object[]{getDisplayName(), dn, e});
822         throw new PspException("Unable to find existing OU nor create new one (%s)", e.getMessage());
823     }
824   }
825 
826 
827   /**
828    * This function creates an OU with the provided DN with the OU-Creation ldif template.
829    *
830    * This function assumes that the parent DN of ouDn exists. In other words, this function
831    * will not try to create any parent OUs.
832    *
833    * @param ouDn
834    * @throws PspException
835    */
836   protected void createOuInExistingLocation(DN ouDn) throws PspException {
837     String ouDnString = ouDn.toMinimallyEncodedString();
838 
839     LOG.info("{}: Creating OU: {}", getDisplayName(), ouDnString);
840 
841     RDN topRDN = ouDn.getRDN();
842 
843     // Get the attribute information recorded in the first RDN
844     LdapAttribute topRdnAttribute = new LdapAttribute(topRDN.getAttributeNames()[0]);
845     topRdnAttribute.addStringValue( topRDN.getAttributeValues());
846 
847     String ldif = evaluateJexlExpression("OuTemplate", config.getOuCreationLdifTemplate(),
848             null, null,
849             null, null,
850             "dn", ouDn.toMinimallyEncodedString(),
851             "ou", topRdnAttribute.getStringValue());
852     ldif = ldif.replaceAll("\\|\\|", "\n");
853 
854     try {
855       Reader reader = new StringReader(ldif);
856       LdifReader ldifReader = new LdifReader(reader);
857       SearchResult ldifResult = ldifReader.read();
858       LdapEntry ldifEntry = ldifResult.getEntry();
859 
860       // Add the current attribute from the RDN if it was not already in the ldif template
861       if ( ldifEntry.getAttribute( topRdnAttribute.getName() ) == null ) {
862         ldifEntry.addAttribute(topRdnAttribute);
863       }
864 
865       performLdapAdd(ldifEntry);
866     } catch ( IOException e ) {
867       LOG.error("{}: Problem while processing ldif to create new OU: {}", new Object[] {getDisplayName(), ldif, e});
868       throw new PspException("LDIF problem creating OU: %s", e.getMessage());
869     }
870   }
871 
872   /**
873    * Perform an LDAP ADD after making sure the new object's OU exists.
874    * @param entryToAdd
875    * @throws PspException
876    */
877   protected void performLdapAdd(LdapEntry entryToAdd) throws PspException {
878     LOG.info("{}: Creating LDAP object: {}", getDisplayName(), entryToAdd.getDn());
879 
880     ensureLdapOusExist(entryToAdd.getDn(), false);
881     ldapSystem.performLdapAdd(entryToAdd);
882   }
883 
884 }