Sfoglia il codice sorgente

parallel user searching for multiple contexts/ldap profiles

Jason Rivard 8 anni fa
parent
commit
6153c7c569
53 ha cambiato i file con 1005 aggiunte e 4197 eliminazioni
  1. 2 0
      import-control.xml
  2. 8 0
      pom.xml
  3. 3 0
      src/main/java/password/pwm/AppProperty.java
  4. 5 0
      src/main/java/password/pwm/PwmApplication.java
  5. 1 0
      src/main/java/password/pwm/PwmConstants.java
  6. 12 5
      src/main/java/password/pwm/config/FormUtility.java
  7. 14 0
      src/main/java/password/pwm/config/PwmSettingCategory.java
  8. 2 1
      src/main/java/password/pwm/config/function/UserMatchViewerFunction.java
  9. 16 7
      src/main/java/password/pwm/config/stored/StoredConfigurationImpl.java
  10. 1 0
      src/main/java/password/pwm/config/stored/StoredConfigurationUtil.java
  11. 1 1
      src/main/java/password/pwm/config/value/FileValue.java
  12. 3 3
      src/main/java/password/pwm/http/filter/AuthenticationFilter.java
  13. 15 12
      src/main/java/password/pwm/http/servlet/ActivateUserServlet.java
  14. 10 8
      src/main/java/password/pwm/http/servlet/ForgottenUsernameServlet.java
  15. 14 8
      src/main/java/password/pwm/http/servlet/GuestRegistrationServlet.java
  16. 13 10
      src/main/java/password/pwm/http/servlet/forgottenpw/ForgottenPasswordServlet.java
  17. 25 15
      src/main/java/password/pwm/http/servlet/helpdesk/HelpdeskServlet.java
  18. 9 6
      src/main/java/password/pwm/http/servlet/newuser/NewUserUtils.java
  19. 8 3
      src/main/java/password/pwm/http/servlet/oauth/OAuthConsumerServlet.java
  20. 25 20
      src/main/java/password/pwm/http/servlet/peoplesearch/PeopleSearchDataReader.java
  21. 8 4
      src/main/java/password/pwm/ldap/LdapOperationsHelper.java
  22. 20 12
      src/main/java/password/pwm/ldap/LdapPermissionTester.java
  23. 5 5
      src/main/java/password/pwm/ldap/auth/SessionAuthenticator.java
  24. 56 0
      src/main/java/password/pwm/ldap/search/SearchConfiguration.java
  25. 372 328
      src/main/java/password/pwm/ldap/search/UserSearchEngine.java
  26. 43 0
      src/main/java/password/pwm/ldap/search/UserSearchJob.java
  27. 131 0
      src/main/java/password/pwm/ldap/search/UserSearchResults.java
  28. 2 0
      src/main/java/password/pwm/svc/PwmServiceManager.java
  29. 6 3
      src/main/java/password/pwm/svc/intruder/RecordManagerImpl.java
  30. 25 10
      src/main/java/password/pwm/svc/report/ReportService.java
  31. 12 8
      src/main/java/password/pwm/svc/report/UserCacheService.java
  32. 7 7
      src/main/java/password/pwm/svc/stats/StatisticsBundle.java
  33. 2 2
      src/main/java/password/pwm/svc/token/CryptoTokenMachine.java
  34. 5 5
      src/main/java/password/pwm/svc/token/DBTokenMachine.java
  35. 13 10
      src/main/java/password/pwm/svc/token/LdapTokenMachine.java
  36. 5 5
      src/main/java/password/pwm/svc/token/LocalDBTokenMachine.java
  37. 2 2
      src/main/java/password/pwm/svc/token/TokenMachine.java
  38. 6 6
      src/main/java/password/pwm/svc/token/TokenPayload.java
  39. 21 20
      src/main/java/password/pwm/svc/token/TokenService.java
  40. 13 6
      src/main/java/password/pwm/util/cli/commands/ExportResponsesCommand.java
  41. 14 11
      src/main/java/password/pwm/util/cli/commands/ResponseStatsCommand.java
  42. 2 1
      src/main/java/password/pwm/util/cli/commands/TokenInfoCommand.java
  43. 35 12
      src/main/java/password/pwm/util/operations/PasswordUtility.java
  44. 0 10
      src/main/java/password/pwm/util/secure/SecureEngine.java
  45. 3 3
      src/main/java/password/pwm/ws/server/RestServerHelper.java
  46. 3 3
      src/main/java/password/pwm/ws/server/rest/RestVerifyOtpServer.java
  47. 3 0
      src/main/resources/password/pwm/AppProperty.properties
  48. 1 1
      src/main/webapp/WEB-INF/jsp/admin-tokenlookup.jsp
  49. 3 3
      src/main/webapp/public/resources/js/newuser.js
  50. 0 2875
      supplemental/PWMAdministrationGuide.pdf
  51. 0 649
      supplemental/history.txt
  52. 0 8
      supplemental/readme.txt
  53. 0 89
      supplemental/script/missing_translation.sh

+ 2 - 0
import-control.xml

@@ -77,6 +77,8 @@
     <allow pkg="org.xeustechnologies"/>
     <allow pkg="net.glxn"/>
     <allow pkg="org.webjars"/>
+    <allow pkg="lombok"/>
+
 
     <!--servlet -->
     <subpackage name="http.servlet">

+ 8 - 0
pom.xml

@@ -515,6 +515,14 @@
     </reporting>
 
     <dependencies>
+        <!-- dev tool -->
+        <dependency>
+            <groupId>org.projectlombok</groupId>
+            <artifactId>lombok</artifactId>
+            <version>1.16.14</version>
+            <scope>provided</scope>
+        </dependency>
+
         <!-- Test dependencies -->
         <dependency>
             <groupId>junit</groupId>

+ 3 - 0
src/main/java/password/pwm/AppProperty.java

@@ -171,6 +171,9 @@ public enum     AppProperty {
     LDAP_BROWSER_MAX_ENTRIES                        ("ldap.browser.maxEntries"),
     LDAP_SEARCH_PAGING_ENABLE                       ("ldap.search.paging.enable"),
     LDAP_SEARCH_PAGING_SIZE                         ("ldap.search.paging.size"),
+    LDAP_SEARCH_PARALLEL_ENABLE                     ("ldap.search.parallel.enable"),
+    LDAP_SEARCH_PARALLEL_FACTOR                     ("ldap.search.parallel.factor"),
+    LDAP_SEARCH_PARALLEL_THREAD_MAX                 ("ldap.search.parallel.threadMax"),
     LOGGING_PATTERN                                 ("logging.pattern"),
     LOGGING_FILE_MAX_SIZE                           ("logging.file.maxSize"),
     LOGGING_FILE_MAX_ROLLOVER                       ("logging.file.maxRollover"),

+ 5 - 0
src/main/java/password/pwm/PwmApplication.java

@@ -38,6 +38,7 @@ import password.pwm.health.HealthMonitor;
 import password.pwm.http.servlet.resource.ResourceServletService;
 import password.pwm.http.state.SessionStateService;
 import password.pwm.ldap.LdapConnectionService;
+import password.pwm.ldap.search.UserSearchEngine;
 import password.pwm.svc.PwmService;
 import password.pwm.svc.PwmServiceManager;
 import password.pwm.svc.cache.CacheService;
@@ -493,6 +494,10 @@ public class PwmApplication {
         return (UrlShortenerService)pwmServiceManager.getService(UrlShortenerService.class);
     }
 
+    public UserSearchEngine getUserSearchEngine() {
+        return (UserSearchEngine)pwmServiceManager.getService(UserSearchEngine.class);
+    }
+
     public VersionChecker getVersionChecker() {
         return (VersionChecker)pwmServiceManager.getService(VersionChecker.class);
     }

+ 1 - 0
src/main/java/password/pwm/PwmConstants.java

@@ -113,6 +113,7 @@ public abstract class PwmConstants {
     public static final SessionLabel REPORTING_SESSION_LABEL = new SessionLabel(SESSION_LABEL_SESSION_ID ,null,"reporting",null,null);
     public static final SessionLabel HEALTH_SESSION_LABEL = new SessionLabel(SESSION_LABEL_SESSION_ID ,null,"health",null,null);
     public static final SessionLabel CLI_SESSION_LABEL= new SessionLabel(SESSION_LABEL_SESSION_ID ,null,"cli",null,null);
+    public static final SessionLabel TOKEN_SESSION_LABEL = new SessionLabel(SESSION_LABEL_SESSION_ID ,null,"token",null,null);
 
     public static final int DATABASE_ACCESSOR_KEY_LENGTH = Integer.parseInt(readPwmConstantsBundle("databaseAccessor.keyLength"));
 

+ 12 - 5
src/main/java/password/pwm/config/FormUtility.java

@@ -37,8 +37,9 @@ import password.pwm.error.PwmError;
 import password.pwm.error.PwmOperationalException;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.http.PwmRequest;
+import password.pwm.ldap.search.SearchConfiguration;
 import password.pwm.ldap.UserDataReader;
-import password.pwm.ldap.UserSearchEngine;
+import password.pwm.ldap.search.UserSearchEngine;
 import password.pwm.svc.cache.CacheKey;
 import password.pwm.svc.cache.CachePolicy;
 import password.pwm.svc.cache.CacheService;
@@ -234,16 +235,22 @@ public class FormUtility {
         final SearchHelper searchHelper = new SearchHelper();
         searchHelper.setFilterAnd(filterClauses);
 
-        final UserSearchEngine.SearchConfiguration searchConfiguration = new UserSearchEngine.SearchConfiguration();
-        searchConfiguration.setFilter(filter.toString());
+        final SearchConfiguration searchConfiguration = SearchConfiguration.builder()
+                .filter(filter.toString())
+                .build();
 
         final int resultSearchSizeLimit = 1 + (excludeDN == null ? 0 : excludeDN.size());
         final long cacheLifetimeMS = Long.parseLong(pwmApplication.getConfig().readAppProperty(AppProperty.CACHE_FORM_UNIQUE_VALUE_LIFETIME_MS));
         final CachePolicy cachePolicy = CachePolicy.makePolicyWithExpirationMS(cacheLifetimeMS);
 
         try {
-            final UserSearchEngine userSearchEngine = new UserSearchEngine(pwmApplication, SessionLabel.SYSTEM_LABEL);
-            final Map<UserIdentity,Map<String,String>> results = new LinkedHashMap<>(userSearchEngine.performMultiUserSearch(searchConfiguration,resultSearchSizeLimit,Collections.<String>emptyList()));
+            final UserSearchEngine userSearchEngine = pwmApplication.getUserSearchEngine();
+            final Map<UserIdentity,Map<String,String>> results = new LinkedHashMap<>(userSearchEngine.performMultiUserSearch(
+                    searchConfiguration,
+                    resultSearchSizeLimit,
+                    Collections.emptyList(),
+                    SessionLabel.SYSTEM_LABEL
+                    ));
 
             if (excludeDN != null && !excludeDN.isEmpty()) {
                 for (final UserIdentity loopIgnoreIdentity : excludeDN) {

+ 14 - 0
src/main/java/password/pwm/config/PwmSettingCategory.java

@@ -380,4 +380,18 @@ public enum PwmSettingCategory {
         }
         return Collections.unmodifiableList(values);
     }
+
+    public static Collection<PwmSettingCategory> associatedProfileCategories(final PwmSettingCategory inputCategory) {
+        final Collection<PwmSettingCategory> returnValues = new ArrayList<>();
+        if (inputCategory != null && inputCategory.hasProfiles()) {
+            PwmSettingCategory topLevelCategory = inputCategory;
+            while (!topLevelCategory.isTopLevelProfile()) {
+                topLevelCategory = topLevelCategory.getParent();
+            }
+            returnValues.add(topLevelCategory);
+            returnValues.addAll(topLevelCategory.getChildCategories());
+        }
+
+        return Collections.unmodifiableCollection(returnValues);
+    }
 }

+ 2 - 1
src/main/java/password/pwm/config/function/UserMatchViewerFunction.java

@@ -28,6 +28,7 @@ import com.novell.ldapchai.provider.ChaiProvider;
 import password.pwm.AppProperty;
 import password.pwm.PwmApplication;
 import password.pwm.PwmConstants;
+import password.pwm.bean.SessionLabel;
 import password.pwm.bean.UserIdentity;
 import password.pwm.config.Configuration;
 import password.pwm.config.PwmSetting;
@@ -108,7 +109,7 @@ public class UserMatchViewerFunction implements SettingUIFunction {
             }
         }
 
-        return LdapPermissionTester.discoverMatchingUsers(tempApplication, maxResultSize, permissions).keySet();
+        return LdapPermissionTester.discoverMatchingUsers(tempApplication, maxResultSize, permissions, SessionLabel.SYSTEM_LABEL).keySet();
     }
 
 

+ 16 - 7
src/main/java/password/pwm/config/stored/StoredConfigurationImpl.java

@@ -52,15 +52,15 @@ import password.pwm.error.PwmOperationalException;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.i18n.Config;
 import password.pwm.i18n.PwmLocaleBundle;
-import password.pwm.util.secure.BCrypt;
-import password.pwm.util.java.JavaHelper;
-import password.pwm.util.java.JsonUtil;
 import password.pwm.util.LocaleHelper;
 import password.pwm.util.PasswordData;
+import password.pwm.util.java.JavaHelper;
+import password.pwm.util.java.JsonUtil;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.java.TimeDuration;
 import password.pwm.util.java.XmlUtil;
 import password.pwm.util.logging.PwmLogger;
+import password.pwm.util.secure.BCrypt;
 import password.pwm.util.secure.PwmRandom;
 import password.pwm.util.secure.PwmSecurityKey;
 import password.pwm.util.secure.SecureEngine;
@@ -71,6 +71,7 @@ import java.io.OutputStream;
 import java.io.Serializable;
 import java.text.ParseException;
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.Date;
 import java.util.HashMap;
@@ -733,6 +734,7 @@ public class StoredConfigurationImpl implements Serializable, StoredConfiguratio
     public void copyProfileID(final PwmSettingCategory category, final String sourceID, final String destinationID, final UserIdentity userIdentity)
             throws PwmUnrecoverableException
     {
+
         if (!category.hasProfiles()) {
             throw PwmUnrecoverableException.newException(PwmError.ERROR_INVALID_CONFIG, "can not copy profile ID for category " + category + ", category does not have profiles");
         }
@@ -743,12 +745,19 @@ public class StoredConfigurationImpl implements Serializable, StoredConfiguratio
         if (existingProfiles.contains(destinationID)) {
             throw PwmUnrecoverableException.newException(PwmError.ERROR_INVALID_CONFIG, "can not copy profile ID for category, destination profileID '" + destinationID+ "' already exists");
         }
-        for (final PwmSetting pwmSetting : category.getSettings()) {
-            if (!isDefaultValue(pwmSetting, sourceID)) {
-                final StoredValue value = readSetting(pwmSetting, sourceID);
-                writeSetting(pwmSetting, destinationID, value, userIdentity);
+
+        {
+            final Collection<PwmSettingCategory> interestedCategories = PwmSettingCategory.associatedProfileCategories(category);
+            for (final PwmSettingCategory interestedCategory : interestedCategories) {
+                for (final PwmSetting pwmSetting : interestedCategory.getSettings()) {
+                    if (!isDefaultValue(pwmSetting, sourceID)) {
+                        final StoredValue value = readSetting(pwmSetting, sourceID);
+                        writeSetting(pwmSetting, destinationID, value, userIdentity);
+                    }
+                }
             }
         }
+
         final List<String> newProfileIDList = new ArrayList<>();
         newProfileIDList.addAll(existingProfiles);
         newProfileIDList.add(destinationID);

+ 1 - 0
src/main/java/password/pwm/config/stored/StoredConfigurationUtil.java

@@ -148,4 +148,5 @@ public abstract class StoredConfigurationUtil {
         }
         return outputObject;
     }
+
 }

+ 1 - 1
src/main/java/password/pwm/config/value/FileValue.java

@@ -103,7 +103,7 @@ public class FileValue extends AbstractValue implements StoredValue {
         public String md5sum()
                 throws PwmUnrecoverableException
         {
-            return SecureEngine.md5sum(new ByteArrayInputStream(contents));
+            return SecureEngine.hash(new ByteArrayInputStream(contents), PwmHashAlgorithm.MD5);
         }
 
         public String sha1sum()

+ 3 - 3
src/main/java/password/pwm/http/filter/AuthenticationFilter.java

@@ -50,7 +50,7 @@ import password.pwm.http.servlet.oauth.OAuthMachine;
 import password.pwm.http.servlet.oauth.OAuthSettings;
 import password.pwm.i18n.Display;
 import password.pwm.ldap.PasswordChangeProgressChecker;
-import password.pwm.ldap.UserSearchEngine;
+import password.pwm.ldap.search.UserSearchEngine;
 import password.pwm.ldap.auth.AuthenticationType;
 import password.pwm.ldap.auth.PwmAuthenticationSource;
 import password.pwm.ldap.auth.SessionAuthenticator;
@@ -476,8 +476,8 @@ public class AuthenticationFilter extends AbstractPwmFilter {
                         pwmSession,
                         PwmAuthenticationSource.BASIC_AUTH
                 );
-                final UserSearchEngine userSearchEngine = new UserSearchEngine(pwmApplication, pwmSession.getLabel());
-                final UserIdentity userIdentity = userSearchEngine.resolveUsername(basicAuthInfo.getUsername(), null, null);
+                final UserSearchEngine userSearchEngine = pwmApplication.getUserSearchEngine();
+                final UserIdentity userIdentity = userSearchEngine.resolveUsername(basicAuthInfo.getUsername(), null, null, pwmSession.getLabel());
                 sessionAuthenticator.authenticateUser(userIdentity, basicAuthInfo.getPassword());
                 pwmSession.getLoginInfoBean().setBasicAuth(basicAuthInfo);
 

+ 15 - 12
src/main/java/password/pwm/http/servlet/ActivateUserServlet.java

@@ -55,8 +55,9 @@ import password.pwm.http.bean.ActivateUserBean;
 import password.pwm.i18n.Message;
 import password.pwm.ldap.LdapPermissionTester;
 import password.pwm.ldap.LdapUserDataReader;
+import password.pwm.ldap.search.SearchConfiguration;
 import password.pwm.ldap.UserDataReader;
-import password.pwm.ldap.UserSearchEngine;
+import password.pwm.ldap.search.UserSearchEngine;
 import password.pwm.ldap.auth.AuthenticationType;
 import password.pwm.ldap.auth.PwmAuthenticationSource;
 import password.pwm.ldap.auth.SessionAuthenticator;
@@ -144,7 +145,7 @@ public class ActivateUserServlet extends AbstractPwmServlet {
     protected void processAction(final PwmRequest pwmRequest)
             throws ServletException, ChaiUnavailableException, IOException, PwmUnrecoverableException
     {
-            //Fetch the session state bean.
+        //Fetch the session state bean.
         final PwmSession pwmSession = pwmRequest.getPwmSession();
         final PwmApplication pwmApplication = pwmRequest.getPwmApplication();
 
@@ -244,13 +245,15 @@ public class ActivateUserServlet extends AbstractPwmServlet {
             // read an ldap user object based on the params
             final UserIdentity userIdentity;
             {
-                final UserSearchEngine userSearchEngine = new UserSearchEngine(pwmApplication, pwmSession.getLabel());
-                final UserSearchEngine.SearchConfiguration searchConfiguration = new UserSearchEngine.SearchConfiguration();
-                searchConfiguration.setContexts(Collections.singletonList(contextParam));
-                searchConfiguration.setFilter(searchFilter);
-                searchConfiguration.setFormValues(formValues);
-                searchConfiguration.setLdapProfile(ldapProfile);
-                userIdentity = userSearchEngine.performSingleUserSearch(searchConfiguration);
+                final UserSearchEngine userSearchEngine = pwmApplication.getUserSearchEngine();
+                final SearchConfiguration searchConfiguration = SearchConfiguration.builder()
+                        .contexts(Collections.singletonList(contextParam))
+                        .filter(searchFilter)
+                        .formValues(formValues)
+                        .ldapProfile(ldapProfile)
+                        .build();
+
+                userIdentity = userSearchEngine.performSingleUserSearch(searchConfiguration, pwmRequest.getSessionLabel());
             }
 
             validateParamsAgainstLDAP(pwmRequest, formValues, userIdentity);
@@ -758,9 +761,9 @@ public class ActivateUserServlet extends AbstractPwmServlet {
         }
         return searchFilter;
     }
-    
-    private static void forwardToActivateUserForm(final PwmRequest pwmRequest) 
-            throws ServletException, PwmUnrecoverableException, IOException 
+
+    private static void forwardToActivateUserForm(final PwmRequest pwmRequest)
+            throws ServletException, PwmUnrecoverableException, IOException
     {
         pwmRequest.addFormInfoToRequestAttr(PwmSetting.ACTIVATE_USER_FORM,false,false);
         pwmRequest.forwardToJsp(JspUrl.ACTIVATE_USER);

+ 10 - 8
src/main/java/password/pwm/http/servlet/ForgottenUsernameServlet.java

@@ -44,8 +44,9 @@ import password.pwm.http.JspUrl;
 import password.pwm.http.PwmRequest;
 import password.pwm.http.PwmSession;
 import password.pwm.ldap.LdapUserDataReader;
+import password.pwm.ldap.search.SearchConfiguration;
 import password.pwm.ldap.UserDataReader;
-import password.pwm.ldap.UserSearchEngine;
+import password.pwm.ldap.search.UserSearchEngine;
 import password.pwm.ldap.UserStatusReader;
 import password.pwm.svc.stats.Statistic;
 import password.pwm.util.CaptchaUtility;
@@ -168,13 +169,14 @@ public class ForgottenUsernameServlet extends AbstractPwmServlet {
 
             final UserIdentity userIdentity;
             {
-                final UserSearchEngine userSearchEngine = new UserSearchEngine(pwmApplication, pwmSession.getLabel());
-                final UserSearchEngine.SearchConfiguration searchConfiguration = new UserSearchEngine.SearchConfiguration();
-                searchConfiguration.setFilter(searchFilter);
-                searchConfiguration.setFormValues(formValues);
-                searchConfiguration.setLdapProfile(ldapProfile);
-                searchConfiguration.setContexts(Collections.singletonList(contextParam));
-                userIdentity = userSearchEngine.performSingleUserSearch(searchConfiguration);
+                final UserSearchEngine userSearchEngine = pwmApplication.getUserSearchEngine();
+                final SearchConfiguration searchConfiguration = SearchConfiguration.builder()
+                        .filter(searchFilter)
+                        .formValues(formValues)
+                        .ldapProfile(ldapProfile)
+                        .contexts(Collections.singletonList(contextParam))
+                        .build();
+                userIdentity = userSearchEngine.performSingleUserSearch(searchConfiguration, pwmSession.getLabel());
             }
 
             if (userIdentity == null) {

+ 14 - 8
src/main/java/password/pwm/http/servlet/GuestRegistrationServlet.java

@@ -52,8 +52,9 @@ import password.pwm.http.bean.GuestRegistrationBean;
 import password.pwm.i18n.Message;
 import password.pwm.ldap.LdapOperationsHelper;
 import password.pwm.ldap.LdapUserDataReader;
+import password.pwm.ldap.search.SearchConfiguration;
 import password.pwm.ldap.UserDataReader;
-import password.pwm.ldap.UserSearchEngine;
+import password.pwm.ldap.search.UserSearchEngine;
 import password.pwm.ldap.UserStatusReader;
 import password.pwm.svc.stats.Statistic;
 import password.pwm.util.FormMap;
@@ -304,15 +305,20 @@ public class GuestRegistrationServlet extends AbstractPwmServlet {
         final String usernameParam = pwmRequest.readParameterAsString("username");
         final GuestRegistrationBean guBean = pwmApplication.getSessionStateService().getBean(pwmRequest, GuestRegistrationBean.class);
 
-        final UserSearchEngine.SearchConfiguration searchConfiguration = new UserSearchEngine.SearchConfiguration();
-        searchConfiguration.setChaiProvider(chaiProvider);
-        searchConfiguration.setContexts(Collections.singletonList(config.readSettingAsString(PwmSetting.GUEST_CONTEXT)));
-        searchConfiguration.setEnableContextValidation(false);
-        searchConfiguration.setUsername(usernameParam);
-        final UserSearchEngine userSearchEngine = new UserSearchEngine(pwmApplication, pwmSession.getLabel());
+
+        SearchConfiguration.builder();
+
+        final SearchConfiguration searchConfiguration = SearchConfiguration.builder()
+                    .chaiProvider(chaiProvider)
+                    .contexts(Collections.singletonList(config.readSettingAsString(PwmSetting.GUEST_CONTEXT)))
+                    .enableContextValidation(false)
+                    .username(usernameParam)
+                    .build();
+
+        final UserSearchEngine userSearchEngine = pwmApplication.getUserSearchEngine();
 
         try {
-            final UserIdentity theGuest = userSearchEngine.performSingleUserSearch(searchConfiguration);
+            final UserIdentity theGuest = userSearchEngine.performSingleUserSearch(searchConfiguration, pwmSession.getLabel());
             final FormMap formProps = guBean.getFormValues();
             try {
                 final List<FormConfiguration> guestUpdateForm = config.readSettingAsForm(PwmSetting.GUEST_UPDATE_FORM);

+ 13 - 10
src/main/java/password/pwm/http/servlet/forgottenpw/ForgottenPasswordServlet.java

@@ -73,8 +73,9 @@ import password.pwm.http.servlet.oauth.OAuthSettings;
 import password.pwm.i18n.Message;
 import password.pwm.ldap.LdapOperationsHelper;
 import password.pwm.ldap.LdapUserDataReader;
+import password.pwm.ldap.search.SearchConfiguration;
 import password.pwm.ldap.UserDataReader;
-import password.pwm.ldap.UserSearchEngine;
+import password.pwm.ldap.search.UserSearchEngine;
 import password.pwm.ldap.UserStatusReader;
 import password.pwm.ldap.auth.AuthenticationType;
 import password.pwm.ldap.auth.AuthenticationUtility;
@@ -389,13 +390,15 @@ public class ForgottenPasswordServlet extends ControlledPwmServlet {
             // convert the username field to an identity
             final UserIdentity userIdentity;
             {
-                final UserSearchEngine userSearchEngine = new UserSearchEngine(pwmRequest);
-                final UserSearchEngine.SearchConfiguration searchConfiguration = new UserSearchEngine.SearchConfiguration();
-                searchConfiguration.setFilter(searchFilter);
-                searchConfiguration.setFormValues(formValues);
-                searchConfiguration.setContexts(Collections.singletonList(contextParam));
-                searchConfiguration.setLdapProfile(ldapProfile);
-                userIdentity = userSearchEngine.performSingleUserSearch(searchConfiguration);
+                final UserSearchEngine userSearchEngine = pwmApplication.getUserSearchEngine();
+                final SearchConfiguration searchConfiguration = SearchConfiguration.builder()
+                        .filter(searchFilter)
+                        .formValues(formValues)
+                        .contexts(Collections.singletonList(contextParam))
+                        .ldapProfile(ldapProfile)
+                        .build();
+
+                userIdentity = userSearchEngine.performSingleUserSearch(searchConfiguration, pwmRequest.getSessionLabel());
             }
 
             if (userIdentity == null) {
@@ -581,9 +584,9 @@ public class ForgottenPasswordServlet extends ControlledPwmServlet {
 
         final UserIdentity oauthUserIdentity;
         {
-            final UserSearchEngine userSearchEngine = new UserSearchEngine(pwmRequest);
+            final UserSearchEngine userSearchEngine =pwmRequest.getPwmApplication().getUserSearchEngine();
             try {
-                oauthUserIdentity = userSearchEngine.resolveUsername(userDNfromOAuth, null, null);
+                oauthUserIdentity = userSearchEngine.resolveUsername(userDNfromOAuth, null, null, pwmRequest.getSessionLabel());
             } catch (PwmOperationalException e) {
                 final String errorMsg = "unexpected error searching for oauth supplied username in ldap; error: " + e.getMessage() ;
                 final ErrorInformation errorInformation = new ErrorInformation(PwmError.ERROR_OAUTH_ERROR, errorMsg);

+ 25 - 15
src/main/java/password/pwm/http/servlet/helpdesk/HelpdeskServlet.java

@@ -61,8 +61,10 @@ import password.pwm.i18n.Message;
 import password.pwm.ldap.LdapOperationsHelper;
 import password.pwm.ldap.LdapPermissionTester;
 import password.pwm.ldap.LdapUserDataReader;
+import password.pwm.ldap.search.SearchConfiguration;
 import password.pwm.ldap.UserDataReader;
-import password.pwm.ldap.UserSearchEngine;
+import password.pwm.ldap.search.UserSearchEngine;
+import password.pwm.ldap.search.UserSearchResults;
 import password.pwm.svc.event.AuditEvent;
 import password.pwm.svc.event.AuditRecordFactory;
 import password.pwm.svc.event.HelpdeskAuditRecord;
@@ -491,27 +493,35 @@ public class HelpdeskServlet extends ControlledPwmServlet {
             return ProcessStatus.Halt;
         }
 
-        final UserSearchEngine userSearchEngine = new UserSearchEngine(pwmRequest.getPwmApplication(), pwmRequest.getSessionLabel());
-        final UserSearchEngine.SearchConfiguration searchConfiguration = new UserSearchEngine.SearchConfiguration();
-        searchConfiguration.setContexts(helpdeskProfile.readSettingAsStringArray(PwmSetting.HELPDESK_SEARCH_BASE));
-        searchConfiguration.setEnableContextValidation(false);
-        searchConfiguration.setUsername(username);
-        searchConfiguration.setEnableValueEscaping(false);
-        searchConfiguration.setFilter(getSearchFilter(pwmRequest.getConfig(),helpdeskProfile));
-        searchConfiguration.setEnableSplitWhitespace(true);
+        final UserSearchEngine userSearchEngine = pwmRequest.getPwmApplication().getUserSearchEngine();
 
 
-        if (!useProxy) {
-            final UserIdentity loggedInUser = pwmRequest.getPwmSession().getUserInfoBean().getUserIdentity();
-            searchConfiguration.setLdapProfile(loggedInUser.getLdapProfileID());
-            searchConfiguration.setChaiProvider(getChaiUser(pwmRequest, helpdeskProfile, loggedInUser).getChaiProvider());
+        final SearchConfiguration searchConfiguration;
+        {
+            final SearchConfiguration.SearchConfigurationBuilder builder = SearchConfiguration.builder();
+            builder.contexts(helpdeskProfile.readSettingAsStringArray(PwmSetting.HELPDESK_SEARCH_BASE));
+            builder.enableContextValidation(false);
+            builder.username(username);
+            builder.enableValueEscaping(false);
+            builder.filter(getSearchFilter(pwmRequest.getConfig(), helpdeskProfile));
+            builder.enableSplitWhitespace(true);
+
+            if (!useProxy) {
+                final UserIdentity loggedInUser = pwmRequest.getPwmSession().getUserInfoBean().getUserIdentity();
+                builder.ldapProfile(loggedInUser.getLdapProfileID());
+                builder.chaiProvider(getChaiUser(pwmRequest, helpdeskProfile, loggedInUser).getChaiProvider());
+            }
+
+            searchConfiguration = builder.build();
         }
 
-        final UserSearchEngine.UserSearchResults results;
+
+
+        final UserSearchResults results;
         final boolean sizeExceeded;
         try {
             final Locale locale = pwmRequest.getLocale();
-            results = userSearchEngine.performMultiUserSearchFromForm(locale, searchConfiguration, maxResults, searchForm);
+            results = userSearchEngine.performMultiUserSearchFromForm(locale, searchConfiguration, maxResults, searchForm, pwmRequest.getSessionLabel());
             sizeExceeded = results.isSizeExceeded();
         } catch (PwmOperationalException e) {
             final ErrorInformation errorInformation = e.getErrorInformation();

+ 9 - 6
src/main/java/password/pwm/http/servlet/newuser/NewUserUtils.java

@@ -51,8 +51,9 @@ import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.http.PwmRequest;
 import password.pwm.http.PwmSession;
 import password.pwm.http.bean.NewUserBean;
+import password.pwm.ldap.search.SearchConfiguration;
 import password.pwm.ldap.UserDataReader;
-import password.pwm.ldap.UserSearchEngine;
+import password.pwm.ldap.search.UserSearchEngine;
 import password.pwm.ldap.auth.PwmAuthenticationSource;
 import password.pwm.ldap.auth.SessionAuthenticator;
 import password.pwm.svc.event.AuditEvent;
@@ -81,7 +82,7 @@ import java.util.Set;
 
 public class NewUserUtils {
     private static PwmLogger LOGGER = password.pwm.util.logging.PwmLogger.forClass(NewUserUtils.class);
-    
+
     private NewUserUtils() {
     }
 
@@ -361,12 +362,14 @@ public class NewUserUtils {
     )
             throws PwmUnrecoverableException, ChaiUnavailableException
     {
-        final UserSearchEngine userSearchEngine = new UserSearchEngine(pwmRequest);
-        final UserSearchEngine.SearchConfiguration searchConfiguration = new UserSearchEngine.SearchConfiguration();
-        searchConfiguration.setUsername(rdnValue);
+        final UserSearchEngine userSearchEngine = pwmRequest.getPwmApplication().getUserSearchEngine();
+        final SearchConfiguration searchConfiguration = SearchConfiguration.builder()
+                .username(rdnValue)
+                .build();
+
         try {
             final Map<UserIdentity, Map<String, String>> results = userSearchEngine.performMultiUserSearch(
-                    searchConfiguration, 2, Collections.<String>emptyList());
+                    searchConfiguration, 2, Collections.emptyList(), pwmRequest.getSessionLabel());
             return results != null && !results.isEmpty();
         } catch (PwmOperationalException e) {
             final String msg = "ldap error while searching for duplicate entry names: " + e.getMessage();

+ 8 - 3
src/main/java/password/pwm/http/servlet/oauth/OAuthConsumerServlet.java

@@ -40,7 +40,7 @@ import password.pwm.http.PwmURL;
 import password.pwm.http.servlet.AbstractPwmServlet;
 import password.pwm.http.servlet.PwmServletDefinition;
 import password.pwm.http.servlet.forgottenpw.ForgottenPasswordServlet;
-import password.pwm.ldap.UserSearchEngine;
+import password.pwm.ldap.search.UserSearchEngine;
 import password.pwm.ldap.auth.AuthenticationType;
 import password.pwm.ldap.auth.PwmAuthenticationSource;
 import password.pwm.ldap.auth.SessionAuthenticator;
@@ -245,8 +245,13 @@ public class OAuthConsumerServlet extends AbstractPwmServlet {
 
         if (userIsAuthenticated) {
             try {
-                final UserSearchEngine userSearchEngine = new UserSearchEngine(pwmRequest);
-                final UserIdentity resolvedIdentity = userSearchEngine.resolveUsername(oauthSuppliedUsername, null, null);
+                final UserSearchEngine userSearchEngine = pwmApplication.getUserSearchEngine();
+                final UserIdentity resolvedIdentity = userSearchEngine.resolveUsername(
+                        oauthSuppliedUsername,
+                        null,
+                        null,
+                        pwmSession.getLabel()
+                );
                 if (resolvedIdentity != null && resolvedIdentity.canonicalEquals(pwmSession.getUserInfoBean().getUserIdentity(),pwmApplication)) {
                     LOGGER.debug(pwmSession, "verified incoming oauth code for already authenticated session does resolve to same as logged in user");
                 } else {

+ 25 - 20
src/main/java/password/pwm/http/servlet/peoplesearch/PeopleSearchDataReader.java

@@ -44,9 +44,11 @@ import password.pwm.http.PwmRequest;
 import password.pwm.i18n.Display;
 import password.pwm.ldap.LdapPermissionTester;
 import password.pwm.ldap.LdapUserDataReader;
+import password.pwm.ldap.search.SearchConfiguration;
 import password.pwm.ldap.UserDataReader;
-import password.pwm.ldap.UserSearchEngine;
+import password.pwm.ldap.search.UserSearchEngine;
 import password.pwm.ldap.UserStatusReader;
+import password.pwm.ldap.search.UserSearchResults;
 import password.pwm.svc.cache.CacheKey;
 import password.pwm.svc.cache.CachePolicy;
 import password.pwm.svc.stats.Statistic;
@@ -198,7 +200,7 @@ public class PeopleSearchDataReader {
             throw e;
         }
 
-        final UserSearchEngine.UserSearchResults detailResults = doDetailLookup(userIdentity);
+        final UserSearchResults detailResults = doDetailLookup(userIdentity);
         final Map<String, String> searchResults = detailResults.getResults().get(userIdentity);
 
         final UserDetailBean userDetailBean = new UserDetailBean();
@@ -563,13 +565,13 @@ public class PeopleSearchDataReader {
         return useProxy || !pwmRequest.isAuthenticated() && publicAccessEnabled;
     }
 
-    private UserSearchEngine.UserSearchResults doDetailLookup(
+    private UserSearchResults doDetailLookup(
             final UserIdentity userIdentity
     )
             throws PwmUnrecoverableException
     {
         final List<FormConfiguration> detailFormConfig = pwmRequest.getConfig().readSettingAsForm(PwmSetting.PEOPLE_SEARCH_DETAIL_FORM);
-        final Map<String, String> attributeHeaderMap = UserSearchEngine.UserSearchResults.fromFormConfiguration(
+        final Map<String, String> attributeHeaderMap = UserSearchResults.fromFormConfiguration(
                 detailFormConfig, pwmRequest.getLocale());
 
         if (config.isOrgChartEnabled()) {
@@ -586,7 +588,7 @@ public class PeopleSearchDataReader {
         try {
             final ChaiUser theUser = getChaiUser(userIdentity);
             final Map<String, String> values = theUser.readStringAttributes(attributeHeaderMap.keySet());
-            return new UserSearchEngine.UserSearchResults(
+            return new UserSearchResults(
                     attributeHeaderMap,
                     Collections.singletonMap(userIdentity, values),
                     false
@@ -637,23 +639,26 @@ public class PeopleSearchDataReader {
         }
 
         final boolean useProxy = useProxy();
-        final UserSearchEngine userSearchEngine = new UserSearchEngine(pwmRequest);
-        final UserSearchEngine.SearchConfiguration searchConfiguration = new UserSearchEngine.SearchConfiguration();
-        searchConfiguration.setContexts(
-                pwmRequest.getConfig().readSettingAsStringArray(PwmSetting.PEOPLE_SEARCH_SEARCH_BASE));
-        searchConfiguration.setEnableContextValidation(false);
-        searchConfiguration.setUsername(username);
-        searchConfiguration.setEnableValueEscaping(false);
-        searchConfiguration.setFilter(getSearchFilter(pwmRequest.getConfig()));
-        searchConfiguration.setEnableSplitWhitespace(true);
+        final UserSearchEngine userSearchEngine = pwmRequest.getPwmApplication().getUserSearchEngine();
 
-
-        if (!useProxy) {
-            searchConfiguration.setLdapProfile(pwmRequest.getPwmSession().getUserInfoBean().getUserIdentity().getLdapProfileID());
-            searchConfiguration.setChaiProvider(pwmRequest.getPwmSession().getSessionManager().getChaiProvider());
+        final SearchConfiguration searchConfiguration;
+        {
+            final SearchConfiguration.SearchConfigurationBuilder builder = SearchConfiguration.builder();
+            builder.contexts(pwmRequest.getConfig().readSettingAsStringArray(PwmSetting.PEOPLE_SEARCH_SEARCH_BASE));
+            builder.enableContextValidation(false);
+            builder.username(username);
+            builder.enableValueEscaping(false);
+            builder.filter(getSearchFilter(pwmRequest.getConfig()));
+            builder.enableSplitWhitespace(true);
+
+            if (!useProxy) {
+                builder.ldapProfile(pwmRequest.getPwmSession().getUserInfoBean().getUserIdentity().getLdapProfileID());
+                builder.chaiProvider(pwmRequest.getPwmSession().getSessionManager().getChaiProvider());
+            }
+            searchConfiguration = builder.build();
         }
 
-        final UserSearchEngine.UserSearchResults results;
+        final UserSearchResults results;
         final boolean sizeExceeded;
         try {
             final List<FormConfiguration> searchForm = pwmRequest.getConfig().readSettingAsForm(
@@ -661,7 +666,7 @@ public class PeopleSearchDataReader {
             final int maxResults = (int) pwmRequest.getConfig().readSettingAsLong(
                     PwmSetting.PEOPLE_SEARCH_RESULT_LIMIT);
             final Locale locale = pwmRequest.getLocale();
-            results = userSearchEngine.performMultiUserSearchFromForm(locale, searchConfiguration, maxResults, searchForm);
+            results = userSearchEngine.performMultiUserSearchFromForm(locale, searchConfiguration, maxResults, searchForm, pwmRequest.getSessionLabel());
             sizeExceeded = results.isSizeExceeded();
         } catch (PwmOperationalException e) {
             final ErrorInformation errorInformation = e.getErrorInformation();

+ 8 - 4
src/main/java/password/pwm/ldap/LdapOperationsHelper.java

@@ -47,6 +47,8 @@ import password.pwm.error.PwmError;
 import password.pwm.error.PwmOperationalException;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.http.PwmSession;
+import password.pwm.ldap.search.SearchConfiguration;
+import password.pwm.ldap.search.UserSearchEngine;
 import password.pwm.svc.stats.Statistic;
 import password.pwm.svc.stats.StatisticsManager;
 import password.pwm.util.PasswordData;
@@ -337,10 +339,12 @@ public class LdapOperationsHelper {
                 if (!"DN".equalsIgnoreCase(guidAttributeName) && !"VENDORGUID".equalsIgnoreCase(guidAttributeName)) {
                     try {
                         // check if it is unique
-                        final UserSearchEngine.SearchConfiguration searchConfiguration = new UserSearchEngine.SearchConfiguration();
-                        searchConfiguration.setFilter("(" + guidAttributeName + "=" + guidValue + ")");
-                        final UserSearchEngine userSearchEngine = new UserSearchEngine(pwmApplication, sessionLabel);
-                        final UserIdentity result = userSearchEngine.performSingleUserSearch(searchConfiguration);
+                        final SearchConfiguration searchConfiguration = SearchConfiguration.builder()
+                                .filter("(" + guidAttributeName + "=" + guidValue + ")")
+                                .build();
+
+                        final UserSearchEngine userSearchEngine = pwmApplication.getUserSearchEngine();
+                        final UserIdentity result = userSearchEngine.performSingleUserSearch(searchConfiguration, sessionLabel);
                         exists = result != null;
                     } catch (PwmOperationalException e) {
                         if (e.getError() != PwmError.ERROR_CANT_MATCH_USER) {

+ 20 - 12
src/main/java/password/pwm/ldap/LdapPermissionTester.java

@@ -41,6 +41,8 @@ import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmOperationalException;
 import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.ldap.search.SearchConfiguration;
+import password.pwm.ldap.search.UserSearchEngine;
 import password.pwm.svc.cache.CacheKey;
 import password.pwm.svc.cache.CachePolicy;
 import password.pwm.util.logging.PwmLogger;
@@ -196,28 +198,31 @@ public class LdapPermissionTester {
     public static Map<UserIdentity, Map<String, String>> discoverMatchingUsers(
             final PwmApplication pwmApplication,
             final int maxResultSize,
-            final List<UserPermission> userPermissions
+            final List<UserPermission> userPermissions,
+            final SessionLabel sessionLabel
     )
             throws Exception
     {
-        final UserSearchEngine userSearchEngine = new UserSearchEngine(pwmApplication, SessionLabel.SYSTEM_LABEL);
+        final UserSearchEngine userSearchEngine = pwmApplication.getUserSearchEngine();
 
         final Map<UserIdentity, Map<String, String>> results = new TreeMap<>();
         for (final UserPermission userPermission : userPermissions) {
             if ((maxResultSize) - results.size() > 0) {
-                final UserSearchEngine.SearchConfiguration searchConfiguration = new UserSearchEngine.SearchConfiguration();
+
+                final SearchConfiguration.SearchConfigurationBuilder builder = SearchConfiguration.builder();
+
                 switch (userPermission.getType()) {
                     case ldapQuery: {
-                        searchConfiguration.setFilter(userPermission.getLdapQuery());
+                        builder.filter(userPermission.getLdapQuery());
                         if (userPermission.getLdapBase() != null && !userPermission.getLdapBase().isEmpty()) {
-                            searchConfiguration.setEnableContextValidation(false);
-                            searchConfiguration.setContexts(Collections.singletonList(userPermission.getLdapBase()));
+                            builder.enableContextValidation(false);
+                            builder.contexts(Collections.singletonList(userPermission.getLdapBase()));
                         }
                     }
                     break;
 
                     case ldapGroup: {
-                        searchConfiguration.setGroupDN(userPermission.getLdapBase());
+                        builder.groupDN(userPermission.getLdapBase());
                     }
                     break;
 
@@ -226,15 +231,18 @@ public class LdapPermissionTester {
                 }
 
                 if (userPermission.getLdapProfileID() != null && !userPermission.getLdapProfileID().isEmpty() && !userPermission.getLdapProfileID().equals(PwmConstants.PROFILE_ID_ALL)) {
-                    searchConfiguration.setLdapProfile(userPermission.getLdapProfileID());
+                    builder.ldapProfile(userPermission.getLdapProfileID());
                 }
 
+                final SearchConfiguration searchConfiguration = builder.build();
+
                 try {
                     results.putAll(userSearchEngine.performMultiUserSearch(
-                                    searchConfiguration,
-                                    (maxResultSize) - results.size(),
-                                    Collections.<String>emptyList())
-                    );
+                            searchConfiguration,
+                            (maxResultSize) - results.size(),
+                            Collections.emptyList(),
+                            sessionLabel
+                    ));
                 } catch (PwmUnrecoverableException e) {
                     LOGGER.error("error reading matching users: " + e.getMessage());
                     throw new PwmOperationalException(e.getErrorInformation());

+ 5 - 5
src/main/java/password/pwm/ldap/auth/SessionAuthenticator.java

@@ -44,7 +44,7 @@ import password.pwm.error.PwmOperationalException;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.http.PwmSession;
 import password.pwm.ldap.LdapOperationsHelper;
-import password.pwm.ldap.UserSearchEngine;
+import password.pwm.ldap.search.UserSearchEngine;
 import password.pwm.ldap.UserStatusReader;
 import password.pwm.svc.intruder.IntruderManager;
 import password.pwm.svc.intruder.RecordType;
@@ -91,8 +91,8 @@ public class SessionAuthenticator {
         pwmApplication.getIntruderManager().check(RecordType.USERNAME, username);
         UserIdentity userIdentity = null;
         try {
-            final UserSearchEngine userSearchEngine = new UserSearchEngine(pwmApplication, sessionLabel);
-            userIdentity = userSearchEngine.resolveUsername(username, context, ldapProfile);
+            final UserSearchEngine userSearchEngine = pwmApplication.getUserSearchEngine();
+            userIdentity = userSearchEngine.resolveUsername(username, context, ldapProfile, sessionLabel);
 
             final AuthenticationRequest authEngine = LDAPAuthenticationRequest.createLDAPAuthenticationRequest(
                     pwmApplication,
@@ -177,8 +177,8 @@ public class SessionAuthenticator {
 
         UserIdentity userIdentity = null;
         try {
-            final UserSearchEngine userSearchEngine = new UserSearchEngine(pwmApplication, sessionLabel);
-            userIdentity = userSearchEngine.resolveUsername(username, null, null);
+            final UserSearchEngine userSearchEngine = pwmApplication.getUserSearchEngine();
+            userIdentity = userSearchEngine.resolveUsername(username, null, null, sessionLabel);
 
             final AuthenticationRequest authEngine = LDAPAuthenticationRequest.createLDAPAuthenticationRequest(
                     pwmApplication,

+ 56 - 0
src/main/java/password/pwm/ldap/search/SearchConfiguration.java

@@ -0,0 +1,56 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2016 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+package password.pwm.ldap.search;
+
+import com.novell.ldapchai.provider.ChaiProvider;
+import lombok.Builder;
+import lombok.Data;
+import password.pwm.config.FormConfiguration;
+
+import java.io.Serializable;
+import java.util.List;
+import java.util.Map;
+
+@Builder
+@Data
+public class SearchConfiguration implements Serializable {
+
+    private String filter;
+    private String ldapProfile;
+    private String username;
+    private String groupDN;
+    private List<String> contexts;
+    private Map<FormConfiguration, String> formValues;
+    private transient ChaiProvider chaiProvider;
+    private long searchTimeout;
+    private boolean enableValueEscaping = true;
+    private boolean enableContextValidation = true;
+    private boolean enableSplitWhitespace = false;
+
+    void validate() {
+        if (this.username != null && this.formValues != null) {
+            throw new IllegalArgumentException("username OR formValues cannot both be supplied");
+        }
+    }
+
+}

+ 372 - 328
src/main/java/password/pwm/ldap/UserSearchEngine.java → src/main/java/password/pwm/ldap/search/UserSearchEngine.java

@@ -20,7 +20,7 @@
  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
  */
 
-package password.pwm.ldap;
+package password.pwm.ldap.search;
 
 import com.novell.ldapchai.ChaiFactory;
 import com.novell.ldapchai.ChaiUser;
@@ -28,11 +28,14 @@ import com.novell.ldapchai.exception.ChaiOperationException;
 import com.novell.ldapchai.exception.ChaiUnavailableException;
 import com.novell.ldapchai.provider.ChaiProvider;
 import com.novell.ldapchai.util.SearchHelper;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
 import password.pwm.AppProperty;
 import password.pwm.PwmApplication;
 import password.pwm.PwmConstants;
 import password.pwm.bean.SessionLabel;
 import password.pwm.bean.UserIdentity;
+import password.pwm.config.Configuration;
 import password.pwm.config.FormConfiguration;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.option.DuplicateMode;
@@ -42,68 +45,97 @@ import password.pwm.error.PwmError;
 import password.pwm.error.PwmException;
 import password.pwm.error.PwmOperationalException;
 import password.pwm.error.PwmUnrecoverableException;
-import password.pwm.http.PwmRequest;
+import password.pwm.health.HealthRecord;
 import password.pwm.svc.PwmService;
 import password.pwm.svc.stats.Statistic;
+import password.pwm.util.java.ConditionalTaskExecutor;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.JsonUtil;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.java.TimeDuration;
+import password.pwm.util.logging.PwmLogLevel;
 import password.pwm.util.logging.PwmLogger;
 
-import java.io.Serializable;
 import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
-import java.util.Comparator;
+import java.util.HashSet;
+import java.util.Iterator;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
-
-public class UserSearchEngine {
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.FutureTask;
+import java.util.concurrent.RejectedExecutionException;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicInteger;
+
+public class UserSearchEngine implements PwmService {
 
     private static final PwmLogger LOGGER = PwmLogger.forClass(UserSearchEngine.class);
 
-    private static int searchCounter = 0;
+    private final AtomicInteger searchCounter = new AtomicInteger(0);
+    private final AtomicInteger foregroundJobCounter = new AtomicInteger(0);
+    private final AtomicInteger backgroundJobCounter = new AtomicInteger(0);
+    private final AtomicInteger rejectionJobCounter = new AtomicInteger(0);
+    private final AtomicInteger canceledJobCounter = new AtomicInteger(0);
+    private final AtomicInteger jobTimeoutCounter = new AtomicInteger(0);
 
     private PwmApplication pwmApplication;
-    private SessionLabel sessionLabel;
 
-    public UserSearchEngine(final PwmApplication pwmApplication, final SessionLabel sessionLabel) {
-        this.pwmApplication = pwmApplication;
-        this.sessionLabel = sessionLabel;
+    private ThreadPoolExecutor executor;
+
+    private final ConditionalTaskExecutor debugOutputTask = new ConditionalTaskExecutor(
+            () -> periodicDebugOutput(),
+            new ConditionalTaskExecutor.TimeDurationPredicate(1, TimeUnit.MINUTES)
+    );
+
+    public UserSearchEngine() {
     }
 
-    public UserSearchEngine(final PwmRequest pwmRequest) {
-        this(pwmRequest.getPwmApplication(), pwmRequest.getSessionLabel());
+    @Override
+    public STATUS status() {
+        return STATUS.OPEN;
     }
 
-    private static String figureSearchFilterForParams(
-            final Map<FormConfiguration, String> formValues,
-            final String searchFilter,
-            final boolean enableValueEscaping
-    )
-    {
-        String newSearchFilter = searchFilter;
+    @Override
+    public void init(final PwmApplication pwmApplication) throws PwmException {
+        this.pwmApplication = pwmApplication;
+        this.executor = createExecutor(pwmApplication);
+        this.periodicDebugOutput();
+    }
 
-        for (final FormConfiguration formItem : formValues.keySet()) {
-            final String attrName = "%" + formItem.getName() + "%";
-            String value = formValues.get(formItem);
-            if (enableValueEscaping) {
-                value = StringUtil.escapeLdapFilter(value);
-            }
-            newSearchFilter = newSearchFilter.replace(attrName, value);
+    @Override
+    public void close() {
+        if (executor != null) {
+            executor.shutdown();
         }
+        executor = null;
+    }
 
-        return newSearchFilter;
+    @Override
+    public List<HealthRecord> healthCheck() {
+        return Collections.emptyList();
+    }
+
+    @Override
+    public ServiceInfo serviceInfo() {
+        return new ServiceInfo(Collections.emptyList());
     }
 
     public UserIdentity resolveUsername(
             final String username,
             final String context,
-            final String profile
+            final String profile,
+            final SessionLabel sessionLabel
     )
             throws ChaiUnavailableException, PwmUnrecoverableException, PwmOperationalException
     {
@@ -128,22 +160,21 @@ public class UserSearchEngine {
             }
         }
 
-        final UserSearchEngine userSearchEngine = new UserSearchEngine(pwmApplication, sessionLabel);
-
         try {
             //see if we need to do a contextless search.
-            if (userSearchEngine.checkIfStringIsDN(username)) {
-                return userSearchEngine.resolveUserDN(username);
+            if (checkIfStringIsDN(username, sessionLabel)) {
+                return resolveUserDN(username);
             } else {
-                final SearchConfiguration searchConfiguration = new SearchConfiguration();
-                searchConfiguration.setUsername(username);
+                final SearchConfiguration.SearchConfigurationBuilder builder = SearchConfiguration.builder();
+                builder.username(username);
                 if (context != null) {
-                    searchConfiguration.setContexts(Collections.singletonList(context));
+                    builder.contexts(Collections.singletonList(context));
                 }
                 if (profile != null) {
-                    searchConfiguration.setLdapProfile(profile);
+                    builder.ldapProfile(profile);
                 }
-                return userSearchEngine.performSingleUserSearch(searchConfiguration);
+                final SearchConfiguration searchConfiguration = builder.build();
+                return performSingleUserSearch(searchConfiguration, sessionLabel);
             }
         } catch (PwmOperationalException e) {
             throw new PwmOperationalException(new ErrorInformation(PwmError.ERROR_CANT_MATCH_USER,e.getErrorInformation().getDetailedErrorMsg(),e.getErrorInformation().getFieldValues()));
@@ -151,15 +182,16 @@ public class UserSearchEngine {
     }
 
     public UserIdentity performSingleUserSearch(
-            final SearchConfiguration searchConfiguration
+            final SearchConfiguration searchConfiguration,
+            final SessionLabel sessionLabel
     )
             throws PwmUnrecoverableException, PwmOperationalException
     {
         final long startTime = System.currentTimeMillis();
         final DuplicateMode dupeMode = pwmApplication.getConfig().readSettingAsEnum(PwmSetting.LDAP_DUPLICATE_MODE, DuplicateMode.class);
         final int searchCount = (dupeMode == DuplicateMode.FIRST_ALL) ? 1 : 2;
-        final Map<UserIdentity,Map<String,String>> searchResults = performMultiUserSearch(searchConfiguration, searchCount, Collections.<String>emptyList());
-        final List<UserIdentity> results = searchResults == null ? Collections.<UserIdentity>emptyList() : new ArrayList<>(searchResults.keySet());
+        final Map<UserIdentity,Map<String,String>> searchResults = performMultiUserSearch(searchConfiguration, searchCount, Collections.emptyList(), sessionLabel);
+        final List<UserIdentity> results = searchResults == null ? Collections.emptyList() : new ArrayList<>(searchResults.keySet());
         if (results.isEmpty()) {
             final String errorMessage;
             if (searchConfiguration.getUsername() != null && searchConfiguration.getUsername().length() > 0) {
@@ -192,14 +224,17 @@ public class UserSearchEngine {
             final Locale locale,
             final SearchConfiguration searchConfiguration,
             final int maxResults,
-            final List<FormConfiguration> formItem
+            final List<FormConfiguration> formItem,
+            final SessionLabel sessionLabel
     )
-            throws PwmUnrecoverableException, ChaiUnavailableException, PwmOperationalException {
+            throws PwmUnrecoverableException, ChaiUnavailableException, PwmOperationalException
+    {
         final Map<String,String> attributeHeaderMap = UserSearchResults.fromFormConfiguration(formItem,locale);
         final Map<UserIdentity,Map<String,String>> searchResults = performMultiUserSearch(
                 searchConfiguration,
                 maxResults + 1,
-                attributeHeaderMap.keySet()
+                attributeHeaderMap.keySet(),
+                sessionLabel
         );
         final boolean resultsExceeded = searchResults.size() > maxResults;
         final Map<UserIdentity,Map<String,String>> returnData = new LinkedHashMap<>();
@@ -215,7 +250,8 @@ public class UserSearchEngine {
     public Map<UserIdentity,Map<String,String>> performMultiUserSearch(
             final SearchConfiguration searchConfiguration,
             final int maxResults,
-            final Collection<String> returnAttributes
+            final Collection<String> returnAttributes,
+            final SessionLabel sessionLabel
     )
             throws PwmUnrecoverableException, PwmOperationalException
     {
@@ -232,58 +268,59 @@ public class UserSearchEngine {
         }
 
         final boolean ignoreUnreachableProfiles = pwmApplication.getConfig().readSettingAsBoolean(PwmSetting.LDAP_IGNORE_UNREACHABLE_PROFILES);
-        final Map<UserIdentity,Map<String,String>> returnMap = new LinkedHashMap<>();
 
         final List<String> errors = new ArrayList<>();
 
         final long profileRetryDelayMS = Long.valueOf(pwmApplication.getConfig().readAppProperty(AppProperty.LDAP_PROFILE_RETRY_DELAY));
+
+        final List<UserSearchJob> searchJobs = new ArrayList<>();
         for (final LdapProfile ldapProfile : ldapProfiles) {
-            if (returnMap.size() < maxResults) {
-                boolean skipProfile = false;
-                final Instant lastLdapFailure = pwmApplication.getLdapConnectionService().getLastLdapFailureTime(ldapProfile);
-                if (ldapProfiles.size() > 1 && lastLdapFailure != null && TimeDuration.fromCurrent(lastLdapFailure).isShorterThan(profileRetryDelayMS)) {
-                    LOGGER.info("skipping user search on ldap profile " + ldapProfile.getIdentifier() + " due to recent unreachable status (" + TimeDuration.fromCurrent(lastLdapFailure).asCompactString() + ")");
-                    skipProfile = true;
-                }
-                if (!skipProfile) {
-                    try {
-                        returnMap.putAll(performMultiUserSearchImpl(
-                                ldapProfile,
-                                searchConfiguration,
-                                maxResults - returnMap.size(),
-                                returnAttributes)
-                        );
-                    } catch (PwmUnrecoverableException e) {
-                        if (e.getError() == PwmError.ERROR_DIRECTORY_UNAVAILABLE) {
-                            pwmApplication.getLdapConnectionService().setLastLdapFailure(ldapProfile,e.getErrorInformation());
-                            if (ignoreUnreachableProfiles) {
-                                errors.add(e.getErrorInformation().getDetailedErrorMsg());
-                                if (errors.size() >= ldapProfiles.size()) {
-                                    final String errorMsg = "all ldap profiles are unreachable; errors: " + JsonUtil.serializeCollection(errors);
-                                    throw new PwmUnrecoverableException(new ErrorInformation(PwmError.ERROR_DIRECTORY_UNAVAILABLE, errorMsg));
-                                }
+            boolean skipProfile = false;
+            final Instant lastLdapFailure = pwmApplication.getLdapConnectionService().getLastLdapFailureTime(ldapProfile);
+
+            if (ldapProfiles.size() > 1 && lastLdapFailure != null && TimeDuration.fromCurrent(lastLdapFailure).isShorterThan(profileRetryDelayMS)) {
+                LOGGER.info("skipping user search on ldap profile " + ldapProfile.getIdentifier() + " due to recent unreachable status (" + TimeDuration.fromCurrent(lastLdapFailure).asCompactString() + ")");
+                skipProfile = true;
+            }
+            if (!skipProfile) {
+                try {
+                    searchJobs.addAll(this.makeSearchJobs(
+                            ldapProfile,
+                            searchConfiguration,
+                            maxResults,
+                            returnAttributes
+                    ));
+                } catch (PwmUnrecoverableException e) {
+                    if (e.getError() == PwmError.ERROR_DIRECTORY_UNAVAILABLE) {
+                        pwmApplication.getLdapConnectionService().setLastLdapFailure(ldapProfile,e.getErrorInformation());
+                        if (ignoreUnreachableProfiles) {
+                            errors.add(e.getErrorInformation().getDetailedErrorMsg());
+                            if (errors.size() >= ldapProfiles.size()) {
+                                final String errorMsg = "all ldap profiles are unreachable; errors: " + JsonUtil.serializeCollection(errors);
+                                throw new PwmUnrecoverableException(new ErrorInformation(PwmError.ERROR_DIRECTORY_UNAVAILABLE, errorMsg));
                             }
-                        } else {
-                            throw e;
                         }
+                    } else {
+                        throw e;
                     }
                 }
             }
         }
-        return returnMap;
+
+        final Map<UserIdentity,Map<String,String>> resultsMap = new LinkedHashMap<>(executeSearchJobs(searchJobs, sessionLabel, searchCounter.getAndIncrement()));
+        final Map<UserIdentity,Map<String,String>> returnMap = trimOrderedMap(resultsMap, maxResults);
+        return Collections.unmodifiableMap(returnMap);
     }
 
 
-    protected Map<UserIdentity,Map<String,String>> performMultiUserSearchImpl(
+    private Collection<UserSearchJob> makeSearchJobs(
             final LdapProfile ldapProfile,
             final SearchConfiguration searchConfiguration,
             final int maxResults,
             final Collection<String> returnAttributes
     )
-            throws PwmUnrecoverableException, PwmOperationalException {
-        final long startTime = System.currentTimeMillis();
-        LOGGER.debug(sessionLabel, "beginning user search process");
-
+            throws PwmUnrecoverableException, PwmOperationalException
+    {
         // check the search configuration data params
         searchConfiguration.validate();
 
@@ -348,69 +385,53 @@ public class UserSearchEngine {
                 pwmApplication.getProxyChaiProvider(ldapProfile.getIdentifier()) :
                 searchConfiguration.getChaiProvider();
 
-        final Map<UserIdentity,Map<String,String>> returnMap;
-        returnMap = new LinkedHashMap<>();
+        final List<UserSearchJob> returnMap = new ArrayList<>();
         for (final String loopContext : searchContexts) {
-            try {
-                final Map<UserIdentity, Map<String, String>> singleContextResults = doSingleContextSearch(
-                        ldapProfile,
-                        searchFilter,
-                        loopContext,
-                        returnAttributes,
-                        maxResults - returnMap.size(),
-                        chaiProvider,
-                        timeLimitMS
-                );
-                returnMap.putAll(singleContextResults);
-            } catch (Throwable t) {
-                final ErrorInformation errorInformation;
-                if (t instanceof PwmException) {
-                    errorInformation = new ErrorInformation(((PwmException) t).getError(), "unexpected error during ldap search ("
-                            + "profile=" + ldapProfile.getIdentifier() + ")"
-                            + ", error: " + t.getMessage());
-                } else {
-                    errorInformation = new ErrorInformation(PwmError.ERROR_UNKNOWN, "unexpected error during ldap search ("
-                            + "profile=" + ldapProfile.getIdentifier() + ")"
-                            + ", error: " + JavaHelper.readHostileExceptionMessage(t));
-                }
-                LOGGER.error(sessionLabel, "error during user search: " + errorInformation.toDebugStr());
-                throw new PwmUnrecoverableException(errorInformation);
-            }
-            if (returnMap.size() >= maxResults) {
-                break;
-            }
+            final UserSearchJob userSearchJob = UserSearchJob.builder()
+                    .ldapProfile(ldapProfile)
+                    .searchFilter(searchFilter)
+                    .context(loopContext)
+                    .returnAttributes(returnAttributes)
+                    .maxResults(maxResults)
+                    .chaiProvider(chaiProvider)
+                    .timeoutMs(timeLimitMS)
+                    .build();
+            returnMap.add(userSearchJob);
         }
 
-        LOGGER.debug(sessionLabel, "completed user search process in " + TimeDuration.fromCurrent(startTime).asCompactString() + ", resultSize=" + returnMap.size());
         return returnMap;
     }
 
-    private Map<UserIdentity,Map<String,String>> doSingleContextSearch(
-            final LdapProfile ldapProfile,
-            final String searchFilter,
-            final String context,
-            final Collection<String> returnAttributes,
-            final int maxResults,
-            final ChaiProvider chaiProvider,
-            final long timeoutMs
+    private Map<UserIdentity,Map<String,String>> executeSearch(
+            final UserSearchJob userSearchJob,
+            final SessionLabel sessionLabel,
+            final int searchID,
+            final int jobID
     )
             throws PwmOperationalException, PwmUnrecoverableException
     {
+        debugOutputTask.conditionallyExecuteTask();
+
         final SearchHelper searchHelper = new SearchHelper();
-        searchHelper.setMaxResults(maxResults);
-        searchHelper.setFilter(searchFilter);
-        searchHelper.setAttributes(returnAttributes);
-        searchHelper.setTimeLimit((int)timeoutMs);
-        final int searchID = searchCounter++;
+        searchHelper.setMaxResults(userSearchJob.getMaxResults());
+        searchHelper.setFilter(userSearchJob.getSearchFilter());
+        searchHelper.setAttributes(userSearchJob.getReturnAttributes());
+        searchHelper.setTimeLimit((int)userSearchJob.getTimeoutMs());
 
-        final String debugInfo = "searchID=" + searchID + " profile=" + ldapProfile.getIdentifier() + " base=" + context
-                + " filter=" + searchHelper.toString() + " maxCount=" + searchHelper.getMaxResults();
-        LOGGER.debug(sessionLabel, "performing ldap search for user; " + debugInfo);
+        final String debugInfo;
+        {
+            final Map<String,String> props = new LinkedHashMap<>();
+            props.put("profile", userSearchJob.getLdapProfile().getIdentifier());
+            props.put("base", userSearchJob.getContext());
+            props.put("maxCount", String.valueOf(searchHelper.getMaxResults()));
+            debugInfo = "[" + StringUtil.mapToString(props) + "]";
+        }
+        log(PwmLogLevel.TRACE, sessionLabel, searchID, jobID, "performing ldap search for user; " + debugInfo);
 
         final Instant startTime = Instant.now();
         final Map<String, Map<String,String>> results;
         try {
-            results = chaiProvider.search(context, searchHelper);
+            results = userSearchJob.getChaiProvider().search(userSearchJob.getContext(), searchHelper);
         } catch (ChaiUnavailableException e) {
             throw new PwmUnrecoverableException(new ErrorInformation(PwmError.ERROR_DIRECTORY_UNAVAILABLE,e.getMessage()));
         } catch (ChaiOperationException e) {
@@ -424,15 +445,15 @@ public class UserSearchEngine {
         }
 
         if (results.isEmpty()) {
-            LOGGER.trace(sessionLabel, "no matches from search (" + searchDuration.asCompactString() +"); " + debugInfo);
+            log(PwmLogLevel.TRACE, sessionLabel, searchID, jobID, "no matches from search (" + searchDuration.asCompactString() +"); " + debugInfo);
             return Collections.emptyMap();
         }
 
-        LOGGER.trace(sessionLabel, "found " + results.size() + " results in " + searchDuration.asCompactString() + "; " + debugInfo);
+        log(PwmLogLevel.TRACE, sessionLabel, searchID, jobID, "found " + results.size() + " results in " + searchDuration.asCompactString() + "; " + debugInfo);
 
         final Map<UserIdentity,Map<String,String>> returnMap = new LinkedHashMap<>();
         for (final String userDN : results.keySet()) {
-            final UserIdentity userIdentity = new UserIdentity(userDN, ldapProfile.getIdentifier());
+            final UserIdentity userIdentity = new UserIdentity(userDN, userSearchJob.getLdapProfile().getIdentifier());
             final Map<String,String> attributeMap = results.get(userDN);
             returnMap.put(userIdentity, attributeMap);
         }
@@ -456,251 +477,274 @@ public class UserSearchEngine {
         throw new PwmOperationalException(PwmError.ERROR_UNKNOWN,"context '" + context + "' is specified, but is not in configuration");
     }
 
-    public static class SearchConfiguration implements Serializable {
-        private String ldapProfile;
-        private String filter;
-        private String username;
-        private String groupDN;
-        private List<String> contexts;
-        private Map<FormConfiguration, String> formValues;
-        private transient ChaiProvider chaiProvider;
-        private long searchTimeout;
-
-        private boolean enableValueEscaping = true;
-        private boolean enableContextValidation = true;
-        private boolean enableSplitWhitespace = false;
-
-        public String getFilter() {
-            return filter;
-        }
-
-        public void setFilter(final String filter) {
-            this.filter = filter;
-        }
-
-        public Map<FormConfiguration, String> getFormValues() {
-            return formValues;
-        }
-
-        public void setFormValues(final Map<FormConfiguration, String> formValues) {
-            this.formValues = formValues;
-        }
-
-        public String getUsername() {
-            return username;
-        }
-
-        public void setUsername(final String username) {
-            this.username = username;
-        }
-
-        public String getGroupDN() {
-            return groupDN;
-        }
-
-        public void setGroupDN(final String groupDN) {
-            this.groupDN = groupDN;
-        }
-
-        public List<String> getContexts() {
-            return contexts;
-        }
-
-        public void setContexts(final List<String> contexts) {
-            this.contexts = contexts;
+    private boolean checkIfStringIsDN(
+            final String input,
+            final SessionLabel sessionLabel
+    )
+    {
+        if (input == null || input.length() < 1) {
+            return false;
         }
 
-        public ChaiProvider getChaiProvider() {
-            return chaiProvider;
+        //if supplied user name starts with username attr assume its the full dn and skip the search
+        final Set<String> namingAttributes = new HashSet<>();
+        for (final LdapProfile ldapProfile : pwmApplication.getConfig().getLdapProfiles().values()) {
+            final String usernameAttribute = ldapProfile.readSettingAsString(PwmSetting.LDAP_NAMING_ATTRIBUTE);
+            if (input.toLowerCase().startsWith(usernameAttribute.toLowerCase() + "=")) {
+                LOGGER.trace(sessionLabel,
+                        "username '" + input + "' appears to be a DN (starts with configured ldap naming attribute'" + usernameAttribute + "'), skipping username search");
+                return true;
+            }
+            namingAttributes.add(usernameAttribute);
         }
 
-        public void setChaiProvider(final ChaiProvider chaiProvider) {
-            this.chaiProvider = chaiProvider;
-        }
+        LOGGER.trace(sessionLabel, "username '" + input + "' does not appear to be a DN (does not start with any of the configured ldap naming attributes '"
+                + StringUtil.collectionToString(namingAttributes,",")
+                + "')");
 
-        public boolean isEnableValueEscaping() {
-            return enableValueEscaping;
-        }
+        return false;
+    }
 
-        public void setEnableValueEscaping(final boolean enableValueEscaping) {
-            this.enableValueEscaping = enableValueEscaping;
-        }
 
-        public boolean isEnableContextValidation() {
-            return enableContextValidation;
+    private UserIdentity resolveUserDN(
+            final String userDN
+    )
+            throws PwmUnrecoverableException, ChaiUnavailableException, PwmOperationalException
+    {
+        final Collection<LdapProfile> ldapProfiles = pwmApplication.getConfig().getLdapProfiles().values();
+        for (final LdapProfile ldapProfile : ldapProfiles) {
+            final ChaiProvider provider = pwmApplication.getProxyChaiProvider(ldapProfile.getIdentifier());
+            final ChaiUser user = ChaiFactory.createChaiUser(userDN, provider);
+            if (user.isValid()) {
+                try {
+                    return new UserIdentity(user.readCanonicalDN(), ldapProfile.getIdentifier());
+                } catch (ChaiOperationException e) {
+                    LOGGER.error("unexpected error reading canonical userDN for '" + userDN + "', error: " + e.getMessage());
+                }
+            }
         }
+        throw new PwmOperationalException(new ErrorInformation(PwmError.ERROR_CANT_MATCH_USER));
+    }
 
-        public void setEnableContextValidation(final boolean enableContextValidation) {
-            this.enableContextValidation = enableContextValidation;
-        }
+    private Map<UserIdentity,Map<String,String>> executeSearchJobs(
+            final Collection<UserSearchJob> userSearchJobs,
+            final SessionLabel sessionLabel,
+            final int searchID
+    )
+            throws PwmUnrecoverableException
+    {
+        // create jobs
+        final List<JobInfo> jobs = new ArrayList<>();
+        {
+            int jobID = 0;
+            for (UserSearchJob userSearchJob : userSearchJobs) {
+                final int loopJobID = jobID++;
 
-        public boolean isEnableSplitWhitespace() {
-            return enableSplitWhitespace;
-        }
+                final FutureTask<Map<UserIdentity, Map<String, String>>> futureTask = new FutureTask<>(()
+                        -> executeSearch(userSearchJob, sessionLabel, searchID, loopJobID));
 
-        public void setEnableSplitWhitespace(final boolean enableSplitWhitespace) {
-            this.enableSplitWhitespace = enableSplitWhitespace;
-        }
+                final JobInfo jobInfo = new JobInfo(searchID, loopJobID, userSearchJob, futureTask);
 
-        private void validate() {
-            if (this.username != null && this.formValues != null) {
-                throw new IllegalArgumentException("username OR formValues cannot both be supplied");
+                jobs.add(jobInfo);
             }
         }
 
-        public String getLdapProfile()
+        final Instant startTime = Instant.now();
         {
-            return ldapProfile;
+            final String filterText = jobs.isEmpty() ? "" : ", filter: " + jobs.iterator().next().getUserSearchJob().getSearchFilter();
+            log(PwmLogLevel.DEBUG, sessionLabel, searchID, -1, "beginning user search process with " + jobs.size() + " search jobs" + filterText);
         }
 
-        public void setLdapProfile(final String ldapProfile)
-        {
-            this.ldapProfile = ldapProfile;
-        }
+        // execute jobs
+        for (Iterator<JobInfo> iterator = jobs.iterator(); iterator.hasNext(); ) {
+            final JobInfo jobInfo = iterator.next();
 
-        public long getSearchTimeout()
-        {
-            return searchTimeout;
-        }
+            boolean submittedToExecutor = false;
 
-        public void setSearchTimeout(final long searchTimeout)
-        {
-            this.searchTimeout = searchTimeout;
-        }
-    }
+            // use current thread to execute one (the last in the loop) task.
+            if (executor != null && iterator.hasNext()) {
+                try {
+                    executor.submit(jobInfo.getFutureTask());
+                    submittedToExecutor = true;
+                    backgroundJobCounter.incrementAndGet();
+                } catch (RejectedExecutionException e) {
+                    // executor is full, so revert to running locally
+                    rejectionJobCounter.incrementAndGet();
+                }
+            }
 
-    public boolean checkIfStringIsDN(final String input) {
-        if (input == null || input.length() < 1) {
-            return false;
+            if (!submittedToExecutor) {
+                try {
+                    jobInfo.getFutureTask().run();
+                    foregroundJobCounter.incrementAndGet();
+                } catch (Throwable t) {
+                    log(PwmLogLevel.ERROR, sessionLabel, searchID, jobInfo.getJobID(), "unexpected error running job in local thread: " + t.getMessage());
+                }
+            }
         }
 
-        //if supplied user name starts with username attr assume its the full dn and skip the search
-        for (final LdapProfile ldapProfile : pwmApplication.getConfig().getLdapProfiles().values()) {
-            final String usernameAttribute = ldapProfile.readSettingAsString(PwmSetting.LDAP_NAMING_ATTRIBUTE);
-            if (input.toLowerCase().startsWith(usernameAttribute.toLowerCase() + "=")) {
-                LOGGER.trace(sessionLabel,
-                        "username '" + input + "' appears to be a DN (starts with configured ldap naming attribute'" + usernameAttribute + "'), skipping username search");
-                return true;
+        // aggregate results
+        final Map<UserIdentity,Map<String,String>> results = new LinkedHashMap<>();
+        for (final JobInfo jobInfo : jobs) {
+            if (results.size() > jobInfo.getUserSearchJob().getMaxResults()) {
+                final FutureTask futureTask = jobInfo.getFutureTask();
+                if (!futureTask.isDone()) {
+                    canceledJobCounter.incrementAndGet();
+                }
+                jobInfo.getFutureTask().cancel(false);
             } else {
-                LOGGER.trace(sessionLabel,
-                        "username '" + input + "' does not appear to be a DN (does not start with configured ldap naming attribute '" + usernameAttribute + "')");
+                final long maxWaitTime = jobInfo.getUserSearchJob().getTimeoutMs() * 3;
+                try {
+                    results.putAll(jobInfo.getFutureTask().get(maxWaitTime, TimeUnit.MILLISECONDS));
+                } catch (InterruptedException e) {
+                    final String errorMsg = "unexpected interruption during search job execution: " + e.getMessage();
+                    log(PwmLogLevel.WARN, sessionLabel, searchID, jobInfo.getJobID(), errorMsg);
+                    LOGGER.error(sessionLabel, errorMsg, e);
+                    throw new PwmUnrecoverableException(new ErrorInformation(PwmError.ERROR_UNKNOWN, errorMsg));
+                } catch (ExecutionException e) {
+                    final Throwable t = e.getCause();
+                    final ErrorInformation errorInformation;
+                    final String errorMsg = "unexpected error during ldap search ("
+                            + "profile=" + jobInfo.getUserSearchJob().getLdapProfile() + ")"
+                            + ", error: " + (t instanceof PwmException ? t.getMessage() : JavaHelper.readHostileExceptionMessage(t));
+                    if (t instanceof PwmException) {
+                        errorInformation = new ErrorInformation(((PwmException) t).getError(), errorMsg);
+                    } else {
+                        errorInformation = new ErrorInformation(PwmError.ERROR_UNKNOWN, errorMsg);
+                    }
+                    log(PwmLogLevel.WARN, sessionLabel, searchID, jobInfo.getJobID(), "error during user search: " + errorInformation.toDebugStr());
+                    throw new PwmUnrecoverableException(errorInformation);
+                } catch (TimeoutException e) {
+                    final String errorMsg = "background search job timeout after " + jobInfo.getUserSearchJob().getTimeoutMs()
+                            + "ms, to ldapProfile '"
+                            + jobInfo.getUserSearchJob().getLdapProfile() + "'";
+                    log(PwmLogLevel.WARN, sessionLabel, searchID, jobInfo.getJobID(), "error during user search: " + errorMsg);
+                    jobTimeoutCounter.incrementAndGet();
+                }
             }
         }
 
-        return false;
+        log(PwmLogLevel.DEBUG, sessionLabel, searchID, -1, "completed user search process in "
+                + TimeDuration.fromCurrent(startTime).asCompactString()
+                + ", intermediate result size=" + results.size());
+        return Collections.unmodifiableMap(results);
     }
 
-    public static class UserSearchResults implements Serializable {
-        private final Map<String,String> headerAttributeMap;
-        private final Map<UserIdentity,Map<String,String>> results;
-        private boolean sizeExceeded;
-
-        public UserSearchResults(final Map<String, String> headerAttributeMap, final Map<UserIdentity, Map<String, String>> results, final boolean sizeExceeded) {
-            this.headerAttributeMap = headerAttributeMap;
-            this.results = Collections.unmodifiableMap(defaultSort(results, headerAttributeMap));
-            this.sizeExceeded = sizeExceeded;
-
-        }
-
-        private static Map<UserIdentity, Map<String, String>> defaultSort(
-                final Map<UserIdentity, Map<String, String>> results,
-                final Map<String,String> headerAttributeMap
-        )
-        {
-            if (headerAttributeMap == null || headerAttributeMap.isEmpty() || results == null) {
-                return results;
-            }
-
-            final String sortAttribute = headerAttributeMap.keySet().iterator().next();
-            final Comparator<UserIdentity> comparator = new Comparator<UserIdentity>() {
-                @Override
-                public int compare(final UserIdentity o1, final UserIdentity o2) {
-                    final String s1 = getSortValueByIdentity(o1);
-                    final String s2 = getSortValueByIdentity(o2);
-                    return s1.compareTo(s2);
-                }
+    @Getter
+    @AllArgsConstructor
+    private static class JobInfo {
+        private final int searchID;
+        private final int jobID;
+        private final UserSearchJob userSearchJob;
+        private final FutureTask<Map<UserIdentity,Map<String,String>>> futureTask;
+    }
 
-                private String getSortValueByIdentity(final UserIdentity userIdentity) {
-                    final Map<String,String> valueMap = results.get(userIdentity);
-                    if (valueMap != null) {
-                        final String sortValue = valueMap.get(sortAttribute);
-                        if (sortValue != null) {
-                            return sortValue;
-                        }
-                    }
-                    return "";
-                }
-            };
+    private Map<String,String> debugProperties() {
+        final Map<String,String> properties = new TreeMap<>();
+        properties.put("searchCount", this.searchCounter.toString());
+        properties.put("backgroundJobCounter", Integer.toString(this.backgroundJobCounter.get()));
+        properties.put("foregroundJobCounter", Integer.toString(this.foregroundJobCounter.get()));
+        properties.put("jvmThreadCount", Integer.toString(Thread.activeCount()));
+        if (executor == null) {
+            properties.put("background-enabled","false");
+        } else {
+            properties.put("background-enabled","true");
+            properties.put("background-maxPoolSize", Integer.toString(executor.getMaximumPoolSize()));
+            properties.put("background-activeCount", Integer.toString(executor.getActiveCount()));
+            properties.put("background-largestPoolSize", Integer.toString(executor.getLargestPoolSize()));
+            properties.put("background-poolSize", Integer.toString(executor.getPoolSize()));
+            properties.put("background-queue-size", Integer.toString(executor.getQueue().size()));
+            properties.put("background-rejectionJobCounter", Integer.toString(rejectionJobCounter.get()));
+            properties.put("background-canceledJobCounter", Integer.toString(canceledJobCounter.get()));
+            properties.put("background-jobTimeoutCounter", Integer.toString(jobTimeoutCounter.get()));
+        }
+        return Collections.unmodifiableMap(properties);
+    }
 
-            final List<UserIdentity> identitySortMap = new ArrayList<>();
-            identitySortMap.addAll(results.keySet());
-            Collections.sort(identitySortMap,comparator);
+    private void periodicDebugOutput() {
+        LOGGER.debug("periodic debug status: " + StringUtil.mapToString(debugProperties()));
+    }
 
-            final Map<UserIdentity, Map<String, String>> sortedResults = new LinkedHashMap<>();
-            for (final UserIdentity userIdentity : identitySortMap) {
-                sortedResults.put(userIdentity, results.get(userIdentity));
-            }
-            return sortedResults;
-        }
+    private void log(final PwmLogLevel level, final SessionLabel sessionLabel, final int searchID, final int jobID, final String message) {
+        final String idMsg = logIdString(searchID, jobID);
+        LOGGER.log(level, sessionLabel, idMsg + " " + message);
+    }
 
-        public Map<String, String> getHeaderAttributeMap() {
-            return headerAttributeMap;
+    private static String logIdString(final int searchID, final int jobID) {
+        String idMsg = "searchID=" + searchID;
+        if (jobID >= 0) {
+            idMsg += "-" + jobID;
         }
+        return idMsg;
+    }
 
-        public Map<UserIdentity, Map<String, String>> getResults() {
-            return results;
-        }
+    private static ThreadPoolExecutor createExecutor(final PwmApplication pwmApplication) {
+        final Configuration configuration = pwmApplication.getConfig();
 
-        public boolean isSizeExceeded() {
-            return sizeExceeded;
+        final boolean enabled = Boolean.parseBoolean(configuration.readAppProperty(AppProperty.LDAP_SEARCH_PARALLEL_ENABLE));
+        if (!enabled) {
+            return null;
         }
 
-        public List<Map<String,Object>> resultsAsJsonOutput(final PwmApplication pwmApplication, final UserIdentity ignoreUser)
-                throws PwmUnrecoverableException
+        final int endPoints;
         {
-            final List<Map<String,Object>> outputList = new ArrayList<>();
-            int idCounter = 0;
-            for (final UserIdentity userIdentity : this.getResults().keySet()) {
-                if (ignoreUser == null || !ignoreUser.equals(userIdentity)) {
-                    final Map<String, Object> rowMap = new LinkedHashMap<>();
-                    for (final String attribute : this.getHeaderAttributeMap().keySet()) {
-                        rowMap.put(attribute, this.getResults().get(userIdentity).get(attribute));
-                    }
-                    rowMap.put("userKey", userIdentity.toObfuscatedKey(pwmApplication));
-                    rowMap.put("id", idCounter);
-                    outputList.add(rowMap);
-                    idCounter++;
-                }
-            }
-            return outputList;
-        }
-
-        public static Map<String,String> fromFormConfiguration(final List<FormConfiguration> formItems, final Locale locale) {
-            final Map<String,String> results = new LinkedHashMap<>();
-            for (final FormConfiguration formItem : formItems) {
-                results.put(formItem.getName(), formItem.getLabel(locale));
+            int counter = 0;
+            for (final LdapProfile ldapProfile : configuration.getLdapProfiles().values()) {
+                final List<String> rootContexts = ldapProfile.readSettingAsStringArray(PwmSetting.LDAP_CONTEXTLESS_ROOT);
+                counter += rootContexts.size();
             }
-            return results;
-        }
+            endPoints = counter;
+        }
+
+        if (endPoints > 1) {
+            final int factor = Integer.parseInt(configuration.readAppProperty(AppProperty.LDAP_SEARCH_PARALLEL_FACTOR));
+            final int maxThreads = Integer.parseInt(configuration.readAppProperty(AppProperty.LDAP_SEARCH_PARALLEL_THREAD_MAX));
+            final int threads = Math.min(maxThreads, (endPoints) * factor);
+            final ThreadFactory threadFactory = JavaHelper.makePwmThreadFactory(JavaHelper.makeThreadName(pwmApplication, UserSearchEngine.class), true);
+            return  new ThreadPoolExecutor(
+                    threads,
+                    threads,
+                    1,
+                    TimeUnit.MINUTES,
+                    new ArrayBlockingQueue<>(threads),
+                    threadFactory
+            );
+        }
+        return null;
     }
 
-    public UserIdentity resolveUserDN(final String userDN) throws PwmUnrecoverableException, ChaiUnavailableException, PwmOperationalException {
-        final Collection<LdapProfile> ldapProfiles = pwmApplication.getConfig().getLdapProfiles().values();
-        final boolean ignoreUnreachableProfiles = pwmApplication.getConfig().readSettingAsBoolean(
-                PwmSetting.LDAP_IGNORE_UNREACHABLE_PROFILES);
-        for (final LdapProfile ldapProfile : ldapProfiles) {
-            final ChaiProvider provider = pwmApplication.getProxyChaiProvider(ldapProfile.getIdentifier());
-            final ChaiUser user = ChaiFactory.createChaiUser(userDN, provider);
-            if (user.isValid()) {
-                try {
-                    return new UserIdentity(user.readCanonicalDN(), ldapProfile.getIdentifier());
-                } catch (ChaiOperationException e) {
-                    LOGGER.error("unexpected error reading canonical userDN for '" + userDN + "', error: " + e.getMessage());
+    private static <K,V> Map<K,V> trimOrderedMap(final Map<K,V> inputMap, final int maxEntries) {
+        final Map<K,V> returnMap = new LinkedHashMap<>(inputMap);
+        if (returnMap.size() > maxEntries) {
+            int counter = 0;
+            for (final Iterator<K> iterator = returnMap.keySet().iterator() ; iterator.hasNext(); ) {
+                iterator.next();
+                counter++;
+                if (counter > maxEntries) {
+                    iterator.remove();
                 }
             }
         }
-        throw new PwmOperationalException(new ErrorInformation(PwmError.ERROR_CANT_MATCH_USER));
+        return Collections.unmodifiableMap(returnMap);
     }
 
+    private static String figureSearchFilterForParams(
+            final Map<FormConfiguration, String> formValues,
+            final String searchFilter,
+            final boolean enableValueEscaping
+    )
+    {
+        String newSearchFilter = searchFilter;
+
+        for (final FormConfiguration formItem : formValues.keySet()) {
+            final String attrName = "%" + formItem.getName() + "%";
+            String value = formValues.get(formItem);
+            if (enableValueEscaping) {
+                value = StringUtil.escapeLdapFilter(value);
+            }
+            newSearchFilter = newSearchFilter.replace(attrName, value);
+        }
 
+        return newSearchFilter;
+    }
 }

+ 43 - 0
src/main/java/password/pwm/ldap/search/UserSearchJob.java

@@ -0,0 +1,43 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2016 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+package password.pwm.ldap.search;
+
+import com.novell.ldapchai.provider.ChaiProvider;
+import lombok.Builder;
+import lombok.Getter;
+import password.pwm.config.profile.LdapProfile;
+
+import java.io.Serializable;
+import java.util.Collection;
+
+@Getter
+@Builder
+public class UserSearchJob implements Serializable {
+    private final LdapProfile ldapProfile;
+    private final String searchFilter;
+    private final String context;
+    private final Collection<String> returnAttributes;
+    private final int maxResults;
+    private final ChaiProvider chaiProvider;
+    private final long timeoutMs;
+}

+ 131 - 0
src/main/java/password/pwm/ldap/search/UserSearchResults.java

@@ -0,0 +1,131 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2016 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+package password.pwm.ldap.search;
+
+import password.pwm.PwmApplication;
+import password.pwm.bean.UserIdentity;
+import password.pwm.config.FormConfiguration;
+import password.pwm.error.PwmUnrecoverableException;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+public class UserSearchResults implements Serializable {
+    private final Map<String,String> headerAttributeMap;
+    private final Map<UserIdentity,Map<String,String>> results;
+    private boolean sizeExceeded;
+
+    public UserSearchResults(final Map<String, String> headerAttributeMap, final Map<UserIdentity, Map<String, String>> results, final boolean sizeExceeded) {
+        this.headerAttributeMap = headerAttributeMap;
+        this.results = Collections.unmodifiableMap(defaultSort(results, headerAttributeMap));
+        this.sizeExceeded = sizeExceeded;
+
+    }
+
+    private static Map<UserIdentity, Map<String, String>> defaultSort(
+            final Map<UserIdentity, Map<String, String>> results,
+            final Map<String,String> headerAttributeMap
+    )
+    {
+        if (headerAttributeMap == null || headerAttributeMap.isEmpty() || results == null) {
+            return results;
+        }
+
+        final String sortAttribute = headerAttributeMap.keySet().iterator().next();
+        final Comparator<UserIdentity> comparator = new Comparator<UserIdentity>() {
+            @Override
+            public int compare(final UserIdentity o1, final UserIdentity o2) {
+                final String s1 = getSortValueByIdentity(o1);
+                final String s2 = getSortValueByIdentity(o2);
+                return s1.compareTo(s2);
+            }
+
+            private String getSortValueByIdentity(final UserIdentity userIdentity) {
+                final Map<String,String> valueMap = results.get(userIdentity);
+                if (valueMap != null) {
+                    final String sortValue = valueMap.get(sortAttribute);
+                    if (sortValue != null) {
+                        return sortValue;
+                    }
+                }
+                return "";
+            }
+        };
+
+        final List<UserIdentity> identitySortMap = new ArrayList<>();
+        identitySortMap.addAll(results.keySet());
+        identitySortMap.sort(comparator);
+
+        final Map<UserIdentity, Map<String, String>> sortedResults = new LinkedHashMap<>();
+        for (final UserIdentity userIdentity : identitySortMap) {
+            sortedResults.put(userIdentity, results.get(userIdentity));
+        }
+        return sortedResults;
+    }
+
+    public Map<String, String> getHeaderAttributeMap() {
+        return headerAttributeMap;
+    }
+
+    public Map<UserIdentity, Map<String, String>> getResults() {
+        return results;
+    }
+
+    public boolean isSizeExceeded() {
+        return sizeExceeded;
+    }
+
+    public List<Map<String,Object>> resultsAsJsonOutput(final PwmApplication pwmApplication, final UserIdentity ignoreUser)
+            throws PwmUnrecoverableException
+    {
+        final List<Map<String,Object>> outputList = new ArrayList<>();
+        int idCounter = 0;
+        for (final UserIdentity userIdentity : this.getResults().keySet()) {
+            if (ignoreUser == null || !ignoreUser.equals(userIdentity)) {
+                final Map<String, Object> rowMap = new LinkedHashMap<>();
+                for (final String attribute : this.getHeaderAttributeMap().keySet()) {
+                    rowMap.put(attribute, this.getResults().get(userIdentity).get(attribute));
+                }
+                rowMap.put("userKey", userIdentity.toObfuscatedKey(pwmApplication));
+                rowMap.put("id", idCounter);
+                outputList.add(rowMap);
+                idCounter++;
+            }
+        }
+        return outputList;
+    }
+
+    public static Map<String,String> fromFormConfiguration(final List<FormConfiguration> formItems, final Locale locale) {
+        final Map<String,String> results = new LinkedHashMap<>();
+        for (final FormConfiguration formItem : formItems) {
+            results.put(formItem.getName(), formItem.getLabel(locale));
+        }
+        return results;
+    }
+}

+ 2 - 0
src/main/java/password/pwm/svc/PwmServiceManager.java

@@ -32,6 +32,7 @@ import password.pwm.health.HealthMonitor;
 import password.pwm.http.servlet.resource.ResourceServletService;
 import password.pwm.http.state.SessionStateService;
 import password.pwm.ldap.LdapConnectionService;
+import password.pwm.ldap.search.UserSearchEngine;
 import password.pwm.svc.cache.CacheService;
 import password.pwm.svc.event.AuditService;
 import password.pwm.svc.intruder.IntruderManager;
@@ -92,6 +93,7 @@ public class PwmServiceManager {
         ResourceServletService( ResourceServletService.class,    false),
         SessionTrackService(    SessionTrackService.class,       false),
         SessionStateSvc(        SessionStateService.class,       false),
+        UserSearchEngine(       UserSearchEngine.class,          true),
 
         ;
 

+ 6 - 3
src/main/java/password/pwm/svc/intruder/RecordManagerImpl.java

@@ -30,6 +30,7 @@ import password.pwm.util.java.ClosableIterator;
 import password.pwm.util.java.JsonUtil;
 import password.pwm.util.java.TimeDuration;
 import password.pwm.util.logging.PwmLogger;
+import password.pwm.util.secure.PwmHashAlgorithm;
 import password.pwm.util.secure.SecureEngine;
 
 class RecordManagerImpl implements RecordManager {
@@ -39,6 +40,8 @@ class RecordManagerImpl implements RecordManager {
     private final RecordStore recordStore;
     private final IntruderSettings settings;
 
+    private static final PwmHashAlgorithm KEY_HASH_ALG = PwmHashAlgorithm.SHA256;
+
     RecordManagerImpl(final RecordType recordType, final RecordStore recordStore, final IntruderSettings settings) {
         this.recordType = recordType;
         this.recordStore = recordStore;
@@ -133,13 +136,13 @@ class RecordManagerImpl implements RecordManager {
     }
 
     private String makeKey(final String subject) throws PwmOperationalException {
-        final String md5sum;
+        final String hash;
         try {
-            md5sum = SecureEngine.md5sum(subject);
+            hash = SecureEngine.hash(subject, KEY_HASH_ALG);
         } catch (PwmUnrecoverableException e) {
             throw new PwmOperationalException(PwmError.ERROR_UNKNOWN,"error generating md5sum for intruder record: " + e.getMessage());
         }
-        return md5sum + recordType.toString();
+        return hash + recordType.toString();
     }
 
 

+ 25 - 10
src/main/java/password/pwm/svc/report/ReportService.java

@@ -39,7 +39,8 @@ import password.pwm.error.PwmException;
 import password.pwm.error.PwmOperationalException;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.health.HealthRecord;
-import password.pwm.ldap.UserSearchEngine;
+import password.pwm.ldap.search.SearchConfiguration;
+import password.pwm.ldap.search.UserSearchEngine;
 import password.pwm.ldap.UserStatusReader;
 import password.pwm.svc.PwmService;
 import password.pwm.svc.stats.EventRateMeter;
@@ -427,20 +428,34 @@ public class ReportService implements PwmService {
         )
                 throws ChaiUnavailableException, ChaiOperationException, PwmUnrecoverableException, PwmOperationalException
         {
-            final UserSearchEngine userSearchEngine = new UserSearchEngine(pwmApplication,null);
-            final UserSearchEngine.SearchConfiguration searchConfiguration = new UserSearchEngine.SearchConfiguration();
-            searchConfiguration.setEnableValueEscaping(false);
-            searchConfiguration.setSearchTimeout(Long.parseLong(pwmApplication.getConfig().readAppProperty(AppProperty.REPORTING_LDAP_SEARCH_TIMEOUT)));
+            final UserSearchEngine userSearchEngine = pwmApplication.getUserSearchEngine();
 
-            if (searchFilter == null) {
-                searchConfiguration.setUsername("*");
-            } else {
-                searchConfiguration.setFilter(searchFilter);
+            final SearchConfiguration searchConfiguration;
+            {
+                final SearchConfiguration.SearchConfigurationBuilder builder = SearchConfiguration.builder();
+
+                builder.enableValueEscaping(false);
+                builder.searchTimeout(Long.parseLong(pwmApplication.getConfig().readAppProperty(AppProperty.REPORTING_LDAP_SEARCH_TIMEOUT)));
+
+                if (searchFilter == null) {
+                    builder.username("*");
+                } else {
+                    builder.filter(searchFilter);
+                }
+
+                searchConfiguration = builder.build();
             }
 
+
             LOGGER.debug(PwmConstants.REPORTING_SESSION_LABEL,"beginning UserReportService user search using parameters: " + (JsonUtil.serialize(searchConfiguration)));
 
-            final Map<UserIdentity,Map<String,String>> searchResults = userSearchEngine.performMultiUserSearch(searchConfiguration, maxResults, Collections.<String>emptyList());
+            final Map<UserIdentity,Map<String,String>> searchResults = userSearchEngine.performMultiUserSearch(
+                    searchConfiguration,
+                    maxResults,
+                    Collections.emptyList(),
+                    PwmConstants.REPORTING_SESSION_LABEL
+
+            );
             LOGGER.debug(PwmConstants.REPORTING_SESSION_LABEL,"user search found " + searchResults.size() + " users for reporting");
             return new ArrayList<>(searchResults.keySet());
         }

+ 12 - 8
src/main/java/password/pwm/svc/report/UserCacheService.java

@@ -38,7 +38,7 @@ import password.pwm.util.java.JsonUtil;
 import password.pwm.util.localdb.LocalDB;
 import password.pwm.util.localdb.LocalDBException;
 import password.pwm.util.logging.PwmLogger;
-import password.pwm.util.secure.SecureEngine;
+import password.pwm.util.secure.SecureService;
 
 import java.util.Collections;
 import java.util.List;
@@ -50,6 +50,8 @@ public class UserCacheService implements PwmService {
     private CacheStoreWrapper cacheStore;
     private STATUS status;
 
+    private PwmApplication pwmApplication;
+
 
     public STATUS status() {
         return status;
@@ -58,7 +60,7 @@ public class UserCacheService implements PwmService {
     public UserCacheRecord updateUserCache(final UserInfoBean userInfoBean)
             throws PwmUnrecoverableException
     {
-        final StorageKey storageKey = StorageKey.fromUserInfoBean(userInfoBean);
+        final StorageKey storageKey = StorageKey.fromUserInfoBean(userInfoBean, pwmApplication);
 
         boolean preExisting = false;
         try {
@@ -91,7 +93,7 @@ public class UserCacheService implements PwmService {
     public void store(final UserCacheRecord userCacheRecord)
             throws LocalDBException, PwmUnrecoverableException
     {
-        final StorageKey storageKey = StorageKey.fromUserGUID(userCacheRecord.getUserGUID());
+        final StorageKey storageKey = StorageKey.fromUserGUID(userCacheRecord.getUserGUID(), pwmApplication);
         cacheStore.write(storageKey, userCacheRecord);
     }
 
@@ -138,6 +140,7 @@ public class UserCacheService implements PwmService {
 
     public void init(final PwmApplication pwmApplication) throws PwmException {
         status = STATUS.OPENING;
+        this.pwmApplication = pwmApplication;
         this.cacheStore = new CacheStoreWrapper(pwmApplication.getLocalDB());
         status = STATUS.OPEN;
     }
@@ -176,24 +179,25 @@ public class UserCacheService implements PwmService {
             return key;
         }
 
-        public static StorageKey fromUserInfoBean(final UserInfoBean userInfoBean)
+        public static StorageKey fromUserInfoBean(final UserInfoBean userInfoBean, final PwmApplication pwmApplication)
                 throws PwmUnrecoverableException
         {
             final String userGUID = userInfoBean.getUserGuid();
-            return fromUserGUID(userGUID);
+            return fromUserGUID(userGUID, pwmApplication);
         }
 
         public static StorageKey fromUserIdentity(final PwmApplication pwmApplication, final UserIdentity userIdentity)
                 throws ChaiUnavailableException, PwmUnrecoverableException
         {
             final String userGUID = LdapOperationsHelper.readLdapGuidValue(pwmApplication, null, userIdentity, true);
-            return fromUserGUID(userGUID);
+            return fromUserGUID(userGUID, pwmApplication);
         }
 
-        private static StorageKey fromUserGUID(final String userGUID)
+        private static StorageKey fromUserGUID(final String userGUID, final PwmApplication pwmApplication)
                 throws PwmUnrecoverableException
         {
-            return new StorageKey(SecureEngine.md5sum(userGUID));
+            final SecureService secureService = pwmApplication.getSecureService();
+            return new StorageKey(secureService.hash(userGUID));
         }
     }
 

+ 7 - 7
src/main/java/password/pwm/svc/stats/StatisticsBundle.java

@@ -55,14 +55,14 @@ public class StatisticsBundle {
 
     public static StatisticsBundle input(final String inputString) {
         final Map<Statistic, String> srcMap = new HashMap<>();
-            final Map<String, String> loadedMap = JsonUtil.deserializeStringMap(inputString);
-            for (final String key : loadedMap.keySet()) {
-                try {
-                    srcMap.put(Statistic.valueOf(key),loadedMap.get(key));
-                } catch (IllegalArgumentException e) {
-                    LOGGER.error("error parsing statistic key '" + key + "', reason: " + e.getMessage());
-                }
+        final Map<String, String> loadedMap = JsonUtil.deserializeStringMap(inputString);
+        for (final String key : loadedMap.keySet()) {
+            try {
+                srcMap.put(Statistic.valueOf(key),loadedMap.get(key));
+            } catch (IllegalArgumentException e) {
+                LOGGER.error("error parsing statistic key '" + key + "', reason: " + e.getMessage());
             }
+        }
         final StatisticsBundle bundle = new StatisticsBundle();
 
         for (final Statistic loopStat : Statistic.values()) {

+ 2 - 2
src/main/java/password/pwm/svc/token/CryptoTokenMachine.java

@@ -53,7 +53,7 @@ class CryptoTokenMachine implements TokenMachine {
         return returnString.toString();
     }
 
-    public TokenPayload retrieveToken(final String tokenKey)
+    public TokenPayload retrieveToken(final String tokenKey, final SessionLabel sessionLabel)
             throws PwmOperationalException, PwmUnrecoverableException
     {
         if (tokenKey == null || tokenKey.length() < 1) {
@@ -65,7 +65,7 @@ class CryptoTokenMachine implements TokenMachine {
     public void storeToken(final String tokenKey, final TokenPayload tokenPayload) throws PwmOperationalException, PwmUnrecoverableException {
     }
 
-    public void removeToken(final String tokenKey) throws PwmOperationalException, PwmUnrecoverableException {
+    public void removeToken(final String tokenKey, final SessionLabel sessionLabel) throws PwmOperationalException, PwmUnrecoverableException {
     }
 
     public int size() throws PwmOperationalException, PwmUnrecoverableException {

+ 5 - 5
src/main/java/password/pwm/svc/token/DBTokenMachine.java

@@ -48,10 +48,10 @@ class DBTokenMachine implements TokenMachine {
         return tokenService.makeUniqueTokenForMachine(sessionLabel, this);
     }
 
-    public TokenPayload retrieveToken(final String tokenKey)
+    public TokenPayload retrieveToken(final String tokenKey, final SessionLabel sessionLabel)
             throws PwmOperationalException, PwmUnrecoverableException
     {
-        final String md5sumToken = TokenService.makeTokenHash(tokenKey);
+        final String md5sumToken = tokenService.makeTokenHash(tokenKey);
         final String storedRawValue = databaseAccessor.get(DatabaseTable.TOKENS,md5sumToken);
 
         if (storedRawValue != null && storedRawValue.length() > 0 ) {
@@ -63,12 +63,12 @@ class DBTokenMachine implements TokenMachine {
 
     public void storeToken(final String tokenKey, final TokenPayload tokenPayload) throws PwmOperationalException, PwmUnrecoverableException {
         final String rawValue = tokenService.toEncryptedString(tokenPayload);
-        final String md5sumToken = TokenService.makeTokenHash(tokenKey);
+        final String md5sumToken = tokenService.makeTokenHash(tokenKey);
         databaseAccessor.put(DatabaseTable.TOKENS, md5sumToken, rawValue);
     }
 
-    public void removeToken(final String tokenKey) throws PwmOperationalException, PwmUnrecoverableException {
-        final String md5sumToken = TokenService.makeTokenHash(tokenKey);
+    public void removeToken(final String tokenKey, final SessionLabel sessionLabel) throws PwmOperationalException, PwmUnrecoverableException {
+        final String md5sumToken = tokenService.makeTokenHash(tokenKey);
         databaseAccessor.remove(DatabaseTable.TOKENS,tokenKey);
         databaseAccessor.remove(DatabaseTable.TOKENS,md5sumToken);
     }

+ 13 - 10
src/main/java/password/pwm/svc/token/LdapTokenMachine.java

@@ -34,8 +34,9 @@ import password.pwm.error.PwmError;
 import password.pwm.error.PwmOperationalException;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.ldap.LdapUserDataReader;
+import password.pwm.ldap.search.SearchConfiguration;
 import password.pwm.ldap.UserDataReader;
-import password.pwm.ldap.UserSearchEngine;
+import password.pwm.ldap.search.UserSearchEngine;
 
 import java.util.Collections;
 import java.util.HashMap;
@@ -65,12 +66,12 @@ class LdapTokenMachine implements TokenMachine {
         return tokenService.makeUniqueTokenForMachine(sessionLabel, this);
     }
 
-    public TokenPayload retrieveToken(final String tokenKey)
+    public TokenPayload retrieveToken(final String tokenKey, final SessionLabel sessionLabel)
             throws PwmOperationalException, PwmUnrecoverableException
     {
         final String searchFilter;
         {
-            final String md5sumToken = TokenService.makeTokenHash(tokenKey);
+            final String md5sumToken = tokenService.makeTokenHash(tokenKey);
             final SearchHelper tempSearchHelper = new SearchHelper();
             final Map<String,String> filterAttributes = new HashMap<>();
             for (final String loopStr : pwmApplication.getConfig().readSettingAsStringArray(PwmSetting.DEFAULT_OBJECT_CLASSES)) {
@@ -82,10 +83,12 @@ class LdapTokenMachine implements TokenMachine {
         }
 
         try {
-            final UserSearchEngine userSearchEngine = new UserSearchEngine(pwmApplication, null);
-            final UserSearchEngine.SearchConfiguration searchConfiguration = new UserSearchEngine.SearchConfiguration();
-            searchConfiguration.setFilter(searchFilter);
-            final UserIdentity user = userSearchEngine.performSingleUserSearch(searchConfiguration);
+            final UserSearchEngine userSearchEngine = pwmApplication.getUserSearchEngine();
+            final SearchConfiguration searchConfiguration = SearchConfiguration.builder()
+                    .filter(searchFilter)
+                    .build();
+
+            final UserIdentity user = userSearchEngine.performSingleUserSearch(searchConfiguration, sessionLabel);
             if (user == null) {
                 return null;
             }
@@ -117,7 +120,7 @@ class LdapTokenMachine implements TokenMachine {
             throws PwmOperationalException, PwmUnrecoverableException
     {
         try {
-            final String md5sumToken = TokenService.makeTokenHash(tokenKey);
+            final String md5sumToken = tokenService.makeTokenHash(tokenKey);
             final String encodedTokenPayload = tokenService.toEncryptedString(tokenPayload);
 
             final UserIdentity userIdentity = tokenPayload.getUserIdentity();
@@ -130,10 +133,10 @@ class LdapTokenMachine implements TokenMachine {
         }
     }
 
-    public void removeToken(final String tokenKey)
+    public void removeToken(final String tokenKey, final SessionLabel sessionLabel)
             throws PwmOperationalException, PwmUnrecoverableException
     {
-        final TokenPayload payload = retrieveToken(tokenKey);
+        final TokenPayload payload = retrieveToken(tokenKey, sessionLabel);
         if (payload != null) {
             final UserIdentity userIdentity = payload.getUserIdentity();
             try {

+ 5 - 5
src/main/java/password/pwm/svc/token/LocalDBTokenMachine.java

@@ -48,10 +48,10 @@ class LocalDBTokenMachine implements TokenMachine {
         return tokenService.makeUniqueTokenForMachine(sessionLabel, this);
     }
 
-    public TokenPayload retrieveToken(final String tokenKey)
+    public TokenPayload retrieveToken(final String tokenKey, final SessionLabel sessionLabel)
             throws PwmOperationalException, PwmUnrecoverableException
     {
-        final String md5sumToken = TokenService.makeTokenHash(tokenKey);
+        final String md5sumToken = tokenService.makeTokenHash(tokenKey);
         final String storedRawValue = localDB.get(LocalDB.DB.TOKENS, md5sumToken);
 
         if (storedRawValue != null && storedRawValue.length() > 0 ) {
@@ -63,14 +63,14 @@ class LocalDBTokenMachine implements TokenMachine {
 
     public void storeToken(final String tokenKey, final TokenPayload tokenPayload) throws PwmOperationalException, PwmUnrecoverableException {
         final String rawValue = tokenService.toEncryptedString(tokenPayload);
-        final String md5sumToken = TokenService.makeTokenHash(tokenKey);
+        final String md5sumToken = tokenService.makeTokenHash(tokenKey);
         localDB.put(LocalDB.DB.TOKENS, md5sumToken, rawValue);
     }
 
-    public void removeToken(final String tokenKey)
+    public void removeToken(final String tokenKey, final SessionLabel sessionLabel)
             throws PwmOperationalException, PwmUnrecoverableException
     {
-        final String md5sumToken = TokenService.makeTokenHash(tokenKey);
+        final String md5sumToken = tokenService.makeTokenHash(tokenKey);
         localDB.remove(LocalDB.DB.TOKENS, tokenKey);
         localDB.remove(LocalDB.DB.TOKENS, md5sumToken);
     }

+ 2 - 2
src/main/java/password/pwm/svc/token/TokenMachine.java

@@ -32,13 +32,13 @@ interface TokenMachine {
     String generateToken( SessionLabel sessionLabel,  TokenPayload tokenPayload)
             throws PwmUnrecoverableException, PwmOperationalException;
 
-    TokenPayload retrieveToken( String tokenKey)
+    TokenPayload retrieveToken(String tokenKey, SessionLabel sessionLabel)
             throws PwmOperationalException, PwmUnrecoverableException;
 
     void storeToken( String tokenKey,  TokenPayload tokenPayload)
             throws PwmOperationalException, PwmUnrecoverableException;
 
-    void removeToken( String tokenKey)
+    void removeToken(String tokenKey, SessionLabel sessionLabel)
             throws PwmOperationalException, PwmUnrecoverableException;
 
     int size()

+ 6 - 6
src/main/java/password/pwm/svc/token/TokenPayload.java

@@ -25,13 +25,13 @@ package password.pwm.svc.token;
 import password.pwm.bean.UserIdentity;
 
 import java.io.Serializable;
+import java.time.Instant;
 import java.util.Collections;
-import java.util.Date;
 import java.util.Map;
 import java.util.Set;
 
 public class TokenPayload implements Serializable {
-    private final java.util.Date date;
+    private final Instant date;
     private final String name;
     private final Map<String,String> data;
     private final UserIdentity user;
@@ -39,15 +39,15 @@ public class TokenPayload implements Serializable {
     private final String guid;
 
     TokenPayload(final String name, final Map<String, String> data, final UserIdentity user, final Set<String> dest, final String guid) {
-        this.date = new Date();
-        this.data = data == null ? Collections.<String,String>emptyMap() : Collections.unmodifiableMap(data);
+        this.date = Instant.now();
+        this.data = data == null ? Collections.emptyMap() : Collections.unmodifiableMap(data);
         this.name = name;
         this.user = user;
-        this.dest = dest == null ? Collections.<String>emptySet() : Collections.unmodifiableSet(dest);
+        this.dest = dest == null ? Collections.emptySet() : Collections.unmodifiableSet(dest);
         this.guid = guid;
     }
 
-    public Date getDate() {
+    public Instant getDate() {
         return date;
     }
 

+ 21 - 20
src/main/java/password/pwm/svc/token/TokenService.java

@@ -62,12 +62,11 @@ import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.macro.MacroMachine;
 import password.pwm.util.operations.PasswordUtility;
 import password.pwm.util.secure.PwmRandom;
-import password.pwm.util.secure.SecureEngine;
+import password.pwm.util.secure.SecureService;
 
 import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Collections;
-import java.util.Date;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
@@ -101,7 +100,7 @@ public class TokenService implements PwmService {
     private TokenMachine tokenMachine;
     private long counter;
 
-    private ServiceInfo serviceInfo = new ServiceInfo(Collections.<DataStorageMethod>emptyList());
+    private ServiceInfo serviceInfo = new ServiceInfo(Collections.emptyList());
     private STATUS status = STATUS.NEW;
 
     private ErrorInformation errorInformation = null;
@@ -115,7 +114,8 @@ public class TokenService implements PwmService {
         final long count = counter++;
         final StringBuilder guid = new StringBuilder();
         try {
-            guid.append(SecureEngine.md5sum(pwmApplication.getInstanceID() + pwmApplication.getStartupTime().toString()));
+            final SecureService secureService = pwmApplication.getSecureService();
+            guid.append(secureService.hash(pwmApplication.getInstanceID() + pwmApplication.getStartupTime().toString()));
             guid.append("-");
             guid.append(count);
         } catch (Exception e) {
@@ -250,7 +250,7 @@ public class TokenService implements PwmService {
 
         final TokenPayload tokenPayload;
         try {
-            tokenPayload = retrieveTokenData(tokenKey);
+            tokenPayload = retrieveTokenData(tokenKey, pwmSession.getLabel());
         } catch (PwmOperationalException e) {
             return;
         }
@@ -270,13 +270,13 @@ public class TokenService implements PwmService {
         StatisticsManager.incrementStat(pwmApplication, Statistic.TOKENS_PASSSED);
     }
 
-    public TokenPayload retrieveTokenData(final String tokenKey)
+    public TokenPayload retrieveTokenData(final String tokenKey, final SessionLabel sessionLabel)
             throws PwmOperationalException
     {
         checkStatus();
 
         try {
-            final TokenPayload storedToken = tokenMachine.retrieveToken(tokenKey);
+            final TokenPayload storedToken = tokenMachine.retrieveToken(tokenKey, sessionLabel);
             if (storedToken != null) {
 
                 if (testIfTokenIsExpired(storedToken)) {
@@ -284,7 +284,7 @@ public class TokenService implements PwmService {
                 }
 
                 if (testIfTokenIsPurgable(storedToken)) {
-                    tokenMachine.removeToken(tokenKey);
+                    tokenMachine.removeToken(tokenKey, sessionLabel);
                 }
 
                 return storedToken;
@@ -345,12 +345,12 @@ public class TokenService implements PwmService {
         if (theToken == null) {
             return false;
         }
-        final Date issueDate = theToken.getDate();
+        final Instant issueDate = theToken.getDate();
         if (issueDate == null) {
             LOGGER.error("retrieved token has no issueDate, marking as expired: " + JsonUtil.serialize(theToken));
             return true;
         }
-        final TimeDuration duration = new TimeDuration(issueDate,new Date());
+        final TimeDuration duration = TimeDuration.fromCurrent(issueDate);
         return duration.isLongerThan(maxTokenAgeMS);
     }
 
@@ -358,12 +358,12 @@ public class TokenService implements PwmService {
         if (theToken == null) {
             return false;
         }
-        final Date issueDate = theToken.getDate();
+        final Instant issueDate = theToken.getDate();
         if (issueDate == null) {
             LOGGER.error("retrieved token has no issueDate, marking as purgable: " + JsonUtil.serialize(theToken));
             return true;
         }
-        final TimeDuration duration = new TimeDuration(issueDate,new Date());
+        final TimeDuration duration = TimeDuration.fromCurrent(issueDate);
         return duration.isLongerThan(maxTokenPurgeAgeMS);
     }
 
@@ -375,10 +375,10 @@ public class TokenService implements PwmService {
         int cleanedTokens = 0;
         final List<String> tempKeyList = new ArrayList<>();
         final int purgeBatchSize = Integer.parseInt(pwmApplication.getConfig().readAppProperty(AppProperty.TOKEN_PURGE_BATCH_SIZE));
-        tempKeyList.addAll(discoverPurgeableTokenKeys(purgeBatchSize));
+        tempKeyList.addAll(discoverPurgeableTokenKeys(purgeBatchSize, PwmConstants.TOKEN_SESSION_LABEL));
         while (status() == STATUS.OPEN && !tempKeyList.isEmpty()) {
             for (final String loopKey : tempKeyList) {
-                tokenMachine.removeToken(loopKey);
+                tokenMachine.removeToken(loopKey, PwmConstants.HEALTH_SESSION_LABEL);
             }
             cleanedTokens = cleanedTokens + tempKeyList.size();
             tempKeyList.clear();
@@ -388,7 +388,7 @@ public class TokenService implements PwmService {
         }
     }
 
-    private List<String> discoverPurgeableTokenKeys(final int maxCount)
+    private List<String> discoverPurgeableTokenKeys(final int maxCount, final SessionLabel sessionLabel)
             throws PwmUnrecoverableException, PwmOperationalException
     {
         final List<String> returnList = new ArrayList<>();
@@ -399,7 +399,7 @@ public class TokenService implements PwmService {
 
             while (status() == STATUS.OPEN && returnList.size() < maxCount && keyIterator.hasNext()) {
                 final String loopKey = keyIterator.next();
-                final TokenPayload loopInfo = tokenMachine.retrieveToken(loopKey);
+                final TokenPayload loopInfo = tokenMachine.retrieveToken(loopKey, sessionLabel);
                 if (loopInfo != null) {
                     if (testIfTokenIsPurgable(loopInfo)) {
                         returnList.add(loopKey);
@@ -469,7 +469,7 @@ public class TokenService implements PwmService {
         while (tokenKey == null && attempts < maxUniqueCreateAttempts) {
             tokenKey = makeRandomCode(configuration);
             LOGGER.trace(sessionLabel, "generated new token random code, checking for uniqueness");
-            if (machine.retrieveToken(tokenKey) != null) {
+            if (machine.retrieveToken(tokenKey, sessionLabel) != null) {
                 tokenKey = null;
             }
             attempts++;
@@ -483,8 +483,9 @@ public class TokenService implements PwmService {
         return tokenKey;
     }
 
-    static String makeTokenHash(final String tokenKey) throws PwmUnrecoverableException {
-        return SecureEngine.md5sum(tokenKey) + "-hash";
+     String makeTokenHash(final String tokenKey) throws PwmUnrecoverableException {
+        final SecureService secureService = pwmApplication.getSecureService();
+        return secureService.hash(tokenKey) + "-hash";
     }
 
     private static boolean tokensAreUsedInConfig(final Configuration configuration) {
@@ -594,7 +595,7 @@ public class TokenService implements PwmService {
     {
         final TokenPayload tokenPayload;
         try {
-            tokenPayload = pwmApplication.getTokenService().retrieveTokenData(userEnteredCode);
+            tokenPayload = pwmApplication.getTokenService().retrieveTokenData(userEnteredCode, pwmSession.getLabel());
         } catch (PwmOperationalException e) {
             final String errorMsg = "unexpected error attempting to read token from storage: " + e.getErrorInformation().toDebugStr();
             throw new PwmOperationalException(PwmError.ERROR_TOKEN_INCORRECT,errorMsg);

+ 13 - 6
src/main/java/password/pwm/util/cli/commands/ExportResponsesCommand.java

@@ -28,7 +28,8 @@ import password.pwm.PwmApplication;
 import password.pwm.PwmConstants;
 import password.pwm.bean.SessionLabel;
 import password.pwm.bean.UserIdentity;
-import password.pwm.ldap.UserSearchEngine;
+import password.pwm.ldap.search.SearchConfiguration;
+import password.pwm.ldap.search.UserSearchEngine;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.JsonUtil;
 import password.pwm.util.java.TimeDuration;
@@ -54,14 +55,20 @@ public class ExportResponsesCommand extends AbstractCliCommand {
         JavaHelper.pause(2000);
 
         final long startTime = System.currentTimeMillis();
-        final UserSearchEngine userSearchEngine = new UserSearchEngine(pwmApplication, SessionLabel.SYSTEM_LABEL);
-        final UserSearchEngine.SearchConfiguration searchConfiguration = new UserSearchEngine.SearchConfiguration();
-        searchConfiguration.setEnableValueEscaping(false);
-        searchConfiguration.setUsername("*");
+        final UserSearchEngine userSearchEngine = pwmApplication.getUserSearchEngine();
+        final SearchConfiguration searchConfiguration = SearchConfiguration.builder()
+                .enableValueEscaping(false)
+                .username("*")
+                .build();
 
         final String systemRecordDelimiter = System.getProperty("line.separator");
         final Writer writer = new BufferedWriter(new PrintWriter(outputFile, PwmConstants.DEFAULT_CHARSET.toString()));
-        final Map<UserIdentity,Map<String,String>> results = userSearchEngine.performMultiUserSearch(searchConfiguration, Integer.MAX_VALUE, Collections.<String>emptyList());
+        final Map<UserIdentity,Map<String,String>> results = userSearchEngine.performMultiUserSearch(
+                searchConfiguration,
+                Integer.MAX_VALUE,
+                Collections.emptyList(),
+                SessionLabel.SYSTEM_LABEL
+        );
         out("searching " + results.size() + " users for stored responses to write to " + outputFile.getAbsolutePath() + "....");
         int counter = 0;
         for (final UserIdentity identity : results.keySet()) {

+ 14 - 11
src/main/java/password/pwm/util/cli/commands/ResponseStatsCommand.java

@@ -29,12 +29,14 @@ import password.pwm.AppProperty;
 import password.pwm.PwmApplication;
 import password.pwm.PwmConstants;
 import password.pwm.bean.ResponseInfoBean;
+import password.pwm.bean.SessionLabel;
 import password.pwm.bean.UserIdentity;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.profile.LdapProfile;
 import password.pwm.error.PwmOperationalException;
 import password.pwm.error.PwmUnrecoverableException;
-import password.pwm.ldap.UserSearchEngine;
+import password.pwm.ldap.search.SearchConfiguration;
+import password.pwm.ldap.search.UserSearchEngine;
 import password.pwm.util.java.JsonUtil;
 import password.pwm.util.java.TimeDuration;
 import password.pwm.util.cli.CliParameters;
@@ -148,20 +150,21 @@ public class ResponseStatsCommand extends AbstractCliCommand {
         final List<UserIdentity> returnList = new ArrayList<>();
 
         for (final LdapProfile ldapProfile : pwmApplication.getConfig().getLdapProfiles().values()) {
-            final UserSearchEngine userSearchEngine = new UserSearchEngine(pwmApplication,null);
-            final UserSearchEngine.SearchConfiguration searchConfiguration = new UserSearchEngine.SearchConfiguration();
-            searchConfiguration.setEnableValueEscaping(false);
-            searchConfiguration.setSearchTimeout(Long.parseLong(pwmApplication.getConfig().readAppProperty(AppProperty.REPORTING_LDAP_SEARCH_TIMEOUT)));
-
-            searchConfiguration.setUsername("*");
-            searchConfiguration.setEnableValueEscaping(false);
-            searchConfiguration.setFilter(ldapProfile.readSettingAsString(PwmSetting.LDAP_USERNAME_SEARCH_FILTER));
-            searchConfiguration.setLdapProfile(ldapProfile.getIdentifier());
+            final UserSearchEngine userSearchEngine = pwmApplication.getUserSearchEngine();
+            final SearchConfiguration searchConfiguration = SearchConfiguration.builder()
+                    .enableValueEscaping(false)
+                    .searchTimeout(Long.parseLong(pwmApplication.getConfig().readAppProperty(AppProperty.REPORTING_LDAP_SEARCH_TIMEOUT)))
+                    .username("*")
+                    .enableValueEscaping(false)
+                    .filter(ldapProfile.readSettingAsString(PwmSetting.LDAP_USERNAME_SEARCH_FILTER))
+                    .ldapProfile(ldapProfile.getIdentifier())
+                    .build();
 
             final Map<UserIdentity,Map<String,String>> searchResults = userSearchEngine.performMultiUserSearch(
                     searchConfiguration,
                     Integer.MAX_VALUE,
-                    Collections.<String>emptyList()
+                    Collections.emptyList(),
+                    SessionLabel.SYSTEM_LABEL
             );
             returnList.addAll(searchResults.keySet());
 

+ 2 - 1
src/main/java/password/pwm/util/cli/commands/TokenInfoCommand.java

@@ -23,6 +23,7 @@
 package password.pwm.util.cli.commands;
 
 import password.pwm.PwmApplication;
+import password.pwm.PwmConstants;
 import password.pwm.svc.token.TokenPayload;
 import password.pwm.svc.token.TokenService;
 import password.pwm.util.cli.CliParameters;
@@ -43,7 +44,7 @@ public class TokenInfoCommand extends AbstractCliCommand {
         TokenPayload tokenPayload = null;
         Exception lookupError = null;
         try {
-            tokenPayload = tokenService.retrieveTokenData(tokenKey);
+            tokenPayload = tokenService.retrieveTokenData(tokenKey, PwmConstants.TOKEN_SESSION_LABEL);
         } catch (Exception e) {
             lookupError = e;
         }

+ 35 - 12
src/main/java/password/pwm/util/operations/PasswordUtility.java

@@ -38,6 +38,7 @@ import com.novell.ldapchai.util.ChaiUtility;
 import password.pwm.AppProperty;
 import password.pwm.Permission;
 import password.pwm.PwmApplication;
+import password.pwm.PwmConstants;
 import password.pwm.bean.EmailItemBean;
 import password.pwm.bean.LoginInfoBean;
 import password.pwm.bean.PasswordStatus;
@@ -98,6 +99,7 @@ import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
+import java.util.concurrent.TimeUnit;
 
 /**
  * @author Jason D. Rivard
@@ -1101,8 +1103,8 @@ public class PasswordUtility {
                 if (oracleDS_PrePasswordAllowChangeTime != null && !oracleDS_PrePasswordAllowChangeTime.isEmpty()) {
                     final Date date = OracleDSEntries.convertZuluToDate(oracleDS_PrePasswordAllowChangeTime);
                     if (new Date().before(date)) {
-                        LOGGER.debug("discovered oracleds allowed change time is set to: " + JavaHelper.toIsoDate(date) + ", won't permit password change");
-                        final String errorMsg = "change not permitted until " + JavaHelper.toIsoDate(date);
+                        LOGGER.debug("discovered oracleds allowed change time is set to: " + PwmConstants.DEFAULT_DATETIME_FORMAT.format(date) + ", won't permit password change");
+                        final String errorMsg = "change not permitted until " + PwmConstants.DEFAULT_DATETIME_FORMAT.format(date);
                         final ErrorInformation errorInformation = new ErrorInformation(PwmError.PASSWORD_TOO_SOON, errorMsg);
                         throw new PwmUnrecoverableException(errorInformation);
                     }
@@ -1113,20 +1115,36 @@ public class PasswordUtility {
             LOGGER.debug(sessionLabel, "unexpected error reading OracleDS password allow modification time: " + e.getMessage());
         }
 
+        final TimeDuration minimumLifetime;
+        {
+            final int minimumLifetimeSeconds = passwordPolicy.getRuleHelper().readIntValue(PwmPasswordRule.MinimumLifetime);
+            if (minimumLifetimeSeconds < 1) {
+                return;
+            }
 
-        final int minimumLifetime = passwordPolicy.getRuleHelper().readIntValue(PwmPasswordRule.MinimumLifetime);
-        if (minimumLifetime < 1) {
-            return;
+            if (lastModified == null) {
+                LOGGER.debug(sessionLabel, "skipping minimum lifetime check, password last set time is unknown");
+                return;
+            }
+
+            minimumLifetime = new TimeDuration(minimumLifetimeSeconds, TimeUnit.SECONDS);
         }
 
-        if (lastModified == null || lastModified.isAfter(Instant.now())) {
-            LOGGER.debug(sessionLabel, "skipping minimum lifetime check, password last set time is unknown");
+        final TimeDuration passwordAge = TimeDuration.fromCurrent(lastModified);
+        LOGGER.trace(sessionLabel, "beginning check for minimum lifetime, lastModified="
+                + PwmConstants.DEFAULT_DATETIME_FORMAT.format(lastModified)
+                + ", minimumLifetimeSeconds=" + minimumLifetime.asCompactString()
+                + ", passwordAge=" + passwordAge.asCompactString());
+
+
+        if (lastModified.isAfter(Instant.now())) {
+            LOGGER.debug(sessionLabel, "skipping minimum lifetime check, password lastModified time is in the future");
             return;
         }
 
-        final TimeDuration passwordAge = TimeDuration.fromCurrent(lastModified);
-        final boolean passwordTooSoon = passwordAge.getTotalSeconds() < minimumLifetime;
+        final boolean passwordTooSoon = passwordAge.isShorterThan(minimumLifetime);
         if (!passwordTooSoon) {
+            LOGGER.trace(sessionLabel, "minimum lifetime check passed, password age ");
             return;
         }
 
@@ -1135,10 +1153,15 @@ public class PasswordUtility {
             return;
         }
 
-        final Date allowedChangeDate = new Date(System.currentTimeMillis() + (minimumLifetime * 1000));
-        final String errorMsg = "last password change is too recent, password cannot be changed until after " + JavaHelper.toIsoDate(allowedChangeDate);
+        final Instant allowedChangeDate = Instant.ofEpochMilli(lastModified.toEpochMilli() + minimumLifetime.getTotalMilliseconds());
+        final String errorMsg = "last password change was at "
+                + PwmConstants.DEFAULT_DATETIME_FORMAT.format(lastModified)
+                + " and is too recent (" + passwordAge.asCompactString()
+                + " ago), password cannot be changed within minimum lifetime of "
+                + minimumLifetime.asCompactString()
+                + ", next eligible time to change is after " + PwmConstants.DEFAULT_DATETIME_FORMAT.format(allowedChangeDate);
+
         final ErrorInformation errorInformation = new ErrorInformation(PwmError.PASSWORD_TOO_SOON,errorMsg);
         throw new PwmOperationalException(errorInformation);
     }
-
 }

+ 0 - 10
src/main/java/password/pwm/util/secure/SecureEngine.java

@@ -221,16 +221,6 @@ public class SecureEngine {
         }
     }
 
-    public static String md5sum(final String input)
-            throws PwmUnrecoverableException {
-        return hash(input, PwmHashAlgorithm.MD5);
-    }
-
-    public static String md5sum(final InputStream is)
-            throws PwmUnrecoverableException {
-        return hash(is, PwmHashAlgorithm.MD5);
-    }
-
     public static String hash(
             final byte[] input,
             final PwmHashAlgorithm algorithm

+ 3 - 3
src/main/java/password/pwm/ws/server/RestServerHelper.java

@@ -38,7 +38,7 @@ import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.http.PwmRequest;
 import password.pwm.http.PwmSession;
 import password.pwm.http.filter.AuthenticationFilter;
-import password.pwm.ldap.UserSearchEngine;
+import password.pwm.ldap.search.UserSearchEngine;
 import password.pwm.svc.intruder.RecordType;
 import password.pwm.util.LocaleHelper;
 import password.pwm.util.PasswordData;
@@ -195,8 +195,8 @@ public abstract class RestServerHelper {
 
 
         try {
-            final UserSearchEngine userSearchEngine = new UserSearchEngine(pwmApplication, pwmSession.getLabel());
-            return userSearchEngine.resolveUsername(username, null, null);
+            final UserSearchEngine userSearchEngine = pwmApplication.getUserSearchEngine();
+            return userSearchEngine.resolveUsername(username, null, null, pwmSession.getLabel());
         } catch (PwmOperationalException e) {
             throw new PwmUnrecoverableException(e.getErrorInformation());
         } catch (ChaiUnavailableException e) {

+ 3 - 3
src/main/java/password/pwm/ws/server/rest/RestVerifyOtpServer.java

@@ -32,7 +32,7 @@ import password.pwm.error.PwmError;
 import password.pwm.error.PwmOperationalException;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.i18n.Message;
-import password.pwm.ldap.UserSearchEngine;
+import password.pwm.ldap.search.UserSearchEngine;
 import password.pwm.util.operations.OtpService;
 import password.pwm.util.operations.otp.OTPUserRecord;
 import password.pwm.ws.server.RestRequestBean;
@@ -83,11 +83,11 @@ public class RestVerifyOtpServer extends AbstractRestServer {
                 throw new PwmUnrecoverableException(new ErrorInformation(PwmError.ERROR_UNAUTHORIZED,"actor does not have required permission"));
             }
 
-            final UserSearchEngine userSearchEngine = new UserSearchEngine(restRequestBean.getPwmApplication(), restRequestBean.getPwmSession().getLabel());
+            final UserSearchEngine userSearchEngine = restRequestBean.getPwmApplication().getUserSearchEngine();
             UserIdentity userIdentity = restRequestBean.getUserIdentity();
             if (userIdentity == null) {
                 final ChaiUser chaiUser = restRequestBean.getPwmSession().getSessionManager().getActor(restRequestBean.getPwmApplication());
-                userIdentity = userSearchEngine.resolveUsername(chaiUser.readUsername(), null, null);
+                userIdentity = userSearchEngine.resolveUsername(chaiUser.readUsername(), null, null, restRequestBean.getPwmSession().getLabel());
             }
 
             final OtpService otpService = restRequestBean.getPwmApplication().getOtpService();

+ 3 - 0
src/main/resources/password/pwm/AppProperty.properties

@@ -151,6 +151,9 @@ ldap.guid.pattern=@UUID@
 ldap.browser.maxEntries=1000
 ldap.search.paging.enable=auto
 ldap.search.paging.size=500
+ldap.search.parallel.enable=true
+ldap.search.parallel.factor=5
+ldap.search.parallel.threadMax=50
 localdb.aggressiveCompact.enabled=false
 localdb.implementation=password.pwm.util.localdb.Xodus_LocalDB
 localdb.initParameters=00

+ 1 - 1
src/main/webapp/WEB-INF/jsp/admin-tokenlookup.jsp

@@ -69,7 +69,7 @@
                 boolean tokenExpired = false;
                 String lookupError = null;
                 try {
-                    tokenPayload = tokenlookup_pwmRequest.getPwmApplication().getTokenService().retrieveTokenData(tokenKey);
+                    tokenPayload = tokenlookup_pwmRequest.getPwmApplication().getTokenService().retrieveTokenData(tokenKey, tokenlookup_pwmRequest.getSessionLabel());
                 } catch (PwmOperationalException e) {
                     tokenExpired= e.getError() == PwmError.ERROR_TOKEN_EXPIRED;
                     lookupError = e.getMessage();

+ 3 - 3
src/main/webapp/public/resources/js/newuser.js

@@ -144,8 +144,8 @@ PWM_NEWUSER.markStrength=function(strength) { //strength meter
 
 PWM_NEWUSER.refreshCreateStatus=function(refreshInterval) {
     require(["dojo","dijit/registry"],function(dojo,registry){
-        var checkStatusUrl = PWM_MAIN.addParamToUrl(window.location.href,"processAction","checkProgress");
-        var completedUrl = PWM_MAIN.addParamToUrl(window.location.href,"processAction","complete");
+        var checkStatusUrl = PWM_MAIN.addParamToUrl(window.location.pathname,"processAction","checkProgress");
+        var completedUrl = PWM_MAIN.addParamToUrl(window.location.pathname,"processAction","complete");
         var loadFunction = function(data) {
             var supportsProgress = (document.createElement('progress').max !== undefined);
             if (supportsProgress) {
@@ -168,4 +168,4 @@ PWM_NEWUSER.refreshCreateStatus=function(refreshInterval) {
         };
         PWM_MAIN.ajaxRequest(checkStatusUrl, loadFunction, {method:'GET'});
     });
-}
+};

File diff suppressed because it is too large
+ 0 - 2875
supplemental/PWMAdministrationGuide.pdf


+ 0 - 649
supplemental/history.txt

@@ -1,649 +0,0 @@
-PWM History/Changelog
-http://code.google.com/p/pwm
-
-[------legend----------------]
-[ + Added feature            ]
-[ * Improved/changed feature ]
-[ - Bug fixed/refactoring    ]
-[ ! security bug fix         ]
-[ ~ partial implementation   ]
-[----------------------------]
-
-[this file is no longer maintained]
-
-v1.8.0
----
-(Changes since v1.7.0 build 1228)
-+ Functionality/option to store intruder table in Database
-+ Functionality/option to store user event history in Database
-+ Intruder tracking for form input values and token send destinations
-+ Show/Hide password fields button replaced with eye hide/close icon on all pages
-+ "Ajaxified" all admin section tables
-+ Added support for BCRYPT, SCRYPT, SHA-256, SHA-512, PBKDF2 and MD5 stored responses
-+ Option for helpdesk functionality to always use application proxy user
-+ Send email on intruder detection to user or admin
-+ Send email on system or user audit events
-+ Support for multiple ldap profiles across all functionality
-+ Added system audit module and events for startup, shutdown, configChange and fatal events
-+ Added user audit events for helpdesk actions, token issued and token consumed events
-+ Replaced most constant "hardcoded" values in application as configurable AppProperty values.
-+ Updated change password UI with icon-based password guide and random password options.
-+ Improved show/hide password option with per-password-field
-+ Added ability to set email TO address per email, using macros, allowing for flexible per-email email user attribute configuration
-+ Added setting for smtp port
-
-- Removed "ADDB" template, replaced with extra Guide option to select responses
-- Refactored and formalized rest security model
-- fixed issue with slash character in DNs (issue #416)
-- cancel button sometimes triggered with enter key on forms (issue #507)
-- Separated ConfigEditor from ConfigManager implementation and user interface
-- Improved several ldap error handling scenarios and error message reporting
-- Refactored configuration module to support profile categories
-- Enabled localDB compression to support lengthy emails and other localdb-size constrained features
-
-! fixed issue where SHA1 responses where hashed only once regardless of iteration count setting
-
-v1.7.1 (February 18, 2014) build 1231
----
-* Updated Hungarian language (issue #469) thanks to torlasz
-+ Added Greek language (issue #527) thanks to smaistros
-- Updated ldapChai library for NetIQ eDirectory 8.8.8
-- SMS queue issue due to incorrect setting type (issue #523, #529) thanks to nils.rekow
-* Updated seedlist.zip (word list), removed offensive words
-* Minor theme changes
-- eDirectory schema LDIF updated (issue #472), thanks to tomgreene
-
-v1.7.0 (September 8, 2013) build 1228
----
-+ Option to disable idle countdown timer added (issue #404)
-+ REST based web services (including documentation and examples) for
-   reading/writing/deleting challenge responses, setting password, reading user status, application health and other functions
-+ Added capability to call external rest web services after most operations.
-+ Improvements in mobile/tablet display support
-+ Challenges refreshed into chosen language when locale is changed on setup challenges page (issue #390)
-+ Norwegian language (Nynorsk) localization (issue #376) thanks hagazaz!
-+ Several fixes for macros to work in more settings (issue #370, #363, #352, others)
-+ Install Guide "wizzard" for new installation
-+ Internal LDAP certificate auto-import, management and validator.
-+ Support for "force change password on next login" with AD
-+ Support for authenticating expired users with AD
-+ Support for reading AD "fine-grained" password policies
-+ Http Header "SSO" authentication option
-+ Hungarian language (magyar) localization (issue #441) thanks torlasz!
-
-! protection against session fixation attacks (issue #391)
-
-
-v1.6.5 (not published)
----
-+ Forgotten password search form now has multiple fields available (issue #207)
-+ Multiple ldap root contexts are now possible for login, helpdesk and other functions (issue #227)
-+ HTTP BasicAuth header default decoding is now "UTF-8"; exposed decoding charset in PwmConstants.properties (issue #34)
-+ Added forgotten password action option to send new password via email (issue #143);
-+ Refactored and improved UI for managing "form" configuration settings.
-+ regex validation of form field values and custom error messages (issue #260)
-+ html5 placeholder form values
-+ most html5 type input form types (url, date, datetime, number, tel, time, week, month)
-+ custom javascript support per form field
-+ select type forms with configurable option values (issue #273)
-+ all pages and content now gzip compressed when supported by browser
-+ new user form password strength meter and validation check
-+ helpdesk user search results
-+ sortable/column select grid for tabled data and user search results throughout pwm
-+ search filter for helpdesk user lookup (issue #228)
-+ added "please select" message during setup response randoms (issue #306)
-+ added helpdesk viewable challenge/responses (issue #229)
-
-
-- Shortcut order is sometimes randomized (issue #277)
-- Null pointer (and other issues) when using ldap token storage method (issue #281)
-- Allow HTML in error messages(
-
-
-v1.6.4 (August 23, 2012) build 1185
----
-+ Norwegian language support; thanks Izaz! (issue #261)
-+ Configuration option to enable/disable eDirectory NMAS password reading
-
-- Workaround for Apache Tomcat v7.0.29 deployment bug (issue #244)
-
-! increased hash loop count for credentials stored in shared history
-
-
-v1.6.3 (August 6, 2012) build 1181
----
-+ Improvements in monitoring and gauge presentation on PWM Admin and health pages.
-+ Backup backup files.  Saving config now keeps 5 previous PwmConfiguration.xml files as backups.
-+ Added setting for show logout button.
-+ Added export of statistics via CSV from PWM Admin GUI and PwmCommand utility.
-
-* Changed packaging structure to place pwm binaries in pwm-servlet.jar instead of WEB-INF/classes
-* Refactored response save configuration settings to allow more flexibility and clarity
-
-- Change language on UpdateProfile no longer clears form values (issue #243)
-- Show/Hide Response button now toggles attribute values as well as responses (issue #246)
-- Forgotten Password not working when user attributes required but no responses are required (issue #245)
-- Password History not working (duplicate local variable error) (issue #240)
-
-! Intruder lockout now backed by PwmDB instead of memory to prevent OOM under DDOS attack.
-
-
-v1.6.2 (July 14, 2012) build 1175
----
-+ Added token (email and sms) support for user activation
-+ Added new user agreement support for user activation
-+ Added crypto and ldap options for tokens
-+ ConfigManager performance improvements for display value editing
-+ Helpdesk random passsword generation and clear responses options
-+ Helpdesk session timeout configuration option
-+ "Force Profile Update" option added to route users through the Profile Update module
-+ Added partial localizations for Thai, Korean, Japanese and Chinese (issue #184)
-+ Support for token expiration to prevent re-use of tokens
-+ Added option for forgotten password query match (issue #180)
-+ Option for confirmation of Profile Update values
-+ Added several rest based web services
-+ Security option to disable external clients from using web services; default is disabled
-+ Improved token security by hashing stored keys and encrypting payloads
-+ Disallow (configurable) challenge text from being used as response text
-+ Added password strength meter check to helpdesk change password
-+ Added language updates for most PWM localizations (issue #232)
-
-* Refactored all JSP pages to be HTML5 compliant (html tags and Doctypes)
-* Improved page load performance for most pages
-* Config menu re-ordering and text improvements
-* Locale list is now configurable (issue #181)
-* Changed chai ResponseSet save format to exclude carriage returns in the ldap value (issue #197)
-
-- Changed servlet version to v2.5, v2.5 container (tomcat 6) and java 6 is now minimum requirement
-- Resolved issue where newuser agreement screen could be bypassed (issue #165)
-- Fixed url path issue (/private url required for public pages) (issue #154)
-- Issue where activate user wasn't performing LDAP compare on correct form values (issue #153)
-- Update Query Match filter not being applied (issue #178)
-- Email address destination token not being inserted (issue #177)
-- Double nested form tags on ActiveUser data entry page (issue #175)
-- PwmMacro engine broken when multiple macros used in a single string (issue #200)
-- Incorrect email encoding for non-latin character sets (issue #190)
-- Novell UserApp responses rejected when using random responses (issue #188)
-- Fixed issue with double-URLDecoding for logoutURL and forwardURL (issue #216)
-- Attribute names with hyphen ("-") not working (issue #221)
-
-
-v1.6.1 (January 16, 2012) build 1123
----
-+ Added "application leave detection" to detect if user leaves PWM site.
-+ Added settings to manage custom CSS & JavaScript from PwmApplication.xml
-+ Added option to override Case Sensitivity detection (issue #127)
-+ Option to skip success messages
-+ Request Sequence detection (duplicate form submit detection) added with option to disable.
-+ Added feature/option to trip a "bad password" auth to ldap in case of forgotten password failure.
-+ Password rule text configuration option to override auto-generated rule text.
-+ Added finnish locale, thanks tami.rauhala!
-+ Introduced PWM Macros for certain configuration settings and display fields.
-+ Version checking and anonymous statistics publishing (configurable)
-
-* Improved helpdesk module and removed administrative "UserInformation" module due to redundancy
-* Idle authentication timeout now configurable, separate from http session timeout
-* Implemented Jersey framework for rest web services
-! properly escape search strings during ldap username searches
-
-- Issue where change password page refresh would not properly show error (issue #131)
-- PwmCommand line broken with "cannot allow mutator operations" (issue #134)
-- Added multi-line option as default for SMS multi-line response (issue #132)
-- Broken responseSet.meetsRequirements() for userapp responses with user-defined questions (issue #140)
-- NullPointerException on password requirements tag when a minimum-interval is set in policy (issue #137)
-- SMTP Password encrypted in configuration (issue #144) 
-    NOTE: when upgrading from 1.6.0, the SMTP Password needs to be re-entered
-
-
-v1.6.0 (October 17, 2011) build 1096
----
-+ Added turkish locale, thanks erdem.bayer! (issue #86)
-+ Added slovak locale, thanks svacko! (issue #87)
-+ Added hebrew locale, thanks dordorqwerty! (issue #92)
-+ Added helpdesk module for password resets (issue #99)
-+ Support for customizable CSS themes, and several default themes included (issue #103)
-+ Support for SMTP authentication (issue #104)
-+ Overhaul of the NewUser registration module,
-   + Password field is now on initial new user password field
-   + Form UI supports more fields in less space
-   + Randomized DN generation
-   + While-you-type form validation
-   + Configurable password policy template user
-   + Configurable minimum wait time on new user creation
-+ Added stored token database (to PwmDB or RDBMS) for forgotten password and new user password
-+ Updated look & feel for form tables throughout the application
-+ UserReport module in /pwm/private/admin and from PwmCommand command line
-
-* Moved public menu options (ForgottenPassword, NewUser, Activate) to login page
-* Moved private menu options to /pwm/private url, and made menu visibility based on permission
-* Continued improvements in configmanager process
-
-- Fixed bug where unsupported browser locale results in blank page/null pointer (issue #83)
-- Fixed bug where non-english server locale results in configuration manager issues (issue #84)
-- Double-byte characters not stored properly in PwmConfiguration.xml (issue #100)
-- Issue where SMS Servlet Gateway URL couldn't be configured with a port number (issue #97)
-- Config file size limit of 50k characters increased to 10mb.
-- Current password required in some cases on forgotten password reset page. (issue #119)
-
-
-v1.5.5 (July 7, 2011) build 1056
----
-- Update profile error removing attribute on blank value (issue #74)
-- Responses required when not configured to be (issue #80)
-
-
-v1.5.4 (July 5, 2011) build 1054
----
-+ Storage of responses in RDBM database (useful for AD, or otherwise un-extensible ldap directories)
-+ Added guest account registration and management
-+ Added "selectable" type configuration syntax, and applied as appropriate
-+ Added SMS token integration to provide for text messaging (sms) of tokens during forgotten password
-+ Added Jasig CAS authentication server integration (http://www.jasig.org/cas) (issue #54)
-+ Improved new installation configuration experience
-* Can now use configuration gui to set all user-displayable text strings
-+ Configurable form "reset" button (issue #71)
-+ More control over pushing users to update profile (issue #72)
-+ Localization updates
-
-- Update Profile now correctly deletes values when blanked by user (issue #59)
-- Renamed Update Attributes servlet to UpdateProfile and changed checkAttributes parameter to checkProfile
-- General refactoring to improve error messages, reporting and handling
-
-~ Basic "PeopleSearch" module added
-
-! Security issue (issue #73)
-
-v1.5.3 (April 2, 2011) build 1026
----
-+ Added locale selector menu to footer (issue #31)
-+ Changed forgotten password sequence so email token is after response questions (issue #26)
-+ Added option to disable reverse dns resolution (in advanced) (issue #35)
-+ Added functionality for token-only, user attribute-only, or response-only forgotten password operation, or any combination of the three
-+ Added support for locally stored (PwmDB) responses
-+ Added command line interface (PwmCommand.bat) to manage locally stored responses
-+ Added forgotten username module
-
-* Added menu title for main index page (issue #30)
-* Added i18n and Dutch localizations for index page (issue #27)
-* Updated Dutch localizations (issue #32, #42, #47)
-* Improved AD complexity password checking (issue #34)
-* Improved health monitor checking and display screens
-
-- Fixed broken login contexts option on activation page (issue #26)
-- Fixed issue with cookieless (URL) sessions not working since v1.5.2
-- Fixed issue wih CommandServlet?processAction=checkExpire not working
-- Improved ConfigManager UI performance and layout, with simple/advanced mode
-- changed from simpleJson java library to google json (gson) library for flexibility and correctness
-
-! Added url escaping to user input reflected html form values to prevent certain types of XSS attacks
-
-
-v1.5.2 (October 22, 2010) build 996
----
-+ Changed main menu to hide un-enabled functions
-+ Added logout button to header when logged in
-+ Added "Password Change Message" configuration option
-+ Added java interfaces (and config settings) for ExternalChangeMethod, ExternalJudgeMethod and ExternalRuleMethod
-+ Strength meter on password page now has tooltip explaining the strength meter
-+ Added config options to show/hide strength meter, random generator
-+ Implemented method to retrieve password expiration time from DirectoryServer389 (chai)
-+ Added change password policy settings for min/max non-alphabetic characters
-+ Added "HealthMonitor" functionality to periodically test system health
-+ PWM now ignores Novell UP nspmComplexityRules that are customized (changed to other than default MS-AD 3of4 policy)
-+ Replaced "challenge.forceAllRandoms" setting with "challenge.minRandomsSetup" to allow more flexibility.
-+ Added "Show Password Guide" configuration and feature
-+ Added "Display Show/Hide Password Fields" configuration and feature
-+ Added "Show Cancel Button" configuration and feature
-+ Added "eDirectory-UserApp Forgotten Password SOAP API" configuration and feature
-+ Updated setup responses to dynamically prevent duplicate questions in simple mode
-+ Added reset to default value for settings in configuration editor
-
-* Modified active user module to write ldap attributes after the first password change subsequent to a successful activate user event. (issue #17)
-* Refactored logging module to more efficiently store log events in the pwmDB
-* Updated setup response screen ajax code to be list 'jittery' like change password screen
-
-- Updated reCaptcha urls and html to new google api 
-- Refactored ExternalPasswordMethod to ExternalChangeMethod java interface
-- Fixed issue with LoginContexts setting not being parsed properly
-- Fixed issue where ldap urls could not contain a hyphen
-- Fixed issue where older/corrupt user history attribute would cause a NullPointerException
-- Removed "dojo.css" include to prevent IE display issues (small fonts in IE)
-- Fixed issue where random generator would sometimes generate really long passwords (especially for complex policies)
-- Resolved issue where SharedHistory would not be purged correctly due to ClassCastException in PwmDBAdaptor
-- Fixed issue where clear button doesn't work on some pages in some browsers
-- Resolved issue where form buttons were overly wide in ie6/7 
-
-v1.5.1 (August 16, 2010) build 975
----
-+ Added idle timeout warning dialog
-+ Added settings to control Admin Alerts for startup, shutdown, config changes, intruder detection, fatal events and daily stats.
-+ Added setting to enable/disable session validation
-+ Updated random password generator to show multiple randomly generated passwords at once on change password screen
-
-* Improved statistics reliability and performance.
-
-- Fixed bug where forgotten password recovery would fail when no required attributes were configured
-- Fixed bug where forgotten password recovery would fail when DirectoryServer389 server was used
-- Fixed a bug where the configuration setting for multiple ldap login contexts could not be read properly
-
-
-v1.5.0 (July 6, 2010) build 959
----
-! Corrected a problem where XSS field inputs were not being properly validated and scrubbed
-! Added form "nonce" validations to prevent certain types of XSS attacks
-! Passwords stored in configuration file are now obfuscated (though config files still need to be protected securely)
-
-+ Added graphical feedback on password confirmation field
-+ Added web based configuration file editor (/pwm/config/ConfigManager) (issue #4)
-+ Switched configuration file format to PwmConfiguration.xml from pwmServlet.properties
-+ Basic support for 389 Directory Server (http://directory.fedoraproject.org/)
-+ Ability to send html email
-+ Added send email option for user activation
-+ Added smtp advanced properties configuration setting
-+ Added ldap advanced properties configuration setting
-+ Added send email token functionality during forgotten password reset
-+ Added config option for "agreement" text to be shown prior to password change.
-+ Added config option for ldap proxy connection idle timeout
-+ Added option to require current password on change password screen
-
-- Replaced vertical strength meter with horizontal strength meter with better CSS degradation
-- Fixed issue with caps lock warning on IE browsers (issue #3)
-- Added/Updated some of the french localization strings (issue #5)
-- Fixed issue with User Information crashing if NMAS is not being used (issue #6)
-- Fixed issue with Log4j (issue #7)
-- Refactored ajax calls to use dojo api, and also to resolve issue with incorrect encoding of i18n characters (issue #9)
-- Removed "aggressiveUrlParsing" setting, values for logoutURL/forwardURL now read url encoded values properly
-- Refactored idle counter javascript to be more efficient
-- Changed default location of pwmDB from META-INF to WEB-INF
-- Removed tabindex html attributes to allow proper tab behavior
-
-
-v1.4.3 (1/10/2010) build 922
----
-+ Improved handling of servlet container clustering and session pausing
-+ Added caps-lock detector during password entry
-+ Enhanced SetupResponses to show select lists when challenge.randomStyle=SETUP and there are no user defined random challenges
-+ Added admin user information debugging page to aid in troubleshooting for admins
-+ Added user user information debugging page to aid in troubleshooting for end users
-+ Added daily stats viewer and chart history in administrator status screen
-
-* Refined password strength meter to allow full strength to be reached, and to be more permissive
-* Tuned ajax communications to decrease request traffic.
-* Added "eventLog.localDbMaxAge" setting to control age of pwmDB event log storage
-* Added "ldapPromiscuousSSL" setting instead of using "autocert" param in the ldap server url configuration
-* Refactored HTML/CSS to improve appearance, correctness of both normal and mobile viewing
-
-- Fixed bug where "previously used" error incorrectly results in unknown error when using non-nmas password method (defect #51)
-- Removed setting "passwordSetMethod", for edirectory, this is replaced by "ldap.edirectory.enableNmas"
-- Refactored ajax channels to use JSON encoding
-- Fixed bug where WARN log status inaccurately reported no discovered nmas challenge set policy (defect #50)
-- Fixed bug where forgotten password doesn't work when there are no required challenges configured (defect #52)
-- Renamed "password.readEdirectoryPasswordPolicy" setting to "ldap.edirectory.readPasswordPolicies"
-- Added "ldap.edirectory.storeNmasResponses" setting, and removed nmas options from "challenge.storageMethod"
-- Replaced "authUsingBind" setting with "ldap.edirectory.alwaysUseProxy" setting
-- Fixed bug where newer Client32 clients forced users to configure responses after PWM saves responses using nmas (defect #53)
-- Refactored ant build.xml script to accommodate new layout used in google project svn
-- Moved XForwardedFor setting to pwmServlet.properties from web.xml as "useXForwardedForHeader" setting.
-- Fixed issue where random password generation sometimes generated out-of-policy passwords
-- Added "ldap.edirectory.readChallengeSets" and removed "challenge.policyMethod" setting
-- Added "ldap.edirectory.storeNmasResponses" setting, removing NMAS options from "challenge.storageMethod" setting
-
-
-
-v1.4.2 (7/23/2009) build 842
-------
-+ Added "usernameSearchFilter" config property to allow non "cn" based usernames throught pwm
-+ Added "challenge.requiredAttributes" config property to require ldap attribute values during forgotten password recovery
-+ Added "forceBasicAuth" web.xml parameter
-+ Added captcha functionality using reCaptcha with "recaptcha.privateKey" / "recaptcha.publicKey" settings.
-+ Added "activateUser.searchFilter" setting, and changed "activateUser.attributes" functionality to allow
-    arbitrary searching for users without having to know the username
-+ Added "aggressiveUrlParsing" web.xml parameter
-+ Added "password.ADComplexity" config property
-+ PWM now honors universal password policy "AD Complexity" configurations
-+ Added "challenge.forceSetup" config property
-+ Added "challenge.allowDuplicateResponses" config property
-+ Added "challenge.forgottenStyle" parameter to allow for mixing unlock/or password reset options during forgotten password use.
-+ Preliminary, undocumented, support for using Active Directory as the ldap directory
-+ Updated logging model to use local PWM database for persistance.  Log4j interface is still available to support external log systems.
-
-* Moved misleading and infrequently changed config property 'userNameAttribute' to web.xml parameter 'ldapNamingAttribute' (defect #36)
-* ChangePassword doesn't work when the "password.WordlistFile" config property is blank (defect #29)
-* ChangePassword screen only no longer shows expiration warning on new user creation, activation or forgotten password.
-
-! Corrected a scenario where multiple forgotten password attempts could exceed pwm user intruder limits over different http sessions
-! Change to prevent response values from being written to debug log.
-
-v1.4.1 (unreleased)
-
-v1.4.0 (10/4/2008) build 776  
-------
-+ Added "cookie-less" functionality and "allowUrlSessions" web.xml setting.  Help for restricted
-     browsers which disallow cookies.
-+ Added "expireWarnTime" setting and feature to pre-warn users of a soon to be expired pw
-
-* HTML look overhaul, now CSS based with graceful degradation for low-capability devices
-
-- Fixed a problem where email alerts are sent with a non-configurable from address (defect #27)
-- Problem where "challenge.caseInsensitive" setting was not honored (defect #28)
-- "Bad Session Proxy" error during forgotten password recovery (defect #26)
-- Placed a commented out text string on the change password jsp to show grace logins remaining (defect #23)
-
-v1.3.1 (unpublished)
-------
-+ Added "Shortcuts" servlet and "shortcut.query.[x]" settings
-* Added Shared Password History functionality and "password.sharedHistory.age" setting
-
-* Updated BerkelyDB version to v3.6.74 to correct a DB corruption bug
-* Removed duplicates from published wordlist files to speed up imports
-* Added a "Apache Derby" implementation of PwmDB
-
-- Fixed password change screen so the clear button resets the strength (defect #13)
-- Fixed password change screen so the clear button moves focus to the new
-    password field (defect #14)
-- Minor log file output enhancements
-- Fixed password change screen to show "too soon for password change" error correctly (defect #18)
-- Fixed password change screen to show "too soon for password change" requirement correctly (defect #18)
-
-v1.3.0 (1/14/2008)
-------
-+ PWM Responses are now case-insensitive (defect#1)
-+ can now read challenge set policy from Universal Password policy
-+ added "challenge.randomStyle" setting to change random behavior
-+ can set c/r min/max length in pwmServlet.properties local and i18n files
-+ localized nmas challenge policies are now supported
-+ PWM Admin status is now determined by ldap query string instead of configured uid/pwd
-+ rebuilt password wordlist checker, supports massive wordlists and doesn't
-    require gobs of memory (uses pluggable DB type, default is embedded berkelyDB),
-    tested with 20 million word dictionary.
-+ added animated gif to pwd sync wait page
-+ support for user-selectable random questions at population time
-+ added password.allowChange.queryMatch setting, to control which user populations
-     can use pwm for changing password.
-+ added UserDebugServlet for checking user password debug info
-+ all user-related log events now use syntax of '{1} event text [ip]' where {1} is a session
-    number, and [ip] is the user's IP/dns address.
-+ added a unique "instanceID" value to all headers, to track which server a user
-    is accessing when multiple servers exsist.
-+ updated ldapChai API to v0.4.1, moved all PWM c/r code into Chai.  Thus, there
-    is now a stand alone API available for manipulating PWM style c/r values in ldap.
-+ upgraded JavaMail API to fix some i18n issues with email.
-+ most URLs have been modified so "Servlet" isn't part of the name.  For example,
-    "ChangePasswordServlet" has been changed to "ChangePassword"
-+ added "strength" meter to password change screen
-
-* random password generation is wordlist based instead of purely random characters, also
-    one passsword is generated at a time instead of displaying a bunch to the user at once.
-* enhanced ajax password verification to improve reliability with different types of browsers
-* enhanced ajax password verification to clear "confirm password" field when "new password field" is modified.
-* added back capability to read NMAS challenge/response policies configured in eDirectory.
-* added nmas check for password history during ajax password verification
-* "debugMode" setting removed from pwmServlet.properties, replaced with log4jconfig.xml "trace" level.
-* XForwadedFor configuration now in web.xml, moved from pwmServlet.properties
-
-! SHA1 Responses are now stored with a random salt to prevent dictionary attacks against
-    response values in ldap
-! disallow "<.*script.*>" and other junk in input feilds to combat against xss attacks,
-    configurable in web.xml.
-! refactored many operations to use the user's ldap bind instead of the proxy user.
-
-
-v1.2.2 (6/20/2006) build 628
-------
-- Updated ldap api to prevent memory leaks
-- Fixed NPE while changing password when used with older eDirectory versions (pre 8.6)
-
-
-v1.2.1 (1/21/2006)
-------
-
-+ Optional NMAS functionality:
-    + Added PWMSHA1+NMAS option for challenge/response storage in nmas to challenge.storageMethod configuration.
-    + Added nmasChange option to passwordSetMethod
-    + Added nmas pre-change policy checker
-+ Encoded language files properly with native2ascii
-- Fixed several encoding issues, UTF-8 is now used for all web traffic
-- Fixed idletimer (missing javascript) method in v1.2.0 distribution
-- Updated to LDAP Chai v0.1.1 (failover fixes to prevent intruder lockout when using authUsingBind option)
-+ Added more configuration options
-+ Added buildNumber information to build process
-
-
-v1.2.0 (12/8/2005)
-------
-+ Integrated LDAP Chai v0.1.0 (Novell Forge LDAP library) (includes several fail-over and pooler fixes)
-- Codebase updated for Java 1.5 language specification
-+ added checkAll command to CommandServlet.
-+ password.MaximumOldPasswordChars option
-
-
-v1.1.4 (unpublished)
-------
-+ All user-viewable screens are now internationalized
-+ Added localizations for spanish, portugese, polish, italian, french, czech
-+ Added support for enforcing universal password password policy option during login
-+ Enhanced idletimer
-+ All HTML pages now use CSS properly, and all have the same borders and tables.
-
-
-v1.1.3 (9/30/2005)
-------
-- fixed thread locked wordList issues under heavy load
-* general enhancements for performance
-    - better ldap connection handling
-    - "proper" session timeout handling
-    - removed synchronization blocks for password checking
-    * vastly improved performance of while-you-type javascript password checker
-+ added Threads admin servlet to see the list of currently running JVM threads
-+ added Configuration admin servlet to view (partially) the running PWM config
-- made PWM more freindly to container restarts (memory leak issues)
-* remove "wordListCache" option from pwmConfig, wordLists are now always cached
-+ added thread list in the admin stats
-+ more stats: users created, users activated
-- bugfix: account sessions stats goes negative after restarting pwm container
-- bugfix: logging timestamps using minutes in place of month field
-+ added "passwordSetMethod" option pwmConfig
-~ refactored all logging code to make replacing/improving log4j easier
-+ removed scriptlets from most user-facing pages (replaced with taglibs)
-+ html/jsp cleanup.  Most pages pass w3c validation now
-+ added idle timeout countdown timer in status bar
-- fixed problem where smtp email errors would backlog in the queue forever
-+ added logout.jsp page (just for default, logout page should still be configured to something else)
-+ idle ldap connections are now closed quickly
-
-
-v1.1.2 (8/30/2005)
-------
-+ random passwords on changepassword.jsp are now cached in the session (performance boost)
-+ ip address intruder detection
-+ userDN intruder detection (seperate from edirectory intruder detection)
-- newuserservlet and general intruder code refactoring
-+ added newUser.writeAttributes option to write admin defined attributes during new user creation
-+ added show/hide button on change password screen (not available in IE due to IE bug)
-- per-keystroke javascript refactoring to improve reliability
-+ added admin-only section and configurable PWM admin username/password
-+ added intruder-lockout status screen
-+ added session monitor
-+ added web-based log monitor
-+ increased scalability of ldap connector
-+ added update attributes functionality (companion checkAttributes function to be added in future)
-+ newuser, activate user and update attribute forms now automatically generated.
-- loginContexts settings now are orderd properly
-
-
-v1.1.1 (8/5/2005)
-------
-- bugfix: "hint to long" error during setuphints
-! security: recover password would allow reset password for users with no responses set in some cases
-
-
-v1.1.0 (8/3/2005)
-------
-+ added support for reading universal password challenge/response sets from ldap
-+ added javascript client side caching for changepassword page
-+ added wordlist (dictionary) server-side caching
-+ added support for multiple hints
-+ added support for user defined hints
-+ added support for random hints
-+ hint answers are now stored using SHA1 hashes
-+ changed default color scheme to more 'novellish' color
-+ added per/user xml history log and viewing jsp
-+ new "activateuser" servlet replaces "validation" functionality in  "newuser" servlet.
-+ support for location (context) selection list on login and password recovery servlets for ldap trees with duplicate userIDs
-- additional bugfixes
-
-
-v1.0.7 (07/11/05)
-------
-- bugfix: tld.fmt caused jsp exceptions on tomcat 4.x
-- bugfix: null pointer when user's 'passwordMinimumLength' attr missing
-- bugfix: ldapchai: ldaps not working with > 1.4.1 jdk, also added more debug info to log
-
-
-v1.0.6 (07/06/05)
-------
-+ bugfix: Passwords with any of these chars: "*()?/" (excluding quotes) would not be able to auth using normal (non-bind) auth
-+ bugfix: no longer confused by aliases.  eDirectory user aliases are now completely ignored.
-+ added auto-suggest passwords on change password page.
-+ added ldap timeout setting
-
-v1.0.5 (unreleased)
-------
-+ added real time check-while-typing functionality for password validations
-+ renamed "nonAlphaNumeric" password rules to "special" to be in line with universal password policies
-+ added edirectory password policy reader, now will read per-user password policies from eDirectory (including universal password policies))
-+ added a pile of password rules to conform with universal password
-+ added regular expression checks for password rules
-+ new scheme for handling password recovery through ldap, should be more reliable
-
-v1.0.4 (06/13/05)
-------
-+ added externalPasswordMethods options
-+ modified pwmSchema.ldif
-+ On a clear day, you can refactor forever.....
-+ added passwordSyncMaxWaitTime options, PWM now waits for the password to be synchronized accross all known servers
-+ fixed bug where iChain SSO was dependent on ichain profile being configured using lowercase.
-+ enhanced iChain auth intergration
-
-
-v1.0.3 (05/19/2005)
-------
-+ fixed a bug where expireCheckDuringAuth didn't always work
-+ updated ant script and zip distribution so war file is buildable
-
-
-v1.0.2 (05/11/2005)
-------
-+ changed war packaging to remove compression to better support certain platforms (ie, NetWare)
-+ added "authUsingBind" configuration option
-+ changed default log4jconfig.xml to default logging to stdout
-+ added footer.jsp
-+ added auto-text for password rules on change password pages
-+ added "expireCheckDuringAuth" configuration option
-- removed unused "expirePauseTime" configuration option
-~ expiremental code for pre-expire password email notifications
-
-
-v1.0.1 (04/06/2005)
-------
-first public release of PWM.

+ 0 - 8
supplemental/readme.txt

@@ -1,8 +0,0 @@
-PWM Readme
-----------
-
-PWM is an open source Password Self Service application.   Instructions, downloads, and project information
-can be found at the PWM Project website:
-
-http://code.google.com/p/pwm
-

+ 0 - 89
supplemental/script/missing_translation.sh

@@ -1,89 +0,0 @@
-#!/bin/bash
-#
-# Password Management Servlets (PWM)
-# http://code.google.com/p/pwm/
-#
-# Copyright (c) 2011 The PWM Project
-#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation; either version 2 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
-#
-
-function usage() {
-  echo "This script can be used on a modern unix like environment in order to"
-  echo "determine the missing strings in localisation property files."
-  echo
-  echo "Usage:"
-  echo "  $0 <path to tomcat>"
-  echo 
-  echo "<path to tomcat> is the path to the root of your tomcat instance."
-  echo
-  echo "A file will be created in the current directory for each language"
-  echo "/property file combination containing a listing of the variables"
-  echo "with a missing translation."
-  echo
-  echo "Example:"
-  echo "  $0 /usr/share/tomcat6"
-}
-
-
-if [ "$1" = "" ]; then
-  usage
-  exit 1
-fi
-
-
-FILEPATH="${1}/webapps/pwm/WEB-INF/classes/password/pwm/config"
-
-shopt -s nullglob
-
-for i in Display Message PwmError
-
-do
-  echo $i
-  if [ $i == "PwmError" ]; then
-    FILEPATH="${1}/webapps/pwm/WEB-INF/classes/password/pwm/error"
-  fi
-
-  for f in $FILEPATH/$i\_* 
-  do
-    echo Processing: $f
-
-    OUTPUTFILE="Missing_`basename $f`"
-	if [ -f "${OUTPUTFILE}" ]; then rm "${OUTPUTFILE}" ; fi
-
-    TMP="Display`date +%s`"
-    TFBASE="/tmp/__${TMP}.properties"
-    TFLANG="/tmp/__${TMP}_`basename $f`"
-    TFMISSING="/tmp/__${TMP}_Missing_`basename $f`"
-
-    grep -v "^[[:space:]]*$" "${FILEPATH}/${i}.properties" | grep -v "^#" | grep "^[[:alpha:]]" | cut -d '=' -f 1 | sort -u > "${TFBASE}"
-    grep -v "^[[:space:]]*$" "${f}" | grep -v "^#" | grep "^[[:alpha:]]" | cut -d '=' -f 1 | sort -u > "${TFLANG}"
-    diff -w "${TFLANG}" "${TFBASE}" | grep "^>" | cut -d " " -f2- > $TFMISSING
-
-    while read line; do
-      grep "$line" "${FILEPATH}/${i}.properties" >> $OUTPUTFILE
-    done < "$TFMISSING"
-
-
-    if [ -f "${TFBASE}" ]; then rm "${TFBASE}" ; fi
-    if [ -f "${TFLANG}" ]; then rm "${TFLANG}" ; fi
-    if [ -f "${TFMISSING}" ]; then rm "${TFMISSING}" ; fi
-
-  done
-done
-
-shopt -u nullglob
-
-exit 0

Some files were not shown because too many files changed in this diff