Browse Source

Merge remote-tracking branch 'origin/master'

rkeil 8 years ago
parent
commit
5470a67178
69 changed files with 1020 additions and 929 deletions
  1. 2 2
      README.md
  2. 12 1
      pom.xml
  3. 8 2
      src/main/java/password/pwm/AppProperty.java
  4. 3 0
      src/main/java/password/pwm/PwmAboutProperty.java
  5. 2 17
      src/main/java/password/pwm/PwmConstants.java
  6. 2 2
      src/main/java/password/pwm/PwmEnvironment.java
  7. 10 47
      src/main/java/password/pwm/bean/EmailItemBean.java
  8. 4 23
      src/main/java/password/pwm/bean/FormNonce.java
  9. 5 13
      src/main/java/password/pwm/bean/PrivateKeyCertificate.java
  10. 11 34
      src/main/java/password/pwm/bean/SessionLabel.java
  11. 6 24
      src/main/java/password/pwm/bean/SmsItemBean.java
  12. 12 56
      src/main/java/password/pwm/bean/StatsPublishBean.java
  13. 17 15
      src/main/java/password/pwm/bean/pub/PublicUserInfoBean.java
  14. 24 1
      src/main/java/password/pwm/config/FormConfiguration.java
  15. 1 1
      src/main/java/password/pwm/config/PwmSettingXml.java
  16. 3 3
      src/main/java/password/pwm/config/stored/NGStorageEngineImpl.java
  17. 5 6
      src/main/java/password/pwm/config/stored/NGStoredConfiguration.java
  18. 5 6
      src/main/java/password/pwm/config/stored/NGStoredConfigurationFactory.java
  19. 2 2
      src/main/java/password/pwm/config/stored/StoredConfiguration.java
  20. 15 14
      src/main/java/password/pwm/config/stored/StoredConfigurationImpl.java
  21. 4 4
      src/main/java/password/pwm/config/stored/ValueMetaData.java
  22. 5 5
      src/main/java/password/pwm/health/ApplianceStatusChecker.java
  23. 4 3
      src/main/java/password/pwm/health/ConfigurationChecker.java
  24. 19 18
      src/main/java/password/pwm/health/LDAPStatusChecker.java
  25. 7 1
      src/main/java/password/pwm/http/filter/RequestInitializationFilter.java
  26. 3 2
      src/main/java/password/pwm/http/servlet/changepw/ChangePasswordServlet.java
  27. 2 2
      src/main/java/password/pwm/http/servlet/configmanager/ConfigManagerServlet.java
  28. 12 8
      src/main/java/password/pwm/http/servlet/configmanager/DebugItemGenerator.java
  29. 9 0
      src/main/java/password/pwm/http/servlet/forgottenpw/ForgottenPasswordServlet.java
  30. 6 6
      src/main/java/password/pwm/http/servlet/forgottenpw/ForgottenPasswordUtil.java
  31. 0 2
      src/main/java/password/pwm/http/servlet/peoplesearch/PeopleSearchServlet.java
  32. 8 27
      src/main/java/password/pwm/http/tag/conditional/PwmIfOptions.java
  33. 1 1
      src/main/java/password/pwm/http/tag/conditional/PwmIfTag.java
  34. 62 16
      src/main/java/password/pwm/ldap/LdapConnectionService.java
  35. 26 10
      src/main/java/password/pwm/ldap/auth/LDAPAuthenticationRequest.java
  36. 2 2
      src/main/java/password/pwm/ldap/search/SearchConfiguration.java
  37. 1 1
      src/main/java/password/pwm/svc/pwnotify/PasswordExpireNotificationEngine.java
  38. 33 32
      src/main/java/password/pwm/svc/report/ReportService.java
  39. 45 12
      src/main/java/password/pwm/svc/token/CryptoTokenMachine.java
  40. 0 91
      src/main/java/password/pwm/svc/token/DBTokenMachine.java
  41. 149 0
      src/main/java/password/pwm/svc/token/DataStoreTokenMachine.java
  42. 23 19
      src/main/java/password/pwm/svc/token/LdapTokenMachine.java
  43. 0 93
      src/main/java/password/pwm/svc/token/LocalDBTokenMachine.java
  44. 70 0
      src/main/java/password/pwm/svc/token/StoredTokenKey.java
  45. 27 0
      src/main/java/password/pwm/svc/token/TokenKey.java
  46. 8 9
      src/main/java/password/pwm/svc/token/TokenMachine.java
  47. 17 23
      src/main/java/password/pwm/svc/token/TokenPayload.java
  48. 83 121
      src/main/java/password/pwm/svc/token/TokenService.java
  49. 1 1
      src/main/java/password/pwm/util/Validator.java
  50. 2 2
      src/main/java/password/pwm/util/cli/commands/ConfigLockCommand.java
  51. 2 2
      src/main/java/password/pwm/util/cli/commands/ConfigResetHttpsCommand.java
  52. 2 2
      src/main/java/password/pwm/util/cli/commands/ConfigSetPasswordCommand.java
  53. 2 2
      src/main/java/password/pwm/util/cli/commands/ConfigUnlockCommand.java
  54. 2 2
      src/main/java/password/pwm/util/cli/commands/ImportHttpsKeyStoreCommand.java
  55. 2 2
      src/main/java/password/pwm/util/cli/commands/TokenInfoCommand.java
  56. 94 1
      src/main/java/password/pwm/util/java/JavaHelper.java
  57. 1 1
      src/main/java/password/pwm/util/localdb/LocalDB.java
  58. 60 82
      src/main/java/password/pwm/util/localdb/Xodus_LocalDB.java
  59. 14 2
      src/main/java/password/pwm/util/operations/cr/NMASCrOperator.java
  60. 7 5
      src/main/java/password/pwm/util/queue/SmsQueueManager.java
  61. 11 1
      src/main/java/password/pwm/ws/server/RestServerHelper.java
  62. 1 1
      src/main/java/password/pwm/ws/server/rest/RestRandomPasswordServer.java
  63. 8 3
      src/main/resources/password/pwm/AppProperty.properties
  64. 23 31
      src/main/webapp/WEB-INF/jsp/admin-dashboard.jsp
  65. 1 1
      src/main/webapp/WEB-INF/jsp/admin-tokenlookup.jsp
  66. 4 4
      src/main/webapp/WEB-INF/jsp/admin-user-debug.jsp
  67. 4 4
      src/main/webapp/WEB-INF/jsp/fragment/token-form-field.jsp
  68. 4 2
      src/main/webapp/WEB-INF/jsp/updateprofile-confirm.jsp
  69. 0 1
      src/main/webapp/public/resources/js/main.js

+ 2 - 2
README.md

@@ -7,8 +7,8 @@ Official project page is at [https://github.com/pwm-project/pwm/](https://github
 # Links
 * [PWM-General Google Group](https://groups.google.com/group/pwm-general) - please ask for assistance here first.
 * [PWM Documentation Wiki](https://github.com/pwm-project/pwm/wiki) - Home for PWM documentation
-* [Current Builds](http://www.pwm-project.org/artifacts/pwm/) - Current downloads built from recent github project commits
-* [PWM Reference](http://www.pwm-project.org/pwm/public/reference/) - Reference documentation built into PWM.
+* [Current Builds](https://www.pwm-project.org/artifacts/pwm/) - Current downloads built from recent github project commits
+* [PWM Reference](https://www.pwm-project.org/pwm/public/reference/) - Reference documentation built into PWM.
 
 Features
 * Web based configuration manager with over 400 configurable settings

+ 12 - 1
pom.xml

@@ -22,7 +22,7 @@
     </licenses>
 
     <organization>
-        <name>Pwm Project</name>
+        <name>PWM Project</name>
         <url>http://www.pwm-project.org</url>
     </organization>
 
@@ -176,6 +176,17 @@
                 <configuration>
                     <archiveClasses>true</archiveClasses>
                     <packagingExcludes>WEB-INF/classes</packagingExcludes>
+                    <archive>
+                        <manifestEntries>
+                            <Implementation-Title>${project.name}</Implementation-Title>
+                            <Implementation-Version>${project.version}</Implementation-Version>
+                            <Implementation-Vendor>${project.organization.name}</Implementation-Vendor>
+                            <Implementation-URL>${project.organization.url}</Implementation-URL>
+                            <Implementation-Build>${build.number}</Implementation-Build>
+                            <Implementation-Revision>${build.revision}</Implementation-Revision>
+                            <Implementation-Version-Display>v${project.version} b${build.number} r${build.revision}</Implementation-Version-Display>
+                        </manifestEntries>
+                    </archive>
                 </configuration>
             </plugin>
             <plugin>

+ 8 - 2
src/main/java/password/pwm/AppProperty.java

@@ -172,6 +172,8 @@ public enum     AppProperty {
     LDAP_CACHE_USER_GUID_ENABLE                     ("ldap.cache.userGuid.enable"),
     LDAP_CACHE_USER_GUID_SECONDS                    ("ldap.cache.userGuid.seconds"),
     LDAP_CHAI_SETTINGS                              ("ldap.chaiSettings"),
+    LDAP_PROXY_CONNECTION_PER_PROFILE               ("ldap.proxy.connectionsPerProfile"),
+    LDAP_PROXY_MAX_CONNECTIONS                      ("ldap.proxy.maxConnections"),
     LDAP_EXTENSIONS_NMAS_ENABLE                     ("ldap.extensions.nmas.enable"),
     LDAP_CONNECTION_TIMEOUT                         ("ldap.connection.timeoutMS"),
     LDAP_PROFILE_RETRY_DELAY                        ("ldap.profile.retryDelayMS"),
@@ -187,6 +189,7 @@ public enum     AppProperty {
     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"),
+    LDAP_ORACLE_POST_TEMPPW_USE_CURRENT_TIME        ("ldap.oracle.postTempPasswordUseCurrentTime"),
     LOGGING_PATTERN                                 ("logging.pattern"),
     LOGGING_FILE_MAX_SIZE                           ("logging.file.maxSize"),
     LOGGING_FILE_MAX_ROLLOVER                       ("logging.file.maxRollover"),
@@ -198,6 +201,7 @@ public enum     AppProperty {
     NMAS_THREADS_MIN_SECONDS                        ("nmas.threads.minSeconds"),
     NMAS_THREADS_MAX_SECONDS                        ("nmas.threads.maxSeconds"),
     NMAS_THREADS_WATCHDOG_FREQUENCY                 ("nmas.threads.watchdogFrequencyMs"),
+    NMAS_THREADS_WATCHDOG_DEBUG                     ("nmas.threads.watchdogDebug"),
     NMAS_IGNORE_NMASCR_DURING_FORCECHECK            ("nmas.ignoreNmasCrDuringForceSetupCheck"),
     NMAS_USE_LOCAL_SASL_FACTORY                     ("nmas.useLocalSaslFactory"),
     NMAS_FORCE_SASL_FACTORY_REGISTRATION            ("nmas.forceSaslFactoryRegistration"),
@@ -247,6 +251,7 @@ public enum     AppProperty {
     SECURITY_HTTP_STRIP_HEADER_REGEX                ("security.http.stripHeaderRegex"),
     SECURITY_HTTP_PERFORM_CSRF_HEADER_CHECKS        ("security.http.performCsrfHeaderChecks"),
     SECURITY_HTTP_PROMISCUOUS_ENABLE                ("security.http.promiscuousEnable"),
+    SECURITY_HTTP_CONFIG_CSP_HEADER                 ("security.http.config.cspHeader"),
     SECURITY_HTTPSSERVER_SELF_FUTURESECONDS         ("security.httpsServer.selfCert.futureSeconds"),
     SECURITY_HTTPSSERVER_SELF_ALG                   ("security.httpsServer.selfCert.alg"),
     SECURITY_HTTPSSERVER_SELF_KEY_SIZE              ("security.httpsServer.selfCert.keySize"),
@@ -267,11 +272,12 @@ public enum     AppProperty {
     SECURITY_DEFAULT_EPHEMERAL_HASH_ALG             ("security.defaultEphemeralHashAlg"),
     SEEDLIST_BUILTIN_PATH                           ("seedlist.builtin.path"),
     SMTP_SUBJECT_ENCODING_CHARSET                   ("smtp.subjectEncodingCharset"),
-    TOKEN_REMOVAL_DELAY_MS                          ("token.removalDelayMS"),
-    TOKEN_PURGE_BATCH_SIZE                          ("token.purgeBatchSize"),
     TOKEN_MAX_UNIQUE_CREATE_ATTEMPTS                ("token.maxUniqueCreateAttempts"),
     TOKEN_RESEND_ENABLED                            ("token.resend.enabled"),
     TOKEN_RESEND_DELAY_MS                           ("token.resend.delayMS"),
+    TOKEN_REMOVE_ON_CLAIM                           ("token.removeOnClaim"),
+    TOKEN_VERIFY_PW_MODIFY_TIME                     ("token.verifyPwModifyTime"),
+
 
     /** Regular expression to be used for matching URLs to be shortened by the URL Shortening Service Class. */
     URL_SHORTNER_URL_REGEX                          ("urlshortener.url.regex"),

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

@@ -31,6 +31,7 @@ import password.pwm.util.java.StringUtil;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.secure.PwmRandom;
 
+import java.nio.charset.Charset;
 import java.time.Instant;
 import java.util.Collections;
 import java.util.Date;
@@ -90,6 +91,7 @@ public enum PwmAboutProperty {
     java_osName,
     java_osVersion,
     java_randomAlgorithm,
+    java_defaultCharset,
 
     database_driverName,
     database_driverVersion,
@@ -182,6 +184,7 @@ public enum PwmAboutProperty {
             aboutMap.put(java_osName,              System.getProperty("os.name"));
             aboutMap.put(java_osVersion,           System.getProperty("os.version"));
             aboutMap.put(java_randomAlgorithm,     PwmRandom.getInstance().getAlgorithm());
+            aboutMap.put(java_defaultCharset,      Charset.defaultCharset().name());
         }
 
         { // build info

+ 2 - 17
src/main/java/password/pwm/PwmConstants.java

@@ -23,18 +23,15 @@
 package password.pwm;
 
 import org.apache.commons.csv.CSVFormat;
-import password.pwm.bean.SessionLabel;
 import password.pwm.util.java.JsonUtil;
 import password.pwm.util.secure.PwmBlockAlgorithm;
 import password.pwm.util.secure.PwmHashAlgorithm;
 
 import java.nio.charset.Charset;
-import java.text.DateFormat;
-import java.text.SimpleDateFormat;
+import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
-import java.util.Date;
 import java.util.List;
 import java.util.Locale;
 import java.util.ResourceBundle;
@@ -49,7 +46,7 @@ public abstract class PwmConstants {
 // ------------------------------ FIELDS ------------------------------
 
     // ------------------------- PUBLIC CONSTANTS -------------------------
-    public static final String BUILD_TIME           = readBuildInfoBundle("build.time",SimpleDateFormat.getDateTimeInstance().format(new Date()));
+    public static final String BUILD_TIME           = readBuildInfoBundle("build.time", Instant.now().toString());
     public static final String BUILD_NUMBER         = readBuildInfoBundle("build.number","0");
     public static final String BUILD_TYPE           = readBuildInfoBundle("build.type","");
     public static final String BUILD_USER           = readBuildInfoBundle("build.user",System.getProperty("user.name"));
@@ -96,11 +93,6 @@ public abstract class PwmConstants {
 
     public static final String DEFAULT_DATETIME_FORMAT_STR = readPwmConstantsBundle("locale.defaultDateTimeFormat");
     public static final TimeZone DEFAULT_TIMEZONE = TimeZone.getTimeZone(readPwmConstantsBundle("locale.defaultTimeZone"));
-    public static final DateFormat DEFAULT_DATETIME_FORMAT = new SimpleDateFormat(DEFAULT_DATETIME_FORMAT_STR, DEFAULT_LOCALE);
-
-    static {
-        DEFAULT_DATETIME_FORMAT.setTimeZone(DEFAULT_TIMEZONE);
-    }
 
     public static final String APPLICATION_PATH_INFO_FILE = readPwmConstantsBundle("applicationPathInfoFile");
 
@@ -109,13 +101,6 @@ public abstract class PwmConstants {
     public static final int TRIAL_MAX_AUTHENTICATIONS = 100;
     public static final int TRIAL_MAX_TOTAL_AUTH = 10000;
 
-    private static final String SESSION_LABEL_SESSION_ID = "#";
-    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 SessionLabel PW_EXP_NOTICE_LABEL = new SessionLabel(SESSION_LABEL_SESSION_ID ,null,"pwExpireNotice",null,null);
-
 
     public static final int DATABASE_ACCESSOR_KEY_LENGTH = Integer.parseInt(readPwmConstantsBundle("databaseAccessor.keyLength"));
 

+ 2 - 2
src/main/java/password/pwm/PwmEnvironment.java

@@ -157,8 +157,8 @@ public class PwmEnvironment {
         this.internalRuntimeInstance = internalRuntimeInstance;
         this.configurationFile = configurationFile;
         this.contextManager = contextManager;
-        this.flags = flags == null ? Collections.<ApplicationFlag>emptySet() : Collections.unmodifiableSet(new HashSet<>(flags));
-        this.parameters = parameters == null ? Collections.<ApplicationParameter, String>emptyMap() : Collections.unmodifiableMap(parameters);
+        this.flags = flags == null ? Collections.emptySet() : Collections.unmodifiableSet(new HashSet<>(flags));
+        this.parameters = parameters == null ? Collections.emptyMap() : Collections.unmodifiableMap(parameters);
 
         this.fileLocker = new FileLocker();
 

+ 10 - 47
src/main/java/password/pwm/bean/EmailItemBean.java

@@ -22,60 +22,23 @@
 
 package password.pwm.bean;
 
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
 import java.io.Serializable;
 
+@Getter
+@AllArgsConstructor
 public class EmailItemBean implements Serializable {
-    private String to;
-    private String from;
-    private String subject;
-    private String bodyPlain;
-    private String bodyHtml;
-
-
-    // --------------------------- CONSTRUCTORS ---------------------------
-    private EmailItemBean() {
-    }
-
-    public EmailItemBean(
-            final String to,
-            final String from,
-            final String subject,
-            final String bodyPlain,
-            final String bodyHtml
-    ) {
-        this.to = to;
-        this.from = from;
-        this.subject = subject;
-        this.bodyPlain = bodyPlain;
-        this.bodyHtml = bodyHtml;
-    }
-
-// --------------------- GETTER / SETTER METHODS ---------------------
-
-    public String getBodyPlain() {
-        return bodyPlain;
-    }
-
-    public String getFrom() {
-        return from;
-    }
-
-    public String getSubject() {
-        return subject;
-    }
-
-    public String getTo() {
-        return to;
-    }
-
-    public String getBodyHtml() {
-        return bodyHtml;
-    }
+    private final String to;
+    private final String from;
+    private final String subject;
+    private final String bodyPlain;
+    private final String bodyHtml;
 
     public String toString() {
         final StringBuilder sb = new StringBuilder();
         sb.append("from: ").append(from).append(", to: ").append(to).append(", subject: ").append(subject);
         return sb.toString();
     }
-
 }

+ 4 - 23
src/main/java/password/pwm/bean/FormNonce.java

@@ -23,10 +23,14 @@
 package password.pwm.bean;
 
 import com.google.gson.annotations.SerializedName;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
 
 import java.io.Serializable;
 import java.time.Instant;
 
+@Getter
+@AllArgsConstructor
 public class FormNonce implements Serializable {
 
     @SerializedName("g")
@@ -40,27 +44,4 @@ public class FormNonce implements Serializable {
 
     @SerializedName("p")
     private final String payload;
-
-    public FormNonce(final String sessionGUID, final Instant timestamp, final int reqCounter, final String payload) {
-        this.sessionGUID = sessionGUID;
-        this.timestamp = timestamp;
-        this.reqCounter = reqCounter;
-        this.payload = payload;
-    }
-
-    public String getSessionGUID() {
-        return sessionGUID;
-    }
-
-    public Instant getTimestamp() {
-        return timestamp;
-    }
-
-    public int getRequestID() {
-        return reqCounter;
-    }
-
-    public String getPayload() {
-        return payload;
-    }
 }

+ 5 - 13
src/main/java/password/pwm/bean/PrivateKeyCertificate.java

@@ -22,24 +22,16 @@
 
 package password.pwm.bean;
 
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
 import java.io.Serializable;
 import java.security.PrivateKey;
 import java.security.cert.X509Certificate;
 
+@Getter
+@AllArgsConstructor
 public class PrivateKeyCertificate implements Serializable {
     private final X509Certificate[] certificates;
     private final PrivateKey key;
-
-    public PrivateKeyCertificate(final X509Certificate[] certificates, final PrivateKey key) {
-        this.certificates = certificates;
-        this.key = key;
-    }
-
-    public X509Certificate[] getCertificates() {
-        return certificates;
-    }
-
-    public PrivateKey getKey() {
-        return key;
-    }
 }

+ 11 - 34
src/main/java/password/pwm/bean/SessionLabel.java

@@ -22,10 +22,21 @@
 
 package password.pwm.bean;
 
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
 import java.io.Serializable;
 
+@Getter
+@AllArgsConstructor
 public class SessionLabel implements Serializable {
     public static final SessionLabel SYSTEM_LABEL = null;
+    public static final String SESSION_LABEL_SESSION_ID = "#";
+    public static final SessionLabel PW_EXP_NOTICE_LABEL = new SessionLabel(SESSION_LABEL_SESSION_ID ,null,"pwExpireNotice",null,null);
+    public static final SessionLabel TOKEN_SESSION_LABEL = new SessionLabel(SESSION_LABEL_SESSION_ID ,null,"token",null,null);
+    public static final SessionLabel CLI_SESSION_LABEL= new SessionLabel(SESSION_LABEL_SESSION_ID ,null,"cli",null,null);
+    public static final SessionLabel HEALTH_SESSION_LABEL = new SessionLabel(SESSION_LABEL_SESSION_ID ,null,"health",null,null);
+    public static final SessionLabel REPORTING_SESSION_LABEL = new SessionLabel(SESSION_LABEL_SESSION_ID ,null,"reporting",null,null);
 
     private final String sessionID;
     private final UserIdentity userIdentity;
@@ -33,40 +44,6 @@ public class SessionLabel implements Serializable {
     private final String srcAddress;
     private final String srcHostname;
 
-    public SessionLabel(final String sessionID, final UserIdentity userIdentity, final String username, final String srcAddress, final String srcHostname)
-    {
-        this.sessionID = sessionID;
-        this.userIdentity = userIdentity;
-        this.username = username;
-        this.srcAddress = srcAddress;
-        this.srcHostname = srcHostname;
-    }
-
-    public String getSessionID()
-    {
-        return sessionID;
-    }
-
-    public String getUsername()
-    {
-        return username;
-    }
-
-    public UserIdentity getUserIdentity()
-    {
-        return userIdentity;
-    }
-
-    public String getSrcAddress()
-    {
-        return srcAddress;
-    }
-
-    public String getSrcHostname()
-    {
-        return srcHostname;
-    }
-    
     public String toString() {
         if (this.getSessionID() == null || this.getSessionID().isEmpty()) {
             return "";

+ 6 - 24
src/main/java/password/pwm/bean/SmsItemBean.java

@@ -23,37 +23,19 @@
 package password.pwm.bean;
 
 
+import lombok.AllArgsConstructor;
+import lombok.Getter;
 import password.pwm.util.java.JsonUtil;
 
 import java.io.Serializable;
 
+@Getter
+@AllArgsConstructor
 public class SmsItemBean implements Serializable {
-    private String to;
-    private String message;
+    private final String to;
+    private final String message;
 
-    // --------------------------- CONSTRUCTORS ---------------------------
-    public SmsItemBean(
-            final String to,
-            final String message
-    ) {
-        this.to = to;
-        this.message = message;
-    }
-
-// --------------------- GETTER / SETTER METHODS ---------------------
-
-    public String getMessage() {
-        return message;
-    }
 
-    public void setMessage(final String message) {
-        this.message = message;
-    }
-
-    public String getTo() {
-        return to;
-    }
-    
     public String toString() {
         return "SMS Item: " + JsonUtil.serialize(this);
     }

+ 12 - 56
src/main/java/password/pwm/bean/StatsPublishBean.java

@@ -22,19 +22,24 @@
 
 package password.pwm.bean;
 
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
 import java.io.Serializable;
 import java.time.Instant;
 import java.util.List;
 import java.util.Map;
 
+@Getter
+@AllArgsConstructor
 public class StatsPublishBean implements Serializable {
-    private String instanceID;
-    private Instant timestamp;
-    private Map<String,String> totalStatistics;
-    private List<String> configuredSettings;
-    private String versionBuild;
-    private String versionVersion;
-    private Map<String,String> otherInfo;
+    private final String instanceID;
+    private final Instant timestamp;
+    private final Map<String,String> totalStatistics;
+    private final List<String> configuredSettings;
+    private final String versionBuild;
+    private final String versionVersion;
+    private final Map<String,String> otherInfo;
 
     public enum KEYS {
         SITE_URL,
@@ -42,53 +47,4 @@ public class StatsPublishBean implements Serializable {
         INSTALL_DATE,
         LDAP_VENDOR
     }
-
-    public StatsPublishBean() {
-    }
-
-    public StatsPublishBean(
-            final String instanceID,
-            final Instant timestamp,
-            final Map<String, String> totalStatistics,
-            final List<String> configuredSettings,
-            final String versionBuild,
-            final String versionVersion,
-            final Map<String,String> otherInfo
-    ) {
-        this.instanceID = instanceID;
-        this.timestamp = timestamp;
-        this.totalStatistics = totalStatistics;
-        this.configuredSettings = configuredSettings;
-        this.versionBuild = versionBuild;
-        this.versionVersion = versionVersion;
-        this.otherInfo = otherInfo;
-    }
-
-    public String getInstanceID() {
-        return instanceID;
-    }
-
-    public Instant getTimestamp() {
-        return timestamp;
-    }
-
-    public Map<String, String> getTotalStatistics() {
-        return totalStatistics;
-    }
-
-    public List<String> getConfiguredSettings() {
-        return configuredSettings;
-    }
-
-    public String getVersionBuild() {
-        return versionBuild;
-    }
-
-    public String getVersionVersion() {
-        return versionVersion;
-    }
-
-    public Map<String, String> getOtherInfo() {
-        return otherInfo;
-    }
 }

+ 17 - 15
src/main/java/password/pwm/bean/pub/PublicUserInfoBean.java

@@ -22,6 +22,7 @@
 
 package password.pwm.bean.pub;
 
+import lombok.Getter;
 import password.pwm.bean.PasswordStatus;
 import password.pwm.bean.UserInfoBean;
 import password.pwm.config.Configuration;
@@ -37,22 +38,23 @@ import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 
+@Getter
 public class PublicUserInfoBean implements Serializable {
-    public String userDN;
-    public String ldapProfile;
-    public String userID;
-    public String userEmailAddress;
-    public Instant passwordExpirationTime;
-    public Instant passwordLastModifiedTime;
-    public boolean requiresNewPassword;
-    public boolean requiresResponseConfig;
-    public boolean requiresUpdateProfile;
-    public boolean requiresInteraction;
+    private String userDN;
+    private String ldapProfile;
+    private String userID;
+    private String userEmailAddress;
+    private Instant passwordExpirationTime;
+    private Instant passwordLastModifiedTime;
+    private boolean requiresNewPassword;
+    private boolean requiresResponseConfig;
+    private boolean requiresUpdateProfile;
+    private boolean requiresInteraction;
 
-    public PasswordStatus passwordStatus;
-    public Map<String, String> passwordPolicy;
-    public List<String> passwordRules;
-    public Map<String, String> attributes;
+    private PasswordStatus passwordStatus;
+    private Map<String, String> passwordPolicy;
+    private List<String> passwordRules;
+    private Map<String, String> attributes;
 
     public static PublicUserInfoBean fromUserInfoBean(final UserInfoBean userInfoBean, final Configuration config, final Locale locale, final MacroMachine macroMachine) {
         final PublicUserInfoBean publicUserInfoBean = new PublicUserInfoBean();
@@ -66,7 +68,7 @@ public class PublicUserInfoBean implements Serializable {
 
         publicUserInfoBean.requiresNewPassword = userInfoBean.isRequiresNewPassword();
         publicUserInfoBean.requiresResponseConfig = userInfoBean.isRequiresResponseConfig();
-        publicUserInfoBean.requiresUpdateProfile = userInfoBean.isRequiresResponseConfig();
+        publicUserInfoBean.requiresUpdateProfile = userInfoBean.isRequiresUpdateProfile();
         publicUserInfoBean.requiresInteraction = userInfoBean.isRequiresNewPassword()
                 || userInfoBean.isRequiresResponseConfig()
                 || userInfoBean.isRequiresUpdateProfile()

+ 24 - 1
src/main/java/password/pwm/config/FormConfiguration.java

@@ -29,8 +29,10 @@ import password.pwm.error.PwmDataValidationException;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmOperationalException;
 import password.pwm.error.PwmUnrecoverableException;
-import password.pwm.util.java.JsonUtil;
+import password.pwm.i18n.Display;
 import password.pwm.util.LocaleHelper;
+import password.pwm.util.java.JsonUtil;
+import password.pwm.util.java.StringUtil;
 
 import java.io.Serializable;
 import java.math.BigInteger;
@@ -345,4 +347,25 @@ public class FormConfiguration implements Serializable {
         final Matcher matcher = pattern.matcher(address);
         return matcher.matches();
     }
+
+    public String displayValue(final String value, final Locale locale, final Configuration config) {
+        if (value == null) {
+            return LocaleHelper.getLocalizedMessage(locale, Display.Value_NotApplicable, config);
+        }
+
+        if (this.getType() == Type.select) {
+            if (this.getSelectOptions() != null) {
+                for (final String key : selectOptions.keySet()) {
+                    if (value.equals(key)) {
+                        final String displayValue = selectOptions.get(key);
+                        if (!StringUtil.isEmpty(displayValue)) {
+                            return displayValue;
+                        }
+                    }
+                }
+            }
+        }
+
+        return value;
+    }
 }

+ 1 - 1
src/main/java/password/pwm/config/PwmSettingXml.java

@@ -74,7 +74,7 @@ public class PwmSettingXml {
                         xmlDocCache = null;
                     }
                 };
-                t.setDaemon(false);
+                t.setDaemon(true);
                 t.start();
 
                 return newDoc;

+ 3 - 3
src/main/java/password/pwm/config/stored/NGStorageEngineImpl.java

@@ -25,7 +25,7 @@ package password.pwm.config.stored;
 import password.pwm.bean.UserIdentity;
 import password.pwm.config.StoredValue;
 
-import java.util.Date;
+import java.time.Instant;
 import java.util.Map;
 import java.util.TreeMap;
 import java.util.concurrent.locks.ReentrantReadWriteLock;
@@ -75,7 +75,7 @@ class NGStorageEngineImpl implements StorageEngine {
                 changeLog.updateChangeLog(reference, value);
             }
             values.put(reference, value);
-            final ValueMetaData valueMetaData = new ValueMetaData(new Date(), userIdentity);
+            final ValueMetaData valueMetaData = new ValueMetaData(Instant.now(), userIdentity);
             metaValues.put(reference, valueMetaData);
         } finally {
             bigLock.writeLock().unlock();
@@ -94,7 +94,7 @@ class NGStorageEngineImpl implements StorageEngine {
             }
             values.remove(reference);
             if (metaValues.containsKey(reference)) {
-                final ValueMetaData valueMetaData = new ValueMetaData(new Date(), userIdentity);
+                final ValueMetaData valueMetaData = new ValueMetaData(Instant.now(), userIdentity);
                 metaValues.put(reference, valueMetaData);
             }
         } finally {

+ 5 - 6
src/main/java/password/pwm/config/stored/NGStoredConfiguration.java

@@ -22,18 +22,17 @@
 
 package password.pwm.config.stored;
 
-import password.pwm.PwmConstants;
 import password.pwm.bean.UserIdentity;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.PwmSettingCategory;
 import password.pwm.config.StoredValue;
 import password.pwm.config.value.StringValue;
 import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.util.java.JavaHelper;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.secure.PwmSecurityKey;
 
-import java.text.ParseException;
-import java.util.Date;
+import java.time.Instant;
 import java.util.Map;
 
 class NGStoredConfiguration implements StoredConfiguration {
@@ -164,12 +163,12 @@ class NGStoredConfiguration implements StoredConfiguration {
         return engine.readMetaData(storedConfigReference);
     }
 
-    public Date modifyTime() {
+    public Instant modifyTime() {
         final String modifyTimeString = readConfigProperty(ConfigurationProperty.MODIFIFICATION_TIMESTAMP);
         if (modifyTimeString != null) {
             try {
-                return PwmConstants.DEFAULT_DATETIME_FORMAT.parse(modifyTimeString);
-            } catch (ParseException e) {
+                return JavaHelper.parseIsoToInstant((modifyTimeString));
+            } catch (Exception e) {
                 LOGGER.error("error parsing last modified timestamp property: " + e.getMessage());
             }
         }

+ 5 - 6
src/main/java/password/pwm/config/stored/NGStoredConfigurationFactory.java

@@ -24,7 +24,6 @@ package password.pwm.config.stored;
 
 import org.jdom2.Document;
 import org.jdom2.Element;
-import password.pwm.PwmConstants;
 import password.pwm.bean.UserIdentity;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.StoredValue;
@@ -32,14 +31,14 @@ import password.pwm.config.value.StringValue;
 import password.pwm.config.value.ValueFactory;
 import password.pwm.error.PwmException;
 import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.XmlUtil;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.secure.PwmSecurityKey;
 
 import java.io.InputStream;
 import java.io.OutputStream;
-import java.text.ParseException;
-import java.util.Date;
+import java.time.Instant;
 import java.util.LinkedHashMap;
 import java.util.Map;
 
@@ -204,12 +203,12 @@ public class NGStoredConfigurationFactory {
         static ValueMetaData readValueMetaData(final Element element)
         {
             final String modifyDateStr = element.getAttributeValue(StoredConfiguration.XML_ATTRIBUTE_MODIFY_TIME);
-            Date modifyDate = null;
+            Instant modifyDate = null;
             try {
                 modifyDate = modifyDateStr == null || modifyDateStr.isEmpty()
                         ? null
-                        : PwmConstants.DEFAULT_DATETIME_FORMAT.parse(modifyDateStr);
-            } catch (ParseException e) {
+                        : JavaHelper.parseIsoToInstant(modifyDateStr);
+            } catch (Exception e) {
                 LOGGER.warn("error parsing stored date: " +  e.getMessage());
             }
             final String modifyUser = element.getAttributeValue(StoredConfiguration.XML_ATTRIBUTE_MODIFY_USER);

+ 2 - 2
src/main/java/password/pwm/config/stored/StoredConfiguration.java

@@ -29,7 +29,7 @@ import password.pwm.config.StoredValue;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.util.secure.PwmSecurityKey;
 
-import java.util.Date;
+import java.time.Instant;
 
 public interface StoredConfiguration {
     String XML_ELEMENT_ROOT = "PwmConfiguration";
@@ -56,7 +56,7 @@ public interface StoredConfiguration {
 
     PwmSecurityKey getKey() throws PwmUnrecoverableException;
 
-    Date modifyTime();
+    Instant modifyTime();
 
     boolean isLocked();
 

+ 15 - 14
src/main/java/password/pwm/config/stored/StoredConfigurationImpl.java

@@ -70,7 +70,7 @@ import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
 import java.io.Serializable;
-import java.text.ParseException;
+import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
@@ -199,7 +199,7 @@ public class StoredConfigurationImpl implements Serializable, StoredConfiguratio
 
     public StoredConfigurationImpl() throws PwmUnrecoverableException {
         ConfigurationCleaner.cleanup(this);
-        final String createTime = PwmConstants.DEFAULT_DATETIME_FORMAT.format(new Date());
+        final String createTime = JavaHelper.toIsoDate(Instant.now());
         document.getRootElement().setAttribute(XML_ATTRIBUTE_CREATE_TIME,createTime);
     }
 
@@ -237,8 +237,8 @@ public class StoredConfigurationImpl implements Serializable, StoredConfiguratio
 
             final XPathExpression xp2 = XPathBuilder.xpathForConfigProperties();
             final Element propertiesElement = (Element)xp2.evaluateFirst(document);
-            propertyElement.setAttribute(XML_ATTRIBUTE_MODIFY_TIME,PwmConstants.DEFAULT_DATETIME_FORMAT.format(new Date()));
-            propertiesElement.setAttribute(XML_ATTRIBUTE_MODIFY_TIME,PwmConstants.DEFAULT_DATETIME_FORMAT.format(new Date()));
+            propertyElement.setAttribute(XML_ATTRIBUTE_MODIFY_TIME, JavaHelper.toIsoDate(Instant.now()));
+            propertiesElement.setAttribute(XML_ATTRIBUTE_MODIFY_TIME, JavaHelper.toIsoDate(Instant.now()));
             propertiesElement.addContent(propertyElement);
         } finally {
             domModifyLock.writeLock().unlock();
@@ -284,7 +284,7 @@ public class StoredConfigurationImpl implements Serializable, StoredConfiguratio
             final ValueMetaData settingMetaData = readSettingMetadata(settingValueRecord.getSetting(), settingValueRecord.getProfile());
             if (settingMetaData != null) {
                 if (settingMetaData.getModifyDate() != null) {
-                    recordMap.put("modifyTime", PwmConstants.DEFAULT_DATETIME_FORMAT.format(settingMetaData.getModifyDate()));
+                    recordMap.put("modifyTime", JavaHelper.toIsoDate(settingMetaData.getModifyDate()));
                 }
                 if (settingMetaData.getUserIdentity() != null) {
                     recordMap.put("modifyUser",settingMetaData.getUserIdentity().toDisplayString());
@@ -532,10 +532,10 @@ public class StoredConfigurationImpl implements Serializable, StoredConfiguratio
             return null;
         }
 
-        Date modifyDate = null;
+        Instant modifyDate = null;
         try {
             if (settingElement.getAttributeValue(XML_ATTRIBUTE_MODIFY_TIME) != null) {
-                modifyDate = PwmConstants.DEFAULT_DATETIME_FORMAT.parse(
+                modifyDate = JavaHelper.parseIsoToInstant(
                         settingElement.getAttributeValue(XML_ATTRIBUTE_MODIFY_TIME));
             }
         } catch (Exception e) {
@@ -724,7 +724,7 @@ public class StoredConfigurationImpl implements Serializable, StoredConfiguratio
                 valueElement.setContent(new CDATA(localeMap.get(locale)));
                 localeBundleElement.addContent(valueElement);
             }
-            localeBundleElement.setAttribute(XML_ATTRIBUTE_MODIFY_TIME,PwmConstants.DEFAULT_DATETIME_FORMAT.format(new Date()));
+            localeBundleElement.setAttribute(XML_ATTRIBUTE_MODIFY_TIME, JavaHelper.toIsoDate(Instant.now()));
             document.getRootElement().addContent(localeBundleElement);
         } finally {
             domModifyLock.writeLock().unlock();
@@ -845,7 +845,7 @@ public class StoredConfigurationImpl implements Serializable, StoredConfiguratio
         if (locked) {
             throw new UnsupportedOperationException("StoredConfiguration is locked and cannot be modified");
         }
-        document.getRootElement().setAttribute(XML_ATTRIBUTE_MODIFY_TIME,PwmConstants.DEFAULT_DATETIME_FORMAT.format(new Date()));
+        document.getRootElement().setAttribute(XML_ATTRIBUTE_MODIFY_TIME, JavaHelper.toIsoDate(Instant.now()));
     }
 
 // -------------------------- INNER CLASSES --------------------------
@@ -1384,8 +1384,8 @@ public class StoredConfigurationImpl implements Serializable, StoredConfiguratio
 
     private static void updateMetaData(final Element settingElement, final UserIdentity userIdentity) {
         final Element settingsElement = settingElement.getDocument().getRootElement().getChild(XML_ELEMENT_SETTINGS);
-        settingElement.setAttribute(XML_ATTRIBUTE_MODIFY_TIME,PwmConstants.DEFAULT_DATETIME_FORMAT.format(new Date()));
-        settingsElement.setAttribute(XML_ATTRIBUTE_MODIFY_TIME,PwmConstants.DEFAULT_DATETIME_FORMAT.format(new Date()));
+        settingElement.setAttribute(XML_ATTRIBUTE_MODIFY_TIME, JavaHelper.toIsoDate(Instant.now()));
+        settingsElement.setAttribute(XML_ATTRIBUTE_MODIFY_TIME, JavaHelper.toIsoDate(Instant.now()));
         settingElement.removeAttribute(XML_ATTRIBUTE_MODIFY_USER);
         settingsElement.removeAttribute(XML_ATTRIBUTE_MODIFY_USER);
         if (userIdentity != null) {
@@ -1517,13 +1517,13 @@ public class StoredConfigurationImpl implements Serializable, StoredConfiguratio
     }
 
     @Override
-    public Date modifyTime() {
+    public Instant modifyTime() {
         final Element rootElement = document.getRootElement();
         final String modifyTimeString = rootElement.getAttributeValue(XML_ATTRIBUTE_MODIFY_TIME);
         if (modifyTimeString != null) {
             try {
-                return PwmConstants.DEFAULT_DATETIME_FORMAT.parse(modifyTimeString);
-            } catch (ParseException e) {
+                return JavaHelper.parseIsoToInstant(modifyTimeString);
+            } catch (Exception e) {
                 LOGGER.error("error parsing root last modified timestamp: " + e.getMessage());
             }
         }
@@ -1565,4 +1565,5 @@ public class StoredConfigurationImpl implements Serializable, StoredConfiguratio
         }
         return new ArrayList<>(loopResults);
     }
+
 }

+ 4 - 4
src/main/java/password/pwm/config/stored/ValueMetaData.java

@@ -25,18 +25,18 @@ package password.pwm.config.stored;
 import password.pwm.bean.UserIdentity;
 
 import java.io.Serializable;
-import java.util.Date;
+import java.time.Instant;
 
 public class ValueMetaData implements Serializable {
-    private Date modifyDate;
+    private Instant modifyDate;
     private UserIdentity userIdentity;
 
-    public ValueMetaData(final Date modifyDate, final UserIdentity userIdentity) {
+    public ValueMetaData(final Instant modifyDate, final UserIdentity userIdentity) {
         this.modifyDate = modifyDate;
         this.userIdentity = userIdentity;
     }
 
-    public Date getModifyDate()
+    public Instant getModifyDate()
     {
         return modifyDate;
     }

+ 5 - 5
src/main/java/password/pwm/health/ApplianceStatusChecker.java

@@ -24,8 +24,8 @@ package password.pwm.health;
 
 import org.apache.commons.io.FileUtils;
 import password.pwm.PwmApplication;
-import password.pwm.PwmConstants;
 import password.pwm.PwmEnvironment;
+import password.pwm.bean.SessionLabel;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmOperationalException;
@@ -69,7 +69,7 @@ public class ApplianceStatusChecker implements HealthChecker {
         try {
             healthRecords.addAll(readApplianceHealthStatus(pwmApplication));
         } catch (Exception e) {
-            LOGGER.error(PwmConstants.HEALTH_SESSION_LABEL, "error communicating with client " + e.getMessage());
+            LOGGER.error(SessionLabel.HEALTH_SESSION_LABEL, "error communicating with client " + e.getMessage());
         }
 
         return healthRecords;
@@ -85,11 +85,11 @@ public class ApplianceStatusChecker implements HealthChecker {
                 .setPromiscuous(true)
                 .create();
 
-        final PwmHttpClient pwmHttpClient = new PwmHttpClient(pwmApplication, PwmConstants.HEALTH_SESSION_LABEL, pwmHttpClientConfiguration);
+        final PwmHttpClient pwmHttpClient = new PwmHttpClient(pwmApplication, SessionLabel.HEALTH_SESSION_LABEL, pwmHttpClientConfiguration);
         final PwmHttpClientRequest pwmHttpClientRequest = new PwmHttpClientRequest(HttpMethod.GET, url, null, requestHeaders);
         final PwmHttpClientResponse response = pwmHttpClient.makeRequest(pwmHttpClientRequest);
 
-        LOGGER.trace(PwmConstants.HEALTH_SESSION_LABEL, "https response from appliance server request: " + response.getBody());
+        LOGGER.trace(SessionLabel.HEALTH_SESSION_LABEL, "https response from appliance server request: " + response.getBody());
 
         final String jsonString = response.getBody();
 
@@ -139,7 +139,7 @@ public class ApplianceStatusChecker implements HealthChecker {
         final String port = pwmApplication.getPwmEnvironment().getParameters().get(PwmEnvironment.ApplicationParameter.AppliancePort);
 
         final String url = "https://" + hostname + ":" + port + "/sspr/appliance-update-status";
-        LOGGER.trace(PwmConstants.HEALTH_SESSION_LABEL, "calculated appliance host url as: " + url);
+        LOGGER.trace(SessionLabel.HEALTH_SESSION_LABEL, "calculated appliance host url as: " + url);
         return url;
     }
 

+ 4 - 3
src/main/java/password/pwm/health/ConfigurationChecker.java

@@ -26,6 +26,7 @@ import password.pwm.AppProperty;
 import password.pwm.PwmApplication;
 import password.pwm.PwmApplicationMode;
 import password.pwm.PwmConstants;
+import password.pwm.bean.SessionLabel;
 import password.pwm.config.Configuration;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.PwmSettingSyntax;
@@ -101,7 +102,7 @@ public class ConfigurationChecker implements HealthChecker {
                         HealthRecord.forMessage(HealthMessage.Config_NoSiteURL, PwmSetting.PWM_SITE_URL.toMenuLocationDebug(null,locale)));
             }
         } catch (PwmException e) {
-            LOGGER.error(PwmConstants.HEALTH_SESSION_LABEL,"error while inspecting site URL setting: " + e.getMessage());
+            LOGGER.error(SessionLabel.HEALTH_SESSION_LABEL,"error while inspecting site URL setting: " + e.getMessage());
         }
 
         if (config.readSettingAsBoolean(PwmSetting.LDAP_ENABLE_WIRE_TRACE)) {
@@ -163,7 +164,7 @@ public class ConfigurationChecker implements HealthChecker {
                                             setting.toMenuLocationDebug(null,locale), String.valueOf(strength)));
                                 }
                             } catch (Exception e) {
-                                LOGGER.error(PwmConstants.HEALTH_SESSION_LABEL,"error while inspecting setting "
+                                LOGGER.error(SessionLabel.HEALTH_SESSION_LABEL,"error while inspecting setting "
                                         + setting.toMenuLocationDebug(null,locale) +  ", error: " + e.getMessage());
                             }
                         }
@@ -181,7 +182,7 @@ public class ConfigurationChecker implements HealthChecker {
                                 String.valueOf(strength)));
                     }
                 } catch (PwmException e) {
-                    LOGGER.error(PwmConstants.HEALTH_SESSION_LABEL,"error while inspecting setting " + setting.toMenuLocationDebug(profile.getIdentifier(),locale) +  ", error: " + e.getMessage());
+                    LOGGER.error(SessionLabel.HEALTH_SESSION_LABEL,"error while inspecting setting " + setting.toMenuLocationDebug(profile.getIdentifier(),locale) +  ", error: " + e.getMessage());
                 }
             }
         }

+ 19 - 18
src/main/java/password/pwm/health/LDAPStatusChecker.java

@@ -38,6 +38,7 @@ import password.pwm.AppProperty;
 import password.pwm.PwmApplication;
 import password.pwm.PwmConstants;
 import password.pwm.bean.PasswordStatus;
+import password.pwm.bean.SessionLabel;
 import password.pwm.bean.UserIdentity;
 import password.pwm.bean.UserInfoBean;
 import password.pwm.config.Configuration;
@@ -165,7 +166,7 @@ public class LDAPStatusChecker implements HealthChecker {
             try {
 
                 chaiProvider = LdapOperationsHelper.createChaiProvider(
-                        PwmConstants.HEALTH_SESSION_LABEL,
+                        SessionLabel.HEALTH_SESSION_LABEL,
                         ldapProfile,
                         config,
                         proxyUserDN,
@@ -182,7 +183,7 @@ public class LDAPStatusChecker implements HealthChecker {
                 return returnRecords;
             } catch (Throwable e) {
                 final String msgString = e.getMessage();
-                LOGGER.trace(PwmConstants.HEALTH_SESSION_LABEL, "unexpected error while testing test user (during object creation): message=" + msgString + " debug info: " + JavaHelper.readHostileExceptionMessage(e));
+                LOGGER.trace(SessionLabel.HEALTH_SESSION_LABEL, "unexpected error while testing test user (during object creation): message=" + msgString + " debug info: " + JavaHelper.readHostileExceptionMessage(e));
                 returnRecords.add(HealthRecord.forMessage(HealthMessage.LDAP_TestUserUnexpected,
                         PwmSetting.LDAP_TEST_USER_DN.toMenuLocationDebug(ldapProfile.getIdentifier(), PwmConstants.DEFAULT_LOCALE),
                         msgString
@@ -200,7 +201,7 @@ public class LDAPStatusChecker implements HealthChecker {
                 return returnRecords;
             }
 
-            LOGGER.trace(PwmConstants.HEALTH_SESSION_LABEL, "beginning process to check ldap test user password read/write operations for profile " + ldapProfile.getIdentifier());
+            LOGGER.trace(SessionLabel.HEALTH_SESSION_LABEL, "beginning process to check ldap test user password read/write operations for profile " + ldapProfile.getIdentifier());
             try {
                 final boolean readPwdEnabled = pwmApplication.getConfig().readSettingAsBoolean(PwmSetting.EDIRECTORY_READ_USER_PWD)
                         && theUser.getChaiProvider().getDirectoryVendor() == ChaiProvider.DIRECTORY_VENDOR.NOVELL_EDIRECTORY;
@@ -209,7 +210,7 @@ public class LDAPStatusChecker implements HealthChecker {
                     try {
                         theUser.readPassword();
                     } catch (Exception e) {
-                        LOGGER.debug(PwmConstants.HEALTH_SESSION_LABEL, "error reading user password from directory " + e.getMessage());
+                        LOGGER.debug(SessionLabel.HEALTH_SESSION_LABEL, "error reading user password from directory " + e.getMessage());
                         returnRecords.add(HealthRecord.forMessage(HealthMessage.LDAP_TestUserReadPwError,
                                 PwmSetting.EDIRECTORY_READ_USER_PWD.toMenuLocationDebug(null, PwmConstants.DEFAULT_LOCALE),
                                 PwmSetting.LDAP_TEST_USER_DN.toMenuLocationDebug(ldapProfile.getIdentifier(), PwmConstants.DEFAULT_LOCALE),
@@ -229,27 +230,27 @@ public class LDAPStatusChecker implements HealthChecker {
                     if (minLifetimeSeconds > 0) {
                         final Instant pwdLastModified = PasswordUtility.determinePwdLastModified(
                                 pwmApplication,
-                                PwmConstants.HEALTH_SESSION_LABEL,
+                                SessionLabel.HEALTH_SESSION_LABEL,
                                 userIdentity
                         );
 
 
                         final PasswordStatus passwordStatus;
                         {
-                            final UserStatusReader userStatusReader = new UserStatusReader(pwmApplication, PwmConstants.HEALTH_SESSION_LABEL);
+                            final UserStatusReader userStatusReader = new UserStatusReader(pwmApplication, SessionLabel.HEALTH_SESSION_LABEL);
                             passwordStatus = userStatusReader.readPasswordStatus(theUser, passwordPolicy, null, null);
                         }
 
                         try {
                             PasswordUtility.checkIfPasswordWithinMinimumLifetime(
                                     theUser,
-                                    PwmConstants.HEALTH_SESSION_LABEL,
+                                    SessionLabel.HEALTH_SESSION_LABEL,
                                     passwordPolicy,
                                     pwdLastModified,
                                     passwordStatus
                             );
                         } catch (PwmException e) {
-                            LOGGER.trace(PwmConstants.HEALTH_SESSION_LABEL, "skipping test user password set: " + e.getMessage());
+                            LOGGER.trace(SessionLabel.HEALTH_SESSION_LABEL, "skipping test user password set: " + e.getMessage());
                             doPasswordChange = false;
                         }
                     }
@@ -257,7 +258,7 @@ public class LDAPStatusChecker implements HealthChecker {
                         final PasswordData newPassword = RandomPasswordGenerator.createRandomPassword(null, passwordPolicy, pwmApplication);
                         try {
                             theUser.setPassword(newPassword.getStringValue());
-                            LOGGER.debug(PwmConstants.HEALTH_SESSION_LABEL, "set random password on test user " + userIdentity.toDisplayString());
+                            LOGGER.debug(SessionLabel.HEALTH_SESSION_LABEL, "set random password on test user " + userIdentity.toDisplayString());
                         } catch (ChaiException e) {
                             returnRecords.add(HealthRecord.forMessage(HealthMessage.LDAP_TestUserWritePwError,
                                     PwmSetting.LDAP_TEST_USER_DN.toMenuLocationDebug(ldapProfile.getIdentifier(), PwmConstants.DEFAULT_LOCALE),
@@ -270,7 +271,7 @@ public class LDAPStatusChecker implements HealthChecker {
                 }
             } catch (Exception e) {
                 final String msg = "error setting test user password: " + JavaHelper.readHostileExceptionMessage(e);
-                LOGGER.error(PwmConstants.HEALTH_SESSION_LABEL, msg, e);
+                LOGGER.error(SessionLabel.HEALTH_SESSION_LABEL, msg, e);
                 returnRecords.add(HealthRecord.forMessage(HealthMessage.LDAP_TestUserUnexpected,
                         PwmSetting.LDAP_TEST_USER_DN.toMenuLocationDebug(ldapProfile.getIdentifier(), PwmConstants.DEFAULT_LOCALE),
                         msg
@@ -283,7 +284,7 @@ public class LDAPStatusChecker implements HealthChecker {
                 final UserStatusReader.Settings readerSettings = new UserStatusReader.Settings();
                 final UserStatusReader userStatusReader = new UserStatusReader(
                         pwmApplication,
-                        PwmConstants.HEALTH_SESSION_LABEL,
+                        SessionLabel.HEALTH_SESSION_LABEL,
                         readerSettings
                 );
                 userStatusReader.populateUserInfoBean(
@@ -322,7 +323,7 @@ public class LDAPStatusChecker implements HealthChecker {
             ChaiProvider chaiProvider = null;
             try {
                 chaiProvider = LdapOperationsHelper.createChaiProvider(
-                        PwmConstants.HEALTH_SESSION_LABEL,
+                        SessionLabel.HEALTH_SESSION_LABEL,
                         config,
                         ldapProfile,
                         Collections.singletonList(loopURL),
@@ -366,7 +367,7 @@ public class LDAPStatusChecker implements HealthChecker {
                 if (proxyPW == null) {
                     return Collections.singletonList(new HealthRecord(HealthStatus.WARN,HealthTopic.LDAP,"Missing Proxy User Password"));
                 }
-                chaiProvider = LdapOperationsHelper.createChaiProvider(PwmConstants.HEALTH_SESSION_LABEL,ldapProfile,config,proxyDN,proxyPW);
+                chaiProvider = LdapOperationsHelper.createChaiProvider(SessionLabel.HEALTH_SESSION_LABEL,ldapProfile,config,proxyDN,proxyPW);
                 final ChaiEntry adminEntry = ChaiFactory.createChaiEntry(proxyDN,chaiProvider);
                 adminEntry.isValid();
                 directoryVendor = chaiProvider.getDirectoryVendor();
@@ -497,7 +498,7 @@ public class LDAPStatusChecker implements HealthChecker {
             return (List<HealthRecord>)healthProperties.get(HealthMonitor.HealthMonitorFlag.LdapVendorSameCheck);
         }
 
-        LOGGER.trace(PwmConstants.HEALTH_SESSION_LABEL,"beginning check for replica vendor sameness");
+        LOGGER.trace(SessionLabel.HEALTH_SESSION_LABEL,"beginning check for replica vendor sameness");
         boolean errorReachingServer = false;
         final Map<String,ChaiProvider.DIRECTORY_VENDOR> replicaVendorMap = new HashMap<>();
 
@@ -515,7 +516,7 @@ public class LDAPStatusChecker implements HealthChecker {
             }
         } catch (Exception e) {
             errorReachingServer = true;
-            LOGGER.error(PwmConstants.HEALTH_SESSION_LABEL,"error during replica vendor sameness check: " + e.getMessage());
+            LOGGER.error(SessionLabel.HEALTH_SESSION_LABEL,"error during replica vendor sameness check: " + e.getMessage());
         }
 
         final ArrayList<HealthRecord> healthRecords = new ArrayList<>();
@@ -534,7 +535,7 @@ public class LDAPStatusChecker implements HealthChecker {
             // cache the error
             healthProperties.put(HealthMonitor.HealthMonitorFlag.LdapVendorSameCheck, healthRecords);
 
-            LOGGER.warn(PwmConstants.HEALTH_SESSION_LABEL,"multiple ldap vendors found: " + vendorMsg.toString());
+            LOGGER.warn(SessionLabel.HEALTH_SESSION_LABEL,"multiple ldap vendors found: " + vendorMsg.toString());
         } else if (discoveredVendors.size() == 1) {
             if (!errorReachingServer) {
                 // cache the no errors
@@ -558,7 +559,7 @@ public class LDAPStatusChecker implements HealthChecker {
             return (List<HealthRecord>)healthProperties.get(HealthMonitor.HealthMonitorFlag.AdPasswordPolicyApiCheck);
         }
 
-        LOGGER.trace(PwmConstants.HEALTH_SESSION_LABEL,"beginning check for ad api password policy (asn " + PwmConstants.LDAP_AD_PASSWORD_POLICY_CONTROL_ASN + ") support");
+        LOGGER.trace(SessionLabel.HEALTH_SESSION_LABEL,"beginning check for ad api password policy (asn " + PwmConstants.LDAP_AD_PASSWORD_POLICY_CONTROL_ASN + ") support");
         boolean errorReachingServer = false;
         final ArrayList<HealthRecord> healthRecords = new ArrayList<>();
 
@@ -588,7 +589,7 @@ public class LDAPStatusChecker implements HealthChecker {
             }
         } catch (Exception e) {
             errorReachingServer = true;
-            LOGGER.error(PwmConstants.HEALTH_SESSION_LABEL,
+            LOGGER.error(SessionLabel.HEALTH_SESSION_LABEL,
                     "error during ad api password policy (asn " + PwmConstants.LDAP_AD_PASSWORD_POLICY_CONTROL_ASN + ") check: " + e.getMessage());
         }
 

+ 7 - 1
src/main/java/password/pwm/http/filter/RequestInitializationFilter.java

@@ -369,7 +369,13 @@ public class RequestInitializationFilter implements Filter {
         resp.setHeader(HttpHeader.Cache_Control, "no-cache, no-store, must-revalidate, proxy-revalidate");
 
         if (pwmSession != null) {
-            final String contentPolicy = config.readSettingAsString(PwmSetting.SECURITY_CSP_HEADER);
+            final String contentPolicy;
+            if (pwmRequest.getURL().isConfigGuideURL() || pwmRequest.getURL().isConfigManagerURL()) {
+                contentPolicy = config.readAppProperty(AppProperty.SECURITY_HTTP_CONFIG_CSP_HEADER);
+            } else {
+                contentPolicy = config.readSettingAsString(PwmSetting.SECURITY_CSP_HEADER);
+            }
+
             if (contentPolicy != null && !contentPolicy.isEmpty()) {
                 final String nonce = pwmRequest.getCspNonce();
                 final String expandedPolicy = contentPolicy.replace("%NONCE%", nonce);

+ 3 - 2
src/main/java/password/pwm/http/servlet/changepw/ChangePasswordServlet.java

@@ -43,6 +43,7 @@ import password.pwm.config.profile.PwmPasswordRule;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmDataValidationException;
 import password.pwm.error.PwmError;
+import password.pwm.error.PwmException;
 import password.pwm.error.PwmOperationalException;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.http.HttpMethod;
@@ -571,13 +572,13 @@ public abstract class ChangePasswordServlet extends ControlledPwmServlet {
                     userInfoBean.getPasswordLastModifiedTime(),
                     userInfoBean.getPasswordState()
             );
-        } catch (PwmOperationalException e) {
+        } catch (PwmException e) {
             final boolean enforceFromForgotten = pwmApplication.getConfig().readSettingAsBoolean(PwmSetting.CHALLENGE_ENFORCE_MINIMUM_PASSWORD_LIFETIME);
             if (!enforceFromForgotten && userInfoBean.isRequiresNewPassword()) {
                 LOGGER.debug(pwmSession, "current password is too young, but skipping enforcement of minimum lifetime check due to setting "
                         + PwmSetting.CHALLENGE_ENFORCE_MINIMUM_PASSWORD_LIFETIME.toMenuLocationDebug(null, pwmSession.getSessionStateBean().getLocale()));
             } else {
-                throw e;
+                throw new PwmUnrecoverableException(e.getErrorInformation());
             }
         }
 

+ 2 - 2
src/main/java/password/pwm/http/servlet/configmanager/ConfigManagerServlet.java

@@ -65,9 +65,9 @@ import javax.servlet.ServletException;
 import javax.servlet.annotation.WebServlet;
 import java.io.IOException;
 import java.io.OutputStream;
+import java.time.Instant;
 import java.util.Collection;
 import java.util.Collections;
-import java.util.Date;
 import java.util.HashMap;
 import java.util.LinkedHashMap;
 import java.util.List;
@@ -175,7 +175,7 @@ public class ConfigManagerServlet extends AbstractPwmServlet {
         pwmRequest.setAttribute(PwmRequestAttribute.ApplicationPath, pwmRequest.getPwmApplication().getPwmEnvironment().getApplicationPath().getAbsolutePath());
         pwmRequest.setAttribute(PwmRequestAttribute.ConfigFilename, configurationReader.getConfigFile().getAbsolutePath());
         {
-            final Date lastModifyTime = configurationReader.getStoredConfiguration().modifyTime();
+            final Instant lastModifyTime = configurationReader.getStoredConfiguration().modifyTime();
             final String output = lastModifyTime == null
                     ? LocaleHelper.getLocalizedMessage(Display.Value_NotApplicable,pwmRequest)
                     : JavaHelper.toIsoDate(lastModifyTime);

+ 12 - 8
src/main/java/password/pwm/http/servlet/configmanager/DebugItemGenerator.java

@@ -344,7 +344,7 @@ public class DebugItemGenerator {
             final PrintWriter writer = new PrintWriter( new OutputStreamWriter(baos, PwmConstants.DEFAULT_CHARSET) );
             final ThreadInfo[] threads = ManagementFactory.getThreadMXBean().dumpAllThreads(true,true);
             for (final ThreadInfo threadInfo : threads) {
-                writer.write(threadInfo.toString());
+                writer.write(JavaHelper.threadInfoToString(threadInfo));
             }
             writer.flush();
             outputStream.write(baos.toByteArray());
@@ -417,13 +417,17 @@ public class DebugItemGenerator {
                     csvPrinter.printComment(StringUtil.join(headerRow,","));
                 }
                 for (final FileSystemUtility.FileSummaryInformation fileSummaryInformation : fileSummaryInformations) {
-                    final List<String> dataRow = new ArrayList<>();
-                    dataRow.add(fileSummaryInformation.getFilepath());
-                    dataRow.add(fileSummaryInformation.getFilename());
-                    dataRow.add(JavaHelper.toIsoDate(fileSummaryInformation.getModified()));
-                    dataRow.add(String.valueOf(fileSummaryInformation.getSize()));
-                    dataRow.add(fileSummaryInformation.getSha1sum());
-                    csvPrinter.printRecord(dataRow);
+                    try {
+                        final List<String> dataRow = new ArrayList<>();
+                        dataRow.add(fileSummaryInformation.getFilepath());
+                        dataRow.add(fileSummaryInformation.getFilename());
+                        dataRow.add(JavaHelper.toIsoDate(fileSummaryInformation.getModified()));
+                        dataRow.add(String.valueOf(fileSummaryInformation.getSize()));
+                        dataRow.add(fileSummaryInformation.getSha1sum());
+                        csvPrinter.printRecord(dataRow);
+                    } catch (Exception e) {
+                        LOGGER.trace("error generating file summary info: " + e.getMessage());
+                    }
                 }
                 csvPrinter.flush();
             }

+ 9 - 0
src/main/java/password/pwm/http/servlet/forgottenpw/ForgottenPasswordServlet.java

@@ -202,6 +202,15 @@ public class ForgottenPasswordServlet extends ControlledPwmServlet {
 
         checkForLocaleSwitch(pwmRequest, forgottenPasswordBean);
 
+        final ProcessAction action = this.readProcessAction(pwmRequest);
+
+        // convert a url command like /public/newuser/12321321 to redirect with a process action.
+        if (action == null) {
+            if (pwmRequest.convertURLtokenCommand()) {
+                return ProcessStatus.Halt;
+            }
+        }
+
         return ProcessStatus.Continue;
     }
 

+ 6 - 6
src/main/java/password/pwm/http/servlet/forgottenpw/ForgottenPasswordUtil.java

@@ -47,7 +47,6 @@ import password.pwm.error.PwmError;
 import password.pwm.error.PwmOperationalException;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.http.PwmRequest;
-import password.pwm.http.PwmRequestAttribute;
 import password.pwm.http.bean.ForgottenPasswordBean;
 import password.pwm.http.filter.AuthenticationFilter;
 import password.pwm.ldap.LdapUserDataReader;
@@ -115,23 +114,24 @@ class ForgottenPasswordUtil {
         return Collections.unmodifiableSet(result);
     }
 
-    static UserInfoBean readUserInfoBean(final PwmRequest pwmRequest, final ForgottenPasswordBean forgottenPasswordBean) throws PwmUnrecoverableException
-    {
+    static UserInfoBean readUserInfoBean(final PwmRequest pwmRequest, final ForgottenPasswordBean forgottenPasswordBean) throws PwmUnrecoverableException {
         if (forgottenPasswordBean.getUserIdentity() == null) {
             return null;
         }
 
+        final String CACHE_KEY = "ForgottenPassword-UserInfoCache";
+
         final UserIdentity userIdentity = forgottenPasswordBean.getUserIdentity();
 
         {
-            final UserInfoBean beanInRequest = (UserInfoBean)pwmRequest.getAttribute(PwmRequestAttribute.ForgottenPasswordUserInfo);
+            final UserInfoBean beanInRequest = (UserInfoBean)pwmRequest.getHttpServletRequest().getSession().getAttribute(CACHE_KEY);
             if (beanInRequest != null) {
                 if (userIdentity.equals(beanInRequest.getUserIdentity())) {
                     LOGGER.trace(pwmRequest, "using request cached UserInfoBean");
                     return beanInRequest;
                 } else {
                     LOGGER.trace(pwmRequest, "request cached userInfoBean is not for current user, clearing.");
-                    pwmRequest.setAttribute(PwmRequestAttribute.ForgottenPasswordUserInfo, null);
+                    pwmRequest.getHttpServletRequest().getSession().setAttribute(CACHE_KEY, null);
                 }
             }
         }
@@ -146,7 +146,7 @@ class ForgottenPasswordUtil {
                 chaiProvider
         );
 
-        pwmRequest.setAttribute(PwmRequestAttribute.ForgottenPasswordUserInfo, userInfoBean);
+        pwmRequest.getHttpServletRequest().getSession().setAttribute(CACHE_KEY, userInfoBean);
 
         return userInfoBean;
     }

+ 0 - 2
src/main/java/password/pwm/http/servlet/peoplesearch/PeopleSearchServlet.java

@@ -151,11 +151,9 @@ public abstract class PeopleSearchServlet extends ControlledPwmServlet {
         }
         final boolean includeDisplayName = pwmRequest.readParameterAsBoolean("includeDisplayName");
 
-        // if not in cache, build results from ldap
         final PeopleSearchDataReader peopleSearchDataReader = new PeopleSearchDataReader(pwmRequest);
 
         final SearchResultBean searchResultBean = peopleSearchDataReader.makeSearchResultBean(username, includeDisplayName);
-        searchResultBean.setFromCache(false);
         final RestResultBean restResultBean = new RestResultBean(searchResultBean);
 
         addExpiresHeadersToResponse(pwmRequest);

+ 8 - 27
src/main/java/password/pwm/http/tag/conditional/PwmIfOptions.java

@@ -22,36 +22,17 @@
 
 package password.pwm.http.tag.conditional;
 
+import lombok.AllArgsConstructor;
+import lombok.Getter;
 import password.pwm.Permission;
 import password.pwm.config.PwmSetting;
 import password.pwm.http.PwmRequestFlag;
 
+@Getter
+@AllArgsConstructor
 class PwmIfOptions {
-    private boolean negate;
-    private Permission permission;
-    private PwmSetting pwmSetting;
-    private PwmRequestFlag requestFlag;
-
-    PwmIfOptions(final boolean negate, final PwmSetting pwmSetting, final Permission permission, final PwmRequestFlag pwmRequestFlag) {
-        this.negate = negate;
-        this.permission = permission;
-        this.pwmSetting = pwmSetting;
-        this.requestFlag = pwmRequestFlag;
-    }
-
-    public boolean isNegate() {
-        return negate;
-    }
-
-    public Permission getPermission() {
-        return permission;
-    }
-
-    public PwmRequestFlag getRequestFlag() {
-        return requestFlag;
-    }
-
-    public PwmSetting getPwmSetting() {
-        return pwmSetting;
-    }
+    private final boolean negate;
+    private final Permission permission;
+    private final PwmSetting pwmSetting;
+    private final PwmRequestFlag requestFlag;
 }

+ 1 - 1
src/main/java/password/pwm/http/tag/conditional/PwmIfTag.java

@@ -86,7 +86,7 @@ public class PwmIfTag extends BodyTagSupport {
                     final PwmIfTest testEnum = test;
                     if (testEnum != null) {
                         try {
-                            final PwmIfOptions options = new PwmIfOptions(negate, setting, permission, requestFlag);
+                            final PwmIfOptions options = new PwmIfOptions(negate, permission, setting, requestFlag);
                             showBody = testEnum.passed(pwmRequest, options);
                         } catch (ChaiUnavailableException e) {
                             LOGGER.error("error testing jsp if '" + testEnum.toString() + "', error: " + e.getMessage());

+ 62 - 16
src/main/java/password/pwm/ldap/LdapConnectionService.java

@@ -3,7 +3,7 @@
  * http://www.pwm-project.org
  *
  * Copyright (c) 2006-2009 Novell, Inc.
- * Copyright (c) 2009-2017 The PWM Project
+ * 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
@@ -24,6 +24,7 @@ package password.pwm.ldap;
 
 import com.google.gson.reflect.TypeToken;
 import com.novell.ldapchai.provider.ChaiProvider;
+import password.pwm.AppProperty;
 import password.pwm.PwmApplication;
 import password.pwm.config.option.DataStorageMethod;
 import password.pwm.config.profile.LdapProfile;
@@ -41,14 +42,18 @@ import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicInteger;
 
 public class LdapConnectionService implements PwmService {
     private static final PwmLogger LOGGER = PwmLogger.forClass(LdapConnectionService.class);
 
-    private final Map<String,ChaiProvider> proxyChaiProviders = new HashMap<>();
-    private final Map<LdapProfile,ErrorInformation> lastLdapErrors = new HashMap<>();
+    private final Map<LdapProfile, Map<Integer,ChaiProvider>> proxyChaiProviders = new ConcurrentHashMap<>();
+    private final Map<LdapProfile, ErrorInformation> lastLdapErrors = new ConcurrentHashMap<>();
     private PwmApplication pwmApplication;
     private STATUS status = STATUS.NEW;
+    private int connectionsPerProfile = 1;
+    private final AtomicInteger slotIncrementer = new AtomicInteger(0);
 
     public STATUS status()
     {
@@ -63,6 +68,13 @@ public class LdapConnectionService implements PwmService {
         // read the lastLoginTime
         this.lastLdapErrors.putAll(readLastLdapFailure(pwmApplication));
 
+        connectionsPerProfile = maxSlotsPerProfile(pwmApplication);
+        LOGGER.trace("allocating " + connectionsPerProfile + " ldap proxy connections per profile");
+
+        for (final LdapProfile ldapProfile: pwmApplication.getConfig().getLdapProfiles().values()) {
+            proxyChaiProviders.put(ldapProfile, new ConcurrentHashMap<>());
+        }
+
         status = STATUS.OPEN;
     }
 
@@ -70,13 +82,15 @@ public class LdapConnectionService implements PwmService {
     {
         status = STATUS.CLOSED;
         LOGGER.trace("closing ldap proxy connections");
-        for (final String id : proxyChaiProviders.keySet()) {
-            final ChaiProvider existingProvider = proxyChaiProviders.get(id);
-
-            try {
-                existingProvider.close();
-            } catch (Exception e) {
-                LOGGER.error("error closing ldap proxy connection: " + e.getMessage(), e);
+        for (final LdapProfile ldapProfile : proxyChaiProviders.keySet()) {
+            for (final int slot : proxyChaiProviders.get(ldapProfile).keySet()) {
+                final ChaiProvider existingProvider = proxyChaiProviders.get(ldapProfile).get(slot);
+
+                try {
+                    existingProvider.close();
+                } catch (Exception e) {
+                    LOGGER.error("error closing ldap proxy connection: " + e.getMessage(), e);
+                }
             }
         }
         proxyChaiProviders.clear();
@@ -93,21 +107,28 @@ public class LdapConnectionService implements PwmService {
     }
 
 
-    public ChaiProvider getProxyChaiProvider(final LdapProfile ldapProfile)
+    public ChaiProvider getProxyChaiProvider(final String identifier)
             throws PwmUnrecoverableException
     {
-        return getProxyChaiProvider(ldapProfile.getIdentifier());
+        final LdapProfile ldapProfile = pwmApplication.getConfig().getLdapProfiles().get(identifier);
+        return getProxyChaiProvider(ldapProfile);
     }
 
-    public ChaiProvider getProxyChaiProvider(final String identifier)
+    public ChaiProvider getProxyChaiProvider(final LdapProfile identifier)
             throws PwmUnrecoverableException
     {
-        final ChaiProvider proxyChaiProvider = proxyChaiProviders.get(identifier == null ? "" : identifier);
+        final int slot = nextSlot();
+
+        final ChaiProvider proxyChaiProvider = proxyChaiProviders.get(identifier).get(slot);
+
         if (proxyChaiProvider != null) {
             return proxyChaiProvider;
         }
 
-        final LdapProfile ldapProfile = pwmApplication.getConfig().getLdapProfiles().get(identifier == null ? "" : identifier);
+        final LdapProfile ldapProfile = identifier == null
+                ? pwmApplication.getConfig().getDefaultLdapProfile()
+                : identifier;
+
         if (ldapProfile == null) {
             final String errorMsg = "unknown ldap profile requested connection: " + identifier;
             throw new PwmUnrecoverableException(new ErrorInformation(PwmError.ERROR_NO_LDAP_CONNECTION,errorMsg));
@@ -120,7 +141,7 @@ public class LdapConnectionService implements PwmService {
                     pwmApplication.getConfig(),
                     pwmApplication.getStatisticsManager()
             );
-            proxyChaiProviders.put(identifier, newProvider);
+            proxyChaiProviders.get(identifier).put(slot, newProvider);
             return newProvider;
         } catch (PwmUnrecoverableException e) {
             setLastLdapFailure(ldapProfile,e.getErrorInformation());
@@ -175,4 +196,29 @@ public class LdapConnectionService implements PwmService {
         }
         return Collections.emptyMap();
     }
+
+    private int nextSlot() {
+        return slotIncrementer.getAndUpdate(operand -> {
+            operand++;
+            if (operand >= connectionsPerProfile) {
+                operand = 0;
+            }
+            return operand;
+        });
+    }
+
+    private int maxSlotsPerProfile(final PwmApplication pwmApplication) {
+        final int maxConnections = Integer.parseInt(pwmApplication.getConfig().readAppProperty(AppProperty.LDAP_PROXY_MAX_CONNECTIONS));
+        final int perProfile = Integer.parseInt(pwmApplication.getConfig().readAppProperty(AppProperty.LDAP_PROXY_CONNECTION_PER_PROFILE));
+        final int profileCount = pwmApplication.getConfig().getLdapProfiles().size();
+
+        if ((perProfile * profileCount) >= maxConnections) {
+            final int adjustedConnections = Math.min(1, (maxConnections / profileCount));
+            LOGGER.warn("connections per profile (" + perProfile + ") multiplied by number of profiles ("
+                    + profileCount + ") exceeds max connections (" + maxConnections  + "), will limit to " + adjustedConnections);
+            return adjustedConnections;
+        }
+
+        return perProfile;
+    }
 }

+ 26 - 10
src/main/java/password/pwm/ldap/auth/LDAPAuthenticationRequest.java

@@ -33,6 +33,7 @@ import com.novell.ldapchai.exception.ImpossiblePasswordPolicyException;
 import com.novell.ldapchai.impl.oracleds.entry.OracleDSEntries;
 import com.novell.ldapchai.provider.ChaiProvider;
 import com.novell.ldapchai.provider.ChaiSetting;
+import password.pwm.AppProperty;
 import password.pwm.PwmApplication;
 import password.pwm.PwmConstants;
 import password.pwm.bean.SessionLabel;
@@ -429,13 +430,18 @@ class LDAPAuthenticationRequest implements AuthenticationRequest {
                 ORACLE_ATTR_PW_ALLOW_CHG_TIME);
         log(PwmLogLevel.TRACE,"read OracleDS value of passwordAllowChangeTime value=" + oracleDS_PrePasswordAllowChangeTime);
 
+
         if (oracleDS_PrePasswordAllowChangeTime != null && !oracleDS_PrePasswordAllowChangeTime.isEmpty()) {
             final Date date = OracleDSEntries.convertZuluToDate(oracleDS_PrePasswordAllowChangeTime);
-            if (new Date().before(date)) {
-                final String errorMsg = "change not permitted until " + JavaHelper.toIsoDate(
-                        date);
-                throw new PwmUnrecoverableException(
-                        new ErrorInformation(PwmError.PASSWORD_TOO_SOON, errorMsg));
+
+            final boolean enforceFromForgotten = pwmApplication.getConfig().readSettingAsBoolean(PwmSetting.CHALLENGE_ENFORCE_MINIMUM_PASSWORD_LIFETIME);
+            if (enforceFromForgotten) {
+                if (new Date().before(date)) {
+                    final String errorMsg = "change not permitted until " + JavaHelper.toIsoDate(
+                            date);
+                    throw new PwmUnrecoverableException(
+                            new ErrorInformation(PwmError.PASSWORD_TOO_SOON, errorMsg));
+                }
             }
         }
 
@@ -471,11 +477,21 @@ class LDAPAuthenticationRequest implements AuthenticationRequest {
                     chaiUser.getEntryDN(),
                     ORACLE_ATTR_PW_ALLOW_CHG_TIME);
             if (oracleDS_PostPasswordAllowChangeTime != null && !oracleDS_PostPasswordAllowChangeTime.isEmpty()) {
-                // password allow change time has appeared, but wasn't present previously, so delete it.
-                log(PwmLogLevel.TRACE, "a new value for passwordAllowChangeTime attribute to user " + chaiUser.getEntryDN() + " has appeared, will remove");
-                chaiProvider.deleteStringAttributeValue(chaiUser.getEntryDN(), ORACLE_ATTR_PW_ALLOW_CHG_TIME,
-                        oracleDS_PostPasswordAllowChangeTime);
-                log(PwmLogLevel.TRACE, "deleted attribute value for passwordAllowChangeTime attribute on user " + chaiUser.getEntryDN());
+                final boolean PostTempUseCurrentTime = Boolean.parseBoolean(pwmApplication.getConfig().readAppProperty(AppProperty.LDAP_ORACLE_POST_TEMPPW_USE_CURRENT_TIME));
+                if (PostTempUseCurrentTime) {
+                    log(PwmLogLevel.TRACE, "a new value for passwordAllowChangeTime attribute to user " + chaiUser.getEntryDN() + " has appeared, will replace with current time value");
+                    final String newTimeValue = OracleDSEntries.convertDateToZulu(new Date());
+                    final Set<String> values = new HashSet<>(Collections.singletonList(newTimeValue));
+                    chaiProvider.writeStringAttribute(chaiUser.getEntryDN(), ORACLE_ATTR_PW_ALLOW_CHG_TIME, values, true);
+                    log(PwmLogLevel.TRACE, "wrote attribute value '" + newTimeValue + "' for passwordAllowChangeTime attribute on user " + chaiUser.getEntryDN());
+                } else {
+                    // password allow change time has appeared, but wasn't present previously, so delete it.
+                    log(PwmLogLevel.TRACE, "a new value for passwordAllowChangeTime attribute to user " + chaiUser.getEntryDN() + " has appeared, will remove");
+                    chaiProvider.deleteStringAttributeValue(chaiUser.getEntryDN(), ORACLE_ATTR_PW_ALLOW_CHG_TIME,
+                            oracleDS_PostPasswordAllowChangeTime);
+                    log(PwmLogLevel.TRACE, "deleted attribute value for passwordAllowChangeTime attribute on user " + chaiUser.getEntryDN());
+                }
+
             }
         }
     }

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

@@ -24,7 +24,7 @@ package password.pwm.ldap.search;
 
 import com.novell.ldapchai.provider.ChaiProvider;
 import lombok.Builder;
-import lombok.Data;
+import lombok.Getter;
 import password.pwm.config.FormConfiguration;
 
 import java.io.Serializable;
@@ -32,7 +32,7 @@ import java.util.List;
 import java.util.Map;
 
 @Builder
-@Data
+@Getter
 public class SearchConfiguration implements Serializable {
 
     private String filter;

+ 1 - 1
src/main/java/password/pwm/svc/pwnotify/PasswordExpireNotificationEngine.java

@@ -62,7 +62,7 @@ public class PasswordExpireNotificationEngine {
 
     private static final PwmLogger LOGGER = PwmLogger.forClass(PasswordExpireNotificationEngine.class);
 
-    private static final SessionLabel SESSION_LABEL = PwmConstants.PW_EXP_NOTICE_LABEL;
+    private static final SessionLabel SESSION_LABEL = SessionLabel.PW_EXP_NOTICE_LABEL;
 
     private final Settings settings;
     private final PwmApplication pwmApplication;

+ 33 - 32
src/main/java/password/pwm/svc/report/ReportService.java

@@ -28,6 +28,7 @@ import com.novell.ldapchai.provider.ChaiProvider;
 import password.pwm.PwmApplication;
 import password.pwm.PwmApplicationMode;
 import password.pwm.PwmConstants;
+import password.pwm.bean.SessionLabel;
 import password.pwm.bean.UserIdentity;
 import password.pwm.bean.UserInfoBean;
 import password.pwm.config.PwmSetting;
@@ -112,13 +113,13 @@ public class ReportService implements PwmService {
         this.pwmApplication = pwmApplication;
 
         if (pwmApplication.getApplicationMode() == PwmApplicationMode.READ_ONLY) {
-            LOGGER.debug(PwmConstants.REPORTING_SESSION_LABEL,"application mode is read-only, will remain closed");
+            LOGGER.debug(SessionLabel.REPORTING_SESSION_LABEL,"application mode is read-only, will remain closed");
             status = STATUS.CLOSED;
             return;
         }
 
         if (pwmApplication.getLocalDB() == null || LocalDB.Status.OPEN != pwmApplication.getLocalDB().status()) {
-            LOGGER.debug(PwmConstants.REPORTING_SESSION_LABEL,"LocalDB is not open, will remain closed");
+            LOGGER.debug(SessionLabel.REPORTING_SESSION_LABEL,"LocalDB is not open, will remain closed");
             status = STATUS.CLOSED;
             return;
         }
@@ -127,7 +128,7 @@ public class ReportService implements PwmService {
             userCacheService = new UserCacheService();
             userCacheService.init(pwmApplication);
         } catch (Exception e) {
-            LOGGER.error(PwmConstants.REPORTING_SESSION_LABEL,"unable to init cache service");
+            LOGGER.error(SessionLabel.REPORTING_SESSION_LABEL,"unable to init cache service");
             status = STATUS.CLOSED;
             return;
         }
@@ -172,7 +173,7 @@ public class ReportService implements PwmService {
         try {
             pwmApplication.writeAppAttribute(PwmApplication.AppAttribute.REPORT_STATUS, reportStatus);
         } catch (Exception e) {
-            LOGGER.error(PwmConstants.REPORTING_SESSION_LABEL,"error writing cached report dredge info into memory: " + e.getMessage());
+            LOGGER.error(SessionLabel.REPORTING_SESSION_LABEL,"error writing cached report dredge info into memory: " + e.getMessage());
         }
     }
 
@@ -199,7 +200,7 @@ public class ReportService implements PwmService {
                 {
                     executorService.execute(new ClearTask());
                     executorService.execute(new ReadLDAPTask());
-                    LOGGER.trace(PwmConstants.REPORTING_SESSION_LABEL,"submitted new ldap dredge task to executorService");
+                    LOGGER.trace(SessionLabel.REPORTING_SESSION_LABEL,"submitted new ldap dredge task to executorService");
                 }
             }
             break;
@@ -283,10 +284,10 @@ public class ReportService implements PwmService {
                         returnBean = userCacheService.readStorageKey(key);
                         if (returnBean != null) {
                             if (returnBean.getCacheTimestamp() == null) {
-                                LOGGER.debug(PwmConstants.REPORTING_SESSION_LABEL,"purging record due to missing cache timestamp: " + JsonUtil.serialize(returnBean));
+                                LOGGER.debug(SessionLabel.REPORTING_SESSION_LABEL,"purging record due to missing cache timestamp: " + JsonUtil.serialize(returnBean));
                                 userCacheService.removeStorageKey(key);
                             } else if (TimeDuration.fromCurrent(returnBean.getCacheTimestamp()).isLongerThan(settings.getMaxCacheAge())) {
-                                LOGGER.debug(PwmConstants.REPORTING_SESSION_LABEL,"purging record due to old age timestamp: " + JsonUtil.serialize(returnBean));
+                                LOGGER.debug(SessionLabel.REPORTING_SESSION_LABEL,"purging record due to old age timestamp: " + JsonUtil.serialize(returnBean));
                                 userCacheService.removeStorageKey(key);
                             } else {
                                 return returnBean;
@@ -368,11 +369,11 @@ public class ReportService implements PwmService {
                 if (e instanceof PwmException) {
                     if (((PwmException) e).getErrorInformation().getError() == PwmError.ERROR_DIRECTORY_UNAVAILABLE) {
                         if (executorService != null) {
-                            LOGGER.error(PwmConstants.REPORTING_SESSION_LABEL, "directory unavailable error during background SearchLDAP, will retry; error: " + e.getMessage());
+                            LOGGER.error(SessionLabel.REPORTING_SESSION_LABEL, "directory unavailable error during background SearchLDAP, will retry; error: " + e.getMessage());
                             executorService.schedule(new ReadLDAPTask(), 10, TimeUnit.MINUTES);
                         }
                     } else {
-                        LOGGER.error(PwmConstants.REPORTING_SESSION_LABEL, "error during background ReadData: " + e.getMessage());
+                        LOGGER.error(SessionLabel.REPORTING_SESSION_LABEL, "error during background ReadData: " + e.getMessage());
                     }
                 }
             } finally {
@@ -391,7 +392,7 @@ public class ReportService implements PwmService {
 
             final Iterator<UserIdentity> memQueue = LdapOperationsHelper.readAllUsersFromLdap(
                     pwmApplication,
-                    PwmConstants.REPORTING_SESSION_LABEL,
+                    SessionLabel.REPORTING_SESSION_LABEL,
                     settings.getSearchFilter(),
                     settings.getMaxSearchSize()
             );
@@ -432,11 +433,11 @@ public class ReportService implements PwmService {
                 if (e instanceof PwmException) {
                     if (((PwmException) e).getErrorInformation().getError() == PwmError.ERROR_DIRECTORY_UNAVAILABLE) {
                         if (executorService != null) {
-                            LOGGER.error(PwmConstants.REPORTING_SESSION_LABEL, "directory unavailable error during background ReadData, will retry; error: " + e.getMessage());
+                            LOGGER.error(SessionLabel.REPORTING_SESSION_LABEL, "directory unavailable error during background ReadData, will retry; error: " + e.getMessage());
                             executorService.schedule(new ProcessWorkQueueTask(), 10, TimeUnit.MINUTES);
                         }
                     } else {
-                        LOGGER.error(PwmConstants.REPORTING_SESSION_LABEL, "error during background ReadData: " + e.getMessage());
+                        LOGGER.error(SessionLabel.REPORTING_SESSION_LABEL, "error during background ReadData: " + e.getMessage());
                     }
                 }
             } finally {
@@ -447,7 +448,7 @@ public class ReportService implements PwmService {
         private void processWorkQueue()
                 throws ChaiUnavailableException, ChaiOperationException, PwmOperationalException, PwmUnrecoverableException
         {
-            LOGGER.debug(PwmConstants.REPORTING_SESSION_LABEL, "beginning process to updating user cache records from ldap");
+            LOGGER.debug(SessionLabel.REPORTING_SESSION_LABEL, "beginning process to updating user cache records from ldap");
             if (status != STATUS.OPEN) {
                 return;
             }
@@ -467,17 +468,17 @@ public class ReportService implements PwmService {
             final Lock updateTimeLock = new ReentrantLock();
 
             try {
-                LOGGER.trace(PwmConstants.REPORTING_SESSION_LABEL, "about to begin ldap processing with thread count of " + threadCount);
+                LOGGER.trace(SessionLabel.REPORTING_SESSION_LABEL, "about to begin ldap processing with thread count of " + threadCount);
                 final BlockingThreadPool threadService = new BlockingThreadPool(threadCount, "reporting-thread");
                 while (status == STATUS.OPEN && !dnQueue.isEmpty() && !cancelFlag) {
                     final UserIdentity userIdentity = UserIdentity.fromDelimitedKey(dnQueue.poll());
                     if (pwmApplication.getConfig().isDevDebugMode()) {
-                        LOGGER.trace(PwmConstants.REPORTING_SESSION_LABEL, "submit " + Instant.now().toString()
+                        LOGGER.trace(SessionLabel.REPORTING_SESSION_LABEL, "submit " + Instant.now().toString()
                                 + " size=" + threadService.getQueue().size());
                     }
                     threadService.blockingSubmit(() -> {
                         if (pwmApplication.getConfig().isDevDebugMode()) {
-                            LOGGER.trace(PwmConstants.REPORTING_SESSION_LABEL, "start " + Instant.now().toString()
+                            LOGGER.trace(SessionLabel.REPORTING_SESSION_LABEL, "start " + Instant.now().toString()
                                     + " size=" + threadService.getQueue().size());
                         }
                         try {
@@ -504,18 +505,18 @@ public class ReportService implements PwmService {
                             errorMsg += e instanceof PwmException ? ((PwmException) e).getErrorInformation().toDebugStr() : e.getMessage();
                             final ErrorInformation errorInformation;
                             errorInformation = new ErrorInformation(PwmError.ERROR_REPORTING_ERROR,errorMsg);
-                            LOGGER.error(PwmConstants.REPORTING_SESSION_LABEL,errorInformation.toDebugStr(), e);
+                            LOGGER.error(SessionLabel.REPORTING_SESSION_LABEL,errorInformation.toDebugStr(), e);
                             reportStatus.setLastError(errorInformation);
                             reportStatus.setErrors(reportStatus.getErrors() + 1);
                         }
                         if (pwmApplication.getConfig().isDevDebugMode()) {
-                            LOGGER.trace(PwmConstants.REPORTING_SESSION_LABEL, "finish " + Instant.now().toString()
+                            LOGGER.trace(SessionLabel.REPORTING_SESSION_LABEL, "finish " + Instant.now().toString()
                                     + " size=" + threadService.getQueue().size());
                         }
                     });
                 }
                 if (pwmApplication.getConfig().isDevDebugMode()) {
-                    LOGGER.trace(PwmConstants.REPORTING_SESSION_LABEL, "exit " + Instant.now().toString()
+                    LOGGER.trace(SessionLabel.REPORTING_SESSION_LABEL, "exit " + Instant.now().toString()
                             + " size=" + threadService.getQueue().size());
                 }
 
@@ -528,7 +529,7 @@ public class ReportService implements PwmService {
                 reportStatus.setFinishDate(Instant.now());
                 saveTempData();
             }
-            LOGGER.debug(PwmConstants.REPORTING_SESSION_LABEL,"update user cache process completed: " + JsonUtil.serialize(reportStatus));
+            LOGGER.debug(SessionLabel.REPORTING_SESSION_LABEL,"update user cache process completed: " + JsonUtil.serialize(reportStatus));
         }
 
 
@@ -549,7 +550,7 @@ public class ReportService implements PwmService {
             final UserInfoBean userInfoBean = new UserInfoBean();
             final UserStatusReader.Settings readerSettings = new UserStatusReader.Settings();
             final ChaiProvider chaiProvider = pwmApplication.getProxyChaiProvider(userIdentity.getLdapProfileID());
-            final UserStatusReader userStatusReader = new UserStatusReader(pwmApplication,PwmConstants.REPORTING_SESSION_LABEL,readerSettings);
+            final UserStatusReader userStatusReader = new UserStatusReader(pwmApplication, SessionLabel.REPORTING_SESSION_LABEL,readerSettings);
             userStatusReader.populateUserInfoBean(
                     userInfoBean,
                     PwmConstants.DEFAULT_LOCALE,
@@ -561,7 +562,7 @@ public class ReportService implements PwmService {
             userCacheService.store(newUserCacheRecord);
             summaryData.update(newUserCacheRecord);
 
-            LOGGER.trace(PwmConstants.REPORTING_SESSION_LABEL,"stored cache for " + userIdentity);
+            LOGGER.trace(SessionLabel.REPORTING_SESSION_LABEL,"stored cache for " + userIdentity);
         }
     }
 
@@ -584,7 +585,7 @@ public class ReportService implements PwmService {
 
             try (ClosableIterator<UserCacheRecord> iterator = iterator()) {
                 final int totalRecords = userCacheService.size();
-                LOGGER.debug(PwmConstants.REPORTING_SESSION_LABEL, "beginning cache review process of " + totalRecords + " records");
+                LOGGER.debug(SessionLabel.REPORTING_SESSION_LABEL, "beginning cache review process of " + totalRecords + " records");
                 Instant lastLogOutputTime = Instant.now();
 
                 while (!cancelFlag && iterator.hasNext() && status == STATUS.OPEN) {
@@ -598,14 +599,14 @@ public class ReportService implements PwmService {
 
                     if (TimeDuration.fromCurrent(lastLogOutputTime).isLongerThan(30, TimeUnit.SECONDS)) {
                         final TimeDuration progressDuration = TimeDuration.fromCurrent(startTime);
-                        LOGGER.trace(PwmConstants.REPORTING_SESSION_LABEL,
+                        LOGGER.trace(SessionLabel.REPORTING_SESSION_LABEL,
                                 "cache review process in progress, examined " + examinedRecords
                                         + " in " + progressDuration.asCompactString());
                         lastLogOutputTime = Instant.now();
                     }
                 }
                 final TimeDuration totalTime = TimeDuration.fromCurrent(startTime);
-                LOGGER.info(PwmConstants.REPORTING_SESSION_LABEL,
+                LOGGER.info(SessionLabel.REPORTING_SESSION_LABEL,
                         "completed cache review process of " + examinedRecords
                                 + " cached report records in " + totalTime.asCompactString());
             }
@@ -626,7 +627,7 @@ public class ReportService implements PwmService {
             try {
                 initTempData();
             } catch (LocalDBException | PwmUnrecoverableException e) {
-                LOGGER.error(PwmConstants.REPORTING_SESSION_LABEL, "error during initialization: " + e.getMessage());
+                LOGGER.error(SessionLabel.REPORTING_SESSION_LABEL, "error during initialization: " + e.getMessage());
                 status = STATUS.CLOSED;
                 return;
             }
@@ -649,17 +650,17 @@ public class ReportService implements PwmService {
             try {
                 reportStatus = pwmApplication.readAppAttribute(PwmApplication.AppAttribute.REPORT_STATUS, ReportStatusInfo.class);
             } catch (Exception e) {
-                LOGGER.error(PwmConstants.REPORTING_SESSION_LABEL,"error loading cached report status info into memory: " + e.getMessage());
+                LOGGER.error(SessionLabel.REPORTING_SESSION_LABEL,"error loading cached report status info into memory: " + e.getMessage());
             }
 
             boolean clearFlag = false;
             if (reportStatus == null) {
                 clearFlag = true;
-                LOGGER.debug(PwmConstants.REPORTING_SESSION_LABEL,"report service did not close cleanly, will clear data.");
+                LOGGER.debug(SessionLabel.REPORTING_SESSION_LABEL,"report service did not close cleanly, will clear data.");
             } else {
                 final String currentSettingCache = settings.getSettingsHash();
                 if (reportStatus.getSettingsHash() != null && !reportStatus.getSettingsHash().equals(currentSettingCache)) {
-                    LOGGER.error(PwmConstants.REPORTING_SESSION_LABEL, "configuration has changed, will clear cached report data");
+                    LOGGER.error(SessionLabel.REPORTING_SESSION_LABEL, "configuration has changed, will clear cached report data");
                     clearFlag = true;
                 }
             }
@@ -677,20 +678,20 @@ public class ReportService implements PwmService {
             try {
                 doClear();
             } catch (LocalDBException | PwmUnrecoverableException e) {
-                LOGGER.error(PwmConstants.REPORTING_SESSION_LABEL, "error during clear operation: " + e.getMessage());
+                LOGGER.error(SessionLabel.REPORTING_SESSION_LABEL, "error during clear operation: " + e.getMessage());
             }
         }
 
         private void doClear() throws LocalDBException, PwmUnrecoverableException {
             final Instant startTime = Instant.now();
-            LOGGER.debug(PwmConstants.REPORTING_SESSION_LABEL,"clearing cached report data");
+            LOGGER.debug(SessionLabel.REPORTING_SESSION_LABEL,"clearing cached report data");
             clearWorkQueue();
             if (userCacheService != null) {
                 userCacheService.clear();
             }
             summaryData = ReportSummaryData.newSummaryData(settings.getTrackDays());
             reportStatus = new ReportStatusInfo(settings.getSettingsHash());
-            LOGGER.debug(PwmConstants.REPORTING_SESSION_LABEL,"finished clearing report " + TimeDuration.fromCurrent(startTime).asCompactString());
+            LOGGER.debug(SessionLabel.REPORTING_SESSION_LABEL,"finished clearing report " + TimeDuration.fromCurrent(startTime).asCompactString());
         }
     }
 }

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

@@ -3,7 +3,7 @@
  * http://www.pwm-project.org
  *
  * Copyright (c) 2006-2009 Novell, Inc.
- * Copyright (c) 2009-2017 The PWM Project
+ * 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
@@ -25,13 +25,11 @@ package password.pwm.svc.token;
 import password.pwm.bean.SessionLabel;
 import password.pwm.error.PwmOperationalException;
 import password.pwm.error.PwmUnrecoverableException;
-
-import java.util.Collections;
-import java.util.Iterator;
+import password.pwm.util.java.ClosableIterator;
 
 class CryptoTokenMachine implements TokenMachine {
 
-    private TokenService tokenService;
+    private final TokenService tokenService;
 
     CryptoTokenMachine(final TokenService tokenService)
             throws PwmOperationalException
@@ -53,27 +51,41 @@ class CryptoTokenMachine implements TokenMachine {
         return returnString.toString();
     }
 
-    public TokenPayload retrieveToken(final String tokenKey, final SessionLabel sessionLabel)
+    public TokenPayload retrieveToken(final TokenKey tokenKey)
             throws PwmOperationalException, PwmUnrecoverableException
     {
-        if (tokenKey == null || tokenKey.length() < 1) {
+        if (tokenKey == null || tokenKey.getStoredHash().length() < 1) {
             return null;
         }
-        return tokenService.fromEncryptedString(tokenKey);
+        return tokenService.fromEncryptedString(tokenKey.getStoredHash());
     }
 
-    public void storeToken(final String tokenKey, final TokenPayload tokenPayload) throws PwmOperationalException, PwmUnrecoverableException {
+    public void storeToken(final TokenKey tokenKey, final TokenPayload tokenPayload) throws PwmOperationalException, PwmUnrecoverableException {
     }
 
-    public void removeToken(final String tokenKey, final SessionLabel sessionLabel) throws PwmOperationalException, PwmUnrecoverableException {
+    public void removeToken(final TokenKey tokenKey) throws PwmOperationalException, PwmUnrecoverableException {
     }
 
     public int size() throws PwmOperationalException, PwmUnrecoverableException {
         return 0;
     }
 
-    public Iterator keyIterator() throws PwmOperationalException, PwmUnrecoverableException {
-        return Collections.<String>emptyList().iterator();
+    public ClosableIterator<TokenKey> keyIterator() throws PwmOperationalException, PwmUnrecoverableException {
+        return new ClosableIterator<TokenKey>() {
+            @Override
+            public void close() {
+            }
+
+            @Override
+            public boolean hasNext() {
+                return false;
+            }
+
+            @Override
+            public TokenKey next() {
+                return null;
+            }
+        };
     }
 
     public void cleanup() {
@@ -83,4 +95,25 @@ class CryptoTokenMachine implements TokenMachine {
         return true;
     }
 
+    public TokenKey keyFromKey(final String key) throws PwmUnrecoverableException {
+        return new CryptoTokenKey(key);
+    }
+
+    @Override
+    public TokenKey keyFromStoredHash(final String storedHash) {
+        return new CryptoTokenKey(storedHash);
+    }
+
+    private static class CryptoTokenKey implements TokenKey {
+        private String value;
+
+        CryptoTokenKey(final String value) {
+            this.value = value;
+        }
+
+        @Override
+        public String getStoredHash() {
+            return value;
+        }
+    }
 }

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

@@ -1,91 +0,0 @@
-/*
- * Password Management Servlets (PWM)
- * http://www.pwm-project.org
- *
- * Copyright (c) 2006-2009 Novell, Inc.
- * Copyright (c) 2009-2017 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.svc.token;
-
-import password.pwm.bean.SessionLabel;
-import password.pwm.error.PwmOperationalException;
-import password.pwm.error.PwmUnrecoverableException;
-import password.pwm.util.db.DatabaseAccessor;
-import password.pwm.util.db.DatabaseTable;
-
-import java.util.Iterator;
-
-class DBTokenMachine implements TokenMachine {
-    private DatabaseAccessor databaseAccessor;
-    private TokenService tokenService;
-
-    DBTokenMachine(final TokenService tokenService, final DatabaseAccessor databaseAccessor) {
-        this.tokenService = tokenService;
-        this.databaseAccessor = databaseAccessor;
-    }
-
-    public String generateToken(
-            final SessionLabel sessionLabel,
-            final TokenPayload tokenPayload
-    )
-            throws PwmUnrecoverableException, PwmOperationalException
-    {
-        return tokenService.makeUniqueTokenForMachine(sessionLabel, this);
-    }
-
-    public TokenPayload retrieveToken(final String tokenKey, final SessionLabel sessionLabel)
-            throws PwmOperationalException, PwmUnrecoverableException
-    {
-        final String md5sumToken = tokenService.makeTokenHash(tokenKey);
-        final String storedRawValue = databaseAccessor.get(DatabaseTable.TOKENS,md5sumToken);
-
-        if (storedRawValue != null && storedRawValue.length() > 0 ) {
-            return tokenService.fromEncryptedString(storedRawValue);
-        }
-
-        return null;
-    }
-
-    public void storeToken(final String tokenKey, final TokenPayload tokenPayload) throws PwmOperationalException, PwmUnrecoverableException {
-        final String rawValue = tokenService.toEncryptedString(tokenPayload);
-        final String md5sumToken = tokenService.makeTokenHash(tokenKey);
-        databaseAccessor.put(DatabaseTable.TOKENS, md5sumToken, rawValue);
-    }
-
-    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);
-    }
-
-    public int size() throws PwmOperationalException, PwmUnrecoverableException {
-        return databaseAccessor.size(DatabaseTable.TOKENS);
-    }
-
-    public Iterator keyIterator() throws PwmOperationalException, PwmUnrecoverableException {
-        return databaseAccessor.iterator(DatabaseTable.TOKENS);
-    }
-
-    public void cleanup() throws PwmUnrecoverableException, PwmOperationalException {
-        tokenService.purgeOutdatedTokens();
-    }
-
-    public boolean supportsName() {
-        return true;
-    }
-}

+ 149 - 0
src/main/java/password/pwm/svc/token/DataStoreTokenMachine.java

@@ -0,0 +1,149 @@
+package password.pwm.svc.token;
+
+import password.pwm.PwmApplication;
+import password.pwm.bean.SessionLabel;
+import password.pwm.config.Configuration;
+import password.pwm.config.PwmSetting;
+import password.pwm.error.PwmException;
+import password.pwm.error.PwmOperationalException;
+import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.svc.PwmService;
+import password.pwm.util.DataStore;
+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 java.time.Instant;
+import java.util.concurrent.TimeUnit;
+
+public class DataStoreTokenMachine implements TokenMachine {
+    private static final PwmLogger LOGGER = PwmLogger.forClass(DataStoreTokenMachine.class);
+    private final TokenService tokenService;
+    private final TimeDuration maxTokenPurgeAge;
+
+    private final DataStore dataStore;
+
+    private final PwmApplication pwmApplication;
+
+    DataStoreTokenMachine(
+            final PwmApplication pwmApplication,
+            final TokenService tokenService,
+            final DataStore dataStore
+    ) {
+        this.pwmApplication = pwmApplication;
+        this.tokenService = tokenService;
+        this.dataStore = dataStore;
+
+        final Configuration configuration = pwmApplication.getConfig();
+
+        final long maxTokenAgeMS = configuration.readSettingAsLong(PwmSetting.TOKEN_LIFETIME) * 1000;
+        this.maxTokenPurgeAge = new TimeDuration(maxTokenAgeMS, TimeUnit.MILLISECONDS);
+    }
+
+    @Override
+    public TokenKey keyFromKey(final String key) throws PwmUnrecoverableException {
+        return StoredTokenKey.fromKeyValue(pwmApplication, key);
+    }
+
+    @Override
+    public TokenKey keyFromStoredHash(final String storedHash) {
+        return StoredTokenKey.fromStoredHash(storedHash);
+    }
+
+    public void cleanup() throws PwmUnrecoverableException, PwmOperationalException {
+        if (size() < 1) {
+            return;
+        }
+
+        purgeOutdatedTokens();
+    }
+
+    private void purgeOutdatedTokens() throws
+            PwmUnrecoverableException, PwmOperationalException
+    {
+        final Instant startTime = Instant.now();
+        LOGGER.trace("beginning purge cycle; database size = " + size());
+        try (ClosableIterator<String> keyIterator = dataStore.iterator()) {
+            while (tokenService.status() == PwmService.STATUS.OPEN && keyIterator.hasNext()) {
+                final String storedHash = keyIterator.next();
+                final TokenKey loopKey = keyFromStoredHash(storedHash);
+                retrieveToken(loopKey); // retrieving token tests validity and causes purging
+            }
+        } catch (Exception e) {
+            LOGGER.error("unexpected error while cleaning expired stored tokens: " + e.getMessage());
+        }
+        LOGGER.trace("completed record purge cycle in " + TimeDuration.fromCurrent(startTime).asCompactString()
+                + "; database size = " + size());
+    }
+
+    private boolean testIfTokenNeedsPurging(final TokenPayload theToken) {
+        if (theToken == null) {
+            return false;
+        }
+        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 = TimeDuration.fromCurrent(issueDate);
+        return duration.isLongerThan(maxTokenPurgeAge);
+    }
+
+    public String generateToken(
+            final SessionLabel sessionLabel,
+            final TokenPayload tokenPayload
+    )
+            throws PwmUnrecoverableException, PwmOperationalException
+    {
+        return tokenService.makeUniqueTokenForMachine(sessionLabel, this);
+    }
+
+    public TokenPayload retrieveToken(final TokenKey tokenKey)
+            throws PwmOperationalException, PwmUnrecoverableException
+    {
+        final String storedHash = tokenKey.getStoredHash();
+        final String storedRawValue = dataStore.get(storedHash);
+
+        if (storedRawValue != null && storedRawValue.length() > 0 ) {
+            final TokenPayload tokenPayload;
+            try {
+                tokenPayload = tokenService.fromEncryptedString(storedRawValue);
+            } catch (PwmException e) {
+                LOGGER.trace("error while trying to decrypted stored token payload for key '" + storedHash + "', will purge record, error: " + e.getMessage());
+                dataStore.remove(storedHash);
+                return null;
+            }
+
+            if (testIfTokenNeedsPurging(tokenPayload)) {
+                LOGGER.trace("stored token key '" + storedHash + "', has an outdated issue date and will be purged");
+                dataStore.remove(storedHash);
+            } else {
+                return tokenPayload;
+            }
+        }
+
+        return null;
+    }
+
+    public void storeToken(final TokenKey tokenKey, final TokenPayload tokenPayload) throws PwmOperationalException, PwmUnrecoverableException {
+        final String rawValue = tokenService.toEncryptedString(tokenPayload);
+        final String storedHash = tokenKey.getStoredHash();
+        dataStore.put(storedHash, rawValue);
+    }
+
+    public void removeToken(final TokenKey tokenKey)
+            throws PwmOperationalException, PwmUnrecoverableException
+    {
+        final String storedHash = tokenKey.getStoredHash();
+        dataStore.remove(storedHash);
+    }
+
+    public int size() throws PwmOperationalException {
+        return dataStore.size();
+    }
+
+    public boolean supportsName() {
+        return true;
+    }
+}

+ 23 - 19
src/main/java/password/pwm/svc/token/LdapTokenMachine.java

@@ -3,7 +3,7 @@
  * http://www.pwm-project.org
  *
  * Copyright (c) 2006-2009 Novell, Inc.
- * Copyright (c) 2009-2017 The PWM Project
+ * 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
@@ -34,16 +34,14 @@ 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.search.SearchConfiguration;
 import password.pwm.ldap.search.UserSearchEngine;
 
-import java.util.Collections;
 import java.util.HashMap;
-import java.util.Iterator;
 import java.util.Map;
 
-class LdapTokenMachine implements TokenMachine {
+class LdapTokenMachine  implements TokenMachine {
     private PwmApplication pwmApplication;
     private String tokenAttribute;
     private final String KEY_VALUE_DELIMITER = " ";
@@ -66,29 +64,28 @@ class LdapTokenMachine implements TokenMachine {
         return tokenService.makeUniqueTokenForMachine(sessionLabel, this);
     }
 
-    public TokenPayload retrieveToken(final String tokenKey, final SessionLabel sessionLabel)
+    public TokenPayload retrieveToken(final TokenKey tokenKey)
             throws PwmOperationalException, PwmUnrecoverableException
     {
         final String searchFilter;
         {
-            final String md5sumToken = tokenService.makeTokenHash(tokenKey);
+            final String storedHash = tokenKey.getStoredHash();
             final SearchHelper tempSearchHelper = new SearchHelper();
             final Map<String,String> filterAttributes = new HashMap<>();
             for (final String loopStr : pwmApplication.getConfig().readSettingAsStringArray(PwmSetting.DEFAULT_OBJECT_CLASSES)) {
                 filterAttributes.put("objectClass", loopStr);
             }
-            filterAttributes.put(tokenAttribute,md5sumToken + "*");
+            filterAttributes.put(tokenAttribute,storedHash + "*");
             tempSearchHelper.setFilterAnd(filterAttributes);
             searchFilter = tempSearchHelper.getFilter();
         }
 
         try {
-            final UserSearchEngine userSearchEngine = pwmApplication.getUserSearchEngine();
+            final UserSearchEngine userSearchEngine = new UserSearchEngine();
             final SearchConfiguration searchConfiguration = SearchConfiguration.builder()
                     .filter(searchFilter)
                     .build();
-
-            final UserIdentity user = userSearchEngine.performSingleUserSearch(searchConfiguration, sessionLabel);
+            final UserIdentity user = userSearchEngine.performSingleUserSearch(searchConfiguration, null);
             if (user == null) {
                 return null;
             }
@@ -116,11 +113,11 @@ class LdapTokenMachine implements TokenMachine {
         return null;
     }
 
-    public void storeToken(final String tokenKey, final TokenPayload tokenPayload)
+    public void storeToken(final TokenKey tokenKey, final TokenPayload tokenPayload)
             throws PwmOperationalException, PwmUnrecoverableException
     {
         try {
-            final String md5sumToken = tokenService.makeTokenHash(tokenKey);
+            final String md5sumToken = tokenKey.getStoredHash();
             final String encodedTokenPayload = tokenService.toEncryptedString(tokenPayload);
 
             final UserIdentity userIdentity = tokenPayload.getUserIdentity();
@@ -133,10 +130,10 @@ class LdapTokenMachine implements TokenMachine {
         }
     }
 
-    public void removeToken(final String tokenKey, final SessionLabel sessionLabel)
+    public void removeToken(final TokenKey tokenKey)
             throws PwmOperationalException, PwmUnrecoverableException
     {
-        final TokenPayload payload = retrieveToken(tokenKey, sessionLabel);
+        final TokenPayload payload = retrieveToken(tokenKey);
         if (payload != null) {
             final UserIdentity userIdentity = payload.getUserIdentity();
             try {
@@ -154,10 +151,6 @@ class LdapTokenMachine implements TokenMachine {
         return -1;
     }
 
-    public Iterator keyIterator() throws PwmOperationalException {
-        return Collections.<String>emptyList().iterator();
-    }
-
     public void cleanup() throws PwmUnrecoverableException, PwmOperationalException {
     }
 
@@ -165,4 +158,15 @@ class LdapTokenMachine implements TokenMachine {
     public boolean supportsName() {
         return false;
     }
+
+    @Override
+    public TokenKey keyFromKey(final String key) throws PwmUnrecoverableException {
+        return StoredTokenKey.fromKeyValue(pwmApplication, key);
+    }
+
+    @Override
+    public TokenKey keyFromStoredHash(final String storedHash) {
+        return StoredTokenKey.fromStoredHash(storedHash);
+    }
+
 }

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

@@ -1,93 +0,0 @@
-/*
- * Password Management Servlets (PWM)
- * http://www.pwm-project.org
- *
- * Copyright (c) 2006-2009 Novell, Inc.
- * Copyright (c) 2009-2017 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.svc.token;
-
-import password.pwm.bean.SessionLabel;
-import password.pwm.error.PwmOperationalException;
-import password.pwm.error.PwmUnrecoverableException;
-import password.pwm.util.localdb.LocalDB;
-
-class LocalDBTokenMachine implements TokenMachine {
-    private LocalDB localDB;
-    private TokenService tokenService;
-
-    LocalDBTokenMachine(
-            final TokenService tokenService,
-            final LocalDB localDB
-    ) {
-        this.tokenService = tokenService;
-        this.localDB = localDB;
-    }
-
-    public String generateToken(
-            final SessionLabel sessionLabel,
-            final TokenPayload tokenPayload
-    )
-            throws PwmUnrecoverableException, PwmOperationalException
-    {
-        return tokenService.makeUniqueTokenForMachine(sessionLabel, this);
-    }
-
-    public TokenPayload retrieveToken(final String tokenKey, final SessionLabel sessionLabel)
-            throws PwmOperationalException, PwmUnrecoverableException
-    {
-        final String md5sumToken = tokenService.makeTokenHash(tokenKey);
-        final String storedRawValue = localDB.get(LocalDB.DB.TOKENS, md5sumToken);
-
-        if (storedRawValue != null && storedRawValue.length() > 0 ) {
-            return tokenService.fromEncryptedString(storedRawValue);
-        }
-
-        return null;
-    }
-
-    public void storeToken(final String tokenKey, final TokenPayload tokenPayload) throws PwmOperationalException, PwmUnrecoverableException {
-        final String rawValue = tokenService.toEncryptedString(tokenPayload);
-        final String md5sumToken = tokenService.makeTokenHash(tokenKey);
-        localDB.put(LocalDB.DB.TOKENS, md5sumToken, rawValue);
-    }
-
-    public void removeToken(final String tokenKey, final SessionLabel sessionLabel)
-            throws PwmOperationalException, PwmUnrecoverableException
-    {
-        final String md5sumToken = tokenService.makeTokenHash(tokenKey);
-        localDB.remove(LocalDB.DB.TOKENS, tokenKey);
-        localDB.remove(LocalDB.DB.TOKENS, md5sumToken);
-    }
-
-    public int size() throws PwmOperationalException {
-        return localDB.size(LocalDB.DB.TOKENS);
-    }
-
-    public LocalDB.LocalDBIterator<String> keyIterator() throws PwmOperationalException {
-        return localDB.iterator(LocalDB.DB.TOKENS);
-    }
-
-    public void cleanup() throws PwmUnrecoverableException, PwmOperationalException {
-        tokenService.purgeOutdatedTokens();
-    }
-
-    public boolean supportsName() {
-        return true;
-    }
-}

+ 70 - 0
src/main/java/password/pwm/svc/token/StoredTokenKey.java

@@ -0,0 +1,70 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2017 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.svc.token;
+
+import password.pwm.PwmApplication;
+import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.util.secure.SecureService;
+
+class StoredTokenKey implements TokenKey {
+    private static final String SUFFIX = "-hash";
+
+    private final String storedHash;
+
+    private StoredTokenKey(final String storedHash) {
+        this.storedHash = storedHash;
+    }
+
+    public String getStoredHash() {
+        return storedHash;
+    }
+    
+    static StoredTokenKey fromStoredHash(final String storedHash) {
+        if (storedHash == null) {
+            throw new NullPointerException();
+        }
+        
+        if (!storedHash.endsWith(SUFFIX)) {
+            throw new IllegalArgumentException("stored hash value has improper suffix");
+        }
+        
+        return new StoredTokenKey(storedHash);
+    }
+
+    static StoredTokenKey fromKeyValue(final PwmApplication pwmApplication, final String input)
+            throws PwmUnrecoverableException
+    {
+        if (input == null) {
+            throw new NullPointerException();
+        }
+
+        if (input.endsWith(SUFFIX)) {
+            throw new IllegalArgumentException("new key value has stored suffix");
+        }
+
+        final SecureService secureService = pwmApplication.getSecureService();
+        final String storedHash = secureService.hash(input) + SUFFIX;
+
+        return new StoredTokenKey(storedHash);
+    }
+}

+ 27 - 0
src/main/java/password/pwm/svc/token/TokenKey.java

@@ -0,0 +1,27 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2017 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.svc.token;
+
+interface TokenKey {
+    String getStoredHash();
+}

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

@@ -3,7 +3,7 @@
  * http://www.pwm-project.org
  *
  * Copyright (c) 2006-2009 Novell, Inc.
- * Copyright (c) 2009-2017 The PWM Project
+ * 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
@@ -26,29 +26,28 @@ import password.pwm.bean.SessionLabel;
 import password.pwm.error.PwmOperationalException;
 import password.pwm.error.PwmUnrecoverableException;
 
-import java.util.Iterator;
-
 interface TokenMachine {
     String generateToken( SessionLabel sessionLabel,  TokenPayload tokenPayload)
             throws PwmUnrecoverableException, PwmOperationalException;
 
-    TokenPayload retrieveToken(String tokenKey, SessionLabel sessionLabel)
+    TokenPayload retrieveToken(TokenKey tokenKey)
             throws PwmOperationalException, PwmUnrecoverableException;
 
-    void storeToken( String tokenKey,  TokenPayload tokenPayload)
+    void storeToken(TokenKey tokenKey, TokenPayload tokenPayload)
             throws PwmOperationalException, PwmUnrecoverableException;
 
-    void removeToken(String tokenKey, SessionLabel sessionLabel)
+    void removeToken(TokenKey tokenKey)
             throws PwmOperationalException, PwmUnrecoverableException;
 
     int size()
             throws PwmOperationalException, PwmUnrecoverableException;
 
-    Iterator<String> keyIterator()
-            throws PwmOperationalException, PwmUnrecoverableException;
-
     void cleanup()
             throws PwmUnrecoverableException, PwmOperationalException;
 
     boolean supportsName();
+
+    TokenKey keyFromKey(String key) throws PwmUnrecoverableException;
+
+    TokenKey keyFromStoredHash(String storedHash);
 }

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

@@ -22,19 +22,27 @@
 
 package password.pwm.svc.token;
 
+import com.google.gson.annotations.SerializedName;
+import lombok.Getter;
 import password.pwm.bean.UserIdentity;
+import password.pwm.util.java.JavaHelper;
+import password.pwm.util.java.JsonUtil;
 
 import java.io.Serializable;
 import java.time.Instant;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.Map;
 import java.util.Set;
 
+@Getter
 public class TokenPayload implements Serializable {
     private final Instant date;
     private final String name;
     private final Map<String,String> data;
-    private final UserIdentity user;
+
+    @SerializedName("user")
+    private final UserIdentity userIdentity;
     private final Set<String> dest;
     private final String guid;
 
@@ -42,32 +50,18 @@ public class TokenPayload implements Serializable {
         this.date = Instant.now();
         this.data = data == null ? Collections.emptyMap() : Collections.unmodifiableMap(data);
         this.name = name;
-        this.user = user;
+        this.userIdentity = user;
         this.dest = dest == null ? Collections.emptySet() : Collections.unmodifiableSet(dest);
         this.guid = guid;
     }
 
-    public Instant getDate() {
-        return date;
-    }
-
-    public String getName() {
-        return name;
-    }
-
-    public Map<String, String> getData() {
-        return data;
-    }
-
-    public UserIdentity getUserIdentity() {
-        return user;
-    }
-
-    public Set<String> getDest() {
-        return dest;
-    }
 
-    public String getGuid() {
-        return guid;
+    public String toDebugString() {
+        final Map<String,String> debugMap = new HashMap<>();
+        debugMap.put("date", JavaHelper.toIsoDate(date));
+        debugMap.put("name", getName());
+        debugMap.put("user", getUserIdentity().toDisplayString());
+        debugMap.put("guid", getGuid());
+        return JsonUtil.serializeMap(debugMap);
     }
 }

+ 83 - 121
src/main/java/password/pwm/svc/token/TokenService.java

@@ -3,7 +3,7 @@
  * http://www.pwm-project.org
  *
  * Copyright (c) 2006-2009 Novell, Inc.
- * Copyright (c) 2009-2017 The PWM Project
+ * 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
@@ -54,10 +54,14 @@ import password.pwm.svc.event.AuditRecordFactory;
 import password.pwm.svc.intruder.RecordType;
 import password.pwm.svc.stats.Statistic;
 import password.pwm.svc.stats.StatisticsManager;
+import password.pwm.util.DataStore;
+import password.pwm.util.db.DatabaseDataStore;
+import password.pwm.util.db.DatabaseTable;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.JsonUtil;
 import password.pwm.util.java.TimeDuration;
 import password.pwm.util.localdb.LocalDB;
+import password.pwm.util.localdb.LocalDBDataStore;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.macro.MacroMachine;
 import password.pwm.util.operations.PasswordUtility;
@@ -67,7 +71,7 @@ import password.pwm.util.secure.SecureService;
 import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Collections;
-import java.util.Iterator;
+
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -87,8 +91,8 @@ public class TokenService implements PwmService {
 
     private static final PwmLogger LOGGER = PwmLogger.forClass(TokenService.class);
 
-    private static final long MAX_CLEANER_INTERVAL_MS = 24 * 60 * 60 * 1000; // one day
-    private static final long MIN_CLEANER_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes
+    private static final TimeDuration MAX_CLEANER_INTERVAL_MS = new TimeDuration(1, TimeUnit.DAYS);
+    private static final TimeDuration MIN_CLEANER_INTERVAL_MS = new TimeDuration(5, TimeUnit.MINUTES);
 
     private ScheduledExecutorService executorService;
 
@@ -96,7 +100,6 @@ public class TokenService implements PwmService {
     private Configuration configuration;
     private TokenStorageMethod storageMethod;
     private long maxTokenAgeMS;
-    private long maxTokenPurgeAgeMS;
     private TokenMachine tokenMachine;
     private long counter;
 
@@ -105,12 +108,19 @@ public class TokenService implements PwmService {
 
     private ErrorInformation errorInformation = null;
 
+    private boolean verifyPwModifyTime = true;
+
     public TokenService()
             throws PwmOperationalException
     {
     }
 
-    public synchronized TokenPayload createTokenPayload(final TokenType name, final Map<String, String> data, final UserIdentity userIdentity, final Set<String> dest) {
+    public synchronized TokenPayload createTokenPayload(
+            final TokenType name,
+            final Map<String, String> data,
+            final UserIdentity userIdentity,
+            final Set<String> dest
+    ) {
         final long count = counter++;
         final StringBuilder guid = new StringBuilder();
         try {
@@ -142,7 +152,6 @@ public class TokenService implements PwmService {
         }
         try {
             maxTokenAgeMS = configuration.readSettingAsLong(PwmSetting.TOKEN_LIFETIME) * 1000;
-            maxTokenPurgeAgeMS = maxTokenAgeMS + Long.parseLong(configuration.readAppProperty(AppProperty.TOKEN_REMOVAL_DELAY_MS));
         } catch (Exception e) {
             final String errorMsg = "unable to parse max token age value: " + e.getMessage();
             errorInformation = new ErrorInformation(PwmError.ERROR_INVALID_CONFIG,errorMsg);
@@ -153,15 +162,19 @@ public class TokenService implements PwmService {
         try {
             DataStorageMethod usedStorageMethod = null;
             switch (storageMethod) {
-                case STORE_LOCALDB:
-                    tokenMachine = new LocalDBTokenMachine(this, pwmApplication.getLocalDB());
+                case STORE_LOCALDB: {
+                    final DataStore dataStore = new LocalDBDataStore(pwmApplication.getLocalDB(), LocalDB.DB.TOKENS);
+                    tokenMachine = new DataStoreTokenMachine(pwmApplication, this, dataStore);
                     usedStorageMethod = DataStorageMethod.LOCALDB;
                     break;
+                }
 
-                case STORE_DB:
-                    tokenMachine = new DBTokenMachine(this, pwmApplication.getDatabaseAccessor());
+                case STORE_DB: {
+                    final DataStore dataStore = new DatabaseDataStore(pwmApplication.getDatabaseAccessor(), DatabaseTable.TOKENS);
+                    tokenMachine = new DataStoreTokenMachine(pwmApplication, this, dataStore);
                     usedStorageMethod = DataStorageMethod.DB;
                     break;
+                }
 
                 case STORE_CRYPTO:
                     tokenMachine = new CryptoTokenMachine(this);
@@ -194,11 +207,13 @@ public class TokenService implements PwmService {
 
         final TimerTask cleanerTask = new CleanerTask();
 
-        final long cleanerFrequency = (maxTokenAgeMS*0.5) > MAX_CLEANER_INTERVAL_MS
-                ? MAX_CLEANER_INTERVAL_MS
-                : (maxTokenAgeMS*0.5) < MIN_CLEANER_INTERVAL_MS
-                ? MIN_CLEANER_INTERVAL_MS
-                : (long)(maxTokenAgeMS*0.5);
+        final long cleanerFrequency = Math.max(
+                MIN_CLEANER_INTERVAL_MS.getTotalMilliseconds(),
+                Math.min(
+                        maxTokenAgeMS / 2,
+                        MAX_CLEANER_INTERVAL_MS.getTotalMilliseconds()
+                ));
+
 
         executorService.scheduleAtFixedRate(cleanerTask, 10 * 1000, cleanerFrequency, TimeUnit.MILLISECONDS);
         LOGGER.trace("token cleanup will occur every " + TimeDuration.asCompactString(cleanerFrequency));
@@ -210,6 +225,8 @@ public class TokenService implements PwmService {
             /* noop */
         }
 
+        verifyPwModifyTime = Boolean.parseBoolean(configuration.readAppProperty(AppProperty.TOKEN_VERIFY_PW_MODIFY_TIME));
+
         status = STATUS.OPEN;
         LOGGER.debug("open");
     }
@@ -226,14 +243,14 @@ public class TokenService implements PwmService {
         final String tokenKey;
         try {
             tokenKey = tokenMachine.generateToken(sessionLabel, tokenPayload);
-            tokenMachine.storeToken(tokenKey, tokenPayload);
+            tokenMachine.storeToken(tokenMachine.keyFromKey(tokenKey), tokenPayload);
         } catch (PwmException e) {
             final String errorMsg = "unexpected error trying to store token in datastore: " + e.getMessage();
             final ErrorInformation errorInformation = new ErrorInformation(e.getError(),errorMsg);
             throw new PwmOperationalException(errorInformation);
         }
 
-        LOGGER.trace(sessionLabel, "generated token type=" + tokenPayload.getName());
+        LOGGER.trace(sessionLabel, "generated toke with payload: "  + tokenPayload.toDebugString());
 
         final AuditRecord auditRecord = new AuditRecordFactory(pwmApplication).createUserAuditRecord(
                 AuditEvent.TOKEN_ISSUED,
@@ -246,18 +263,28 @@ public class TokenService implements PwmService {
     }
 
 
-    public void markTokenAsClaimed(final String tokenKey, final PwmSession pwmSession) throws PwmUnrecoverableException {
-
-        final TokenPayload tokenPayload;
-        try {
-            tokenPayload = retrieveTokenData(tokenKey, pwmSession.getLabel());
-        } catch (PwmOperationalException e) {
+    private void markTokenAsClaimed(
+            final TokenKey tokenKey,
+            final PwmSession pwmSession,
+            final TokenPayload tokenPayload
+    )
+            throws PwmUnrecoverableException
+    {
+        if (tokenPayload == null || tokenPayload.getUserIdentity() == null) {
             return;
         }
 
-        if (tokenPayload == null || tokenPayload.getUserIdentity() == null) {
-            return;
+        final boolean removeOnClaim = Boolean.parseBoolean(configuration.readAppProperty(AppProperty.TOKEN_REMOVE_ON_CLAIM));
+
+        if (removeOnClaim) {
+            try {
+                LOGGER.trace(pwmSession, "removing claimed token: " + tokenPayload.toDebugString());
+                tokenMachine.removeToken(tokenKey);
+            } catch (PwmOperationalException e) {
+                LOGGER.error(pwmSession, "error clearing claimed token: " + e.getMessage());
+            }
         }
+
         final AuditRecord auditRecord = new AuditRecordFactory(pwmApplication).createUserAuditRecord(
                 AuditEvent.TOKEN_CLAIMED,
                 tokenPayload.getUserIdentity(),
@@ -266,27 +293,22 @@ public class TokenService implements PwmService {
         );
         pwmApplication.getAuditManager().submit(auditRecord);
 
-
         StatisticsManager.incrementStat(pwmApplication, Statistic.TOKENS_PASSSED);
     }
 
-    public TokenPayload retrieveTokenData(final String tokenKey, final SessionLabel sessionLabel)
+    public TokenPayload retrieveTokenData(final SessionLabel sessionLabel, final String tokenKey)
             throws PwmOperationalException
     {
         checkStatus();
 
         try {
-            final TokenPayload storedToken = tokenMachine.retrieveToken(tokenKey, sessionLabel);
+            final TokenPayload storedToken = tokenMachine.retrieveToken(tokenMachine.keyFromKey(tokenKey));
             if (storedToken != null) {
 
-                if (testIfTokenIsExpired(storedToken)) {
+                if (testIfTokenIsExpired(sessionLabel, storedToken)) {
                     throw new PwmOperationalException(new ErrorInformation(PwmError.ERROR_TOKEN_EXPIRED));
                 }
 
-                if (testIfTokenIsPurgable(storedToken)) {
-                    tokenMachine.removeToken(tokenKey, sessionLabel);
-                }
-
                 return storedToken;
             }
         } catch (PwmException e) {
@@ -340,88 +362,19 @@ public class TokenService implements PwmService {
         return returnRecords;
     }
 
-
-    private boolean testIfTokenIsExpired(final TokenPayload theToken) {
+    private boolean testIfTokenIsExpired(final SessionLabel sessionLabel, final TokenPayload theToken) {
         if (theToken == null) {
             return false;
         }
         final Instant issueDate = theToken.getDate();
         if (issueDate == null) {
-            LOGGER.error("retrieved token has no issueDate, marking as expired: " + JsonUtil.serialize(theToken));
+            LOGGER.error(sessionLabel, "retrieved token has no issueDate, marking as expired: " + theToken.toDebugString());
             return true;
         }
         final TimeDuration duration = TimeDuration.fromCurrent(issueDate);
         return duration.isLongerThan(maxTokenAgeMS);
     }
 
-    private boolean testIfTokenIsPurgable(final TokenPayload theToken) {
-        if (theToken == null) {
-            return false;
-        }
-        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 = TimeDuration.fromCurrent(issueDate);
-        return duration.isLongerThan(maxTokenPurgeAgeMS);
-    }
-
-
-    void purgeOutdatedTokens() throws
-            PwmUnrecoverableException, PwmOperationalException
-    {
-        final long startTime = System.currentTimeMillis();
-        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, PwmConstants.TOKEN_SESSION_LABEL));
-        while (status() == STATUS.OPEN && !tempKeyList.isEmpty()) {
-            for (final String loopKey : tempKeyList) {
-                tokenMachine.removeToken(loopKey, PwmConstants.HEALTH_SESSION_LABEL);
-            }
-            cleanedTokens = cleanedTokens + tempKeyList.size();
-            tempKeyList.clear();
-        }
-        if (cleanedTokens > 0) {
-            LOGGER.trace("cleaner thread removed " + cleanedTokens + " tokens in " + TimeDuration.fromCurrent(startTime).asCompactString());
-        }
-    }
-
-    private List<String> discoverPurgeableTokenKeys(final int maxCount, final SessionLabel sessionLabel)
-            throws PwmUnrecoverableException, PwmOperationalException
-    {
-        final List<String> returnList = new ArrayList<>();
-        Iterator<String> keyIterator = null;
-
-        try {
-            keyIterator = tokenMachine.keyIterator();
-
-            while (status() == STATUS.OPEN && returnList.size() < maxCount && keyIterator.hasNext()) {
-                final String loopKey = keyIterator.next();
-                final TokenPayload loopInfo = tokenMachine.retrieveToken(loopKey, sessionLabel);
-                if (loopInfo != null) {
-                    if (testIfTokenIsPurgable(loopInfo)) {
-                        returnList.add(loopKey);
-                    }
-                }
-            }
-        } catch (Exception e) {
-            LOGGER.error("unexpected error while cleaning expired stored tokens: " + e.getMessage());
-        } finally {
-            if (keyIterator != null && storageMethod == TokenStorageMethod.STORE_LOCALDB) {
-                try {
-                    ((LocalDB.LocalDBIterator)keyIterator).close();
-                } catch (Exception e) {
-                    LOGGER.error("unexpected error returning LocalDB token DB iterator: " + e.getMessage());
-                }
-            }
-        }
-
-        return returnList;
-    }
-
-
     private static String makeRandomCode(final Configuration config) {
         final String RANDOM_CHARS = config.readSettingAsString(PwmSetting.TOKEN_CHARACTERS);
         final int CODE_LENGTH = (int) config.readSettingAsLong(PwmSetting.TOKEN_LENGTH);
@@ -469,7 +422,8 @@ 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, sessionLabel) != null) {
+            final TokenPayload existingPayload = machine.retrieveToken(tokenMachine.keyFromKey(tokenKey));
+            if (existingPayload != null) {
                 tokenKey = null;
             }
             attempts++;
@@ -479,15 +433,10 @@ public class TokenService implements PwmService {
             throw new PwmOperationalException(new ErrorInformation(PwmError.ERROR_UNKNOWN,"unable to generate a unique token key after " + attempts + " attempts"));
         }
 
-        LOGGER.trace(sessionLabel, "found new, unique, random token value");
+        LOGGER.trace(sessionLabel, "created new unique random token value after " + attempts + " attempts");
         return tokenKey;
     }
 
-     String makeTokenHash(final String tokenKey) throws PwmUnrecoverableException {
-        final SecureService secureService = pwmApplication.getSecureService();
-        return secureService.hash(tokenKey) + "-hash";
-    }
-
     private static boolean tokensAreUsedInConfig(final Configuration configuration) {
         if (configuration.readSettingAsBoolean(PwmSetting.NEWUSER_ENABLE)) {
             for (final NewUserProfile newUserProfile : configuration.getNewUserProfiles().values()) {
@@ -529,7 +478,7 @@ public class TokenService implements PwmService {
             final String decryptedString = pwmApplication.getSecureService().decryptStringValue(deWhiteSpacedToken);
             return JsonUtil.deserialize(decryptedString, TokenPayload.class);
         } catch (PwmUnrecoverableException e) {
-            final String errorMsg = "unable to decrypt user supplied token value: " + e.getErrorInformation().toDebugStr();
+            final String errorMsg = "unable to decrypt token payload: " + e.getErrorInformation().toDebugStr();
             final ErrorInformation errorInformation = new ErrorInformation(PwmError.ERROR_TOKEN_INCORRECT,errorMsg);
             throw new PwmOperationalException(errorInformation);
         }
@@ -550,7 +499,6 @@ public class TokenService implements PwmService {
     {
         try {
             final TokenPayload tokenPayload = processUserEnteredCodeImpl(
-                    pwmApplication,
                     pwmSession,
                     sessionUserIdentity,
                     tokenType,
@@ -561,7 +509,7 @@ public class TokenService implements PwmService {
                     pwmApplication.getIntruderManager().clear(RecordType.TOKEN_DEST, dest);
                 }
             }
-            pwmApplication.getTokenService().markTokenAsClaimed(userEnteredCode, pwmSession);
+            markTokenAsClaimed(tokenMachine.keyFromKey(userEnteredCode), pwmSession, tokenPayload);
             return tokenPayload;
         } catch (Exception e) {
             final ErrorInformation errorInformation;
@@ -584,8 +532,7 @@ public class TokenService implements PwmService {
         }
     }
 
-    private static TokenPayload processUserEnteredCodeImpl(
-            final PwmApplication pwmApplication,
+    private TokenPayload processUserEnteredCodeImpl(
             final PwmSession pwmSession,
             final UserIdentity sessionUserIdentity,
             final TokenType tokenType,
@@ -595,7 +542,7 @@ public class TokenService implements PwmService {
     {
         final TokenPayload tokenPayload;
         try {
-            tokenPayload = pwmApplication.getTokenService().retrieveTokenData(userEnteredCode, pwmSession.getLabel());
+            tokenPayload = pwmApplication.getTokenService().retrieveTokenData(pwmSession.getLabel(), userEnteredCode);
         } 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);
@@ -606,7 +553,7 @@ public class TokenService implements PwmService {
             throw new PwmOperationalException(errorInformation);
         }
 
-        LOGGER.trace(pwmSession, "retrieved tokenPayload: " + JsonUtil.serialize(tokenPayload));
+        LOGGER.trace(pwmSession, "retrieved tokenPayload: " + tokenPayload.toDebugString());
 
         if (tokenType != null && pwmApplication.getTokenService().supportsName()) {
             if (!tokenType.matchesName(tokenPayload.getName()) ) {
@@ -624,17 +571,32 @@ public class TokenService implements PwmService {
         }
 
         // check if password-last-modified is same as when tried to read it before.
-        if (tokenPayload.getUserIdentity() != null && tokenPayload.getData() != null && tokenPayload.getData().containsKey(PwmConstants.TOKEN_KEY_PWD_CHG_DATE)) {
+        if (verifyPwModifyTime
+                && tokenPayload.getUserIdentity() != null
+                && tokenPayload.getData() != null
+                && tokenPayload.getData().containsKey(PwmConstants.TOKEN_KEY_PWD_CHG_DATE)
+                ) {
             try {
                 final Instant userLastPasswordChange = PasswordUtility.determinePwdLastModified(
                         pwmApplication,
                         pwmSession.getLabel(),
                         tokenPayload.getUserIdentity());
+
                 final String dateStringInToken = tokenPayload.getData().get(PwmConstants.TOKEN_KEY_PWD_CHG_DATE);
+
+                LOGGER.trace(pwmSession, "tokenPayload=" + tokenPayload.toDebugString()
+                        + ", sessionUser=" + (sessionUserIdentity == null ? "null" : sessionUserIdentity.toDisplayString())
+                        + ", payloadUserIdentity=" + tokenPayload.getUserIdentity().toDisplayString()
+                        + ", userLastPasswordChange=" + JavaHelper.toIsoDate(userLastPasswordChange)
+                        + ", dateStringInToken=" + dateStringInToken);
+
                 if (userLastPasswordChange != null && dateStringInToken != null) {
+
                     final String userChangeString = JavaHelper.toIsoDate(userLastPasswordChange);
+
                     if (!dateStringInToken.equalsIgnoreCase(userChangeString)) {
-                        final String errorString = "user password has changed since token issued, token rejected";
+                        final String errorString = "user password has changed since token issued, token rejected; currentValue=" + userChangeString + ", tokenValue=" + dateStringInToken;
+                        LOGGER.trace(pwmSession, errorString + "; token=" + tokenPayload.toDebugString());
                         final ErrorInformation errorInformation = new ErrorInformation(PwmError.ERROR_TOKEN_EXPIRED, errorString);
                         throw new PwmOperationalException(errorInformation);
                     }

+ 1 - 1
src/main/java/password/pwm/util/Validator.java

@@ -98,7 +98,7 @@ public class Validator {
                         submittedPwmFormID,
                         FormNonce.class
                 );
-                final String submittedRequestVerificationKey = String.valueOf(formNonce.getRequestID());
+                final String submittedRequestVerificationKey = String.valueOf(formNonce.getReqCounter());
                 if (!requestVerificationKey.equals(submittedRequestVerificationKey)) {
                     final String debugMsg = "expectedPageID=" + requestVerificationKey
                             + ", submittedPageID=" + submittedRequestVerificationKey

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

@@ -22,7 +22,7 @@
 
 package password.pwm.util.cli.commands;
 
-import password.pwm.PwmConstants;
+import password.pwm.bean.SessionLabel;
 import password.pwm.config.stored.ConfigurationProperty;
 import password.pwm.config.stored.ConfigurationReader;
 import password.pwm.config.stored.StoredConfigurationImpl;
@@ -40,7 +40,7 @@ public class ConfigLockCommand extends AbstractCliCommand {
         }
 
         storedConfiguration.writeConfigProperty(ConfigurationProperty.CONFIG_IS_EDITABLE,Boolean.toString(false));
-        configurationReader.saveConfiguration(storedConfiguration, cliEnvironment.getPwmApplication(), PwmConstants.CLI_SESSION_LABEL);
+        configurationReader.saveConfiguration(storedConfiguration, cliEnvironment.getPwmApplication(), SessionLabel.CLI_SESSION_LABEL);
         out("success");
     }
 

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

@@ -22,7 +22,7 @@
 
 package password.pwm.util.cli.commands;
 
-import password.pwm.PwmConstants;
+import password.pwm.bean.SessionLabel;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.PwmSettingCategory;
 import password.pwm.config.stored.ConfigurationReader;
@@ -53,7 +53,7 @@ public class ConfigResetHttpsCommand
         for (final PwmSetting setting : PwmSettingCategory.HTTPS_SERVER.getSettings()) {
             storedConfiguration.resetSetting(setting,null,null);
         }
-        configurationReader.saveConfiguration(storedConfiguration, cliEnvironment.getPwmApplication(), PwmConstants.CLI_SESSION_LABEL);
+        configurationReader.saveConfiguration(storedConfiguration, cliEnvironment.getPwmApplication(), SessionLabel.CLI_SESSION_LABEL);
         out("success");
     }
 

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

@@ -22,7 +22,7 @@
 
 package password.pwm.util.cli.commands;
 
-import password.pwm.PwmConstants;
+import password.pwm.bean.SessionLabel;
 import password.pwm.config.stored.ConfigurationReader;
 import password.pwm.config.stored.StoredConfigurationImpl;
 import password.pwm.util.cli.CliParameters;
@@ -38,7 +38,7 @@ public class ConfigSetPasswordCommand extends AbstractCliCommand {
         final StoredConfigurationImpl storedConfiguration = configurationReader.getStoredConfiguration();
         final String password = getOptionalPassword();
         storedConfiguration.setPassword(password);
-        configurationReader.saveConfiguration(storedConfiguration, cliEnvironment.getPwmApplication(), PwmConstants.CLI_SESSION_LABEL);
+        configurationReader.saveConfiguration(storedConfiguration, cliEnvironment.getPwmApplication(), SessionLabel.CLI_SESSION_LABEL);
         out("success");
     }
 

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

@@ -22,7 +22,7 @@
 
 package password.pwm.util.cli.commands;
 
-import password.pwm.PwmConstants;
+import password.pwm.bean.SessionLabel;
 import password.pwm.config.stored.ConfigurationProperty;
 import password.pwm.config.stored.ConfigurationReader;
 import password.pwm.config.stored.StoredConfigurationImpl;
@@ -40,7 +40,7 @@ public class ConfigUnlockCommand extends AbstractCliCommand {
         }
         
         storedConfiguration.writeConfigProperty(ConfigurationProperty.CONFIG_IS_EDITABLE,Boolean.toString(true));
-        configurationReader.saveConfiguration(storedConfiguration, cliEnvironment.getPwmApplication(), PwmConstants.CLI_SESSION_LABEL);
+        configurationReader.saveConfiguration(storedConfiguration, cliEnvironment.getPwmApplication(), SessionLabel.CLI_SESSION_LABEL);
         out("success");
     }
 

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

@@ -22,7 +22,7 @@
 
 package password.pwm.util.cli.commands;
 
-import password.pwm.PwmConstants;
+import password.pwm.bean.SessionLabel;
 import password.pwm.config.stored.ConfigurationReader;
 import password.pwm.config.stored.StoredConfigurationImpl;
 import password.pwm.util.PasswordData;
@@ -76,7 +76,7 @@ public class ImportHttpsKeyStoreCommand extends AbstractCliCommand {
             return;
         }
 
-        configurationReader.saveConfiguration(storedConfiguration, cliEnvironment.getPwmApplication(), PwmConstants.CLI_SESSION_LABEL);
+        configurationReader.saveConfiguration(storedConfiguration, cliEnvironment.getPwmApplication(), SessionLabel.CLI_SESSION_LABEL);
         out("success");
     }
 

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

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

+ 94 - 1
src/main/java/password/pwm/util/java/JavaHelper.java

@@ -34,7 +34,12 @@ import java.io.OutputStream;
 import java.io.OutputStreamWriter;
 import java.io.PrintWriter;
 import java.io.StringWriter;
+import java.lang.management.LockInfo;
+import java.lang.management.MonitorInfo;
+import java.lang.management.ThreadInfo;
 import java.lang.reflect.Method;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
 import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -255,7 +260,22 @@ public class JavaHelper {
     }
 
     public static String toIsoDate(final Date date) {
-        return date == null ? "" : PwmConstants.DEFAULT_DATETIME_FORMAT.format(date);
+        if (date == null) {
+            return "";
+        }
+
+        final DateFormat dateFormat = new SimpleDateFormat(
+                PwmConstants.DEFAULT_DATETIME_FORMAT_STR,
+                PwmConstants.DEFAULT_LOCALE
+        );
+
+        dateFormat.setTimeZone(PwmConstants.DEFAULT_TIMEZONE);
+
+        return dateFormat.format(date);
+    }
+
+    public static Instant parseIsoToInstant(final String input) {
+        return Instant.parse(input);
     }
 
     public static boolean closeAndWaitExecutor(final ExecutorService executor, final TimeDuration timeDuration)
@@ -338,4 +358,77 @@ public class JavaHelper {
                         true
                 ));
     }
+
+
+    /**
+     * Copy of {@link ThreadInfo#toString()} but with the MAX_FRAMES changed from 8 to 256.
+     */
+    public static String threadInfoToString(final ThreadInfo threadInfo) {
+        final int MAX_FRAMES = 256;
+        final StringBuilder sb = new StringBuilder("\"" + threadInfo.getThreadName() + "\"" +
+                " Id=" + threadInfo.getThreadId() + " " +
+                threadInfo.getThreadState());
+        if (threadInfo.getLockName() != null) {
+            sb.append(" on " + threadInfo.getLockName());
+        }
+        if (threadInfo.getLockOwnerName() != null) {
+            sb.append(" owned by \"" + threadInfo.getLockOwnerName() +
+                    "\" Id=" + threadInfo.getLockOwnerId());
+        }
+        if (threadInfo.isSuspended()) {
+            sb.append(" (suspended)");
+        }
+        if (threadInfo.isInNative()) {
+            sb.append(" (in native)");
+        }
+        sb.append('\n');
+
+        int counter = 0;
+        for (; counter < threadInfo.getStackTrace().length && counter < MAX_FRAMES; counter++) {
+            final StackTraceElement ste = threadInfo.getStackTrace()[counter];
+            sb.append("\tat ").append(ste.toString());
+            sb.append('\n');
+            if (counter == 0 && threadInfo.getLockInfo() != null) {
+                final Thread.State ts = threadInfo.getThreadState();
+                switch (ts) {
+                    case BLOCKED:
+                        sb.append("\t-  blocked on " + threadInfo.getLockInfo());
+                        sb.append('\n');
+                        break;
+                    case WAITING:
+                        sb.append("\t-  waiting on " + threadInfo.getLockInfo());
+                        sb.append('\n');
+                        break;
+                    case TIMED_WAITING:
+                        sb.append("\t-  waiting on " + threadInfo.getLockInfo());
+                        sb.append('\n');
+                        break;
+                    default:
+                }
+            }
+
+            for (MonitorInfo mi : threadInfo.getLockedMonitors()) {
+                if (mi.getLockedStackDepth() == counter) {
+                    sb.append("\t-  locked " + mi);
+                    sb.append('\n');
+                }
+            }
+        }
+        if (counter < threadInfo.getStackTrace().length) {
+            sb.append("\t...");
+            sb.append('\n');
+        }
+
+        final LockInfo[] locks = threadInfo.getLockedSynchronizers();
+        if (locks.length > 0) {
+            sb.append("\n\tNumber of locked synchronizers = " + locks.length);
+            sb.append('\n');
+            for (LockInfo li : locks) {
+                sb.append("\t- " + li);
+                sb.append('\n');
+            }
+        }
+        sb.append('\n');
+        return sb.toString();
+    }
 }

+ 1 - 1
src/main/java/password/pwm/util/localdb/LocalDB.java

@@ -44,7 +44,7 @@ import java.util.Map;
 public interface LocalDB {
 // -------------------------- OTHER METHODS --------------------------
 
-    int MAX_KEY_LENGTH = 128;
+    int MAX_KEY_LENGTH = 256;
     int MAX_VALUE_LENGTH = 1024 * 100;
 
     enum Status {

+ 60 - 82
src/main/java/password/pwm/util/localdb/Xodus_LocalDB.java

@@ -24,6 +24,7 @@ package password.pwm.util.localdb;
 
 import jetbrains.exodus.ArrayByteIterable;
 import jetbrains.exodus.ByteIterable;
+import jetbrains.exodus.InvalidSettingException;
 import jetbrains.exodus.bindings.StringBinding;
 import jetbrains.exodus.env.Cursor;
 import jetbrains.exodus.env.Environment;
@@ -33,11 +34,8 @@ import jetbrains.exodus.env.Environments;
 import jetbrains.exodus.env.Store;
 import jetbrains.exodus.env.StoreConfig;
 import jetbrains.exodus.env.Transaction;
-import jetbrains.exodus.env.TransactionalComputable;
-import jetbrains.exodus.env.TransactionalExecutable;
 import jetbrains.exodus.management.Statistics;
 import jetbrains.exodus.management.StatisticsItem;
-import org.jetbrains.annotations.NotNull;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmError;
 import password.pwm.util.java.ConditionalTaskExecutor;
@@ -94,12 +92,9 @@ public class Xodus_LocalDB implements LocalDBProvider {
 
     private final Map<LocalDB.DB,Store> cachedStoreObjects = new HashMap<>();
 
-    private final ConditionalTaskExecutor outputLogExecutor = new ConditionalTaskExecutor(new Runnable() {
-        @Override
-        public void run() {
-            outputStats();
-        }
-    },new ConditionalTaskExecutor.TimeDurationPredicate(STATS_OUTPUT_INTERVAL).setNextTimeFromNow(1, TimeUnit.MINUTES));
+    private final ConditionalTaskExecutor outputLogExecutor = new ConditionalTaskExecutor(
+            () -> outputStats(),new ConditionalTaskExecutor.TimeDurationPredicate(STATS_OUTPUT_INTERVAL).setNextTimeFromNow(1, TimeUnit.MINUTES)
+    );
 
     private BindMachine bindMachine = new BindMachine(BindMachine.DEFAULT_ENABLE_COMPRESSION, BindMachine.DEFAULT_MIN_COMPRESSION_LENGTH);
 
@@ -117,19 +112,7 @@ public class Xodus_LocalDB implements LocalDBProvider {
         LOGGER.trace("begin environment open");
         final Instant startTime = Instant.now();
 
-        final EnvironmentConfig environmentConfig = new EnvironmentConfig();
-        environmentConfig.setLogDurableWrite(true);
-        environmentConfig.setGcEnabled(true);
-        environmentConfig.setEnvCloseForcedly(true);
-        environmentConfig.setFullFileReadonly(false);
-        environmentConfig.setGcStartIn(0);
-        environmentConfig.setGcUtilizationFromScratch(true);
-        environmentConfig.setMemoryUsage(50 * 1024 * 1024);
-
-        for (final String key : initParameters.keySet()) {
-            final String value = initParameters.get(key);
-            environmentConfig.setSetting(key,value);
-        }
+        final EnvironmentConfig environmentConfig = makeEnvironmentConfig(initParameters);
 
         {
             final boolean compressionEnabled = initParameters.containsKey(Property.Compression_Enabled.getKeyName())
@@ -149,13 +132,10 @@ public class Xodus_LocalDB implements LocalDBProvider {
         environment = Environments.newInstance(dbDirectory.getAbsolutePath() + File.separator + "xodus", environmentConfig);
         LOGGER.trace("environment open (" + TimeDuration.fromCurrent(startTime).asCompactString() + ")");
 
-        environment.executeInTransaction(new TransactionalExecutable() {
-            @Override
-            public void execute(@NotNull final Transaction txn) {
-                for (final LocalDB.DB db : LocalDB.DB.values()) {
-                    final Store store = initStore(db, txn);
-                    cachedStoreObjects.put(db,store);
-                }
+        environment.executeInTransaction(txn -> {
+            for (final LocalDB.DB db : LocalDB.DB.values()) {
+                final Store store = initStore(db, txn);
+                cachedStoreObjects.put(db,store);
             }
         });
 
@@ -175,15 +155,31 @@ public class Xodus_LocalDB implements LocalDBProvider {
         LOGGER.debug("closed");
     }
 
+    private EnvironmentConfig makeEnvironmentConfig(final Map<String, String> initParameters) {
+        final EnvironmentConfig environmentConfig = new EnvironmentConfig();
+        environmentConfig.setEnvCloseForcedly(true);
+        environmentConfig.setMemoryUsage(50 * 1024 * 1024);
+
+        for (final String key : initParameters.keySet()) {
+            final String value = initParameters.get(key);
+            final Map<String,String> singleMap = Collections.singletonMap(key,value);
+            try {
+                environmentConfig.setSettings(singleMap);
+                LOGGER.trace("set env setting from appProperty: " + key + "=" + value);
+            } catch (InvalidSettingException e) {
+                LOGGER.warn("problem setting configured env settings: " + e.getMessage());
+            }
+        }
+
+        return environmentConfig;
+    }
+
     @Override
     public int size(final LocalDB.DB db) throws LocalDBException {
         checkStatus(false);
-        return environment.computeInReadonlyTransaction(new TransactionalComputable<Integer>() {
-            @Override
-            public Integer compute(@NotNull final Transaction transaction) {
-                final Store store = getStore(db);
-                return (int) store.count(transaction);
-            }
+        return environment.computeInReadonlyTransaction(transaction -> {
+            final Store store = getStore(db);
+            return (int) store.count(transaction);
         });
     }
 
@@ -196,16 +192,13 @@ public class Xodus_LocalDB implements LocalDBProvider {
     @Override
     public String get(final LocalDB.DB db, final String key) throws LocalDBException {
         checkStatus(false);
-        return environment.computeInReadonlyTransaction(new TransactionalComputable<String>() {
-            @Override
-            public String compute(@NotNull final Transaction transaction) {
-                final Store store = getStore(db);
-                final ByteIterable returnValue = store.get(transaction, bindMachine.keyToEntry(key));
-                if (returnValue != null) {
-                    return bindMachine.entryToValue(returnValue);
-                }
-                return null;
+        return environment.computeInReadonlyTransaction(transaction -> {
+            final Store store = getStore(db);
+            final ByteIterable returnValue = store.get(transaction, bindMachine.keyToEntry(key));
+            if (returnValue != null) {
+                return bindMachine.entryToValue(returnValue);
             }
+            return null;
         });
     }
 
@@ -294,16 +287,13 @@ public class Xodus_LocalDB implements LocalDBProvider {
     @Override
     public void putAll(final LocalDB.DB db, final Map<String, String> keyValueMap) throws LocalDBException {
         checkStatus(true);
-        environment.executeInTransaction(new TransactionalExecutable() {
-            @Override
-            public void execute(@NotNull final Transaction transaction) {
-                final Store store = getStore(db);
-                for (final String key : keyValueMap.keySet()) {
-                    final String value = keyValueMap.get(key);
-                    final ByteIterable k = bindMachine.keyToEntry(key);
-                    final ByteIterable v = bindMachine.valueToEntry(value);
-                    store.put(transaction,k,v);
-                }
+        environment.executeInTransaction(transaction -> {
+            final Store store = getStore(db);
+            for (final String key : keyValueMap.keySet()) {
+                final String value = keyValueMap.get(key);
+                final ByteIterable k = bindMachine.keyToEntry(key);
+                final ByteIterable v = bindMachine.valueToEntry(value);
+                store.put(transaction,k,v);
             }
         });
         outputLogExecutor.conditionallyExecuteTask();
@@ -313,39 +303,30 @@ public class Xodus_LocalDB implements LocalDBProvider {
     @Override
     public boolean put(final LocalDB.DB db, final String key, final String value) throws LocalDBException {
         checkStatus(true);
-        return environment.computeInTransaction(new TransactionalComputable<Boolean>() {
-            @Override
-            public Boolean compute(@NotNull final Transaction transaction) {
-                final ByteIterable k = bindMachine.keyToEntry(key);
-                final ByteIterable v = bindMachine.valueToEntry(value);
-                final Store store = getStore(db);
-                return store.put(transaction,k,v);
-            }
+        return environment.computeInTransaction(transaction -> {
+            final ByteIterable k = bindMachine.keyToEntry(key);
+            final ByteIterable v = bindMachine.valueToEntry(value);
+            final Store store = getStore(db);
+            return store.put(transaction,k,v);
         });
     }
 
     @Override
     public boolean remove(final LocalDB.DB db, final String key) throws LocalDBException {
         checkStatus(true);
-        return environment.computeInTransaction(new TransactionalComputable<Boolean>() {
-            @Override
-            public Boolean compute(@NotNull final Transaction transaction) {
-                final Store store = getStore(db);
-                return store.delete(transaction, bindMachine.keyToEntry(key));
-            }
+        return environment.computeInTransaction(transaction -> {
+            final Store store = getStore(db);
+            return store.delete(transaction, bindMachine.keyToEntry(key));
         });
     }
 
     @Override
     public void removeAll(final LocalDB.DB db, final Collection<String> keys) throws LocalDBException {
         checkStatus(true);
-        environment.executeInTransaction(new TransactionalExecutable() {
-            @Override
-            public void execute(@NotNull final Transaction transaction) {
-                final Store store = getStore(db);
-                for (final String key : keys) {
-                    store.delete(transaction, bindMachine.keyToEntry(key));
-                }
+        environment.executeInTransaction(transaction -> {
+            final Store store = getStore(db);
+            for (final String key : keys) {
+                store.delete(transaction, bindMachine.keyToEntry(key));
             }
         });
     }
@@ -358,13 +339,10 @@ public class Xodus_LocalDB implements LocalDBProvider {
         LOGGER.trace("begin truncate of " + db.toString() + ", size=" + this.size(db));
         final Date startDate = new Date();
 
-        environment.executeInTransaction(new TransactionalExecutable() {
-            @Override
-            public void execute(@NotNull final Transaction transaction) {
-                environment.truncateStore(db.toString(), transaction);
-                final Store newStoreReference = environment.openStore(db.toString(), StoreConfig.USE_EXISTING, transaction);
-                cachedStoreObjects.put(db, newStoreReference);
-            }
+        environment.executeInTransaction(transaction -> {
+            environment.truncateStore(db.toString(), transaction);
+            final Store newStoreReference = environment.openStore(db.toString(), StoreConfig.USE_EXISTING, transaction);
+            cachedStoreObjects.put(db, newStoreReference);
         });
 
         LOGGER.trace("completed truncate of " + db.toString()

+ 14 - 2
src/main/java/password/pwm/util/operations/cr/NMASCrOperator.java

@@ -217,7 +217,8 @@ public class NMASCrOperator implements CrOperator {
                     LOGGER.debug("starting NMASCrOperator watchdog timer, maxIdleThreadTime=" + maxThreadIdleTime.asCompactString());
                     timer = new Timer(PwmConstants.PWM_APP_NAME + "-NMASCrOperator watchdog timer",true);
                     final long frequency = Long.parseLong(pwmApplication.getConfig().readAppProperty(AppProperty.NMAS_THREADS_WATCHDOG_FREQUENCY));
-                    timer.schedule(new ThreadWatchdogTask(),frequency,frequency);
+                    final boolean debugOutput = Boolean.parseBoolean(pwmApplication.getConfig().readAppProperty(AppProperty.NMAS_THREADS_WATCHDOG_DEBUG));
+                    timer.schedule(new ThreadWatchdogTask(debugOutput),frequency,frequency);
                 }
             }
         }
@@ -833,9 +834,20 @@ public class NMASCrOperator implements CrOperator {
     }
 
     private class ThreadWatchdogTask extends TimerTask {
+
+        private final boolean debugOutput;
+
+        ThreadWatchdogTask(final boolean debugOutput)
+        {
+            this.debugOutput = debugOutput;
+        }
+
         @Override
         public void run() {
-            logThreadInfo();
+            if (debugOutput) {
+                logThreadInfo();
+            }
+
             final List<NMASSessionThread> threads = new ArrayList<>(sessionMonitorThreads);
             for (final NMASSessionThread thread : threads) {
                 final TimeDuration idleTime = TimeDuration.fromCurrent(thread.getLastActivityTimestamp());

+ 7 - 5
src/main/java/password/pwm/util/queue/SmsQueueManager.java

@@ -172,24 +172,26 @@ public class SmsQueueManager implements PwmService {
     public void addSmsToQueue(final SmsItemBean smsItem)
             throws PwmUnrecoverableException
     {
-        shortenMessageIfNeeded(smsItem);
-        if (!determineIfItemCanBeDelivered(smsItem)) {
+        final SmsItemBean shortenedBean = shortenMessageIfNeeded(smsItem);
+        if (!determineIfItemCanBeDelivered(shortenedBean)) {
             return;
         }
 
         try {
-            workQueueProcessor.submit(smsItem);
+            workQueueProcessor.submit(shortenedBean);
         } catch (Exception e) {
             LOGGER.error("error writing to LocalDB queue, discarding sms send request: " + e.getMessage());
         }
     }
 
-    protected void shortenMessageIfNeeded(final SmsItemBean smsItem) throws PwmUnrecoverableException {
+    SmsItemBean shortenMessageIfNeeded(final SmsItemBean smsItem) throws PwmUnrecoverableException {
         final Boolean shorten = pwmApplication.getConfig().readSettingAsBoolean(PwmSetting.SMS_USE_URL_SHORTENER);
         if (shorten) {
             final String message = smsItem.getMessage();
-            smsItem.setMessage(pwmApplication.getUrlShortener().shortenUrlInText(message));
+            final String shortenedMessage = pwmApplication.getUrlShortener().shortenUrlInText(message);
+            return new SmsItemBean(smsItem.getTo(), shortenedMessage);
         }
+        return smsItem;
     }
 
     public static boolean smsIsConfigured(final Configuration config) {

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

@@ -193,10 +193,20 @@ public abstract class RestServerHelper {
             }
         }
 
+        final String ldapProfileID;
+        final String effectiveUsername;
+        if (username.contains("|")) {
+            final int pipeIndex = username.indexOf("|");
+            ldapProfileID = username.substring(0, pipeIndex);
+            effectiveUsername = username.substring(pipeIndex + 1, username.length());
+        } else {
+            ldapProfileID = null;
+            effectiveUsername = username;
+        }
 
         try {
             final UserSearchEngine userSearchEngine = pwmApplication.getUserSearchEngine();
-            return userSearchEngine.resolveUsername(username, null, null, pwmSession.getLabel());
+            return userSearchEngine.resolveUsername(effectiveUsername, null, ldapProfileID, pwmSession.getLabel());
         } catch (PwmOperationalException e) {
             throw new PwmUnrecoverableException(e.getErrorInformation());
         } catch (ChaiUnavailableException e) {

+ 1 - 1
src/main/java/password/pwm/ws/server/rest/RestRandomPasswordServer.java

@@ -224,7 +224,7 @@ public class RestRandomPasswordServer extends AbstractRestServer {
 
         if (!jsonInput.noUser && restRequestBean.getPwmSession().isAuthenticated()) {
             if (jsonInput.username != null && !jsonInput.username.isEmpty()) {
-                final UserIdentity userIdentity = UserIdentity.fromKey(jsonInput.username, restRequestBean.getPwmApplication());
+                final UserIdentity userIdentity = restRequestBean.getUserIdentity();
                 final HelpdeskProfile helpdeskProfile = restRequestBean.getPwmSession().getSessionManager().getHelpdeskProfile(restRequestBean.getPwmApplication());
                 final boolean useProxy = helpdeskProfile.readSettingAsBoolean(PwmSetting.HELPDESK_USE_PROXY);
 

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

@@ -151,6 +151,8 @@ ldap.cache.canonical.seconds=60
 ldap.cache.userGuid.enable=true
 ldap.cache.userGuid.seconds=3600
 ldap.chaiSettings=
+ldap.proxy.connectionsPerProfile=10
+ldap.proxy.maxConnections=50
 ldap.extensions.nmas.enable=true
 ldap.connection.timeoutMS=30000
 ldap.profile.retryDelayMS=30000
@@ -166,6 +168,7 @@ ldap.search.paging.size=500
 ldap.search.parallel.enable=true
 ldap.search.parallel.factor=5
 ldap.search.parallel.threadMax=50
+ldap.oracle.postTempPasswordUseCurrentTime=false
 localdb.aggressiveCompact.enabled=false
 localdb.implementation=password.pwm.util.localdb.Xodus_LocalDB
 localdb.initParameters=00
@@ -185,6 +188,7 @@ nmas.threads.maxCount=500
 nmas.threads.minSeconds=1800
 nmas.threads.maxSeconds=3000
 nmas.threads.watchdogFrequencyMs=1000
+nmas.threads.watchdogDebug=false
 nmas.ignoreNmasCrDuringForceSetupCheck=false
 nmas.useLocalSaslFactory=true
 nmas.forceSaslFactoryRegistration=true
@@ -218,7 +222,7 @@ queue.sms.retryTimeoutMs=10000
 queue.sms.maxAgeMs=86400000
 queue.sms.maxCount=100000
 queue.syslog.retryTimeoutMs=30000
-queue.syslog.maxAgeMs=86400000
+queue.syslog.maxAgeMs=2592000000
 queue.syslog.maxCount=100000
 reporting.ldap.searchTimeoutMs=1800000
 reporting.ldap.searchThreads=8
@@ -229,6 +233,7 @@ security.html.stripInlineJavascript=false
 security.http.stripHeaderRegex=\\n|\\r|(?ism)%0A|%0D
 security.http.performCsrfHeaderChecks=false
 security.http.promiscuousEnable=false
+security.http.config.cspHeader=default-src 'self'; object-src 'none'; img-src 'self' data:; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-eval' 'unsafe-inline'; report-uri /sspr/public/command/cspReport
 security.httpsServer.selfCert.futureSeconds=63113904
 security.httpsServer.selfCert.alg=RSA
 security.httpsServer.selfCert.keySize=2048
@@ -249,11 +254,11 @@ security.defaultEphemeralHashAlg=SHA512
 security.config.minSecurityKeyLength=32
 seedlist.builtin.path=/WEB-INF/seedlist.zip
 smtp.subjectEncodingCharset=UTF8
-token.removalDelayMS=86400000
-token.purgeBatchSize=1000
 token.maxUniqueCreateAttempts=100
 token.resend.enabled=true
 token.resend.delayMS=3000
+token.verifyPwModifyTime=true
+token.removeOnClaim=true
 urlshortener.url.regex=(https?://([^:@]+(:[^@]+)?@)?([a-zA-Z0-9.]+|d{1,3}.d{1,3}.d{1,3}.d{1,3}|[[0-9a-fA-F:]+])(:d{1,5})?/*[a-zA-Z0-9/\%_.]*?*[a-zA-Z0-9/\%_.=&#]*)
 wordlist.builtin.path=/WEB-INF/wordlist.zip
 ws.restClient.pwRule.haltOnError=true

+ 23 - 31
src/main/webapp/WEB-INF/jsp/admin-dashboard.jsp

@@ -44,6 +44,8 @@
 <%@ page import="java.util.Locale" %>
 <%@ page import="java.util.Map" %>
 <%@ page import="java.util.TreeMap" %>
+<%@ page import="java.lang.management.ThreadInfo" %>
+<%@ page import="java.lang.management.ManagementFactory" %>
 <!DOCTYPE html>
 <%@ page language="java" session="true" isThreadSafe="true"
          contentType="text/html" %>
@@ -51,7 +53,7 @@
 <%
     final Locale locale = JspUtility.locale(request);
     final NumberFormat numberFormat = NumberFormat.getInstance(locale);
-    final Map<Thread,StackTraceElement[]> threads = Thread.getAllStackTraces();
+    final ThreadInfo[] threads = ManagementFactory.getThreadMXBean().dumpAllThreads(true,true);
     SessionTrackService sessionTrackService = null;
 
     PwmRequest dashboard_pwmRequest = null;
@@ -638,7 +640,7 @@
                             Threads
                         </td>
                         <td>
-                            <%= threads.size() %>
+                            <%= threads.length %>
                         </td>
                     </tr>
                 </table>
@@ -680,6 +682,7 @@
                 </table>
             </div>
             <div id="ThreadsTab" data-dojo-type="dijit.layout.ContentPane" title="Threads" class="tabContent">
+                <% if (dashboard_pwmApplication.getLocalDB() != null && dashboard_pwmRequest.readParameterAsBoolean("showThreadDetails")) { %>
                 <div style="max-height: 400px; overflow: auto;">
                     <table class="nomargin">
                         <tr>
@@ -689,54 +692,33 @@
                             <td style="font-weight:bold;">
                                 Name
                             </td>
-                            <td style="font-weight:bold;">
-                                Priority
-                            </td>
                             <td style="font-weight:bold;">
                                 State
                             </td>
-                            <td style="font-weight:bold;">
-                                Daemon
-                            </td>
                         </tr>
                         <%
                             try {
-                                final TreeMap<Long,Thread> sortedThreads = new TreeMap<Long, Thread>();
-                                for (final Thread t : threads.keySet()) {
-                                    sortedThreads.put(t.getId(),t);
-                                }
-
-                                for (final Thread t : sortedThreads.values()) {
+                                for (final ThreadInfo t : threads) {
                         %>
-                        <tr id="thread_<%=t.getId()%>">
-                            <td>
-                                <%= t.getId() %>
-                            </td>
-                            <td>
-                                <%= t.getName() != null ? t.getName() : JspUtility.getMessage(pageContext, Display.Value_NotApplicable) %>
-                            </td>
+                        <tr id="thread_<%=t.getThreadId()%>">
                             <td>
-                                <%= t.getPriority() %>
+                                <%= t.getThreadId() %>
                             </td>
                             <td>
-                                <%= t.getState().toString().toLowerCase() %>
+                                <%= t.getThreadName() != null ? t.getThreadName() : JspUtility.getMessage(pageContext, Display.Value_NotApplicable) %>
                             </td>
                             <td>
-                                <%= String.valueOf(t.isDaemon()) %>
+                                <%= t.getThreadState().toString().toLowerCase() %>
                             </td>
                         </tr>
                         <%
-                            final StringBuilder threadTrace = new StringBuilder();
-                            for (final StackTraceElement traceElement : threads.get(t)) {
-                                threadTrace.append(traceElement.toString());
-                                threadTrace.append("\n");
-                            }
+                            final String threadTrace = JavaHelper.threadInfoToString(t);
                         %>
                         <pwm:script>
                             <script type="application/javascript">
                                 PWM_GLOBAL['startupFunctions'].push(function(){
-                                    PWM_MAIN.addEventHandler('thread_<%=t.getId()%>','click',function(){
-                                        PWM_MAIN.showDialog({title:'Thread <%=t.getId()%>',text:'<pre>' +'<%=StringUtil.escapeJS(threadTrace.toString())%>' + '</pre>'})
+                                    PWM_MAIN.addEventHandler('thread_<%=t.getThreadId()%>','click',function(){
+                                        PWM_MAIN.showDialog({class:'wide',title:'Thread <%=t.getThreadId()%>',text:'<pre>' +'<%=StringUtil.escapeJS(threadTrace)%>' + '</pre>'})
                                     });
                                 });
                             </script>
@@ -745,6 +727,11 @@
                         <% } catch (Exception e) { /* */ } %>
                     </table>
                 </div>
+                <% } else { %>
+                <div class="noborder" style="text-align:center; width:100%;">
+                    <a style="cursor: pointer" id="button-showThreadDetails">Show thread details</a> (may be slow to load)
+                </div>
+                <% } %>
             </div>
         </div>
     </div>
@@ -763,6 +750,11 @@
                         PWM_MAIN.goto('dashboard?showLocalDBCounts=true');
                     }})
                 });
+                PWM_MAIN.addEventHandler('button-showThreadDetails','click',function(){
+                    PWM_MAIN.showWaitDialog({loadFunction:function(){
+                        PWM_MAIN.goto('dashboard?showThreadDetails=true');
+                    }})
+                });
             });
         });
     </script>

+ 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, tokenlookup_pwmRequest.getSessionLabel());
+                    tokenPayload = tokenlookup_pwmRequest.getPwmApplication().getTokenService().retrieveTokenData(tokenlookup_pwmRequest.getSessionLabel(), tokenKey);
                 } catch (PwmOperationalException e) {
                     tokenExpired= e.getError() == PwmError.ERROR_TOKEN_EXPIRED;
                     lookupError = e.getMessage();

+ 4 - 4
src/main/webapp/WEB-INF/jsp/admin-user-debug.jsp

@@ -161,7 +161,7 @@
                 <td class="key">
                     Password Readable From LDAP
                 </td>
-                <td id="PasswordViolatesPolicy">
+                <td>
                     <%= JspUtility.freindlyWrite(pageContext, userDebugDataBean.isPasswordReadable()) %>
                 </td>
             </tr>
@@ -294,7 +294,7 @@
             <% if (responseInfoBean == null) { %>
             <tr>
                 <td>Stored Responses</td>
-                <td><pwm:display key="Display_NotApplicable"/></td>
+                <td><pwm:display key="<%=Display.Value_NotApplicable.toString()%>"/></td>
             </tr>
             <% } else { %>
             <tr>
@@ -362,7 +362,7 @@
                 <% final Map<Challenge,String> helpdeskCrMap = responseInfoBean.getHelpdeskCrMap(); %>
                 <% if (helpdeskCrMap == null) { %>
                 <td>
-                <pwm:display key="Display_NotApplicable"/>
+                <pwm:display key="<%=Display.Value_NotApplicable.toString()%>"/>
                 </td>
                 <% } else { %>
                 <td>
@@ -383,7 +383,7 @@
             <% if (challengeProfile == null) { %>
             <tr>
                 <td>Assigned Profile</td>
-                <td><pwm:display key="Display_NotApplicable"/></td>
+                <td><pwm:display key="<%=Display.Value_NotApplicable.toString()%>"/></td>
             </tr>
             <% } else { %>
             <tr>

+ 4 - 4
src/main/webapp/WEB-INF/jsp/fragment/token-form-field.jsp

@@ -1,9 +1,12 @@
+<%@ page import="password.pwm.http.tag.conditional.PwmIfTest" %>
+<%@ page import="password.pwm.PwmConstants" %>
+
 <%--
   ~ Password Management Servlets (PWM)
   ~ http://www.pwm-project.org
   ~
   ~ Copyright (c) 2006-2009 Novell, Inc.
-  ~ Copyright (c) 2009-2017 The PWM Project
+  ~ 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
@@ -20,9 +23,6 @@
   ~ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
   --%>
 
-<%@ page import="password.pwm.http.tag.conditional.PwmIfTest" %>
-<%@ page import="password.pwm.PwmConstants" %>
-
 <%@ page language="java" session="true" isThreadSafe="true" contentType="text/html" %>
 <%@ taglib uri="pwm" prefix="pwm" %>
 <h2><label for="<%=PwmConstants.PARAM_TOKEN%>"><pwm:display key="Field_Code"/></label></h2>

+ 4 - 2
src/main/webapp/WEB-INF/jsp/updateprofile-confirm.jsp

@@ -55,9 +55,11 @@
                         final String value = formDataMap.get(formConfiguration);
                         if (formConfiguration.getType() == FormConfiguration.Type.checkbox) {
                     %>
-                    <%= LocaleHelper.booleanString(Boolean.parseBoolean(value),pwmRequest)%>
+                    <label class="checkboxWrapper">
+                        <input id="<%=formConfiguration.getName()%>" name="<%=formConfiguration.getName()%>" disabled type="checkbox" <%=(Boolean.parseBoolean(value))?"checked":""%>/>
+                    </label>
                     <% } else { %>
-                    <%=StringUtil.escapeHtml(value)%>
+                    <%=StringUtil.escapeHtml(formConfiguration.displayValue(value, JspUtility.locale(request), JspUtility.getPwmRequest(pageContext).getConfig()))%>
                     <% } %>
                 </td>
             </tr>

+ 0 - 1
src/main/webapp/public/resources/js/main.js

@@ -690,7 +690,6 @@ PWM_MAIN.initLocaleSelectorMenu = function(attachNode) {
 
 
 PWM_MAIN.showErrorDialog = function(error, options) {
-    debugger;
     options = options === undefined ? {} : options;
     var forceReload = false;
     var body = '';