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.Filter;
25  import com.unboundid.ldap.sdk.LDAPException;
26  import org.apache.commons.lang.StringUtils;
27  import org.ldaptive.*;
28  import org.ldaptive.io.LdifReader;
29  
30  import edu.internet2.middleware.subject.Subject;
31  import static edu.internet2.middleware.grouper.pspng.PspUtils.*;
32  
33  
34  
35  /**
36   * This class is the workhorse for provisioning LDAP groups from
37   * grouper.
38   *
39   * @author bert
40   *
41   */
42  public class LdapGroupProvisioner extends LdapProvisioner<LdapGroupProvisionerConfiguration> {
43  
44    public LdapGroupProvisioner(String provisionerName, LdapGroupProvisionerConfiguration config, boolean fullSyncMode) {
45      super(provisionerName, config, fullSyncMode);
46  
47      LOG.debug("Constructing LdapGroupProvisioner: {}", provisionerName);
48    }
49  
50    public static Class<? extends ProvisionerConfiguration> getPropertyClass() {
51      return LdapGroupProvisionerConfiguration.class;
52    }
53  
54  
55    @Override
56    protected void addMembership(GrouperGroupInfo grouperGroupInfo, LdapGroup ldapGroup,
57        Subject subject, LdapUser ldapUser) throws PspException {
58  
59      // TODO: Look in memory cache to see if change is necessary:
60      // a) User object's group-listing attribute
61      // or b) if the group-membership attribute is being fetched
62  
63      if ( ldapUser == null && config.needsTargetSystemUsers() ) {
64        LOG.warn("{}: Skipping adding membership to group {} because ldap user does not exist: {}",
65            new Object[]{getDisplayName(), grouperGroupInfo, subject});
66        return;
67      }
68  
69      if ( ldapGroup == null ) {
70        // Create the group if it hasn't been created yet. List the user so that creation can be combined
71        // with membership addition
72  
73        // This will normally occur when the schema requires members and group-creation is delayed until
74        // the this method is being called to add the first member
75        ldapGroup = createGroup(grouperGroupInfo, Arrays.asList(subject));
76        cacheGroup(grouperGroupInfo, ldapGroup);
77      }
78      else {
79        String membershipAttributeValue = evaluateJexlExpression("MemberAttributeValue", config.getMemberAttributeValueFormat(), subject, ldapUser, grouperGroupInfo, ldapGroup);
80        if ( membershipAttributeValue != null ) {
81          scheduleGroupModification(grouperGroupInfo, ldapGroup, AttributeModificationType.ADD, Arrays.asList(membershipAttributeValue));
82          JobStatistics jobStatistics = this.getJobStatistics();
83          if (jobStatistics != null) {
84            jobStatistics.insertCount.addAndGet(1);
85          }
86        }
87      }
88    }
89  
90  
91    protected void scheduleGroupModification(GrouperGroupInfo grouperGroupInfo, LdapGroup ldapGroup, AttributeModificationType modType, Collection<String> membershipValuesToChange) {
92      String attributeName = config.getMemberAttributeName();
93  
94      for ( String value : membershipValuesToChange )
95        // ADD/REMOVE <value> to/from <attribute> of <group>
96        LOG.info("Will change LDAP: {} {} {} {} of {}",
97            new Object[] {modType, value,
98            modType == AttributeModificationType.ADD ? "to" : "from",
99            attributeName, ldapGroup});
100 
101     scheduleLdapModification(
102         new ModifyRequest(
103             ldapGroup.getLdapObject().getDn(),
104             new AttributeModification(
105                 modType,
106                 new LdapAttribute(attributeName, membershipValuesToChange.toArray(new String[0])))));
107   }
108 
109   @Override
110   protected void deleteMembership(GrouperGroupInfo grouperGroupInfo, LdapGroup ldapGroup ,
111       Subject subject, LdapUser ldapUser) throws PspException {
112     if ( ldapGroup  == null ) {
113       LOG.warn("{}: Ignoring request to remove {} from a group that doesn't exist: {}",
114           new Object[]{getDisplayName(), subject.getId(), grouperGroupInfo});
115       return;
116     }
117 
118     if ( ldapUser == null && config.needsTargetSystemUsers() ) {
119       LOG.warn("{}: Skipping removing membership from group {} because ldap user does not exist: {}",
120           new Object[]{getDisplayName(), grouperGroupInfo, subject});
121       return;
122     }
123 
124     // TODO: Look in memory cache to see if change is necessary:
125     // a) User object's group-listing attribute
126     // or b) if the group-membership attribute is being fetched
127 
128     String membershipAttributeValue = evaluateJexlExpression("MemberAttributeValue", config.getMemberAttributeValueFormat(), subject, ldapUser, grouperGroupInfo, ldapGroup);
129 
130     if ( membershipAttributeValue != null ) {
131       JobStatistics jobStatistics = this.getJobStatistics();
132       if (jobStatistics != null) {
133         jobStatistics.deleteCount.addAndGet(1);
134       }
135       scheduleGroupModification(grouperGroupInfo, ldapGroup, AttributeModificationType.REMOVE, Arrays.asList(membershipAttributeValue));
136     }
137   }
138 
139   @Override
140   protected boolean doFullSync(
141       GrouperGroupInfo grouperGroupInfo, LdapGroup ldapGroup ,
142       Set<Subject> correctSubjects, Map<Subject, LdapUser> tsUserMap,
143       Set<LdapUser> correctTSUsers,
144       JobStatistics stats) throws PspException {
145 
146     stats.totalCount.set(correctSubjects.size());
147 
148     // Looking for bug
149     // Make sure the group we've been passed has been fetched with the membership attribute
150     if ( ldapGroup != null )
151       ldapGroup.getLdapObject().getStringValues(config.getMemberAttributeName());
152 
153     // If the group does not exist yet, then create it with all the correct members
154     if ( ldapGroup  == null ) {
155 
156       // If the schema requires member attribute, then don't do anything if there aren't any members
157       if ( config.areEmptyGroupsSupported() ) {
158         if ( correctSubjects.size() == 0 ) {
159           LOG.info("{}: Nothing to do because empty group already not present in ldap system", getDisplayName() );
160           return false;
161         }
162       }
163 
164       ldapGroup  = createGroup(grouperGroupInfo, correctSubjects);
165       stats.insertCount.addAndGet(correctSubjects.size());
166 
167       // Make note of the group if it was created
168       if ( ldapGroup != null ) {
169         cacheGroup(grouperGroupInfo, ldapGroup);
170       }
171       return true;
172     } else {
173         // The LDAP group exists, let's make sure the non-membership attributes are still accurate
174         ldapGroup = updateGroupFromTemplate(grouperGroupInfo, ldapGroup);
175         cacheGroup(grouperGroupInfo, ldapGroup);
176     }
177 
178     // Delete an empty group if the schema requires a membership
179     if ( !config.areEmptyGroupsSupported() && correctSubjects.size() == 0 ) {
180       LOG.info("{}: Deleting empty group because schema requires its member attribute", getDisplayName());
181       deleteGroup(grouperGroupInfo, ldapGroup);
182 
183       // Update stats with the number of values removed by group deletion
184       Collection<String> membershipValues = ldapGroup.getLdapObject().getStringValues(config.getMemberAttributeName());
185       stats.deleteCount.addAndGet(membershipValues.size());
186 
187       return true;
188     }
189 
190     Set<String> correctMembershipValues = getStringSet(config.isMemberAttributeCaseSensitive());
191 
192     for ( Subject correctSubject: correctSubjects ) {
193       String membershipAttributeValue = evaluateJexlExpression("MemberAttributeValue", config.getMemberAttributeValueFormat(), correctSubject, tsUserMap.get(correctSubject), grouperGroupInfo, ldapGroup);
194 
195       if ( membershipAttributeValue != null ) {
196         correctMembershipValues.add(membershipAttributeValue);
197       }
198     }
199 
200     Collection<String> currentMembershipValues = getStringSet(config.isMemberAttributeCaseSensitive(), ldapGroup.getLdapObject().getStringValues(config.getMemberAttributeName()));
201 
202     // If configured to ignore the null or empty DN on the membership attribute do
203     // so but only if the membership attribute is "member". This may be extended to
204     // other membership attributes over time but currently focuses on the use case
205     // where the objectClass is groupOfNames and the membership attribute that requires
206     // DN syntax is member.
207     if(config.allowEmptyDnAttributeValues()) {
208         if(config.getMemberAttributeName().equals("member")) {
209             currentMembershipValues.removeIf(v -> v.equals(""));
210         }
211     }
212 
213     LOG.info("{}: Full-sync comparison for {}: Target-subject count: Correct/Actual: {}/{}",
214             new Object[] {getDisplayName(), grouperGroupInfo, correctMembershipValues.size(), currentMembershipValues.size()});
215 
216     LOG.debug("{}: Full-sync comparison: Correct: {}", getDisplayName(), correctMembershipValues);
217     LOG.debug("{}: Full-sync comparison: Actual: {}", getDisplayName(), currentMembershipValues);
218 
219     // EXTRA = CURRENT - CORRECT
220       Collection<String> extraValues = subtractStringCollections(
221               config.isMemberAttributeCaseSensitive(), currentMembershipValues, correctMembershipValues);
222 
223       stats.deleteCount.addAndGet(extraValues.size());
224 
225       LOG.info("{}: Group {} has {} extra values",
226           new Object[] {getDisplayName(), grouperGroupInfo, extraValues.size()});
227       if ( extraValues.size() > 0 ) {
228         getLdapSystem().performLdapModify(
229                 new ModifyRequest(
230                         ldapGroup.dn,
231                         new AttributeModification(
232                                 AttributeModificationType.REMOVE,
233                                 new LdapAttribute(config.getMemberAttributeName(),extraValues.toArray(new String[0])))),
234                 config.isMemberAttributeCaseSensitive(),
235                 true);
236       }
237 
238     // MISSING = CORRECT - CURRENT
239       Collection<String> missingValues = subtractStringCollections(
240               config.isMemberAttributeCaseSensitive(), correctMembershipValues, currentMembershipValues);
241 
242       stats.insertCount.addAndGet(missingValues.size());
243 
244       LOG.info("{}: Group {} has {} missing values",
245           new Object[]{getDisplayName(), grouperGroupInfo, missingValues.size()});
246       if ( missingValues.size() > 0 ) {
247         getLdapSystem().performLdapModify(
248                 new ModifyRequest(
249                         ldapGroup.dn,
250                         new AttributeModification(
251                                 AttributeModificationType.ADD,
252                                 new LdapAttribute(config.getMemberAttributeName(),missingValues.toArray(new String[0])))),
253                 config.isMemberAttributeCaseSensitive(),
254                 true);
255 
256     }
257 
258     return extraValues.size()>0 || missingValues.size()>0;
259   }
260 
261   /**
262    * This method compares the existing LdapGroup to how the groupCreationTemplate might have
263    * changed due to group changes (eg, a changed group name) or due to template changes
264    * @param grouperGroupInfo
265    * @param existingLdapGroup
266    * @return An up-to-date LdapGroup: either existingLdapGroup if no changes were needed, or a newly-read group
267    */
268   protected LdapGroupg/LdapGroup.html#LdapGroup">LdapGroup updateGroupFromTemplate(GrouperGroupInfo grouperGroupInfo, LdapGroup existingLdapGroup) throws PspException {
269     LOG.debug("{}: Making sure (non-membership) attributes of group are up to date: {}", getDisplayName(), existingLdapGroup.dn);
270 
271     try {
272       String ldifFromTemplate = getGroupLdifFromTemplate(grouperGroupInfo, config.removeNullDnFromGroupLdifCreationTemplate());
273       LdapEntry ldapEntryFromTemplate = getLdapEntryFromLdif(ldifFromTemplate);
274 
275       ensureLdapOusExist(ldapEntryFromTemplate.getDn(), false);
276       if ( getLdapSystem().makeLdapObjectCorrect(ldapEntryFromTemplate, existingLdapGroup.ldapObject.ldapEntry, config.isMemberAttributeCaseSensitive()) ) {
277         LdapGroup result = fetchTargetSystemGroup(grouperGroupInfo);
278         return result;
279       }
280       else {
281         return existingLdapGroup;
282       }
283     }
284     catch (PspException e) {
285       LOG.error("{}: Problem checking and updating group's template attributes", getDisplayName(), e);
286       throw e;
287     }
288     catch (IOException e) {
289       LOG.error("{}: Problem checking and updating group's tempalte attributes", getDisplayName(), e);
290       throw new PspException("IO Exception while checking and updating group's template attributes", e);
291     }
292   }
293 
294 
295 
296   @Override
297 	protected void doFullSync_cleanupExtraGroups(JobStatistics stats) throws PspException {
298 
299     // (1) Get all the groups that are in LDAP
300     String filterString = config.getAllGroupSearchFilter();
301     if ( StringUtils.isEmpty(filterString) ) {
302       LOG.error("{}: Cannot cleanup extra groups without a configured all-group search filter", getDisplayName());
303       return;
304     }
305 
306     String baseDn = config.getGroupSearchBaseDn();
307 
308     if ( StringUtils.isEmpty(baseDn)) {
309       LOG.error("{}: Cannot cleanup extra groups without a configured group-search base dn", getDisplayName());
310       return;
311     }
312 
313     // Get all the LDAP Groups that match the filter
314     List<LdapObject> allProvisionedGroups
315             = getLdapSystem().performLdapSearchRequest(
316             -1, baseDn, SearchScope.SUBTREE,
317                     Arrays.asList(getLdapAttributesToFetch()), filterString);
318 
319 
320     // See what LDAP Groups match the correct list of groups
321 
322     Collection<GrouperGroupInfo> groupsThatShouldBeProvisioned = getAllGroupsForProvisioner();
323     Map<GrouperGroupInfo, LdapGroup> ldapGroupsThatShouldBeProvisioned = fetchTargetSystemGroupsInBatches(groupsThatShouldBeProvisioned);
324 
325     Set<String> correctGroupDNs = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
326     for(LdapGroup correctLdapGroup : ldapGroupsThatShouldBeProvisioned.values()) {
327       String correctLdapGroupDn = correctLdapGroup.getLdapObject().getDn();
328       correctGroupDNs.add(correctLdapGroupDn);
329     }
330 
331 
332     List<LdapObject> groupsToDelete = new ArrayList<LdapObject>();
333     for (LdapObject aProvisionedGroup : allProvisionedGroups) {
334       if ( ! correctGroupDNs.contains(aProvisionedGroup.getDn()) ) {
335         groupsToDelete.add(aProvisionedGroup);
336       }
337     }
338 
339     LOG.info("{}: There are {} groups that we should delete", getDisplayName(), groupsToDelete.size());
340 
341     for ( LdapObject groupToRemove : groupsToDelete ) {
342       int numMembershipsBeingDeleted = groupToRemove.getStringValues(config.getMemberAttributeName()).size();
343       stats.deleteCount.addAndGet(numMembershipsBeingDeleted);
344 
345       getLdapSystem().performLdapDelete(groupToRemove.getDn());
346     }
347 
348   }
349 
350 
351   @Override
352   protected LdapGroup createGroup(GrouperGroupInfo grouperGroup, Collection<Subject> initialMembers) throws PspException {
353     if ( !config.areEmptyGroupsSupported() && initialMembers.size() == 0 ) {
354       LOG.warn("Not Creating LDAP group because empty groups are not supported: {}", grouperGroup);
355       return null;
356     }
357 
358     LOG.info("Creating LDAP group for GrouperGroup: {} ", grouperGroup);
359     String ldif = getGroupLdifFromTemplate(grouperGroup);
360 
361     // If initialMembers were specified, then add the ldif necessary to include them
362     if ( initialMembers != null && initialMembers.size() > 0 ) {
363 
364       // Find all the values for the membership attribute
365       Collection<String> membershipValues = new HashSet<String>(initialMembers.size());
366 
367       for ( Subject subject : initialMembers ) {
368         LdapUser ldapUser;
369         String membershipAttributeValue = null;
370         if (!config.needsTargetSystemUsers()) {
371           membershipAttributeValue = evaluateJexlExpression("MemberAttributeValue", config.getMemberAttributeValueFormat(), subject, null, grouperGroup, null);
372         }
373         else {
374           ldapUser = getTargetSystemUser(subject);
375           if ( ldapUser != null ) {
376             membershipAttributeValue = evaluateJexlExpression("MemberAttributeValue", config.getMemberAttributeValueFormat(), subject, ldapUser, grouperGroup, null);
377           }
378         }
379 
380         if ( membershipAttributeValue != null ) {
381           membershipValues.add(membershipAttributeValue);
382         }
383       }
384 
385       JobStatistics jobStatistics = this.getJobStatistics();
386       if (jobStatistics != null) {
387         jobStatistics.insertCount.addAndGet(membershipValues.size());
388       }
389 
390       StringBuilder ldifForMemberships = new StringBuilder();
391       for ( String attributeValue : membershipValues ) {
392         ldifForMemberships.append(String.format("%s: %s\n", config.getMemberAttributeName(), attributeValue));
393       }
394       ldif = ldif.concat("\n");
395       ldif = ldif.concat(ldifForMemberships.toString());
396     }
397 
398     Connection conn = getLdapSystem().getLdapConnection();
399     try {
400       LOG.debug("{}: LDIF for new group (with partial DN): {}", getDisplayName(), ldif.replaceAll("\\n", "||"));
401       LdapEntry ldifEntry = getLdapEntryFromLdif(ldif);
402 
403       // Check to see if any attributes ended up without any values/
404       for ( String attributeName : ldifEntry.getAttributeNames() ) {
405 
406         // If the attribute value requires DN syntax and we allow the null DN
407         // (an empty DN) then continue and examine the next attribute.
408         if(config.allowEmptyDnAttributeValues()) {
409             List<String> attributeDnSyntaxList = Arrays.asList(config.getAttributesNeededingDnEscaping());
410             if(attributeDnSyntaxList.contains(attributeName)) {
411                 LOG.debug("{}: attribute {} requires DN syntax but is allowed to hold the null DN", getDisplayName(), attributeName);
412                 continue;
413             }
414         }
415         LdapAttribute attribute = ldifEntry.getAttribute(attributeName);
416         if ( LdapSystem.attributeHasNoValues(attribute) ) {
417           LOG.warn("{}: LDIF for new group did not define any values for {}", getDisplayName(), attributeName);
418           ldifEntry.removeAttribute(attributeName);
419         }
420       }
421       LOG.debug("{}: Adding group: {}", getDisplayName(), ldifEntry);
422       
423       performLdapAdd(ldifEntry);
424       
425       // Read the group that was just created
426       LOG.debug("Reading group that was just added to ldap server: {}", grouperGroup);
427       LdapGroup result = fetchTargetSystemGroup(grouperGroup);
428 
429       if ( result == null ) {
430         LOG.error("{}: Group could not be found after it was created: {}", getDisplayName(), grouperGroup);
431       }
432       return result;
433     } catch (PspException e) {
434       LOG.error("Problem while creating new group: {}", ldif, e);
435       throw e;
436     } catch ( IOException e ) {
437       LOG.error("IO problem while creating group: {}", ldif, e);
438       throw new PspException("IO problem while creating group: %s", e.getMessage());
439     }
440     finally {
441       conn.close();
442     }
443   }
444 
445   /**
446    * This returns an LdapEntry from the provided ldif. NOTE: The DN of the LDIF is extended
447    * with the configuration's groupCreationBaseDn.
448    *
449    * @param ldif
450    * @return
451    * @throws IOException
452    */
453   private LdapEntry getLdapEntryFromLdif(String ldif) throws IOException {
454     Reader reader = new StringReader(ldif);
455     LdifReader ldifReader = new LdifReader(reader);
456     SearchResult ldifResult = ldifReader.read();
457     LdapEntry ldifEntry = ldifResult.getEntry();
458 
459     // Update DN to be relative to groupCreationBaseDn
460     String actualDn = String.format("%s,%s", ldifEntry.getDn(),config.getGroupCreationBaseDn());
461     ldifEntry.setDn(actualDn);
462     return ldifEntry;
463   }
464 
465   /**
466    * Fills in the GroupCreationLdifTemplate for the provided group
467    * @param grouperGroup
468    * @param stripMembershipAttributeWithNullDn
469    * @return
470    * @throws PspException
471    */
472   private String getGroupLdifFromTemplate(GrouperGroupInfo grouperGroup, boolean stripMembershipAttributeWithNullDn) throws PspException {
473     String ldif = config.getGroupCreationLdifTemplate();
474 
475     if(stripMembershipAttributeWithNullDn) {
476         LOG.debug("Stripping membership attribute {} with null DN value. LDIF string before is: {}", config.getMemberAttributeName(), ldif);
477         ldif = ldif.replaceAll(config.getMemberAttributeName() + ":\\|\\|", "");
478         LOG.debug("LDIF string after is: {}", ldif);
479     }
480 
481     ldif = ldif.replaceAll("\\|\\|", "\n");
482     ldif = evaluateJexlExpression("GroupTemplate", ldif, null, null, grouperGroup, null);
483     ldif = sanityCheckDnAttributesOfLdif(ldif, "Group ldif for %s", grouperGroup);
484 
485     return ldif;
486   }
487 
488   private String getGroupLdifFromTemplate(GrouperGroupInfo grouperGroup) throws PspException {
489       return getGroupLdifFromTemplate(grouperGroup, false);
490   }
491 
492   @Override
493   protected Map<GrouperGroupInfo, LdapGroup> fetchTargetSystemGroups(
494       Collection<GrouperGroupInfo> grouperGroupsToFetch) throws PspException {
495     if ( grouperGroupsToFetch.size() > config.getGroupSearch_batchSize() )
496       throw new IllegalArgumentException("LdapGroupProvisioner.fetchTargetSystemGroups: invoked with too many groups to fetch");
497     
498     // If this is a full-sync provisioner, then we want to make sure we get the member attribute of the
499     // group so we see all members.
500     String[] returnAttributes = getLdapAttributesToFetch();
501 
502     if ( grouperGroupsToFetch.size() > 1 && config.isBulkGroupSearchingEnabled() ) {
503       StringBuilder combinedLdapFilter = new StringBuilder();
504 
505       // Start the combined ldap filter as an OR-query
506       combinedLdapFilter.append("(|");
507 
508       for (GrouperGroupInfo grouperGroup : grouperGroupsToFetch) {
509         SearchFilter f = getGroupLdapFilter(grouperGroup);
510         String groupFilterString = f.format();
511 
512         // Wrap the subject's filter in (...) if it doesn't start with (
513         if (groupFilterString.startsWith("("))
514           combinedLdapFilter.append(groupFilterString);
515         else
516           combinedLdapFilter.append('(').append(groupFilterString).append(')');
517       }
518       combinedLdapFilter.append(')');
519 
520       // Actually do the search
521       List<LdapObject> searchResult;
522 
523       LOG.debug("{}: Searching for {} groups with:: {}",
524               new Object[]{getDisplayName(), grouperGroupsToFetch.size(), combinedLdapFilter});
525 
526       try {
527         searchResult = getLdapSystem().performLdapSearchRequest(
528                 -1, config.getGroupSearchBaseDn(), SearchScope.SUBTREE,
529                 Arrays.asList(returnAttributes),
530                 combinedLdapFilter.toString());
531       } catch (PspException e) {
532         LOG.error("Problem fetching groups with filter '{}' on base '{}'",
533                 new Object[]{combinedLdapFilter, config.getGroupSearchBaseDn(), e});
534         throw e;
535       }
536 
537       LOG.debug("{}: Group search returned {} groups", getDisplayName(), searchResult.size());
538 
539       // Now we have a bag of LdapObjects, but we don't know which goes with which grouperGroup.
540       // We're going to go through the Grouper Groups and their filters and compare
541       // them to the Ldap data we've fetched into memory.
542       Map<GrouperGroupInfo, LdapGroup> result = new HashMap<GrouperGroupInfo, LdapGroup>();
543 
544       Set<LdapObject> matchedFetchResults = new HashSet<LdapObject>();
545 
546       // For every group we tried to bulk fetch, find the matching LdapObject that came back
547       for (GrouperGroupInfo groupToFetch : grouperGroupsToFetch) {
548         SearchFilter f = getGroupLdapFilter(groupToFetch);
549 
550         for (LdapObject aFetchedLdapObject : searchResult) {
551           if (aFetchedLdapObject.matchesLdapFilter(f)) {
552             result.put(groupToFetch, new LdapGroup(aFetchedLdapObject));
553             matchedFetchResults.add(aFetchedLdapObject);
554             break;
555           }
556         }
557       }
558 
559       Set<LdapObject> unmatchedFetchResults = new HashSet<LdapObject>(searchResult);
560       unmatchedFetchResults.removeAll(matchedFetchResults);
561 
562       // We're done if everything matched up
563       if ( unmatchedFetchResults.size() == 0 ) {
564         return result;
565       }
566       else {
567         for (LdapObject unmatchedFetchResult : unmatchedFetchResults) {
568           LOG.warn("{}: Bulk fetch failed (returned unmatchable group data). "
569                           + "This can be caused by searching for a DN with escaping or by singleGroupSearchFilter ({}) that are not included "
570                           + "in groupSearchAttributes ({})?): {}",
571                   new Object[]{getDisplayName(), config.getSingleGroupSearchFilter(), config.getGroupSearchAttributes(), unmatchedFetchResult.getDn()});
572         }
573         LOG.warn("{}: Slower fetching will be attempted", getDisplayName());
574 
575         // Fall through to the one-by-one group searching below. This is slower, but doesn't require the
576         // result-matching step that just failed
577       }
578     }
579 
580     // Do simple ldap searching
581     Map<GrouperGroupInfo, LdapGroup> result = new HashMap<GrouperGroupInfo, LdapGroup>();
582 
583     for (GrouperGroupInfo grouperGroup : grouperGroupsToFetch) {
584       SearchFilter groupLdapFilter = null;
585       if (grouperGroup == null) {
586         continue;
587       }
588       try {
589         groupLdapFilter = getGroupLdapFilter(grouperGroup);
590       } catch (DeletedGroupException dge) {
591         LOG.debug("{}: " + dge.getMessage(), getDisplayName());
592         // cant find, just let full sync deal with it
593         continue;
594       }
595       try {
596         LOG.debug("{}: Searching for group {} with:: {}",
597                 new Object[]{getDisplayName(), grouperGroup, groupLdapFilter});
598 
599         // Actually do the search
600         List<LdapObject> searchResult = getLdapSystem().performLdapSearchRequest(
601                 -1, config.getGroupSearchBaseDn(), SearchScope.SUBTREE,
602                 Arrays.asList(returnAttributes),
603                 groupLdapFilter);
604 
605         if (searchResult.size() == 1) {
606           LdapObject ldapObject = searchResult.iterator().next();
607           LOG.debug("{}: Group search returned {}", getDisplayName(), ldapObject.getDn());
608           result.put(grouperGroup, new LdapGroup(ldapObject));
609         }
610         else if ( searchResult.size() > 1 ){
611           LOG.error("{}: Search for group {} with '{}' returned multiple matches: {}",
612                   new Object[]{getDisplayName(), grouperGroup, groupLdapFilter, searchResult});
613           throw new PspException("Search for ldap group returned multiple matches");
614         }
615         else if ( searchResult.size() == 0 ) {
616           // No match found ==> result will not include an entry for this grouperGroup
617           LOG.debug("{}: Group search did not return any results", getDisplayName());
618         }
619       } catch (PspException e) {
620         LOG.error("{}: Problem fetching group with filter '{}' on base '{}'",
621                 new Object[]{getDisplayName(), groupLdapFilter, config.getGroupSearchBaseDn(), e});
622         throw e;
623       }
624     }
625 
626     return result;
627 }
628 
629   private String[] getLdapAttributesToFetch() {
630     String returnAttributes[] = config.getGroupSearchAttributes();
631     if ( fullSyncMode ) {
632       LOG.debug("Fetching membership attribute, too");
633       // Add the membership attribute to the list of attributes to fetch
634       returnAttributes = Arrays.copyOf(returnAttributes, returnAttributes.length + 1);
635       returnAttributes[returnAttributes.length-1] = config.getMemberAttributeName();
636     } else {
637       LOG.debug("Fetching without membership attribute");
638     }
639     return returnAttributes;
640   }
641 
642 
643   private SearchFilter getGroupLdapFilter(GrouperGroupInfo grouperGroup) throws PspException {
644     String result = evaluateJexlExpression("SingleGroupSearchFilter", config.getSingleGroupSearchFilter(), null, null, grouperGroup, null);
645     if ( StringUtils.isEmpty(result) )
646       throw new RuntimeException("Group searching requires singleGroupSearchFilter to be configured correctly");
647 
648     // If the filter contains '||', then this filter is requesting parameter substitution
649     String filterPieces[] = result.split("\\|\\|");
650     SearchFilter filter = new SearchFilter(filterPieces[0]);
651     // If the filter is not using ldap-filter parameters, check its syntax
652     if ( filterPieces.length == 1 ) {
653       try {
654         // Use unboundid to sanity-check/parse filter
655         Filter.create(result);
656       }
657       catch (LDAPException e) {
658         LOG.warn("{}: Group ldap filter was invalid. " +
659                         "Perhaps its filter clauses needed to be escaped with utils.escapeLdapFilter or use ldap-filter positional parameters. " +
660                         "Group={}. Bad filter={}. ",
661                 new Object[]{getDisplayName(), grouperGroup, result});
662 
663         // We're going to proceed here just in case the filter-checking logic is too
664         // sensitive. The ldap server will eventually see the filter and make its own decision
665       }
666     } else {
667       // Set the positional parameters
668 
669       for (int i = 1; i < filterPieces.length; i++)
670         filter.setParameter(i - 1, filterPieces[i].trim());
671     }
672 
673     LOG.trace("{}: Filter for group {}: {}",
674         new Object[] {getDisplayName(), grouperGroup, filter});
675 
676     return filter;
677   }
678 
679 
680   @Override
681   protected void deleteGroup(GrouperGroupInfo grouperGroupInfo, LdapGroup ldapGroup)
682       throws PspException {
683     if ( ldapGroup == null ) {
684       LOG.warn("Nothing to do: Unable to delete group {} because the group wasn't found on target system", grouperGroupInfo);
685       return;
686     }
687     
688     String dn = ldapGroup.getLdapObject().getDn();
689     
690     LOG.info("Deleting group {} by deleting DN {}", grouperGroupInfo, dn);
691     JobStatistics jobStatistics = this.getJobStatistics();
692     if (jobStatistics != null) {
693       jobStatistics.deleteCount.addAndGet(1);
694     }
695     
696     getLdapSystem().performLdapDelete(dn);
697   }
698 }