View Javadoc
1   package edu.internet2.middleware.grouper.pspng;
2   
3   import java.io.*;
4   import java.util.*;
5   import java.util.regex.Matcher;
6   import java.util.regex.Pattern;
7   
8   import javax.net.ssl.SSLSocket;
9   import javax.net.ssl.SSLSocketFactory;
10  
11  import com.unboundid.ldap.sdk.DN;
12  import edu.internet2.middleware.morphString.Morph;
13  import org.apache.commons.lang.StringUtils;
14  import org.ldaptive.*;
15  import org.ldaptive.ad.handler.RangeEntryHandler;
16  import org.ldaptive.control.util.PagedResultsClient;
17  import org.ldaptive.handler.HandlerResult;
18  import org.ldaptive.handler.SearchEntryHandler;
19  import org.ldaptive.pool.BlockingConnectionPool;
20  import org.ldaptive.pool.PoolConfig;
21  import org.ldaptive.pool.PoolException;
22  import org.ldaptive.pool.SearchValidator;
23  import org.ldaptive.props.BindConnectionInitializerPropertySource;
24  import org.ldaptive.props.ConnectionConfigPropertySource;
25  import org.ldaptive.props.DefaultConnectionFactoryPropertySource;
26  import org.ldaptive.props.PoolConfigPropertySource;
27  import org.ldaptive.props.SearchRequestPropertySource;
28  import org.ldaptive.sasl.GssApiConfig;
29  import org.slf4j.Logger;
30  import org.slf4j.LoggerFactory;
31  
32  import edu.internet2.middleware.grouper.app.loader.GrouperLoaderConfig;
33  import edu.internet2.middleware.grouper.util.GrouperUtil;
34  import static edu.internet2.middleware.grouper.pspng.PspUtils.*;
35  
36  /**
37   * This class encapsulates an LDAP system configured by a collection of
38   * properties defined withing grouper-loader.properties
39   * @author bert
40   *
41   */
42  public class LdapSystem {
43    private static final Logger LOG = LoggerFactory.getLogger(LdapSystem.class);
44  
45    // What ldaptive properties will be decrypted if their values are Morph files?
46    // (We don't decrypt all properties because that would prevent the use of slashes in the property values)
47    public static final String ENCRYPTABLE_LDAPTIVE_PROPERTIES[]
48            = new String[]{"org.ldaptive.bindCredential"};
49  
50    public final String ldapSystemName;
51    protected Properties _ldaptiveProperties = new Properties();
52    
53    private final boolean isActiveDirectory;
54    private BlockingConnectionPool ldapPool;
55  
56    protected boolean searchResultPagingEnabled_defaultValue = true;
57    protected int searchResultPagingSize_default_value = 100;
58  
59  
60    public static boolean attributeHasNoValues(final LdapAttribute attribute) {
61      if ( attribute == null ) {
62        return true;
63      }
64  
65      Collection<String> values = attribute.getStringValues();
66  
67      return values.size() == 0  || values.iterator().next().length() == 0;
68    }
69  
70  
71    public LdapSystem(String ldapSystemName, boolean isActiveDirectory) {
72      this.ldapSystemName = ldapSystemName;
73      this.isActiveDirectory = isActiveDirectory;
74      getLdaptiveProperties();
75    }
76  
77    
78    private BlockingConnectionPool buildLdapConnectionPool() throws PspException {
79      BlockingConnectionPool result;
80    
81      LOG.info("{}: Creating LDAP Pool", ldapSystemName);
82      Properties ldaptiveProperties = getLdaptiveProperties();
83  
84      Properties loggableProperties = new Properties();
85      loggableProperties.putAll(ldaptiveProperties);
86  
87      for ( String propertyToMask : ENCRYPTABLE_LDAPTIVE_PROPERTIES )
88      {
89        if ( loggableProperties.containsKey(propertyToMask) )
90        {
91          loggableProperties.put(propertyToMask, "**masked**");
92        }
93      }
94      
95      LOG.info("Setting up LDAP Connection with properties: {}", loggableProperties);
96  
97      // Setup ldaptive ConnectionConfig
98      ConnectionConfig connConfig = new ConnectionConfig();
99      ConnectionConfigPropertySource ccpSource = new ConnectionConfigPropertySource(connConfig, ldaptiveProperties);
100     ccpSource.initialize();
101   
102     //GrouperLoaderLdapServer grouperLoaderLdapProperties 
103     //  = GrouperLoaderConfig.retrieveLdapProfile(ldapSystemName);
104     
105     /////////////
106     // Binding
107     BindConnectionInitializer binder = new BindConnectionInitializer();
108   
109     BindConnectionInitializerPropertySource bcip = new BindConnectionInitializerPropertySource(binder, ldaptiveProperties);
110     bcip.initialize();
111   
112     // I'm not sure if SaslRealm and/or SaslAuthorizationId can be used independently
113     // Therefore, we'll initialize gssApiConfig when either one of them is used.
114     // And, then, we'll attach the gssApiConfig to the binder if there is a gssApiConfig
115     GssApiConfig gssApiConfig = null;
116     String val = (String) ldaptiveProperties.get("org.ldaptive.saslRealm");
117     if (!StringUtils.isBlank(val)) {
118       LOG.info("Processing saslRealm");
119       if ( gssApiConfig == null )
120         gssApiConfig = new GssApiConfig();
121       gssApiConfig.setRealm(val);
122       
123     }
124     
125     val = (String) ldaptiveProperties.get("org.ldaptive.saslAuthorizationId");
126     if (!StringUtils.isBlank(val)) {
127       LOG.info("Processing saslAuthorizationId");
128       if ( gssApiConfig == null )
129         gssApiConfig = new GssApiConfig();
130       gssApiConfig.setAuthorizationId(val);
131     }
132   
133     // If there was a sasl/gssapi attribute, then save the gssApiConfig
134     if ( gssApiConfig != null ) {
135       LOG.info("Setting gssApiConfig");
136       binder.setBindSaslConfig(gssApiConfig);
137     }
138     
139     DefaultConnectionFactory connectionFactory = new DefaultConnectionFactory();
140     DefaultConnectionFactoryPropertySource dcfSource = new DefaultConnectionFactoryPropertySource(connectionFactory, ldaptiveProperties);
141     dcfSource.initialize();
142 
143     // Test the ConnectionFactory before error messages are buried behind the pool
144     Connection conn = connectionFactory.getConnection();
145     performTestLdapRead(conn);
146     
147     /////////////
148     // PoolConfig
149     
150     PoolConfig ldapPoolConfig = new PoolConfig();
151     PoolConfigPropertySource pcps = new PoolConfigPropertySource(ldapPoolConfig, ldaptiveProperties);
152     pcps.initialize();
153 
154     // Make sure some kind of validation is turned on
155     if ( !ldapPoolConfig.isValidateOnCheckIn() &&
156          !ldapPoolConfig.isValidateOnCheckOut() &&
157          !ldapPoolConfig.isValidatePeriodically() ) {
158       LOG.debug("{}: Using default onCheckOut ldap-connection validation", ldapSystemName);
159       ldapPoolConfig.setValidateOnCheckOut(true);
160     }
161       
162     result = new BlockingConnectionPool(ldapPoolConfig, connectionFactory);
163     result.setValidator(new SearchValidator());
164     result.initialize();
165     
166     ////////////
167     // Test the connection obtained from pool
168     try {
169       conn = result.getConnection();
170       performTestLdapRead(conn);
171     } catch (LdapException e) {
172       LOG.error("Problem while testing ldap pool", e);
173       throw new PspException("Problem testing ldap pool: %s", e.getMessage());
174     }
175     
176     
177     return result;
178   }
179 
180   public void log(LdapEntry ldapEntry, String ldapEntryDescriptionFormat, Object... ldapEntryDescriptionArgs)  {
181     String ldapEntryDescription;
182     if (LOG.isInfoEnabled() || LOG.isDebugEnabled()) {
183       ldapEntryDescription = String.format(ldapEntryDescriptionFormat, ldapEntryDescriptionArgs);
184     } else {
185       return;
186     }
187 
188     // INFO log is a count of each attribute's values
189     if ( LOG.isInfoEnabled() ) {
190       StringBuilder sb = new StringBuilder();
191       sb.append(String.format("dn=%s|", ldapEntry == null ? "null" : ldapEntry.getDn()));
192       if (ldapEntry != null && ldapEntry.getAttributes() != null) {
193         for (LdapAttribute attribute : ldapEntry.getAttributes()) {
194           sb.append(String.format("%d %s values|", attribute.size(), attribute.getName()));
195         }
196       }
197       LOG.info("{}: {} Entry Summary: {}", ldapSystemName, ldapEntryDescription, sb.toString());
198     }
199 
200     LOG.debug("{}: {} Entry Details: {}", ldapSystemName, ldapEntryDescription, ldapEntry);
201   }
202 
203   public void log( ModifyRequest modifyRequest, String descriptionFormat, Object... descriptionArgs)  {
204     String ldapEntryDescription;
205     if (LOG.isInfoEnabled() || LOG.isDebugEnabled()) {
206       ldapEntryDescription = String.format(descriptionFormat, descriptionArgs);
207     } else {
208       return;
209     }
210 
211     // INFO log is a count of each attribute's values
212     if ( LOG.isInfoEnabled() ) {
213       StringBuilder sb = new StringBuilder();
214       sb.append(String.format("dn=%s|", modifyRequest == null ? "null" : modifyRequest.getDn()));
215 
216       if (modifyRequest != null && modifyRequest.getAttributeModifications() != null) {
217         for (AttributeModification mod : modifyRequest.getAttributeModifications()) {
218           sb.append(String.format("%s %d %s values|",
219                   mod.getAttributeModificationType(), mod.getAttribute().size(), mod.getAttribute().getName()));
220         }
221       }
222       LOG.info("{}: {} Mod Summary: {}", ldapSystemName, ldapEntryDescription, sb.toString());
223     }
224 
225     LOG.debug("{}: {} Mod Details: {}", ldapSystemName, ldapEntryDescription, modifyRequest);
226   }
227 
228 
229   protected void performTestLdapRead(Connection conn) throws PspException {
230     LOG.info("{}: Performing test read of directory root", ldapSystemName);
231     SearchExecutor searchExecutor = new SearchExecutor();
232     SearchRequestPropertySource srSource = new SearchRequestPropertySource(searchExecutor, getLdaptiveProperties());
233     srSource.initialize();
234 
235     String baseDn = GrouperLoaderConfig.retrieveConfig().propertyValueString("ldap." + ldapSystemName + ".uiTestSearchDn", "");
236     String filter = GrouperLoaderConfig.retrieveConfig().propertyValueString("ldap." + ldapSystemName + ".uiTestFilter", "objectclass=*");
237     
238     filter = StringUtils.trim(filter);
239     if (filter.startsWith("(") && filter.endsWith(")")) {
240       filter = filter.substring(1, filter.length()-1);
241     }
242     
243     SearchRequest read = new SearchRequest(baseDn, filter);
244     read.setSearchScope(SearchScope.OBJECT);
245 
246     // Turn on attribute-value paging if this is an active directory target
247     if ( isActiveDirectory() )
248       read.setSearchEntryHandlers(new RangeEntryHandler());
249  
250     try {
251       conn.open();
252       SearchOperation searchOp = new SearchOperation(conn);
253       
254       Response<SearchResult> response = searchOp.execute(read);
255       SearchResult searchResult = response.getResult();
256     
257       LdapEntry searchResultEntry = searchResult.getEntry();
258       log(searchResultEntry, "Ldap test success");
259     }
260     catch (LdapException e) {
261       LOG.error("Ldap problem",e);
262       throw new PspException("Problem testing ldap connection: %s", e.getMessage());
263     }
264     finally {
265       conn.close();
266     }
267   }
268 
269   
270   
271   public BlockingConnectionPool getLdapPool() throws PspException {
272     if ( ldapPool != null )
273       return ldapPool;
274     
275     // We don't have a pool setup yet. Synchronize so we're sure we only make one pool.
276     synchronized(this) {
277       // Check if another thread has created our pool while we were waiting for the semaphore
278       if ( ldapPool != null )
279         return ldapPool;
280       
281      ldapPool = buildLdapConnectionPool();
282     }
283   
284     return ldapPool;
285   }
286 
287   
288   
289   public boolean isActiveDirectory() {
290     return isActiveDirectory;
291   }
292 
293   
294   
295   public Properties getLdaptiveProperties() {
296     if ( _ldaptiveProperties.size() == 0 ) {
297       String ldapPropertyPrefix = "ldap." + ldapSystemName.toLowerCase() + ".";
298       
299       for (String propName : GrouperLoaderConfig.retrieveConfig().propertyNames()) {
300         if ( propName.toLowerCase().startsWith(ldapPropertyPrefix) ) {
301           String propValue = GrouperLoaderConfig.retrieveConfig().propertyValueString(propName, "");
302           
303           // Get the part of the property after ldapPropertyPrefix 'ldap.person.'
304           String propNameTail = propName.substring(ldapPropertyPrefix.length());
305           _ldaptiveProperties.put("org.ldaptive." + propNameTail, propValue);
306           
307           // Some compatibility between old vtldap properties and ldaptive versions
308           // url (vtldap) ==> ldapUrl
309           if ( propNameTail.equalsIgnoreCase("url") ) {
310             LOG.info("Setting org.ldaptive.ldapUrl");
311             _ldaptiveProperties.put("org.ldaptive.ldapUrl", propValue);
312           }
313           // tls (vtldap) ==> useStartTls
314           if ( propNameTail.equalsIgnoreCase("tls") ) {
315             LOG.info("Setting org.ldaptive.useStartTLS");
316             _ldaptiveProperties.put("org.ldaptive.useStartTLS", propValue);
317           }
318           // user (vtldap) ==> bindDn
319           if ( propNameTail.equalsIgnoreCase("user") )
320           {
321             LOG.info("Setting org.ldaptive.bindDn");
322             _ldaptiveProperties.put("org.ldaptive.bindDn", propValue);
323           }
324           // pass (vtldap) ==> bindCredential
325           if ( propNameTail.equalsIgnoreCase("pass") )
326           {
327             LOG.info("Setting org.ldaptive.bindCredential");
328             _ldaptiveProperties.put("org.ldaptive.bindCredential", propValue);
329           }
330         }
331       }
332     }
333 
334     // Go through the properties that can be encrypted and decrypt them if they're Morph files
335     for (String encryptablePropertyKey : ENCRYPTABLE_LDAPTIVE_PROPERTIES) {
336       String value = _ldaptiveProperties.getProperty(encryptablePropertyKey);
337       value = Morph.decryptIfFile(value);
338       _ldaptiveProperties.put(encryptablePropertyKey, value);
339     }
340     return _ldaptiveProperties;
341   }
342 
343   
344   
345   public int getSearchResultPagingSize() {
346     Object searchResultPagingSize = getLdaptiveProperties().get("org.ldaptive.searchResultPagingSize");
347     
348     return GrouperUtil.intValue(searchResultPagingSize, searchResultPagingSize_default_value);
349   }
350 
351   
352   
353   public boolean isSearchResultPagingEnabled() {
354     Object searchResultPagingEnabled = getLdaptiveProperties().get("org.ldaptive.searchResultPagingEnabled");
355     
356     return GrouperUtil.booleanValue(searchResultPagingEnabled, searchResultPagingEnabled_defaultValue);
357   }
358 
359   
360   
361   protected Connection getLdapConnection() throws PspException {
362     BlockingConnectionPool pool = getLdapPool();
363     try {
364       Connection conn = pool.getConnection();
365       return conn;
366     } catch (PoolException e) {
367       LOG.error("LDAP Pool Exception", e);
368       throw new PspException("Problem connecting to ldap server %s: %s",pool, e.getMessage());
369     }
370   }
371   
372   
373 
374   /**
375    * Returns ldaptive search executor configured according to properties
376    * @return
377    */
378   public SearchExecutor getSearchExecutor() {
379     SearchExecutor searchExecutor = new SearchExecutor();
380     SearchRequestPropertySource srSource = new SearchRequestPropertySource(searchExecutor, getLdaptiveProperties());
381     srSource.initialize();
382     
383     return searchExecutor;
384   }
385 
386   
387   
388   protected void performLdapAdd(LdapEntry entryToAdd) throws PspException {
389     log(entryToAdd, "Creating LDAP object");
390 
391     Connection conn = getLdapConnection();
392     try {
393       // Actually ADD the object
394       conn.open();
395       conn.getProviderConnection().add(new AddRequest(entryToAdd.getDn(), entryToAdd.getAttributes()));
396     } catch (LdapException e) {
397       if ( e.getResultCode() == ResultCode.ENTRY_ALREADY_EXISTS ) {
398         LOG.warn("{}: Skipping LDAP ADD because object already existed: {}", ldapSystemName, entryToAdd.getDn());
399       } else {
400         LOG.error("{}: Problem while creating new ldap object: {}",
401                 new Object[] {ldapSystemName, entryToAdd, e});
402 
403         throw new PspException("LDAP problem creating object: %s", e.getMessage());
404       }
405     }
406     finally {
407       conn.close();
408     }
409   
410   }
411   
412   
413 
414   protected void performLdapDelete(String dnToDelete) throws PspException {
415     LOG.info("{}: Deleting LDAP object: {}", ldapSystemName, dnToDelete);
416     
417     Connection conn = getLdapConnection();
418     try {
419       // Actually DELETE the account
420       conn.open();
421       conn.getProviderConnection().delete(new DeleteRequest(dnToDelete));
422     } catch (LdapException e) {
423       LOG.error("Problem while deleting ldap object: {}", dnToDelete, e);
424       throw new PspException("LDAP problem deleting object: %s", e.getMessage());
425     }
426     finally {
427       conn.close();
428     }
429   
430   }
431 
432   public void performLdapModify(ModifyRequest mod, boolean valuesAreCaseSensitive) throws PspException {
433     performLdapModify(mod, valuesAreCaseSensitive,true);
434   }
435 
436   /**
437    * This performs a modification and optionally retries it by comparing attributeValues
438    * being added/removed to those already on the ldap server
439    * @param mod
440    * @param retryIfFails Should the Modify be retried if something goes wrong. This retry
441    *                     will do attributeValue-by-attributeValue comparison to
442    *                     make the retry as safe as possible
443    * @throws PspException
444    */
445   public void performLdapModify(ModifyRequest mod, boolean valuesAreCaseSensitive, boolean retryIfFails) throws PspException {
446     log(mod, "Performing ldap mod (%s retry)", retryIfFails ? "with" : "without");
447 
448     Connection conn = getLdapConnection();
449     try {
450       conn.open();
451       conn.getProviderConnection().modify(mod);
452     } catch (LdapException e) {
453 
454       // Abort with Exception if retries are disabled
455       if ( !retryIfFails ) {
456         throw new PspException("%s: Unrecoverable problem modifying ldap object: %s %s",
457                 ldapSystemName, mod, e.getMessage());
458       }
459 
460 
461       LOG.warn("{}: Problem while modifying ldap system based on grouper expectations. Starting to perform adaptive modifications based on data already on server: {}: {}",
462               new Object[]{ldapSystemName, mod.getDn(), e.getResultCode()});
463 
464       // First case: a single attribute being modified with a single value
465       //   Perform a quick ldap comparison and check to see if the object
466       //   already matches the modification
467       //
468       //   If the object doesn't already match, then it was a real ldap failure... there
469       //   is no way to simplify it or otherwise retry it
470       if ( mod.getAttributeModifications().length == 1 &&
471            mod.getAttributeModifications()[0].getAttribute().getStringValues().size() == 1 ) {
472         AttributeModification modification = mod.getAttributeModifications()[0];
473 
474         boolean attributeMatches = performLdapComparison(mod.getDn(), modification.getAttribute());
475 
476         if ( attributeMatches && modification.getAttributeModificationType() == AttributeModificationType.ADD ) {
477           LOG.info("{}: Change not necessary: System already had attribute value", ldapSystemName);
478           return;
479         }
480         else if ( !attributeMatches && modification.getAttributeModificationType() == AttributeModificationType.REMOVE ) {
481           LOG.info("{}: Change not necessary: System already had attribute value removed", ldapSystemName);
482           return;
483         }
484         else {
485           LOG.error("{}: Single-attribute-value Ldap mod-{} failed when Ldap server {} already have {}={}. Mod that failed: {}",
486                   ldapSystemName, modification.getAttributeModificationType().toString().toLowerCase(),
487                   attributeMatches ? "does" : "does not",
488                   modification.getAttribute().getName(), modification.getAttribute().getStringValue(),
489                   mod,
490                   e);
491           throw new PspException("LDAP Modification Failed");
492         }
493       }
494 
495       // This wasn't a single-attribute change, or multiple values were being changed.
496       // Therefore: Read what is in the LDAP server and implement the differences
497 
498 
499       // Gather up the attributes that were modified so we can read them from server
500       Set<String> attributeNames = new HashSet<>();
501       for ( AttributeModification attributeMod : mod.getAttributeModifications()) {
502         attributeNames.add(attributeMod.getAttribute().getName());
503       }
504 
505       // Read the current values of those attributes
506       LOG.info("{}: Modification retrying... reading object to know what needs to change: {}",
507         ldapSystemName, mod.getDn());
508 
509       LdapObject currentLdapObject = performLdapRead(mod.getDn(), attributeNames);
510       log(currentLdapObject.ldapEntry, "Data already on ldap server");
511 
512       // Go back through the requested mods and see if they are redundant
513       for ( AttributeModification attributeMod : mod.getAttributeModifications()) {
514         String attributeName = attributeMod.getAttribute().getName();
515 
516         LOG.info("{}: Summary: Comparing modification of {} to what is already in LDAP: {}/{} Values",
517                 ldapSystemName,
518                 attributeName,
519                 attributeMod.getAttributeModificationType(),
520                 attributeMod.getAttribute().size());
521         LOG.debug("{}: Details: Comparing modification of {} to what is already in LDAP: {}/{}",
522                 ldapSystemName,
523                 attributeName,
524                 attributeMod.getAttributeModificationType(),
525                 attributeMod.getAttribute());
526 
527         Collection<String> currentValues = currentLdapObject.getStringValues(attributeName);
528         Collection<String> modifyValues  = attributeMod.getAttribute().getStringValues();
529 
530         LOG.info("{}: Comparing Attribute {}. #Values on server already {}. #Values in mod/{}: {}",
531           ldapSystemName, attributeName, currentValues.size(), attributeMod.getAttributeModificationType(), modifyValues.size());
532 
533         switch (attributeMod.getAttributeModificationType()) {
534           case ADD:
535             // See if any modifyValues are missing from currentValues
536             //
537             // Subtract currentValues from modifyValues (case-insensitively)
538             Set<String> valuesNotAlreadyOnServer =
539                     subtractStringCollections(valuesAreCaseSensitive, modifyValues, currentValues);
540 
541             LOG.debug("{}: {}: Values on server: {}",
542                     ldapSystemName, attributeName, currentValues);
543             LOG.debug("{}: {}: Modify/Add values: {}",
544                     ldapSystemName, attributeName, modifyValues);
545 
546             LOG.info("{}: {}: Need to add {} values",
547                     ldapSystemName, attributeName, valuesNotAlreadyOnServer.size());
548 
549             for ( String valueToChange : valuesNotAlreadyOnServer ) {
550               performLdapModify( new ModifyRequest( mod.getDn(),
551                       new AttributeModification(AttributeModificationType.ADD,
552                               new LdapAttribute(attributeName, valueToChange))),
553                       valuesAreCaseSensitive,false);
554             }
555             break;
556 
557           case REMOVE:
558             // For Mod.REMOVE, not specifying any values means to remove them all
559             if ( modifyValues.size() == 0 ) {
560               modifyValues.addAll(currentValues);
561             }
562 
563             // See if any modifyValues are still in currentValues
564             //
565             // Intersect modifyValues and currentValues
566             Set<String> valuesStillOnServer
567                     = intersectStringCollections(valuesAreCaseSensitive, modifyValues, currentValues);
568             LOG.debug("{}: {}: Values on server: {}",
569                     ldapSystemName, attributeName, currentValues);
570             LOG.debug("{}: {}: Modify/Delete values: {}",
571                     ldapSystemName, attributeName, modifyValues);
572 
573             LOG.info("{}: {}: {} values need to be REMOVEd",
574                     ldapSystemName, attributeName, valuesStillOnServer.size());
575 
576             for (String valueToChange : valuesStillOnServer) {
577               performLdapModify(new ModifyRequest(mod.getDn(),
578                               new AttributeModification(AttributeModificationType.REMOVE,
579                                       new LdapAttribute(attributeName, valueToChange))),
580                       valuesAreCaseSensitive,false);
581             }
582             break;
583 
584           case REPLACE:
585             // See if any differences between modifyValues and currentValues
586             // (Subtract in both directions)
587 
588             LOG.debug("{}: {}: Values on server: {}",
589                     ldapSystemName, attributeName, currentValues);
590             LOG.debug("{}: {}: Modify/Replace values: {}",
591                     ldapSystemName, attributeName, modifyValues);
592 
593             Set<String> extraValuesOnServer =
594                     subtractStringCollections(valuesAreCaseSensitive, currentValues, modifyValues);
595             LOG.info("{}: REPLACE: {}: {} values still need to be REMOVEd",
596                     ldapSystemName, attributeNames, extraValuesOnServer.size());
597 
598             for (String valueToChange : extraValuesOnServer) {
599               performLdapModify(new ModifyRequest(mod.getDn(),
600                               new AttributeModification(AttributeModificationType.REMOVE,
601                                       new LdapAttribute(attributeName, valueToChange))),
602                       valuesAreCaseSensitive,false);
603             }
604 
605             Set<String> missingValuesOnServer =
606                     subtractStringCollections(valuesAreCaseSensitive, modifyValues, currentValues);
607 
608             LOG.info("{}: REPLACE: {}: {} values need to be ADDed",
609                     ldapSystemName, attributeName, missingValuesOnServer.size());
610 
611             for ( String valueToChange : missingValuesOnServer ) {
612               performLdapModify( new ModifyRequest( mod.getDn(),
613                               new AttributeModification(AttributeModificationType.ADD,
614                                       new LdapAttribute(attributeName, valueToChange))),
615                       valuesAreCaseSensitive, false);
616             }
617         }
618       }
619     }
620     finally {
621       conn.close();
622     }
623   }
624 
625   private boolean performLdapComparison(String dn, LdapAttribute attribute) throws PspException {
626     LOG.info("{}: Performaing Ldap comparison operation: {} on {}",
627             new Object[]{ldapSystemName, attribute, LdapObject.getDnSummary(dn,2)});
628 
629     Connection conn = getLdapConnection();
630     try {
631       try {
632         conn.open();
633         CompareOperation compare = new CompareOperation(conn);
634 
635         boolean result = compare.execute(new CompareRequest(dn, attribute)).getResult();
636         return result;
637 
638       } catch (LdapException ldapException) {
639         ResultCode resultCode = ldapException.getResultCode();
640 
641         // A couple errors mean that object does not match attribute values
642         if (resultCode == ResultCode.NO_SUCH_OBJECT || resultCode == ResultCode.NO_SUCH_ATTRIBUTE) {
643           return false;
644         } else {
645           LOG.error("{}: Error performing compare operation: {}",
646                   new Object[]{ldapSystemName, attribute, ldapException});
647 
648           throw new PspException("LDAP problem performing ldap comparison: %s", ldapException.getMessage());
649         }
650       }
651     }
652     finally {
653       conn.close();
654     }
655 
656   }
657 
658 
659   void performLdapModifyDn(ModifyDnRequest mod) throws PspException {
660     LOG.info("{}: Performing Ldap mod-dn operation: {}", ldapSystemName, mod);
661 
662     Connection conn = getLdapConnection();
663     try {
664       conn.open();
665       conn.getProviderConnection().modifyDn(mod);
666     } catch (LdapException e) {
667       LOG.error("Problem while modifying dn of ldap object: {}", mod, e);
668       throw new PspException("LDAP problem modifying dn of ldap object: %s", e.getMessage());
669     }
670     finally {
671       conn.close();
672     }
673   }
674 
675 
676 
677 
678   protected LdapObject performLdapRead(DN dn, String... attributes) throws PspException {
679     return performLdapRead(dn.toMinimallyEncodedString(), attributes);
680   }
681   
682   protected LdapObject performLdapRead(String dn, Collection<String> attributes) throws PspException {
683     return performLdapRead(dn, attributes.toArray(new String[0]));
684   }
685 
686   protected LdapObject performLdapRead(String dn, String... attributes) throws PspException {
687     LOG.debug("Doing ldap read: {} attributes {}", dn, Arrays.toString(attributes));
688     
689     Connection conn = getLdapConnection();
690     try {
691       conn.open();
692   
693       SearchRequest read = new SearchRequest(dn, "objectclass=*");
694       read.setSearchScope(SearchScope.OBJECT);
695       read.setReturnAttributes(attributes);
696       
697       // Turn on attribute-value paging if this is an active directory target
698       if ( isActiveDirectory() ) {
699         LOG.info("Active Directory: Searching with Ldap RangeEntryHandler");
700         read.setSearchEntryHandlers(new RangeEntryHandler());
701       }
702 
703       SearchOperation searchOp = new SearchOperation(conn);
704       
705       Response<SearchResult> response = searchOp.execute(read);
706       SearchResult searchResult = response.getResult();
707       
708       LdapEntry result = searchResult.getEntry();
709       
710       if ( result == null ) {
711         LOG.debug("{}: Object does not exist: {}", ldapSystemName, dn);
712         return null;
713       } else {
714         LOG.debug("{}: Object does exist: {}", ldapSystemName, dn);
715         return new LdapObject(result, attributes);
716       }
717     }
718     catch (LdapException e) {
719       if ( e.getResultCode() == ResultCode.NO_SUCH_OBJECT ) {
720         LOG.warn("{}: Ldap object does not exist: '{}'", ldapSystemName, dn);
721         return null;
722       }
723       
724       LOG.error("Problem during ldap read {}", dn, e);
725       throw new PspException("Problem during LDAP read: %s", e.getMessage());
726     }
727     finally {
728       if ( conn != null )
729         conn.close();
730     }
731   }
732 
733   /**
734    * 
735    * @param request
736    *
737    * @return
738    * @throws LdapException
739    */
740   protected void performLdapSearchRequest(int approximateNumResultsExpected, SearchRequest request, SearchEntryHandler callback) throws PspException {
741     LOG.debug("Doing ldap search: {} / {} / {}", 
742         new Object[] {request.getSearchFilter(), request.getBaseDn(), Arrays.toString(request.getReturnAttributes())});
743     List<LdapObject> result = new ArrayList<LdapObject>();
744     
745     Connection conn = getLdapConnection();
746     try {
747       conn.open();
748       
749       // Turn on attribute-value paging if this is an active directory target
750       if ( isActiveDirectory() ) {
751         LOG.debug("Using attribute-value paging");
752         request.setSearchEntryHandlers(
753                 new RangeEntryHandler(),
754                 new LdapSearchProgressHandler(approximateNumResultsExpected, LOG, "Performing ldap search"),
755                 callback);
756       }
757       else {
758         LOG.debug("Not using attribute-value paging");
759         request.setSearchEntryHandlers(
760                 new LdapSearchProgressHandler(approximateNumResultsExpected, LOG, "Performing ldap search"),
761                 callback);
762       }
763       
764       // Perform search. This is slightly different if paging is enabled or not.
765       if ( isSearchResultPagingEnabled() ) {
766         PagedResultsClient client = new PagedResultsClient(conn, getSearchResultPagingSize());
767         LOG.debug("Using ldap search-result paging");
768         client.executeToCompletion(request);
769       }
770       else {
771         LOG.debug("Not using ldap search-result paging");
772         SearchOperation searchOp = new SearchOperation(conn);
773         searchOp.execute(request);
774       }
775       
776     }
777     catch (LdapException e) {
778       if ( e.getResultCode() == ResultCode.NO_SUCH_OBJECT ) {
779         LOG.warn("Search base does not exist: {} (No such object ldap error)", request.getBaseDn());
780         return;
781       }
782       
783       LOG.error("Problem during ldap search {}", request, e);
784       throw new PspException("LDAP problem while searching: " + e.getMessage());
785     }
786     catch (RuntimeException e) {
787       LOG.error("Runtime problem during ldap search {}", request, e);
788       throw e;
789     }
790     finally {
791       if ( conn != null )
792         conn.close();
793     }
794   }
795 
796 
797 
798   public List<LdapObject> performLdapSearchRequest(int approximateNumResultsExpected, String searchBaseDn, SearchScope scope, Collection<String> attributesToReturn, String filterTemplate, Object... filterParams)
799   throws PspException {
800     SearchFilter filter = new SearchFilter(filterTemplate);
801 
802     for (int i=0; i<filterParams.length; i++) {
803       filter.setParameter(i, filterParams[i]);
804     }
805 
806     return performLdapSearchRequest(approximateNumResultsExpected, searchBaseDn, scope, attributesToReturn, filter);
807   }
808 
809 
810   public List<LdapObject> performLdapSearchRequest(int approximateNumResultsExpected, String searchBaseDn, SearchScope scope, Collection<String> attributesToReturn, SearchFilter filter)
811           throws PspException {
812     LOG.debug("Running ldap search: <{}>/{}: {} << {}",
813             searchBaseDn, scope, filter.getFilter(), filter.getParameters());
814 
815     final SearchRequest request = new SearchRequest(searchBaseDn, filter, attributesToReturn.toArray(new String[0]));
816     request.setSearchScope(scope);
817 
818 
819     final List<LdapObject> result = new ArrayList<>();
820     SearchEntryHandler searchCallback = new SearchEntryHandler() {
821       @Override
822       public HandlerResult<SearchEntry> handle(Connection connection, SearchRequest searchRequest, SearchEntry searchEntry) throws LdapException {
823         LOG.debug("Ldap result: {}", searchEntry.getDn());
824         result.add(new LdapObject(searchEntry, request.getReturnAttributes()));
825         return null;
826       }
827 
828       @Override
829       public void initializeRequest(SearchRequest searchRequest) {
830 
831       }
832     };
833     performLdapSearchRequest(approximateNumResultsExpected, request, searchCallback);
834 
835     LOG.info("LDAP search returned {} entries", result.size());
836 
837     if ( LOG.isTraceEnabled() ) {
838       int i=0;
839       for (LdapObject ldapObject : result ) {
840         i++;
841         LOG.trace("...ldap-search result {} of {}: {}", new Object[]{i, result.size(), ldapObject.getMap()});
842       }
843     }
844     return result;
845 
846   }
847 
848 
849   public Set<String> performLdapSearchRequest_returningValuesOfAnAttribute(int approximateNumResultsExpected, String searchBaseDn, SearchScope scope, final String attributeToReturn, String filterTemplate, Object... filterParams)
850           throws PspException {
851     SearchFilter filter = new SearchFilter(filterTemplate);
852     LOG.debug("Running ldap search: <{}>/{}: {} << {}",
853             new Object[]{searchBaseDn, scope, filterTemplate, Arrays.toString(filterParams)});
854 
855     for (int i=0; i<filterParams.length; i++) {
856       filter.setParameter(i, filterParams[i]);
857     }
858 
859     final SearchRequest request = new SearchRequest(searchBaseDn, filter, new String[]{attributeToReturn});
860     request.setSearchScope(scope);
861 
862 
863     // Create a place to hold the String-only results and a handler to put them into it
864     final Set<String> result = new HashSet<>();
865     SearchEntryHandler searchCallback = new SearchEntryHandler() {
866       @Override
867       public HandlerResult<SearchEntry> handle(Connection connection, SearchRequest searchRequest, SearchEntry searchEntry) throws LdapException {
868 
869         if ( attributeToReturn.equalsIgnoreCase("dn") || attributeToReturn.equalsIgnoreCase("distinguishedName") ) {
870           result.add(searchEntry.getDn().toLowerCase());
871         } else {
872           LdapAttribute attribute = searchEntry.getAttribute(attributeToReturn);
873           if (attribute != null)
874             result.addAll(attribute.getStringValues());
875         }
876         return null;
877       }
878 
879       @Override
880       public void initializeRequest(SearchRequest searchRequest) {
881 
882       }
883     };
884 
885     performLdapSearchRequest(approximateNumResultsExpected, request, searchCallback);
886 
887     LOG.info("LDAP search returned {} entries", result.size());
888 
889     if ( LOG.isTraceEnabled() ) {
890       int i=0;
891       for (String attributeValue : result ) {
892         i++;
893         LOG.trace("...ldap-search result {} of {}: {}", i, result.size(), attributeValue);
894       }
895     }
896     return result;
897 
898   }
899 
900 
901   public boolean makeLdapObjectCorrect(LdapEntry correctEntry,
902                                          LdapEntry existingEntry,
903                                        boolean valuesAreCaseSensitive)
904           throws PspException
905   {
906     boolean changedDn = false, changedAttributes = false;
907 
908     changedDn = makeLdapDnCorrect(correctEntry, existingEntry);
909     if ( changedDn ) {
910       LOG.info("{}: Rereading entry after changing DN", ldapSystemName, correctEntry.getDn());
911 
912       LdapObject rereadLdapObject = performLdapRead(correctEntry.getDn(), getAttributeNames(existingEntry));
913 
914       // this should always be found, but checking just in case
915       if ( rereadLdapObject!= null ) {
916         existingEntry = rereadLdapObject.ldapEntry;
917       }
918     }
919 
920     changedAttributes = makeLdapDataCorrect(correctEntry, existingEntry, valuesAreCaseSensitive);
921 
922     return changedDn || changedAttributes;
923 
924 /*
925     if ( changed ) {
926       return fetchTargetSystemGroup(grouperGroupInfo);
927     }
928     else {
929       return existingGroup;
930     }
931 */
932   }
933 
934 
935   /**
936    * Read a fresh copy of an ldapEntry, using the dn and attribute list from the provided
937    * entry.
938    *
939    * @param ldapEntry Source of DN and attributes that should be read.
940    * @return
941    * @throws PspException
942    */
943 
944   public LdapEntry rereadEntry(LdapEntry ldapEntry) throws PspException {
945     Collection<String> attributeNames = getAttributeNames(ldapEntry);
946 
947     try {
948       LOG.debug("{}: Rereading entry {}", ldapSystemName, ldapEntry.getDn());
949       LdapObject result = performLdapRead(ldapEntry.getDn(), attributeNames);
950       return result.ldapEntry;
951     } catch (PspException e) {
952       LOG.error("{} Unable to reread ldap object {}", ldapSystemName, ldapEntry.getDn(), e);
953       throw e;
954     }
955   }
956 
957   /**
958    * Get the names of the attributes present in a given LdapEntry
959    * @param ldapEntry
960    * @return
961    */
962   private Collection<String> getAttributeNames(LdapEntry ldapEntry) {
963     Collection<String> attributeNames = new HashSet<>();
964 
965     for (LdapAttribute attribute : ldapEntry.getAttributes() ) {
966       attributeNames.add(attribute.getName());
967     }
968     return attributeNames;
969   }
970 
971   protected boolean makeLdapDataCorrect(LdapEntry correctEntry,
972                                         LdapEntry existingEntry,
973                                         boolean valuesAreCaseSensitive)
974         throws PspException
975   {
976     boolean changed = false ;
977     for ( String attributeName : correctEntry.getAttributeNames() ) {
978       LdapAttribute correctAttribute = correctEntry.getAttribute(attributeName);
979       if ( attributeHasNoValues(correctAttribute) ) {
980         correctAttribute = null;
981       }
982 
983       LdapAttribute existingAttribute= existingEntry.getAttribute(attributeName);
984 
985       // If there should not be any values for this attribute, delete any existing values
986       if ( correctAttribute == null ) {
987         if ( existingAttribute != null ) {
988           changed = true;
989           LOG.info("{}: Attribute {} is incorrect: {} current values, Correct values: none",
990                   correctEntry.getDn(), attributeName,
991                   (existingAttribute != null ? existingAttribute.size() : "<none>"));
992 
993           AttributeModification mod = new AttributeModification(AttributeModificationType.REMOVE, existingAttribute);
994           ModifyRequest modRequest = new ModifyRequest(correctEntry.getDn(), mod);
995           performLdapModify(modRequest, valuesAreCaseSensitive);
996         }
997       }
998       else if ( !correctAttribute.equals(existingAttribute) ) {
999         // Attribute is different. Update existing one
1000         changed = true;
1001         LOG.info("{}: Attribute {} is incorrect: {} Current values, {} Correct values",
1002                 correctEntry.getDn(),
1003                 attributeName,
1004                 (existingAttribute != null ? existingAttribute.size() : "<none>"),
1005                 (correctAttribute  != null ? correctAttribute.size() : "<none>" ));
1006 
1007         AttributeModification mod = new AttributeModification(AttributeModificationType.REPLACE, correctAttribute);
1008         ModifyRequest modRequest = new ModifyRequest(correctEntry.getDn(), mod);
1009         performLdapModify(modRequest, valuesAreCaseSensitive);
1010       }
1011     }
1012     return changed;
1013   }
1014 
1015   /**
1016    * Moves the ldap object if necessary. It does require the OU to already exist because
1017    * OU templates and OU caching would make OU-creation here too intertwined with
1018    * the provisioning objects
1019    *
1020    * @param correctEntry
1021    * @param existingEntry
1022    * @return
1023    * @throws PspException
1024    */
1025   protected boolean makeLdapDnCorrect(LdapEntry correctEntry, LdapEntry existingEntry) throws PspException {
1026     // Compare DNs
1027     String correctDn = correctEntry.getDn();
1028     String existingDn= existingEntry.getDn();
1029 
1030     // TODO: This should do case-sensitive comparisons of the first RDN and case-insensitive comparisons of the rest
1031     if ( !correctDn.equalsIgnoreCase(existingDn) ) {
1032       // The DNs do not match. Existing object needs to be moved
1033       LOG.debug("{}: DN needs to change to {}", existingDn, correctDn);
1034 
1035       // Now modify the DN
1036       ModifyDnRequest moddn = new ModifyDnRequest(existingDn, correctDn);
1037       moddn.setDeleteOldRDn(true);
1038 
1039       performLdapModifyDn(moddn);
1040       return true;
1041     }
1042     return false;
1043   }
1044 
1045 
1046   public boolean test() {
1047     String ldapUrlString = (String) getLdaptiveProperties().get("org.ldaptive.ldapUrl");
1048     if ( ldapUrlString == null ) {
1049       LOG.error("Could not find LDAP URL");
1050       return false;
1051     }
1052     
1053     LOG.info("LDAP Url: " + ldapUrlString);
1054     
1055     if ( !ldapUrlString.startsWith("ldaps") ) {
1056       LOG.warn("Not an SSL ldap url");
1057     }
1058     else {        
1059       LOG.info("Testing SSL before the LDAP test");
1060       try {
1061         // ldaps://host[:port]...
1062         Pattern urlPattern = Pattern.compile("ldaps://([^:]*)(:[0-9]+)?.*");
1063         Matcher m = urlPattern.matcher(ldapUrlString);
1064         if ( !m.matches() ) {
1065           LOG.error("Unable to parse ldap url: " + ldapUrlString);
1066           return false;
1067         }
1068         
1069         String host = m.group(1);
1070         String portString = m.group(2);
1071         int port;
1072         if ( portString == null || portString.length() == 0 ) {
1073           port=636;
1074         }
1075         else {
1076           port=Integer.parseInt(portString.substring(1));
1077         }
1078         
1079         LOG.info("  Making SSL connection to {}:{}", host, port);
1080         
1081         SSLSocketFactory sslsocketfactory = (SSLSocketFactory) SSLSocketFactory.getDefault();
1082         SSLSocket sslsocket = (SSLSocket) sslsocketfactory.createSocket(host, port);
1083 
1084         InputStream in = sslsocket.getInputStream();
1085         OutputStream out = sslsocket.getOutputStream();
1086 
1087         // Write a test byte to get a reaction :)
1088         out.write(1);
1089 
1090         while (in.available() > 0) {
1091             System.out.print(in.read());
1092         }
1093         LOG.info("Successfully connected");
1094 
1095       } catch (Exception exception) {
1096           exception.printStackTrace();
1097       }
1098     }
1099     
1100     try {
1101       BlockingConnectionPool pool = buildLdapConnectionPool();
1102       LOG.info("Success: Ldap pool built");
1103 
1104       performTestLdapRead(pool.getConnection());
1105       LOG.info("Success: Test ldap read");
1106       return true;
1107     }
1108     catch (LdapException e) {
1109       LOG.error("LDAP Failure",e);
1110       return false;
1111     }
1112     catch (PspException e) {
1113       LOG.error("LDAP Failure",e);
1114       return false;
1115     }
1116   }
1117   
1118   public static void main(String[] args) {
1119     if ( args.length != 1 ) {
1120       LOG.error("USAGE: <ldap-pool-name from grouper-loader.properties>");
1121       System.exit(1);
1122     }
1123    
1124     LOG.info("Starting LDAP-connection test");
1125     LdapSystemper/pspng/LdapSystem.html#LdapSystem">LdapSystem system = new LdapSystem(args[0], false);
1126     system.test();
1127   }
1128 }