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