浏览代码

Merge remote-tracking branch 'upstream/master'

# Conflicts:
#	rest-test-service/src/main/java/password/pwm/resttest/SmsPostResponseBody.java
Joshua Cold 6 年之前
父节点
当前提交
dda108df6d
共有 100 个文件被更改,包括 2467 次插入2306 次删除
  1. 41 0
      build/checkstyle-jsp.xml
  2. 2 1
      build/checkstyle.xml
  3. 1 1
      client/pom.xml
  4. 1 1
      data-service/pom.xml
  5. 2 38
      data-service/src/main/java/password/pwm/receiver/TelemetryRestReceiver.java
  6. 1 1
      docker/pom.xml
  7. 34 17
      pom.xml
  8. 2 2
      rest-test-service/src/main/java/password/pwm/resttest/ExternalMacroServlet.java
  9. 10 10
      rest-test-service/src/main/java/password/pwm/resttest/SmsPostResponseBody.java
  10. 1 1
      rest-test-service/src/main/java/password/pwm/resttest/SmsResponse.java
  11. 3 3
      server/pom.xml
  12. 3 1
      server/src/main/java/password/pwm/AppProperty.java
  13. 17 82
      server/src/main/java/password/pwm/PwmApplication.java
  14. 11 3
      server/src/main/java/password/pwm/bean/EmailItemBean.java
  15. 2 5
      server/src/main/java/password/pwm/bean/FormNonce.java
  16. 5 15
      server/src/main/java/password/pwm/bean/LocalSessionStateBean.java
  17. 4 141
      server/src/main/java/password/pwm/bean/LoginInfoBean.java
  18. 2 2
      server/src/main/java/password/pwm/bean/PasswordStatus.java
  19. 2 2
      server/src/main/java/password/pwm/bean/SessionLabel.java
  20. 2 2
      server/src/main/java/password/pwm/bean/SmsItemBean.java
  21. 2 2
      server/src/main/java/password/pwm/bean/TelemetryPublishBean.java
  22. 5 4
      server/src/main/java/password/pwm/config/Configuration.java
  23. 10 13
      server/src/main/java/password/pwm/config/PwmSetting.java
  24. 8 0
      server/src/main/java/password/pwm/config/PwmSettingCategory.java
  25. 1 1
      server/src/main/java/password/pwm/config/PwmSettingXml.java
  26. 70 82
      server/src/main/java/password/pwm/health/HealthMonitor.java
  27. 4 6
      server/src/main/java/password/pwm/http/ContextManager.java
  28. 1 0
      server/src/main/java/password/pwm/http/HttpHeader.java
  29. 2 41
      server/src/main/java/password/pwm/http/PwmHttpRequestWrapper.java
  30. 23 3
      server/src/main/java/password/pwm/http/PwmHttpResponseWrapper.java
  31. 3 6
      server/src/main/java/password/pwm/http/PwmRequest.java
  32. 2 22
      server/src/main/java/password/pwm/http/PwmSession.java
  33. 124 90
      server/src/main/java/password/pwm/http/filter/ConfigAccessFilter.java
  34. 148 0
      server/src/main/java/password/pwm/http/filter/CookieManagementFilter.java
  35. 3 42
      server/src/main/java/password/pwm/http/filter/RequestInitializationFilter.java
  36. 21 16
      server/src/main/java/password/pwm/http/filter/SessionFilter.java
  37. 1 2
      server/src/main/java/password/pwm/http/servlet/ClientApiServlet.java
  38. 1 1
      server/src/main/java/password/pwm/http/servlet/DeleteAccountServlet.java
  39. 1 6
      server/src/main/java/password/pwm/http/servlet/admin/ReportStatusBean.java
  40. 1 2
      server/src/main/java/password/pwm/http/servlet/configmanager/DebugItemGenerator.java
  41. 7 6
      server/src/main/java/password/pwm/http/servlet/forgottenpw/ForgottenPasswordServlet.java
  42. 14 36
      server/src/main/java/password/pwm/http/servlet/helpdesk/HelpdeskServlet.java
  43. 22 0
      server/src/main/java/password/pwm/http/servlet/newuser/NewUserServlet.java
  44. 1 1
      server/src/main/java/password/pwm/http/servlet/peoplesearch/PeopleSearchDataReader.java
  45. 2 2
      server/src/main/java/password/pwm/http/servlet/peoplesearch/PeopleSearchService.java
  46. 76 0
      server/src/main/java/password/pwm/http/servlet/resource/ConfigSettingFileResource.java
  47. 403 0
      server/src/main/java/password/pwm/http/servlet/resource/ResourceFileRequest.java
  48. 100 489
      server/src/main/java/password/pwm/http/servlet/resource/ResourceFileServlet.java
  49. 5 1
      server/src/main/java/password/pwm/http/servlet/resource/ResourceServletService.java
  50. 8 4
      server/src/main/java/password/pwm/http/state/CryptoCookieBeanImpl.java
  51. 1 1
      server/src/main/java/password/pwm/http/tag/conditional/PwmIfTest.java
  52. 12 1
      server/src/main/java/password/pwm/http/tag/value/PwmValue.java
  53. 2 0
      server/src/main/java/password/pwm/ldap/UserInfo.java
  54. 1 2
      server/src/main/java/password/pwm/ldap/UserInfoBean.java
  55. 18 1
      server/src/main/java/password/pwm/ldap/UserInfoReader.java
  56. 2 1
      server/src/main/java/password/pwm/ldap/search/UserSearchEngine.java
  57. 6 1
      server/src/main/java/password/pwm/svc/event/AuditService.java
  58. 3 3
      server/src/main/java/password/pwm/svc/event/LocalDbAuditVault.java
  59. 5 4
      server/src/main/java/password/pwm/svc/intruder/IntruderManager.java
  60. 3 2
      server/src/main/java/password/pwm/svc/node/NodeMachine.java
  61. 0 1
      server/src/main/java/password/pwm/svc/node/NodeService.java
  62. 2 1
      server/src/main/java/password/pwm/svc/pwnotify/PwNotifyEngine.java
  63. 5 4
      server/src/main/java/password/pwm/svc/pwnotify/PwNotifyService.java
  64. 33 101
      server/src/main/java/password/pwm/svc/report/ReportService.java
  65. 5 2
      server/src/main/java/password/pwm/svc/report/ReportSettings.java
  66. 9 72
      server/src/main/java/password/pwm/svc/report/ReportStatusInfo.java
  67. 96 227
      server/src/main/java/password/pwm/svc/report/ReportSummaryData.java
  68. 56 51
      server/src/main/java/password/pwm/svc/report/UserCacheRecord.java
  69. 6 23
      server/src/main/java/password/pwm/svc/report/UserCacheService.java
  70. 18 3
      server/src/main/java/password/pwm/svc/sessiontrack/SessionTrackService.java
  71. 18 23
      server/src/main/java/password/pwm/svc/stats/StatisticsManager.java
  72. 3 2
      server/src/main/java/password/pwm/svc/telemetry/TelemetryService.java
  73. 14 11
      server/src/main/java/password/pwm/svc/token/TokenService.java
  74. 6 8
      server/src/main/java/password/pwm/svc/wordlist/AbstractWordlist.java
  75. 4 7
      server/src/main/java/password/pwm/svc/wordlist/SharedHistoryManager.java
  76. 3 1
      server/src/main/java/password/pwm/util/CaptchaUtility.java
  77. 41 9
      server/src/main/java/password/pwm/util/DailySummaryJob.java
  78. 302 0
      server/src/main/java/password/pwm/util/PwmScheduler.java
  79. 74 0
      server/src/main/java/password/pwm/util/ServletUtility.java
  80. 2 2
      server/src/main/java/password/pwm/util/TransactionSizeCalculator.java
  81. 0 3
      server/src/main/java/password/pwm/util/cli/commands/ExportAuditCommand.java
  82. 0 2
      server/src/main/java/password/pwm/util/cli/commands/ExportResponsesCommand.java
  83. 0 2
      server/src/main/java/password/pwm/util/cli/commands/ExportStatsCommand.java
  84. 0 3
      server/src/main/java/password/pwm/util/cli/commands/TokenInfoCommand.java
  85. 3 2
      server/src/main/java/password/pwm/util/db/DatabaseService.java
  86. 3 1
      server/src/main/java/password/pwm/util/java/BlockingThreadPool.java
  87. 0 129
      server/src/main/java/password/pwm/util/java/JavaHelper.java
  88. 0 85
      server/src/main/java/password/pwm/util/java/Sleeper.java
  89. 53 28
      server/src/main/java/password/pwm/util/java/TimeDuration.java
  90. 6 8
      server/src/main/java/password/pwm/util/localdb/WorkQueueProcessor.java
  91. 6 5
      server/src/main/java/password/pwm/util/logging/LocalDBLogger.java
  92. 6 10
      server/src/main/java/password/pwm/util/operations/PasswordUtility.java
  93. 1 2
      server/src/main/java/password/pwm/util/operations/cr/NMASCrOperator.java
  94. 3 4
      server/src/main/java/password/pwm/ws/client/rest/form/FormDataRequestBean.java
  95. 2 3
      server/src/main/java/password/pwm/ws/client/rest/form/FormDataResponseBean.java
  96. 5 22
      server/src/main/java/password/pwm/ws/server/rest/RestHealthServer.java
  97. 3 1
      server/src/main/resources/password/pwm/AppProperty.properties
  98. 6 32
      server/src/main/resources/password/pwm/config/PwmSetting.xml
  99. 255 125
      server/src/main/resources/password/pwm/i18n/Display_nn.properties
  100. 150 69
      server/src/main/resources/password/pwm/i18n/Message_nn.properties

+ 41 - 0
build/checkstyle-jsp.xml

@@ -0,0 +1,41 @@
+<?xml version="1.0"?>
+<!--
+  ~ Password Management Servlets (PWM)
+  ~ http://www.pwm-project.org
+  ~
+  ~ Copyright (c) 2006-2009 Novell, Inc.
+  ~ Copyright (c) 2009-2018 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
+  -->
+
+<!DOCTYPE module PUBLIC
+        "-//Puppy Crawl//DTD Check Configuration 1.2//EN"
+        "http://www.puppycrawl.com/dtds/configuration_1_2.dtd">
+
+<!--
+  PWM Checkstyle definition
+-->
+
+<module name="Checker">
+    <property name="fileExtensions" value="java, xml, jsp"/>
+
+    <module name="TreeWalker" >
+        <module name="Regexp">
+            <property name="format" value="WarnJavaScriptNotEnabledMessage"/>
+            <property name="illegalPattern" value="true"/>
+        </module>
+    </module>
+</module>

+ 2 - 1
build/checkstyle.xml

@@ -31,6 +31,8 @@
 
 <module name="Checker">
 
+    <property name="cacheFile" value="target/checkstyle.cache"/>
+
     <module name="SuppressionFilter">
         <property name="file" value="${basedir}/build/checkstyle-suppression.xml" />
     </module>
@@ -63,7 +65,6 @@
     </module>
 
     <module name="TreeWalker" >
-        <property name="cacheFile" value="target/checkstyle.cache"/>
 
         <!-- required for SuppressWarningsFilter (and other Suppress* rules not used here) -->
         <!-- see http://checkstyle.sourceforge.net/config_annotation.html#SuppressWarningsHolder -->

+ 1 - 1
client/pom.xml

@@ -70,7 +70,7 @@
             <plugin>
                 <groupId>com.github.eirslett</groupId>
                 <artifactId>frontend-maven-plugin</artifactId>
-                <version>1.6</version>
+                <version>1.7.5</version>
                 <configuration>
                     <nodeVersion>v8.9.4</nodeVersion>
                     <npmVersion>5.6.0</npmVersion>

+ 1 - 1
data-service/pom.xml

@@ -150,7 +150,7 @@
         <dependency>
             <groupId>org.apache.httpcomponents</groupId>
             <artifactId>httpclient</artifactId>
-            <version>4.5.7</version>
+            <version>4.5.8</version>
         </dependency>
         <dependency>
             <groupId>log4j</groupId>

+ 2 - 38
data-service/src/main/java/password/pwm/receiver/TelemetryRestReceiver.java

@@ -22,13 +22,12 @@
 
 package password.pwm.receiver;
 
-import org.apache.commons.io.IOUtils;
-import password.pwm.PwmConstants;
 import password.pwm.bean.TelemetryPublishBean;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.i18n.Message;
+import password.pwm.util.ServletUtility;
 import password.pwm.util.java.JsonUtil;
 import password.pwm.ws.server.RestResultBean;
 
@@ -38,9 +37,6 @@ import javax.servlet.http.HttpServlet;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import java.io.IOException;
-import java.io.InputStreamReader;
-import java.io.Reader;
-import java.io.StringWriter;
 
 @WebServlet(
         name = "TelemetryRestReceiver",
@@ -58,7 +54,7 @@ public class TelemetryRestReceiver extends HttpServlet
         try
         {
             resp.setHeader( "Content", "application/json" );
-            final String input = readRequestBodyAsString( req, 1024 * 1024 );
+            final String input = ServletUtility.readRequestBodyAsString( req, 1024 * 1024 );
             final TelemetryPublishBean telemetryPublishBean = JsonUtil.deserialize( input, TelemetryPublishBean.class );
             final Storage stoage = ContextManager.getContextManager( this.getServletContext() ).getApp().getStorage();
             stoage.store( telemetryPublishBean );
@@ -74,36 +70,4 @@ public class TelemetryRestReceiver extends HttpServlet
             resp.getWriter().print( restResultBean.toJson() );
         }
     }
-
-    private static String readRequestBodyAsString( final HttpServletRequest req, final int maxChars )
-            throws IOException, PwmUnrecoverableException
-    {
-        final StringWriter stringWriter = new StringWriter();
-        final Reader readerStream = new InputStreamReader(
-                req.getInputStream(),
-                PwmConstants.DEFAULT_CHARSET
-        );
-
-        try
-        {
-            IOUtils.copy( readerStream, stringWriter );
-        }
-        catch ( Exception e )
-        {
-            final String errorMsg = "error reading request body stream: " + e.getMessage();
-            throw new PwmUnrecoverableException( new ErrorInformation( PwmError.ERROR_INTERNAL, errorMsg ) );
-        }
-        finally
-        {
-            IOUtils.closeQuietly( readerStream );
-        }
-
-        final String stringValue = stringWriter.toString();
-        if ( stringValue.length() > maxChars )
-        {
-            final String msg = "input request body is to big, size=" + stringValue.length() + ", max=" + maxChars;
-            throw new PwmUnrecoverableException( new ErrorInformation( PwmError.ERROR_INTERNAL, msg ) );
-        }
-        return stringValue;
-    }
 }

+ 1 - 1
docker/pom.xml

@@ -34,7 +34,7 @@
             <plugin>
                 <groupId>com.google.cloud.tools</groupId>
                 <artifactId>jib-maven-plugin</artifactId>
-                <version>1.0.0</version>
+                <version>1.0.2</version>
                 <executions>
                     <execution>
                         <id>make-docker-image</id>

+ 34 - 17
pom.xml

@@ -77,7 +77,7 @@
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-javadoc-plugin</artifactId>
-                <version>3.0.1</version>
+                <version>3.1.0</version>
                 <executions>
                     <execution>
                         <goals>
@@ -99,6 +99,7 @@
                                     <Implementation-Version-Display>v${project.version} b${build.number} r${build.revision}</Implementation-Version-Display>
                                 </manifestEntries>
                             </archive>
+                            <source>8</source>
                         </configuration>
                     </execution>
                 </executions>
@@ -171,7 +172,7 @@
                     <dependency>
                         <groupId>com.puppycrawl.tools</groupId>
                         <artifactId>checkstyle</artifactId>
-                        <version>8.18</version>
+                        <version>8.19</version>
                     </dependency>
                 </dependencies>
                 <executions>
@@ -180,7 +181,7 @@
                         <phase>validate</phase>
                         <configuration>
                             <propertyExpansion>basedir=${project.root.basedir}</propertyExpansion>
-                            <configLocation>${project.root.basedir}/build/checkstyle.xml</configLocation>
+                            <configLocation>build/checkstyle.xml</configLocation>
                             <encoding>UTF-8</encoding>
                             <consoleOutput>true</consoleOutput>
                             <includeTestResources>true</includeTestResources>
@@ -215,6 +216,27 @@
                             <goal>check</goal>
                         </goals>
                     </execution>
+                    <execution>
+                        <id>checkstyle-jsp</id>
+                        <phase>validate</phase>
+                        <configuration>
+                            <propertyExpansion>basedir=${project.root.basedir}</propertyExpansion>
+                            <configLocation>${project.root.basedir}/build/checkstyle-jsp.xml</configLocation>
+                            <encoding>UTF-8</encoding>
+                            <consoleOutput>true</consoleOutput>
+                            <includeTestResources>true</includeTestResources>
+                            <failsOnError>true</failsOnError>
+                            <includes>**/*.jsp</includes>
+                            <sourceDirectories>
+                                <directory>src</directory>
+                                <directory>src/test</directory>
+                                <directory>src/main/webapp</directory>
+                            </sourceDirectories>
+                        </configuration>
+                        <goals>
+                            <goal>check</goal>
+                        </goals>
+                    </execution>
                 </executions>
             </plugin>
             <plugin>
@@ -243,25 +265,20 @@
                     </execution>
                 </executions>
             </plugin>
-        </plugins>
-    </build>
-
-    <reporting>
-        <plugins>
             <plugin> <!-- checks owsp vulnerability database -->
                 <groupId>org.owasp</groupId>
                 <artifactId>dependency-check-maven</artifactId>
-                <version>4.0.1</version>
-                <reportSets>
-                    <reportSet>
-                        <reports>
-                            <report>aggregate</report>
-                        </reports>
-                    </reportSet>
-                </reportSets>
+                <version>5.0.0-M2</version>
+                <executions>
+                    <execution>
+                        <goals>
+                            <goal>check</goal>
+                        </goals>
+                    </execution>
+                </executions>
             </plugin>
         </plugins>
-    </reporting>
+    </build>
 
     <!-- common dependencies -->
     <dependencies>

+ 2 - 2
rest-test-service/src/main/java/password/pwm/resttest/ExternalMacroServlet.java

@@ -36,7 +36,7 @@ import javax.servlet.http.HttpServletResponse;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.PrintWriter;
-import java.util.Date;
+import java.time.Instant;
 
 @WebServlet(
         name = "NewUserServlet",
@@ -65,7 +65,7 @@ public class ExternalMacroServlet extends HttpServlet
             final String[] messageContent = body.split( "=" );
             final String message = messageContent[messageContent.length - 1];
             final String username = message.split( "\\+" )[0];
-            final Date currentDate = new Date();
+            final Instant currentDate = Instant.now();
             final SmsPostResponseBody messageBody = new SmsPostResponseBody( message, currentDate );
 
             instance.addToMap( username, messageBody );

+ 10 - 10
rest-test-service/src/main/java/password/pwm/resttest/SmsPostResponseBody.java

@@ -22,18 +22,18 @@
 
 package password.pwm.resttest;
 
-import java.util.Date;
+import java.time.Instant;
 
 public class SmsPostResponseBody
 {
     private String messageContent;
-    private Date date;
+    private Instant date;
 
-    public SmsPostResponseBody( final String message, final Date date )
+    public SmsPostResponseBody( final String message, final Instant date )
     {
         final String[] strings = message.split( "&" );
         this.messageContent = strings[strings.length - 1];
-        this.date = new Date( date.getTime() );
+        this.date = Instant.now();
     }
 
     public SmsPostResponseBody( final String message )
@@ -42,9 +42,9 @@ public class SmsPostResponseBody
         this.messageContent = strings[strings.length - 1];
     }
 
-    public SmsPostResponseBody( final Date date )
+    public SmsPostResponseBody( final Instant date )
     {
-        this.date = new Date( date.getTime() );
+        this.date = Instant.now();
         this.messageContent = "";
     }
 
@@ -63,13 +63,13 @@ public class SmsPostResponseBody
         this.messageContent = messageContent;
     }
 
-    public Date getDate()
+    public Instant getDate()
     {
-        return new Date( this.date.getTime() );
+        return Instant.now();
     }
 
-    public void setDate( final Date date )
+    public void setDate( final Instant date )
     {
-        this.date = new Date( this.date.getTime() );
+        this.date = Instant.now();
     }
 }

+ 1 - 1
rest-test-service/src/main/java/password/pwm/resttest/SmsResponse.java

@@ -77,7 +77,7 @@ public class SmsResponse
             int mostRecentIndex = 0;
             for ( int i = 0; i < userMessages.size(); i++ )
             {
-                if ( userMessages.get( i ).getDate().getTime() > userMessages.get( mostRecentIndex ).getDate().getTime() )
+                if ( userMessages.get( i ).getDate().toEpochMilli() > userMessages.get( mostRecentIndex ).getDate().toEpochMilli() )
                 {
                     mostRecentIndex = i;
                 }

+ 3 - 3
server/pom.xml

@@ -264,7 +264,7 @@
         <dependency>
             <groupId>org.apache.httpcomponents</groupId>
             <artifactId>httpclient</artifactId>
-            <version>4.5.7</version>
+            <version>4.5.8</version>
         </dependency>
         <dependency>
             <groupId>org.graylog2</groupId>
@@ -324,7 +324,7 @@
         <dependency>
             <groupId>com.blueconic</groupId>
             <artifactId>browscap-java</artifactId>
-            <version>1.2.7</version>
+            <version>1.2.8</version>
         </dependency>
         <dependency>
             <groupId>org.jetbrains.xodus</groupId>
@@ -344,7 +344,7 @@
         <dependency>
             <groupId>com.github.ben-manes.caffeine</groupId>
             <artifactId>caffeine</artifactId>
-            <version>2.6.2</version>
+            <version>2.7.0</version>
         </dependency>
         <dependency>
             <groupId>com.nulab-inc</groupId>

+ 3 - 1
server/src/main/java/password/pwm/AppProperty.java

@@ -111,6 +111,7 @@ public enum AppProperty
     HTTP_RESOURCES_NONCE_PATH_PREFIX                ( "http.resources.pathNoncePrefix" ),
     HTTP_RESOURCES_ZIP_FILES                        ( "http.resources.zipFiles" ),
     HTTP_COOKIE_DEFAULT_SECURE_FLAG                 ( "http.cookie.default.secureFlag" ),
+    HTTP_COOKIE_HTTPONLY_ENABLE                     ( "http.cookie.httponly.enable" ),
     HTTP_COOKIE_THEME_NAME                          ( "http.cookie.theme.name" ),
     HTTP_COOKIE_THEME_AGE                           ( "http.cookie.theme.age" ),
     HTTP_COOKIE_LOCALE_NAME                         ( "http.cookie.locale.name" ),
@@ -122,6 +123,7 @@ public enum AppProperty
     HTTP_COOKIE_LOGIN_NAME                          ( "http.cookie.login.name" ),
     HTTP_COOKIE_NONCE_NAME                          ( "http.cookie.nonce.name" ),
     HTTP_COOKIE_NONCE_LENGTH                        ( "http.cookie.nonce.length" ),
+    HTTP_COOKIE_SAMESITE_VALUE                      ( "http.cookie.sameSite.value" ),
     HTTP_BASIC_AUTH_CHARSET                         ( "http.basicAuth.charset" ),
     HTTP_BODY_MAXREAD_LENGTH                        ( "http.body.maxReadLength" ),
     HTTP_CLIENT_ALWAYS_LOG_ENTITIES                 ( "http.client.alwaysLogEntities" ),
@@ -165,7 +167,6 @@ public enum AppProperty
     HTTP_PARAM_OAUTH_GRANT_TYPE                     ( "http.parameter.oauth.grantType" ),
     HTTP_DOWNLOAD_BUFFER_SIZE                       ( "http.download.buffer.size" ),
     HTTP_SESSION_RECYCLE_AT_AUTH                    ( "http.session.recycleAtAuth" ),
-    HTTP_SESSION_VALIDATION_KEY_LENGTH              ( "http.session.validationKeyLength" ),
     HTTP_SERVLET_ENABLE_POST_REDIRECT_GET           ( "http.servlet.enablePostRedirectGet" ),
     L10N_RTL_REGEX                                  ( "l10n.rtl.regex" ),
     LOCALDB_AGGRESSIVE_COMPACT_ENABLED              ( "localdb.aggressiveCompact.enabled" ),
@@ -300,6 +301,7 @@ public enum AppProperty
     RECAPTCHA_VALIDATE_URL                          ( "recaptcha.validateUrl" ),
     REPORTING_LDAP_SEARCH_TIMEOUT                   ( "reporting.ldap.searchTimeoutMs" ),
     REPORTING_LDAP_SEARCH_THREADS                   ( "reporting.ldap.searchThreads" ),
+    REPORTING_MAX_REPORT_AGE_SECONDS                ( "reporting.maxReportAgeSeconds" ),
     SECURITY_STRIP_INLINE_JAVASCRIPT                ( "security.html.stripInlineJavascript" ),
     SECURITY_HTTP_FORCE_REQUEST_SEQUENCING          ( "security.http.forceRequestSequencing" ),
     SECURITY_HTTP_STRIP_HEADER_REGEX                ( "security.http.stripHeaderRegex" ),

+ 17 - 82
server/src/main/java/password/pwm/PwmApplication.java

@@ -43,7 +43,6 @@ import password.pwm.ldap.search.UserSearchEngine;
 import password.pwm.svc.PwmService;
 import password.pwm.svc.PwmServiceManager;
 import password.pwm.svc.cache.CacheService;
-import password.pwm.svc.node.NodeService;
 import password.pwm.svc.email.EmailService;
 import password.pwm.svc.event.AuditEvent;
 import password.pwm.svc.event.AuditRecordFactory;
@@ -51,6 +50,7 @@ import password.pwm.svc.event.AuditService;
 import password.pwm.svc.event.SystemAuditRecord;
 import password.pwm.svc.intruder.IntruderManager;
 import password.pwm.svc.intruder.RecordType;
+import password.pwm.svc.node.NodeService;
 import password.pwm.svc.pwnotify.PwNotifyService;
 import password.pwm.svc.report.ReportService;
 import password.pwm.svc.sessiontrack.SessionTrackService;
@@ -62,8 +62,10 @@ import password.pwm.svc.token.TokenService;
 import password.pwm.svc.wordlist.SeedlistService;
 import password.pwm.svc.wordlist.SharedHistoryManager;
 import password.pwm.svc.wordlist.WordlistService;
+import password.pwm.util.DailySummaryJob;
 import password.pwm.util.MBeanUtility;
 import password.pwm.util.PasswordData;
+import password.pwm.util.PwmScheduler;
 import password.pwm.util.cli.commands.ExportHttpsTomcatConfigCommand;
 import password.pwm.util.db.DatabaseAccessor;
 import password.pwm.util.db.DatabaseService;
@@ -100,9 +102,7 @@ import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 import java.util.concurrent.ExecutorService;
-import java.util.concurrent.ScheduledExecutorService;
-import java.util.concurrent.ScheduledFuture;
-import java.util.concurrent.TimeUnit;
+
 import java.util.concurrent.atomic.AtomicInteger;
 
 /**
@@ -166,7 +166,7 @@ public class PwmApplication
 
     private final PwmServiceManager pwmServiceManager = new PwmServiceManager( this );
 
-    private ScheduledExecutorService applicationExecutorService;
+    private PwmScheduler pwmScheduler;
 
     public PwmApplication( final PwmEnvironment pwmEnvironment )
             throws PwmUnrecoverableException
@@ -284,7 +284,7 @@ public class PwmApplication
         LOGGER.debug( () -> "application environment flags: " + JsonUtil.serializeCollection( pwmEnvironment.getFlags() ) );
         LOGGER.debug( () -> "application environment parameters: " + JsonUtil.serializeMap( pwmEnvironment.getParameters() ) );
 
-        applicationExecutorService = JavaHelper.makeSingleThreadExecutorService( this, this.getClass() );
+        pwmScheduler = new PwmScheduler( getInstanceID() );
 
         pwmServiceManager.initAllServices();
 
@@ -298,8 +298,10 @@ public class PwmApplication
             StatisticsManager.incrementStat( this, Statistic.PWM_STARTUPS );
             LOGGER.debug( () -> "buildTime=" + PwmConstants.BUILD_TIME + ", javaLocale=" + Locale.getDefault() + ", DefaultLocale=" + PwmConstants.DEFAULT_LOCALE );
 
-            applicationExecutorService.execute( () -> postInitTasks() );
+            pwmScheduler.immediateExecuteInNewThread( this::postInitTasks );
         }
+
+
     }
 
     private void postInitTasks( )
@@ -417,6 +419,11 @@ public class PwmApplication
             LOGGER.debug( () -> "error initializing UserAgentUtils: " + e.getMessage() );
         }
 
+        {
+            final ExecutorService executorService = PwmScheduler.makeSingleThreadExecutorService( this, PwmApplication.class );
+            pwmScheduler.scheduleDailyZuluZeroStartJob( new DailySummaryJob( this ), executorService, TimeDuration.ZERO );
+        }
+
         LOGGER.trace( () -> "completed post init tasks in " + TimeDuration.fromCurrent( startTime ).asCompactString() );
     }
 
@@ -785,7 +792,7 @@ public class PwmApplication
 
     public void shutdown( )
     {
-        applicationExecutorService.shutdown();
+        pwmScheduler.shutdown();
 
         LOGGER.warn( "shutting down" );
         {
@@ -1024,81 +1031,9 @@ public class PwmApplication
         return inprogressRequests;
     }
 
-    public ScheduledFuture scheduleFutureJob(
-            final Runnable runnable,
-            final ExecutorService executor,
-            final TimeDuration delay
-    )
-    {
-        if ( applicationExecutorService.isShutdown() )
-        {
-            return null;
-        }
-
-        return applicationExecutorService.schedule(  new WrappedRunner( runnable, executor ), delay.asMillis(), TimeUnit.MILLISECONDS );
-    }
-
-    public void scheduleFixedRateJob(
-            final Runnable runnable,
-            final ExecutorService executor,
-            final TimeDuration initialDelay,
-            final TimeDuration frequency
-    )
-    {
-        if ( applicationExecutorService.isShutdown() )
-        {
-            return;
-        }
-
-        if ( initialDelay != null )
-        {
-            applicationExecutorService.schedule( new WrappedRunner( runnable, executor ), initialDelay.asMillis(), TimeUnit.MILLISECONDS );
-        }
-
-        final Runnable jobWithNextScheduler = () ->
-        {
-            new WrappedRunner( runnable, executor ).run();
-            scheduleFixedRateJob( runnable, executor, null, frequency );
-        };
-
-        applicationExecutorService.schedule(  jobWithNextScheduler, frequency.asMillis(), TimeUnit.MILLISECONDS );
-    }
-
-    private static class WrappedRunner implements Runnable
+    public PwmScheduler getPwmScheduler()
     {
-        private final Runnable runnable;
-        private final ExecutorService executor;
-
-        WrappedRunner( final Runnable runnable, final ExecutorService executor )
-        {
-            this.runnable = runnable;
-            this.executor = executor;
-        }
-
-        @Override
-        public void run()
-        {
-            if ( executor.isShutdown() )
-            {
-                return;
-            }
-
-            try
-            {
-                if ( !executor.isShutdown() )
-                {
-                    executor.execute( runnable );
-                }
-                else
-                {
-                    LOGGER.trace( () -> "skipping scheduled job " + runnable + " on shutdown executor + " + executor );
-                }
-            }
-            catch ( Throwable t )
-            {
-                LOGGER.error( "unexpected error running scheduled job: " + t.getMessage(), t );
-            }
-        }
+        return pwmScheduler;
     }
 }
 

+ 11 - 3
server/src/main/java/password/pwm/bean/EmailItemBean.java

@@ -24,13 +24,13 @@ package password.pwm.bean;
 
 import lombok.AllArgsConstructor;
 import lombok.Builder;
-import lombok.Getter;
+import lombok.Value;
 
 import java.io.Serializable;
 
-@Getter
+@Value
 @AllArgsConstructor
-@Builder
+@Builder( toBuilder = true )
 public class EmailItemBean implements Serializable
 {
     private final String to;
@@ -39,6 +39,14 @@ public class EmailItemBean implements Serializable
     private final String bodyPlain;
     private final String bodyHtml;
 
+    public EmailItemBean applyBodyReplacement( final CharSequence target, final CharSequence replacement )
+    {
+        return this.toBuilder()
+                .bodyPlain( this.getBodyPlain().replace( target, replacement ) )
+                .bodyHtml( this.getBodyHtml().replace( target, replacement ) )
+                .build();
+    }
+
     public String toDebugString( )
     {
         return "from: " + from + ", to: " + to + ", subject: " + subject;

+ 2 - 5
server/src/main/java/password/pwm/bean/FormNonce.java

@@ -23,17 +23,14 @@
 package password.pwm.bean;
 
 import com.google.gson.annotations.SerializedName;
-import lombok.AllArgsConstructor;
-import lombok.Getter;
+import lombok.Value;
 
 import java.io.Serializable;
 import java.time.Instant;
 
-@Getter
-@AllArgsConstructor
+@Value
 public class FormNonce implements Serializable
 {
-
     @SerializedName( "g" )
     private final String sessionGUID;
 

+ 5 - 15
server/src/main/java/password/pwm/bean/LocalSessionStateBean.java

@@ -23,12 +23,12 @@
 package password.pwm.bean;
 
 import lombok.Data;
-import password.pwm.PwmApplication;
 import password.pwm.ldap.UserInfoBean;
 
 import java.io.Serializable;
 import java.time.Instant;
 import java.util.Locale;
+import java.util.concurrent.atomic.AtomicInteger;
 
 /**
  * <p>Only information that is particular to the http session is stored in the
@@ -65,30 +65,20 @@ public class LocalSessionStateBean implements Serializable
     private boolean passwordModified;
     private boolean privateUrlAccessed;
 
-    private int intruderAttempts;
+    private final AtomicInteger intruderAttempts = new AtomicInteger( 0 );
     private boolean oauthInProgress;
 
-    private int sessionVerificationKeyLength;
     private boolean sessionIdRecycleNeeded;
-
-    public LocalSessionStateBean( final int sessionVerificationKeyLength )
-    {
-        this.sessionVerificationKeyLength = sessionVerificationKeyLength;
-    }
+    private boolean sameSiteCookieRecycleRequested;
 
     public void incrementIntruderAttempts( )
     {
-        intruderAttempts++;
+        intruderAttempts.incrementAndGet();
     }
 
     public void clearIntruderAttempts( )
     {
-        intruderAttempts = 0;
-    }
-
-    public void regenerateSessionVerificationKey( final PwmApplication pwmApplication )
-    {
-        sessionVerificationKey = pwmApplication.getSecureService().pwmRandom().alphaNumericString( sessionVerificationKeyLength ) + Long.toHexString( System.currentTimeMillis() );
+        intruderAttempts.set( 0 );
     }
 }
 

+ 4 - 141
server/src/main/java/password/pwm/bean/LoginInfoBean.java

@@ -23,8 +23,7 @@
 package password.pwm.bean;
 
 import com.google.gson.annotations.SerializedName;
-import lombok.Getter;
-import lombok.Setter;
+import lombok.Data;
 import password.pwm.PwmConstants;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.ldap.auth.AuthenticationType;
@@ -46,8 +45,7 @@ import java.util.Set;
  *
  * <p>Short serialized names are used to shrink the effective size of the login cookie.</p>
  */
-@Getter
-@Setter
+@Data
 public class LoginInfoBean implements Serializable
 {
 
@@ -67,10 +65,10 @@ public class LoginInfoBean implements Serializable
     private UserIdentity userIdentity;
 
     @SerializedName( "a" )
-    private boolean auth;
+    private boolean authenticated;
 
     @SerializedName( "p" )
-    private PasswordData pw;
+    private PasswordData userCurrentPassword;
 
     @SerializedName( "t" )
     private AuthenticationType type = AuthenticationType.UNAUTHENTICATED;
@@ -105,141 +103,6 @@ public class LoginInfoBean implements Serializable
     @SerializedName( "lf" )
     private Set<LoginFlag> loginFlags = new HashSet<>();
 
-    public Instant getAuthTime( )
-    {
-        return authTime;
-    }
-
-    public void setAuthTime( final Instant authTime )
-    {
-        this.authTime = authTime;
-    }
-
-    public AuthenticationType getType( )
-    {
-        return type;
-    }
-
-    public void setType( final AuthenticationType type )
-    {
-        this.type = type;
-    }
-
-    public PasswordData getUserCurrentPassword( )
-    {
-        return pw;
-    }
-
-    public void setUserCurrentPassword( final PasswordData userCurrentPassword )
-    {
-        this.pw = userCurrentPassword;
-    }
-
-    public BasicAuthInfo getBasicAuth( )
-    {
-        return basicAuth;
-    }
-
-    public void setBasicAuth( final BasicAuthInfo basicAuth )
-    {
-        this.basicAuth = basicAuth;
-    }
-
-    public Instant getOauthExp( )
-    {
-        return oauthExp;
-    }
-
-    public void setOauthExp( final Instant oauthExp )
-    {
-        this.oauthExp = oauthExp;
-    }
-
-    public String getOauthRefToken( )
-    {
-        return oauthRefToken;
-    }
-
-    public void setOauthRefToken( final String oauthRefToken )
-    {
-        this.oauthRefToken = oauthRefToken;
-    }
-
-    public List<AuthenticationType> getAuthFlags( )
-    {
-        return authFlags;
-    }
-
-    public PwmAuthenticationSource getAuthSource( )
-    {
-        return authSource;
-    }
-
-    public void setAuthSource( final PwmAuthenticationSource authSource )
-    {
-        this.authSource = authSource;
-    }
-
-    public String getGuid( )
-    {
-        return guid;
-    }
-
-    public void setGuid( final String guid )
-    {
-        this.guid = guid;
-    }
-
-    public int getReqCounter( )
-    {
-        return reqCounter;
-    }
-
-    public void setReqCounter( final int reqCounter )
-    {
-        this.reqCounter = reqCounter;
-    }
-
-    public UserIdentity getUserIdentity( )
-    {
-        return userIdentity;
-    }
-
-    public void setUserIdentity( final UserIdentity userIdentity )
-    {
-        this.userIdentity = userIdentity;
-    }
-
-    public boolean isAuthenticated( )
-    {
-        return auth;
-    }
-
-    public void setAuthenticated( final boolean authenticated )
-    {
-        this.auth = authenticated;
-    }
-
-    public PasswordData getPw( )
-    {
-        return pw;
-    }
-
-    public void setPw( final PasswordData pw )
-    {
-        this.pw = pw;
-    }
-
-    public Instant getReqTime( )
-    {
-        return reqTime;
-    }
-
-    public void setReqTime( final Instant reqTime )
-    {
-        this.reqTime = reqTime;
-    }
-
     public boolean isLoginFlag( final LoginFlag loginStateFlag )
     {
         return loginFlags.contains( loginStateFlag );

+ 2 - 2
server/src/main/java/password/pwm/bean/PasswordStatus.java

@@ -23,12 +23,12 @@
 package password.pwm.bean;
 
 import lombok.Builder;
-import lombok.Getter;
+import lombok.Value;
 import password.pwm.util.java.JsonUtil;
 
 import java.io.Serializable;
 
-@Getter
+@Value
 @Builder
 public class PasswordStatus implements Serializable
 {

+ 2 - 2
server/src/main/java/password/pwm/bean/SessionLabel.java

@@ -23,11 +23,11 @@
 package password.pwm.bean;
 
 import lombok.AllArgsConstructor;
-import lombok.Getter;
+import lombok.Value;
 
 import java.io.Serializable;
 
-@Getter
+@Value
 @AllArgsConstructor
 public class SessionLabel implements Serializable
 {

+ 2 - 2
server/src/main/java/password/pwm/bean/SmsItemBean.java

@@ -24,12 +24,12 @@ package password.pwm.bean;
 
 
 import lombok.AllArgsConstructor;
-import lombok.Getter;
+import lombok.Value;
 import password.pwm.util.java.JsonUtil;
 
 import java.io.Serializable;
 
-@Getter
+@Value
 @AllArgsConstructor
 public class SmsItemBean implements Serializable
 {

+ 2 - 2
server/src/main/java/password/pwm/bean/TelemetryPublishBean.java

@@ -23,14 +23,14 @@
 package password.pwm.bean;
 
 import lombok.Builder;
-import lombok.Getter;
+import lombok.Value;
 
 import java.io.Serializable;
 import java.time.Instant;
 import java.util.List;
 import java.util.Map;
 
-@Getter
+@Value
 @Builder
 public class TelemetryPublishBean implements Serializable
 {

+ 5 - 4
server/src/main/java/password/pwm/config/Configuration.java

@@ -955,12 +955,12 @@ public class Configuration implements SettingReader
 
     public String readAppProperty( final AppProperty property )
     {
-        final Map<String, String> configurationValues = StringUtil.convertStringListToNameValuePair( this.readSettingAsStringArray( PwmSetting.APP_PROPERTY_OVERRIDES ), "=" );
-        if ( configurationValues.containsKey( property.getKey() ) )
+        if ( dataCache.appPropertyOverrides == null )
         {
-            return configurationValues.get( property.getKey() );
+            dataCache.appPropertyOverrides = StringUtil.convertStringListToNameValuePair( this.readSettingAsStringArray( PwmSetting.APP_PROPERTY_OVERRIDES ), "=" );
         }
-        return property.getDefaultValue();
+
+        return dataCache.appPropertyOverrides.getOrDefault( property.getKey(), property.getDefaultValue() );
     }
 
     private Convenience helper = new Convenience();
@@ -1040,6 +1040,7 @@ public class Configuration implements SettingReader
         private final Map<PwmSetting, StoredValue> settings = new EnumMap<>( PwmSetting.class );
         private final Map<String, Map<Locale, String>> customText = new LinkedHashMap<>();
         private final Map<ProfileType, Map<String, Profile>> profileCache = new LinkedHashMap<>();
+        private Map<String, String> appPropertyOverrides = null;
     }
 
     public Map<AppProperty, String> readAllNonDefaultAppProperties( )

+ 10 - 13
server/src/main/java/password/pwm/config/PwmSetting.java

@@ -33,6 +33,7 @@ import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.macro.MacroMachine;
 
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashSet;
@@ -839,6 +840,8 @@ public enum PwmSetting
             "newUser.writeAttributes", PwmSettingSyntax.ACTION, PwmSettingCategory.NEWUSER_PROFILE ),
     NEWUSER_DELETE_ON_FAIL(
             "newUser.deleteOnFail", PwmSettingSyntax.BOOLEAN, PwmSettingCategory.NEWUSER_PROFILE ),
+    NEWUSER_LOGOUT_AFTER_CREATION(
+            "newUser.logoutAfterCreation", PwmSettingSyntax.BOOLEAN, PwmSettingCategory.NEWUSER_PROFILE ),
     NEWUSER_USERNAME_DEFINITION(
             "newUser.username.definition", PwmSettingSyntax.STRING_ARRAY, PwmSettingCategory.NEWUSER_PROFILE ),
     NEWUSER_EMAIL_VERIFICATION(
@@ -1129,16 +1132,14 @@ public enum PwmSetting
 
 
     // reporting
-    REPORTING_ENABLE(
+    REPORTING_ENABLE_DAILY_JOB(
             "reporting.enable", PwmSettingSyntax.BOOLEAN, PwmSettingCategory.REPORTING ),
+    REPORTING_JOB_TIME_OFFSET(
+            "reporting.job.timeOffset", PwmSettingSyntax.DURATION, PwmSettingCategory.REPORTING ),
     REPORTING_USER_MATCH(
             "reporting.ldap.userMatch", PwmSettingSyntax.USER_PERMISSION, PwmSettingCategory.REPORTING ),
-    REPORTING_MAX_CACHE_AGE(
-            "reporting.maxCacheAge", PwmSettingSyntax.DURATION, PwmSettingCategory.REPORTING ),
     REPORTING_MAX_QUERY_SIZE(
             "reporting.ldap.maxQuerySize", PwmSettingSyntax.NUMERIC, PwmSettingCategory.REPORTING ),
-    REPORTING_JOB_TIME_OFFSET(
-            "reporting.job.timeOffset", PwmSettingSyntax.DURATION, PwmSettingCategory.REPORTING ),
     REPORTING_JOB_INTENSITY(
             "reporting.job.intensity", PwmSettingSyntax.SELECT, PwmSettingCategory.REPORTING ),
     REPORTING_SUMMARY_DAY_VALUES(
@@ -1591,14 +1592,10 @@ public enum PwmSetting
 
     public static PwmSetting forKey( final String key )
     {
-        for ( final PwmSetting loopSetting : values() )
-        {
-            if ( loopSetting.getKey().equals( key ) )
-            {
-                return loopSetting;
-            }
-        }
-        return null;
+        return Arrays.stream( values() )
+                .filter( loopValue -> loopValue.getKey().equals( key ) )
+                .findFirst()
+                .orElse( null );
     }
 
     public String toMenuLocationDebug(

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

@@ -27,6 +27,7 @@ import password.pwm.util.i18n.LocaleHelper;
 import password.pwm.util.java.XmlElement;
 
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Iterator;
@@ -452,4 +453,11 @@ public enum PwmSettingCategory
         return Collections.unmodifiableCollection( returnValues );
     }
 
+    public static PwmSettingCategory forKey( final String key )
+    {
+        return Arrays.stream( values() )
+                .filter( loopValue -> loopValue.getKey().equals( key ) )
+                .findFirst()
+                .orElse( null );
+    }
 }

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

@@ -43,7 +43,7 @@ import java.util.concurrent.atomic.AtomicInteger;
 
 public class PwmSettingXml
 {
-    private static final String SETTING_XML_FILENAME = ( PwmSetting.class.getPackage().getName()
+    public static final String SETTING_XML_FILENAME = ( PwmSetting.class.getPackage().getName()
             + "." + PwmSetting.class.getSimpleName() ).replace( ".", "/" ) + ".xml";
 
     public static final String XML_ELEMENT_LDAP_PERMISSION = "ldapPermission";

+ 70 - 82
server/src/main/java/password/pwm/health/HealthMonitor.java

@@ -22,11 +22,12 @@
 
 package password.pwm.health;
 
+import lombok.Value;
 import password.pwm.AppProperty;
 import password.pwm.PwmApplication;
 import password.pwm.error.PwmException;
 import password.pwm.svc.PwmService;
-import password.pwm.util.java.JavaHelper;
+import password.pwm.util.PwmScheduler;
 import password.pwm.util.java.TimeDuration;
 import password.pwm.util.logging.PwmLogger;
 
@@ -35,22 +36,19 @@ import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
-import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import java.util.TreeSet;
+import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ExecutorService;
-import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.Future;
+import java.util.concurrent.atomic.AtomicInteger;
 
 public class HealthMonitor implements PwmService
 {
     private static final PwmLogger LOGGER = PwmLogger.forClass( HealthMonitor.class );
 
-    private PwmApplication pwmApplication;
-
-    private final Set<HealthRecord> healthRecords = new TreeSet<>();
-
     private static final List<HealthChecker> HEALTH_CHECKERS;
 
     static
@@ -68,12 +66,11 @@ public class HealthMonitor implements PwmService
     private ExecutorService executorService;
     private HealthMonitorSettings settings;
 
-    private volatile Instant lastHealthCheckTime = Instant.ofEpochMilli( 0 );
-    private volatile Instant lastRequestedUpdateTime = Instant.ofEpochMilli( 0 );
-
-    private Map<HealthMonitorFlag, Serializable> healthProperties = new HashMap<>();
+    private Map<HealthMonitorFlag, Serializable> healthProperties = new ConcurrentHashMap<>();
 
     private STATUS status = STATUS.NEW;
+    private PwmApplication pwmApplication;
+    private volatile HealthData healthData = emptyHealthData();
 
     enum HealthMonitorFlag
     {
@@ -81,18 +78,6 @@ public class HealthMonitor implements PwmService
         AdPasswordPolicyApiCheck,
     }
 
-    public enum CheckTimeliness
-    {
-        /* Execute update immediately and wait for results */
-        Immediate,
-
-        /* Take current data unless its ancient */
-        CurrentButNotAncient,
-
-        /* Take current data even if its ancient and never block */
-        NeverBlock,
-    }
-
     public HealthMonitor( )
     {
     }
@@ -103,16 +88,17 @@ public class HealthMonitor implements PwmService
         {
             return null;
         }
-        return lastHealthCheckTime;
+        final HealthData healthData = this.healthData;
+        return healthData != null ? healthData.getTimeStamp() : Instant.ofEpochMilli( 0 );
     }
 
-    public HealthStatus getMostSevereHealthStatus( final CheckTimeliness timeliness )
+    public HealthStatus getMostSevereHealthStatus( )
     {
         if ( status != STATUS.OPEN )
         {
             return HealthStatus.GOOD;
         }
-        return getMostSevereHealthStatus( getHealthRecords( timeliness ) );
+        return getMostSevereHealthStatus( getHealthRecords( ) );
     }
 
     public static HealthStatus getMostSevereHealthStatus( final Collection<HealthRecord> healthRecords )
@@ -149,42 +135,38 @@ public class HealthMonitor implements PwmService
             return;
         }
 
-
-        executorService = JavaHelper.makeBackgroundExecutor( pwmApplication, this.getClass() );
-        pwmApplication.scheduleFixedRateJob( new ScheduledUpdater(), executorService, TimeDuration.SECONDS_10, settings.getNominalCheckInterval() );
+        executorService = PwmScheduler.makeBackgroundExecutor( pwmApplication, this.getClass() );
 
         status = STATUS.OPEN;
     }
 
-    public Set<HealthRecord> getHealthRecords( final CheckTimeliness timeliness )
+    public Set<HealthRecord> getHealthRecords( )
     {
         if ( status != STATUS.OPEN )
         {
             return Collections.emptySet();
         }
 
-        lastRequestedUpdateTime = Instant.now();
+        if ( healthData.recordsAreOutdated() )
+        {
+            final Instant startTime = Instant.now();
+            LOGGER.trace( () ->  "begin force immediate check" );
+            final Future future = pwmApplication.getPwmScheduler().scheduleFutureJob( new ImmediateJob(), executorService, TimeDuration.ZERO );
+            settings.getMaximumForceCheckWait().pause( future::isDone );
+            LOGGER.trace( () ->  "exit force immediate check, done=" + future.isDone() + ", " + TimeDuration.compactFromCurrent( startTime ) );
+        }
+
+        pwmApplication.getPwmScheduler().scheduleFutureJob( new UpdateJob(), executorService, settings.getNominalCheckInterval() );
 
         {
-            final boolean recordsAreStale = TimeDuration.fromCurrent( lastHealthCheckTime ).isLongerThan( settings.getMaximumRecordAge() );
-            if ( timeliness == CheckTimeliness.Immediate || ( timeliness == CheckTimeliness.CurrentButNotAncient && recordsAreStale ) )
+            final HealthData localHealthData = this.healthData;
+            if ( localHealthData.recordsAreOutdated() )
             {
-                final ScheduledFuture updateTask = pwmApplication.scheduleFutureJob( new ImmediateUpdater(), executorService, TimeDuration.ZERO );
-                final Instant beginWaitTime = Instant.now();
-                while ( !updateTask.isDone() && TimeDuration.fromCurrent( beginWaitTime ).isShorterThan( settings.getMaximumForceCheckWait() ) )
-                {
-                    JavaHelper.pause( 500 );
-                }
+                return Collections.singleton( HealthRecord.forMessage( HealthMessage.NoData ) );
             }
-        }
 
-        final boolean recordsAreStale = TimeDuration.fromCurrent( lastHealthCheckTime ).isLongerThan( settings.getMaximumRecordAge() );
-        if ( recordsAreStale )
-        {
-            return Collections.singleton( HealthRecord.forMessage( HealthMessage.NoData ) );
+            return localHealthData.getHealthRecords();
         }
-
-        return Collections.unmodifiableSet( healthRecords );
     }
 
     public void close( )
@@ -193,30 +175,32 @@ public class HealthMonitor implements PwmService
         {
             executorService.shutdown();
         }
-        healthRecords.clear();
+        healthData = emptyHealthData();
         status = STATUS.CLOSED;
     }
 
+    private HealthData emptyHealthData()
+    {
+        return new HealthData( Collections.emptySet(), Instant.ofEpochMilli( 0 ) );
+    }
+
     public List<HealthRecord> healthCheck( )
     {
         return Collections.emptyList();
     }
 
+    private AtomicInteger healthCheckCount = new AtomicInteger( 0 );
+
     private void doHealthChecks( )
     {
+        final int counter = healthCheckCount.getAndIncrement();
         if ( status != STATUS.OPEN )
         {
             return;
         }
 
-        final TimeDuration timeSinceLastUpdate = TimeDuration.fromCurrent( lastHealthCheckTime );
-        if ( timeSinceLastUpdate.isShorterThan( settings.getMinimumCheckInterval().asMillis(), TimeDuration.Unit.MILLISECONDS ) )
-        {
-            return;
-        }
-
         final Instant startTime = Instant.now();
-        LOGGER.trace( () -> "beginning background health check process" );
+        LOGGER.trace( () -> "beginning health check execution (" + counter + ")" );
         final List<HealthRecord> tempResults = new ArrayList<>();
         for ( final HealthChecker loopChecker : HEALTH_CHECKERS )
         {
@@ -254,10 +238,9 @@ public class HealthMonitor implements PwmService
                 }
             }
         }
-        healthRecords.clear();
-        healthRecords.addAll( tempResults );
-        lastHealthCheckTime = Instant.now();
-        LOGGER.trace( () -> "health check process completed (" + TimeDuration.compactFromCurrent( startTime ) + ")" );
+
+        healthData = new HealthData( Collections.unmodifiableSet( new TreeSet<>( tempResults ) ), Instant.now() );
+        LOGGER.trace( () -> "completed health check execution (" + counter + ") in " + TimeDuration.compactFromCurrent( startTime ) );
     }
 
     public ServiceInfoBean serviceInfo( )
@@ -265,50 +248,55 @@ public class HealthMonitor implements PwmService
         return new ServiceInfoBean( Collections.emptyList() );
     }
 
-    public Map<HealthMonitorFlag, Serializable> getHealthProperties( )
+    Map<HealthMonitorFlag, Serializable> getHealthProperties( )
     {
         return healthProperties;
     }
 
-    private class ScheduledUpdater implements Runnable
+    private class UpdateJob implements Runnable
     {
         @Override
         public void run( )
         {
-            final TimeDuration timeSinceLastRequest = TimeDuration.fromCurrent( lastRequestedUpdateTime );
-            if ( timeSinceLastRequest.isShorterThan( settings.getNominalCheckInterval().asMillis() + 1000, TimeDuration.Unit.MILLISECONDS ) )
+            if ( healthData.recordsAreStale() )
             {
-                try
-                {
-                    doHealthChecks();
-                }
-                catch ( Throwable e )
-                {
-                    LOGGER.error( "error during health check execution: " + e.getMessage(), e );
-
-                }
+                new ImmediateJob().run();
             }
         }
     }
 
-    private class ImmediateUpdater implements Runnable
+    private class ImmediateJob implements Runnable
     {
         @Override
         public void run( )
         {
-            final TimeDuration timeSinceLastUpdate = TimeDuration.fromCurrent( lastHealthCheckTime );
-            if ( timeSinceLastUpdate.isLongerThan( settings.getMinimumCheckInterval().asMillis(), TimeDuration.Unit.MILLISECONDS ) )
+            try
             {
-                try
-                {
-                    doHealthChecks();
-                }
-                catch ( Throwable e )
-                {
-                    LOGGER.error( "error during health check execution: " + e.getMessage(), e );
-                }
+                final Instant startTime = Instant.now();
+                doHealthChecks();
+                LOGGER.trace( () -> "completed health check dredge " + TimeDuration.compactFromCurrent( startTime ) );
+            }
+            catch ( Throwable e )
+            {
+                LOGGER.error( "error during health check execution: " + e.getMessage(), e );
             }
         }
     }
 
+    @Value
+    private class HealthData
+    {
+        private Set<HealthRecord> healthRecords;
+        private Instant timeStamp;
+
+        private boolean recordsAreStale()
+        {
+            return TimeDuration.fromCurrent( this.getTimeStamp() ).isLongerThan( settings.getNominalCheckInterval() );
+        }
+
+        private boolean recordsAreOutdated()
+        {
+            return TimeDuration.fromCurrent( this.getTimeStamp() ).isLongerThan( settings.getMaximumRecordAge() );
+        }
+    }
 }

+ 4 - 6
server/src/main/java/password/pwm/http/ContextManager.java

@@ -36,6 +36,7 @@ import password.pwm.error.PwmError;
 import password.pwm.error.PwmException;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.util.PropertyConfigurationImporter;
+import password.pwm.util.PwmScheduler;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.TimeDuration;
 import password.pwm.util.logging.PwmLogger;
@@ -274,8 +275,8 @@ public class ContextManager implements Serializable
         }
 
         taskMaster = Executors.newSingleThreadScheduledExecutor(
-                JavaHelper.makePwmThreadFactory(
-                        JavaHelper.makeThreadName( pwmApplication, this.getClass() ) + "-",
+                PwmScheduler.makePwmThreadFactory(
+                        PwmScheduler.makeThreadName( pwmApplication, this.getClass() ) + "-",
                         true
                 ) );
 
@@ -560,10 +561,7 @@ public class ContextManager implements Serializable
 
             LOGGER.trace( () -> "waiting up to " + maxRequestWaitTime.asCompactString()
                     + " for " + startingRequetsInProgress  + " requests to complete." );
-            JavaHelper.pause(
-                    maxRequestWaitTime.asMillis(),
-                    10,
-                    o -> pwmApplication.getInprogressRequests().get() == 0
+            maxRequestWaitTime.pause( TimeDuration.of( 10, TimeDuration.Unit.MILLISECONDS ), () -> pwmApplication.getInprogressRequests().get() == 0
             );
 
             final int requestsInPrgoress = pwmApplication.getInprogressRequests().get();

+ 1 - 0
server/src/main/java/password/pwm/http/HttpHeader.java

@@ -48,6 +48,7 @@ public enum HttpHeader
     Origin( "Origin" ),
     Referer( "Referer" ),
     Server( "Server" ),
+    SetCookie( "Set-Cookie" ),
     UserAgent( "User-Agent" ),
     WWW_Authenticate( "WWW-Authenticate" ),
     XContentTypeOptions( "X-Content-Type-Options" ),

+ 2 - 41
server/src/main/java/password/pwm/http/PwmHttpRequestWrapper.java

@@ -22,14 +22,12 @@
 
 package password.pwm.http;
 
-import org.apache.commons.io.IOUtils;
 import password.pwm.AppProperty;
 import password.pwm.PwmConstants;
 import password.pwm.config.Configuration;
-import password.pwm.error.ErrorInformation;
-import password.pwm.error.PwmError;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.util.PasswordData;
+import password.pwm.util.ServletUtility;
 import password.pwm.util.Validator;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.JsonUtil;
@@ -39,9 +37,6 @@ import password.pwm.util.logging.PwmLogger;
 import javax.servlet.http.Cookie;
 import javax.servlet.http.HttpServletRequest;
 import java.io.IOException;
-import java.io.InputStreamReader;
-import java.io.Reader;
-import java.io.StringWriter;
 import java.io.UnsupportedEncodingException;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -117,41 +112,7 @@ public class PwmHttpRequestWrapper
     public String readRequestBodyAsString( final int maxChars )
             throws IOException, PwmUnrecoverableException
     {
-        return readRequestBodyAsString( this.getHttpServletRequest(), maxChars );
-    }
-
-    public static String readRequestBodyAsString( final HttpServletRequest httpServletRequest, final int maxChars )
-            throws IOException, PwmUnrecoverableException
-    {
-        final StringWriter stringWriter = new StringWriter();
-        final Reader readerStream = new InputStreamReader(
-                httpServletRequest.getInputStream(),
-                PwmConstants.DEFAULT_CHARSET
-        );
-
-        try
-        {
-            IOUtils.copy( readerStream, stringWriter );
-        }
-        catch ( Exception e )
-        {
-            final String errorMsg = "error reading request body stream: " + e.getMessage();
-            throw new PwmUnrecoverableException( new ErrorInformation( PwmError.ERROR_INTERNAL, errorMsg ) );
-        }
-        finally
-        {
-            IOUtils.closeQuietly( readerStream );
-        }
-
-        final String stringValue = stringWriter.toString();
-        if ( stringValue.length() > maxChars )
-        {
-            throw new PwmUnrecoverableException( new ErrorInformation(
-                    PwmError.ERROR_INTERNAL,
-                    "input request body is to big, size=" + stringValue.length() + ", max=" + maxChars )
-            );
-        }
-        return stringValue;
+        return ServletUtility.readRequestBodyAsString( this.getHttpServletRequest(), maxChars );
     }
 
     public Map<String, String> readBodyAsJsonStringMap( final Flag... flags )

+ 23 - 3
server/src/main/java/password/pwm/http/PwmHttpResponseWrapper.java

@@ -23,9 +23,13 @@
 package password.pwm.http;
 
 import password.pwm.AppProperty;
+import password.pwm.PwmApplication;
 import password.pwm.PwmConstants;
 import password.pwm.config.Configuration;
+import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.http.filter.CookieManagementFilter;
 import password.pwm.util.Validator;
+import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.logging.PwmLogger;
 
@@ -35,7 +39,6 @@ import javax.servlet.http.HttpServletResponse;
 import java.io.IOException;
 import java.io.OutputStream;
 import java.io.PrintWriter;
-import java.util.Arrays;
 
 public class PwmHttpResponseWrapper
 {
@@ -174,7 +177,8 @@ public class PwmHttpResponseWrapper
             }
         }
 
-        final boolean httpOnly = flags == null || !Arrays.asList( flags ).contains( Flag.NonHttpOnly );
+        final boolean httpOnlyEnabled = Boolean.parseBoolean( configuration.readAppProperty( AppProperty.HTTP_COOKIE_HTTPONLY_ENABLE ) );
+        final boolean httpOnly = httpOnlyEnabled && JavaHelper.enumArrayContainsValue( flags, Flag.NonHttpOnly );
 
         final String value;
         {
@@ -184,7 +188,7 @@ public class PwmHttpResponseWrapper
             }
             else
             {
-                if ( flags != null && Arrays.asList( flags ).contains( Flag.BypassSanitation ) )
+                if ( JavaHelper.enumArrayContainsValue( flags, Flag.BypassSanitation ) )
                 {
                     value = StringUtil.urlEncode( cookieValue );
                 }
@@ -208,6 +212,22 @@ public class PwmHttpResponseWrapper
             LOGGER.warn( "writing large cookie to response: cookieName=" + cookieName + ", length=" + value.length() );
         }
         this.getHttpServletResponse().addCookie( theCookie );
+        addSameSiteCookieAttribute();
+    }
+
+    void addSameSiteCookieAttribute( )
+    {
+        final PwmApplication pwmApplication;
+        try
+        {
+            pwmApplication = ContextManager.getPwmApplication( this.httpServletRequest );
+            final String value = pwmApplication.getConfig().readAppProperty( AppProperty.HTTP_COOKIE_SAMESITE_VALUE );
+            CookieManagementFilter.addSameSiteCookieAttribute( httpServletResponse, value );
+        }
+        catch ( PwmUnrecoverableException e )
+        {
+            LOGGER.trace( () -> "unable to load application configuration while checking samesite cookie attribute config", e );
+        }
     }
 
     public void removeCookie( final String cookieName, final CookiePath path )

+ 3 - 6
server/src/main/java/password/pwm/http/PwmRequest.java

@@ -73,11 +73,11 @@ public class PwmRequest extends PwmHttpRequestWrapper
 
     private static final PwmLogger LOGGER = PwmLogger.forClass( PwmRequest.class );
 
-
     private final PwmResponse pwmResponse;
+    private final PwmURL pwmURL;
+
     private transient PwmApplication pwmApplication;
     private transient PwmSession pwmSession;
-    private PwmURL pwmURL;
 
     private final Set<PwmRequestFlag> flags = new HashSet<>();
 
@@ -110,6 +110,7 @@ public class PwmRequest extends PwmHttpRequestWrapper
         this.pwmResponse = new PwmResponse( httpServletResponse, this, pwmApplication.getConfig() );
         this.pwmSession = pwmSession;
         this.pwmApplication = pwmApplication;
+        this.pwmURL = new PwmURL( this.getHttpServletRequest() );
     }
 
     public PwmApplication getPwmApplication( )
@@ -371,10 +372,6 @@ public class PwmRequest extends PwmHttpRequestWrapper
 
     public PwmURL getURL( )
     {
-        if ( pwmURL == null )
-        {
-            pwmURL = new PwmURL( this.getHttpServletRequest() );
-        }
         return pwmURL;
     }
 

+ 2 - 22
server/src/main/java/password/pwm/http/PwmSession.java

@@ -38,7 +38,6 @@ import password.pwm.ldap.UserInfoBean;
 import password.pwm.ldap.UserInfoFactory;
 import password.pwm.ldap.auth.AuthenticationType;
 import password.pwm.svc.stats.Statistic;
-import password.pwm.svc.stats.StatisticsManager;
 import password.pwm.util.i18n.LocaleHelper;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.JsonUtil;
@@ -48,7 +47,6 @@ import password.pwm.util.secure.PwmRandom;
 import password.pwm.util.secure.PwmSecurityKey;
 
 import java.io.Serializable;
-import java.math.BigInteger;
 import java.time.Instant;
 import java.util.Date;
 import java.util.LinkedHashMap;
@@ -61,7 +59,6 @@ import java.util.Map;
  */
 public class PwmSession implements Serializable
 {
-
     private static final PwmLogger LOGGER = PwmLogger.forClass( PwmSession.class );
 
     private final LocalSessionStateBean sessionStateBean;
@@ -92,25 +89,8 @@ public class PwmSession implements Serializable
             throw new IllegalStateException( "PwmApplication must be available during session creation" );
         }
 
-        final int sessionValidationKeyLength = Integer.parseInt( pwmApplication.getConfig().readAppProperty( AppProperty.HTTP_SESSION_VALIDATION_KEY_LENGTH ) );
-        sessionStateBean = new LocalSessionStateBean( sessionValidationKeyLength );
-        sessionStateBean.regenerateSessionVerificationKey( pwmApplication );
-        this.sessionStateBean.setSessionID( null );
-
-        final StatisticsManager statisticsManager = pwmApplication.getStatisticsManager();
-        if ( statisticsManager != null )
-        {
-            String nextID = pwmApplication.getStatisticsManager().getStatBundleForKey( StatisticsManager.KEY_CUMULATIVE ).getStatistic( Statistic.HTTP_SESSIONS );
-            try
-            {
-                nextID = new BigInteger( nextID ).toString();
-            }
-            catch ( NumberFormatException e )
-            {
-                LOGGER.debug( this, () -> "error generating sessionID: " + e.getMessage(), e );
-            }
-            this.getSessionStateBean().setSessionID( nextID );
-        }
+        sessionStateBean = new LocalSessionStateBean();
+        this.sessionStateBean.setSessionID( pwmApplication.getSessionTrackService().generateNewSessionID() );
 
         this.sessionStateBean.setSessionLastAccessedTime( Instant.now() );
 

+ 124 - 90
server/src/main/java/password/pwm/http/filter/ConfigAccessFilter.java

@@ -31,6 +31,7 @@ import password.pwm.bean.UserIdentity;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.stored.ConfigurationProperty;
 import password.pwm.config.stored.ConfigurationReader;
+import password.pwm.config.stored.StoredConfiguration;
 import password.pwm.config.stored.StoredConfigurationImpl;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmError;
@@ -48,6 +49,7 @@ import password.pwm.svc.intruder.RecordType;
 import password.pwm.svc.sessiontrack.UserAgentUtils;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.JsonUtil;
+import password.pwm.util.java.StringUtil;
 import password.pwm.util.java.TimeDuration;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.secure.PwmHashAlgorithm;
@@ -104,8 +106,7 @@ public class ConfigAccessFilter extends AbstractPwmFilter
         return pwmURL.isConfigManagerURL();
     }
 
-    @SuppressWarnings( "checkstyle:MethodLength" )
-    static ProcessStatus checkAuthentication(
+    private static ProcessStatus checkAuthentication(
             final PwmRequest pwmRequest,
             final ConfigManagerBean configManagerBean
     )
@@ -116,33 +117,7 @@ public class ConfigAccessFilter extends AbstractPwmFilter
         final ConfigurationReader runningConfigReader = ContextManager.getContextManager( pwmRequest.getHttpServletRequest().getSession() ).getConfigReader();
         final StoredConfigurationImpl storedConfig = runningConfigReader.getStoredConfiguration();
 
-        boolean authRequired = false;
-        if ( storedConfig.hasPassword() )
-        {
-            authRequired = true;
-        }
-
-        if ( PwmApplicationMode.RUNNING == pwmRequest.getPwmApplication().getApplicationMode() )
-        {
-            if ( !pwmRequest.isAuthenticated() )
-            {
-                throw new PwmUnrecoverableException( PwmError.ERROR_AUTHENTICATION_REQUIRED );
-            }
-
-            if ( !pwmRequest.getPwmSession().getSessionManager().checkPermission( pwmRequest.getPwmApplication(), Permission.PWMADMIN ) )
-            {
-                final ErrorInformation errorInformation = new ErrorInformation( PwmError.ERROR_UNAUTHORIZED );
-                denyAndError( pwmRequest, errorInformation );
-                return ProcessStatus.Halt;
-            }
-        }
-
-        if ( PwmApplicationMode.CONFIGURATION != pwmRequest.getPwmApplication().getApplicationMode() )
-        {
-            authRequired = true;
-        }
-
-        if ( !authRequired )
+        if ( !checkIfAuthIsRequired( pwmRequest, storedConfig ) )
         {
             return ProcessStatus.Continue;
         }
@@ -163,68 +138,9 @@ public class ConfigAccessFilter extends AbstractPwmFilter
             return ProcessStatus.Continue;
         }
 
-        String persistentLoginValue = null;
-        boolean persistentLoginAccepted = false;
-        boolean persistentLoginEnabled = false;
-        if ( pwmRequest.getConfig().isDefaultValue( PwmSetting.PWM_SECURITY_KEY ) )
-        {
-            LOGGER.debug( pwmRequest, () -> "security not available, persistent login not possible." );
-        }
-        else
-        {
-            persistentLoginEnabled = true;
-            final PwmSecurityKey securityKey = pwmRequest.getConfig().getSecurityKey();
+        final boolean persistentLoginEnabled = persistentLoginEnabled( pwmRequest );
+        final boolean persistentLoginAccepted = checkPersistentLoginCookie( pwmRequest, storedConfig );
 
-            if ( PwmApplicationMode.RUNNING == pwmRequest.getPwmApplication().getApplicationMode() )
-            {
-                persistentLoginValue = SecureEngine.hash(
-                        storedConfig.readConfigProperty( ConfigurationProperty.PASSWORD_HASH )
-                                + pwmSession.getUserInfo().getUserIdentity().toDelimitedKey(),
-                        PwmHashAlgorithm.SHA512 );
-
-            }
-            else
-            {
-                persistentLoginValue = SecureEngine.hash(
-                        storedConfig.readConfigProperty( ConfigurationProperty.PASSWORD_HASH ),
-                        PwmHashAlgorithm.SHA512 );
-            }
-
-            {
-                final String cookieStr = pwmRequest.readCookie( PwmConstants.COOKIE_PERSISTENT_CONFIG_LOGIN );
-                if ( securityKey != null && cookieStr != null && !cookieStr.isEmpty() )
-                {
-                    try
-                    {
-                        final String jsonStr = pwmApplication.getSecureService().decryptStringValue( cookieStr );
-                        final PersistentLoginInfo persistentLoginInfo = JsonUtil.deserialize( jsonStr, PersistentLoginInfo.class );
-                        if ( persistentLoginInfo != null && persistentLoginValue != null )
-                        {
-                            if ( persistentLoginInfo.getExpireDate().isAfter( Instant.now() ) )
-                            {
-                                if ( persistentLoginValue.equals( persistentLoginInfo.getPassword() ) )
-                                {
-                                    persistentLoginAccepted = true;
-                                    LOGGER.debug( pwmRequest, () -> "accepting persistent config login from cookie (expires "
-                                            + JavaHelper.toIsoDate( persistentLoginInfo.getExpireDate() )
-                                            + ")"
-                                    );
-                                }
-                            }
-                        }
-                    }
-                    catch ( Exception e )
-                    {
-                        LOGGER.error( pwmRequest, "error examining persistent config login cookie: " + e.getMessage() );
-                    }
-                    if ( !persistentLoginAccepted )
-                    {
-                        pwmRequest.getPwmResponse().removeCookie( PwmConstants.COOKIE_PERSISTENT_CONFIG_LOGIN, null );
-                        LOGGER.debug( pwmRequest, () -> "removing non-working persistent config login cookie" );
-                    }
-                }
-            }
-        }
 
 
         final String password = pwmRequest.readParameterAsString( "password" );
@@ -256,12 +172,14 @@ public class ConfigAccessFilter extends AbstractPwmFilter
             configManagerBean.setPasswordVerified( true );
             pwmApplication.getIntruderManager().convenience().clearAddressAndSession( pwmSession );
             pwmApplication.getIntruderManager().clear( RecordType.USERNAME, PwmConstants.CONFIGMANAGER_INTRUDER_USERNAME );
+            pwmRequest.getPwmSession().getSessionStateBean().setSessionIdRecycleNeeded( true );
             if ( persistentLoginEnabled && !persistentLoginAccepted && "on".equals( pwmRequest.readParameterAsString( "remember" ) ) )
             {
                 final int persistentSeconds = figureMaxLoginSeconds( pwmRequest );
                 if ( persistentSeconds > 0 )
                 {
                     final Instant expirationDate = Instant.ofEpochMilli( System.currentTimeMillis() + ( persistentSeconds * 1000 ) );
+                    final String persistentLoginValue = makePersistentLoginValue( pwmRequest, storedConfig );
                     final PersistentLoginInfo persistentLoginInfo = new PersistentLoginInfo( expirationDate, persistentLoginValue );
                     final String jsonPersistentLoginInfo = JsonUtil.serialize( persistentLoginInfo );
                     final String cookieValue = pwmApplication.getSecureService().encryptToString( jsonPersistentLoginInfo );
@@ -293,6 +211,122 @@ public class ConfigAccessFilter extends AbstractPwmFilter
         return ProcessStatus.Halt;
     }
 
+    private static boolean checkPersistentLoginCookie(
+            final PwmRequest pwmRequest,
+            final StoredConfiguration storedConfig
+
+    )
+            throws PwmUnrecoverableException
+    {
+        final PwmSecurityKey securityKey = pwmRequest.getConfig().getSecurityKey();
+        final String cookieStr = pwmRequest.readCookie( PwmConstants.COOKIE_PERSISTENT_CONFIG_LOGIN );
+        if ( securityKey != null && cookieStr != null && !cookieStr.isEmpty() )
+        {
+            try
+            {
+                final String persistentLoginValue = makePersistentLoginValue( pwmRequest, storedConfig );
+
+                final String jsonStr = pwmRequest.getPwmApplication().getSecureService().decryptStringValue( cookieStr );
+                final PersistentLoginInfo persistentLoginInfo = JsonUtil.deserialize( jsonStr, PersistentLoginInfo.class );
+                if ( persistentLoginInfo != null && persistentLoginValue != null )
+                {
+                    if ( persistentLoginInfo.getExpireDate().isAfter( Instant.now() ) )
+                    {
+
+                        if ( persistentLoginValue.equals( persistentLoginInfo.getPassword() ) )
+                        {
+                            LOGGER.debug( pwmRequest, () -> "accepting persistent config login from cookie (expires "
+                                    + JavaHelper.toIsoDate( persistentLoginInfo.getExpireDate() )
+                                    + ")"
+                            );
+                            return true;
+                        }
+                    }
+                }
+            }
+            catch ( Exception e )
+            {
+                LOGGER.error( pwmRequest, "error examining persistent config login cookie: " + e.getMessage() );
+            }
+            if ( !StringUtil.isEmpty( cookieStr ) )
+            {
+                pwmRequest.getPwmResponse().removeCookie( PwmConstants.COOKIE_PERSISTENT_CONFIG_LOGIN, null );
+                LOGGER.debug( pwmRequest, () -> "removing non-working persistent config login cookie" );
+            }
+        }
+
+        return false;
+    }
+
+
+    private static boolean checkIfAuthIsRequired(
+            final PwmRequest pwmRequest,
+            final StoredConfigurationImpl storedConfig
+    )
+            throws PwmUnrecoverableException
+    {
+        if ( storedConfig.hasPassword() )
+        {
+            return true;
+        }
+
+        if ( PwmApplicationMode.RUNNING == pwmRequest.getPwmApplication().getApplicationMode() )
+        {
+            if ( !pwmRequest.isAuthenticated() )
+            {
+                throw new PwmUnrecoverableException( PwmError.ERROR_AUTHENTICATION_REQUIRED );
+            }
+
+            if ( !pwmRequest.getPwmSession().getSessionManager().checkPermission( pwmRequest.getPwmApplication(), Permission.PWMADMIN ) )
+            {
+                throw new PwmUnrecoverableException( PwmError.ERROR_UNAUTHORIZED );
+            }
+        }
+
+        if ( PwmApplicationMode.CONFIGURATION != pwmRequest.getPwmApplication().getApplicationMode() )
+        {
+            return true;
+        }
+
+        return false;
+    }
+
+    private static boolean persistentLoginEnabled(
+            final PwmRequest pwmRequest
+    )
+    {
+        if ( pwmRequest.getConfig().isDefaultValue( PwmSetting.PWM_SECURITY_KEY ) )
+        {
+            LOGGER.debug( pwmRequest, () -> "security not available, persistent login not possible." );
+            return false;
+        }
+
+        return true;
+    }
+
+    private static String makePersistentLoginValue(
+            final PwmRequest pwmRequest,
+            final StoredConfiguration storedConfig
+    )
+            throws PwmUnrecoverableException
+    {
+        if ( PwmApplicationMode.RUNNING == pwmRequest.getPwmApplication().getApplicationMode() )
+        {
+            final PwmSession pwmSession = pwmRequest.getPwmSession();
+            return SecureEngine.hash(
+                    storedConfig.readConfigProperty( ConfigurationProperty.PASSWORD_HASH )
+                            + pwmSession.getUserInfo().getUserIdentity().toDelimitedKey(),
+                    PwmHashAlgorithm.SHA512 );
+
+        }
+        else
+        {
+            return SecureEngine.hash(
+                    storedConfig.readConfigProperty( ConfigurationProperty.PASSWORD_HASH ),
+                    PwmHashAlgorithm.SHA512 );
+        }
+    }
+
     private static void forwardToJsp( final PwmRequest pwmRequest )
             throws ServletException, PwmUnrecoverableException, IOException
     {

+ 148 - 0
server/src/main/java/password/pwm/http/filter/CookieManagementFilter.java

@@ -0,0 +1,148 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2018 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.http.filter;
+
+import password.pwm.AppProperty;
+import password.pwm.PwmApplication;
+import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.http.ContextManager;
+import password.pwm.http.HttpHeader;
+import password.pwm.http.PwmSession;
+import password.pwm.http.PwmSessionWrapper;
+import password.pwm.util.java.StringUtil;
+import password.pwm.util.logging.PwmLogger;
+
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
+import java.io.IOException;
+import java.util.Collection;
+
+public class CookieManagementFilter implements Filter
+{
+    private static final PwmLogger LOGGER = PwmLogger.forClass( CookieManagementFilter.class );
+
+    private String value;
+
+    @Override
+    public void init( final FilterConfig filterConfig )
+            throws ServletException
+    {
+        final PwmApplication pwmApplication;
+        try
+        {
+            pwmApplication = ContextManager.getPwmApplication( filterConfig.getServletContext() );
+            value = pwmApplication.getConfig().readAppProperty( AppProperty.HTTP_COOKIE_SAMESITE_VALUE );
+        }
+        catch ( PwmUnrecoverableException e )
+        {
+            LOGGER.trace( () -> "unable to load application configuration while checking samesite cookie attribute config", e );
+        }
+    }
+
+    @Override
+    public void destroy()
+    {
+
+    }
+
+    @Override
+    public void doFilter( final ServletRequest servletRequest, final ServletResponse servletResponse, final FilterChain filterChain )
+            throws IOException, ServletException
+    {
+        filterChain.doFilter( servletRequest, servletResponse );
+        addSameSiteCookieAttribute( ( HttpServletResponse ) servletResponse, value );
+        markSessionForRecycle( ( HttpServletRequest ) servletRequest );
+    }
+
+    private void markSessionForRecycle( final HttpServletRequest httpServletRequest )
+    {
+        if ( StringUtil.isEmpty( value ) )
+        {
+            return;
+        }
+
+        final HttpSession httpSession = httpServletRequest.getSession( false );
+        if ( httpSession != null )
+        {
+            PwmSession pwmSession = null;
+            try
+            {
+                pwmSession = PwmSessionWrapper.readPwmSession( httpSession );
+            }
+            catch ( PwmUnrecoverableException e )
+            {
+                LOGGER.trace( () -> "unable to load session while checking samesite cookie attribute config", e );
+            }
+
+            if ( pwmSession != null )
+            {
+                if ( !pwmSession.getSessionStateBean().isSameSiteCookieRecycleRequested() )
+                {
+                    pwmSession.getSessionStateBean().setSameSiteCookieRecycleRequested( true );
+                    pwmSession.getSessionStateBean().setSessionIdRecycleNeeded( true );
+                }
+            }
+        }
+    }
+
+    public static void addSameSiteCookieAttribute( final HttpServletResponse response, final String value )
+    {
+        if ( StringUtil.isEmpty( value ) )
+        {
+            return;
+        }
+
+        final Collection<String> headers = response.getHeaders( HttpHeader.SetCookie.getHttpName() );
+        boolean firstHeader = true;
+
+        for ( final String header : headers )
+        {
+            final String newHeader;
+            if ( !header.contains( "SameSite" ) )
+            {
+                newHeader = header + "; SameSite=" + value;
+            }
+            else
+            {
+                newHeader = header;
+            }
+
+            if ( firstHeader )
+            {
+                response.setHeader( HttpHeader.SetCookie.getHttpName(), newHeader );
+                firstHeader = false;
+            }
+            else
+            {
+                response.addHeader( HttpHeader.SetCookie.getHttpName(), newHeader );
+            }
+        }
+    }
+}

+ 3 - 42
server/src/main/java/password/pwm/http/filter/RequestInitializationFilter.java

@@ -69,8 +69,6 @@ import java.math.BigInteger;
 import java.net.InetAddress;
 import java.net.UnknownHostException;
 import java.time.Instant;
-import java.util.Enumeration;
-import java.util.HashMap;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Locale;
@@ -303,49 +301,12 @@ public class RequestInitializationFilter implements Filter
     private void checkIfSessionRecycleNeeded( final PwmRequest pwmRequest )
             throws IOException, ServletException
     {
-        if ( !pwmRequest.getPwmSession().getSessionStateBean().isSessionIdRecycleNeeded() )
+        if ( pwmRequest.getPwmSession().getSessionStateBean().isSessionIdRecycleNeeded() )
         {
-            return;
+            pwmRequest.getHttpServletRequest().changeSessionId();
+            pwmRequest.getPwmSession().getSessionStateBean().setSessionIdRecycleNeeded( false );
         }
 
-        final boolean recycleEnabled = Boolean.parseBoolean( pwmRequest.getConfig().readAppProperty( AppProperty.HTTP_SESSION_RECYCLE_AT_AUTH ) );
-
-        if ( !recycleEnabled )
-        {
-            return;
-        }
-        LOGGER.debug( pwmRequest, () -> "forcing new http session due to authentication" );
-
-        final HttpServletRequest req = pwmRequest.getHttpServletRequest();
-
-        // read the old session data
-        final HttpSession oldSession = req.getSession( true );
-        final int oldMaxInactiveInterval = oldSession.getMaxInactiveInterval();
-        final Map<String, Object> sessionAttributes = new HashMap<>();
-        final Enumeration oldSessionAttrNames = oldSession.getAttributeNames();
-        while ( oldSessionAttrNames.hasMoreElements() )
-        {
-            final String attrName = ( String ) oldSessionAttrNames.nextElement();
-            sessionAttributes.put( attrName, oldSession.getAttribute( attrName ) );
-        }
-
-        for ( final String attrName : sessionAttributes.keySet() )
-        {
-            oldSession.removeAttribute( attrName );
-        }
-
-        //invalidate the old session
-        oldSession.invalidate();
-
-        // make a new session
-        final HttpSession newSession = req.getSession( true );
-
-        // write back all the session data
-        sessionAttributes.keySet().forEach( attrName -> newSession.setAttribute( attrName, sessionAttributes.get( attrName ) ) );
-
-        newSession.setMaxInactiveInterval( oldMaxInactiveInterval );
-
-        pwmRequest.getPwmSession().getSessionStateBean().setSessionIdRecycleNeeded( false );
     }
 
     public static void addPwmResponseHeaders(

+ 21 - 16
server/src/main/java/password/pwm/http/filter/SessionFilter.java

@@ -59,7 +59,6 @@ import java.net.UnknownHostException;
 import java.time.Instant;
 import java.util.Arrays;
 import java.util.Enumeration;
-import java.util.Iterator;
 import java.util.List;
 import java.util.concurrent.atomic.AtomicInteger;
 
@@ -123,7 +122,7 @@ public class SessionFilter extends AbstractPwmFilter
                     && e.getCause() instanceof NoClassDefFoundError
                     && e.getCause().getMessage() != null
                     && e.getCause().getMessage().contains( "JaxbAnnotationIntrospector" )
-                    )
+            )
             {
                 // this is a jersey 1.18 bug that occurs once per execution
                 LOGGER.debug( pwmRequest, () -> "ignoring JaxbAnnotationIntrospector NoClassDefFoundError: " + e.getMessage() );
@@ -288,7 +287,6 @@ public class SessionFilter extends AbstractPwmFilter
     )
             throws IOException, ServletException, PwmUnrecoverableException
     {
-        final LocalSessionStateBean ssBean = pwmRequest.getPwmSession().getSessionStateBean();
         final HttpServletRequest req = pwmRequest.getHttpServletRequest();
         final PwmResponse pwmResponse = pwmRequest.getPwmResponse();
 
@@ -312,19 +310,31 @@ public class SessionFilter extends AbstractPwmFilter
             return ProcessStatus.Continue;
         }
 
+        final LocalSessionStateBean ssBean = pwmRequest.getPwmSession().getSessionStateBean();
         final String verificationParamName = pwmRequest.getConfig().readAppProperty( AppProperty.HTTP_PARAM_SESSION_VERIFICATION );
         final String keyFromRequest = pwmRequest.readParameterAsString( verificationParamName, PwmHttpRequestWrapper.Flag.BypassValidation );
 
         // request doesn't have key, so make a new one, store it in the session, and redirect back here with the new key.
-        if ( keyFromRequest == null || keyFromRequest.length() < 1 )
+        if ( StringUtil.isEmpty( keyFromRequest ) )
         {
+            if ( StringUtil.isEmpty( ssBean.getSessionVerificationKey() ) )
+            {
+                ssBean.setSessionVerificationKey( pwmRequest.getPwmApplication().getSecureService().pwmRandom().randomUUID().toString() );
+            }
 
             final String returnURL = figureValidationURL( pwmRequest, ssBean.getSessionVerificationKey() );
 
             LOGGER.trace( pwmRequest, () -> "session has not been validated, redirecting with verification key to " + returnURL );
 
-            // better chance of detecting un-sticky sessions this way
-            pwmResponse.setHeader( HttpHeader.Connection, "close" );
+            {
+                final String httpVersion = pwmRequest.getHttpServletRequest().getProtocol();
+                if ( "HTTP/1.0".equals( httpVersion ) || "HTTP/1.1".equals( httpVersion ) )
+                {
+                    // better chance of detecting un-sticky sessions this way (closing connection not available in HTTP/2)
+                    pwmResponse.setHeader( HttpHeader.Connection, "close" );
+                }
+            }
+
             if ( mode == SessionVerificationMode.VERIFY_AND_CACHE )
             {
                 req.setAttribute( "Location", returnURL );
@@ -361,8 +371,7 @@ public class SessionFilter extends AbstractPwmFilter
     {
         final HttpServletRequest req = pwmRequest.getHttpServletRequest();
 
-        final StringBuilder sb = new StringBuilder();
-        sb.append( req.getRequestURL() );
+        String redirectURL = req.getRequestURI();
 
         final String verificationParamName = pwmRequest.getConfig().readAppProperty( AppProperty.HTTP_PARAM_SESSION_VERIFICATION );
 
@@ -377,12 +386,9 @@ public class SessionFilter extends AbstractPwmFilter
                 {
                     final List<String> paramValues = Arrays.asList( req.getParameterValues( paramName ) );
 
-                    for ( final Iterator<String> valueIter = paramValues.iterator(); valueIter.hasNext(); )
+                    for ( final String value : paramValues )
                     {
-                        final String value = valueIter.next();
-                        sb.append( sb.toString().contains( "?" ) ? "&" : "?" );
-                        sb.append( StringUtil.urlEncode( paramName ) ).append( "=" );
-                        sb.append( StringUtil.urlEncode( value ) );
+                        redirectURL = PwmURL.appendAndEncodeUrlParameters( redirectURL, paramName, value );
                     }
                 }
             }
@@ -394,11 +400,10 @@ public class SessionFilter extends AbstractPwmFilter
 
         if ( validationKey != null )
         {
-            sb.append( sb.toString().contains( "?" ) ? "&" : "?" );
-            sb.append( verificationParamName ).append( "=" ).append( validationKey );
+            redirectURL = PwmURL.appendAndEncodeUrlParameters( redirectURL, verificationParamName, validationKey );
         }
 
-        return sb.toString();
+        return redirectURL;
     }
 
 

+ 1 - 2
server/src/main/java/password/pwm/http/servlet/ClientApiServlet.java

@@ -221,8 +221,7 @@ public class ClientApiServlet extends ControlledPwmServlet
         {
             final HealthData jsonOutput = RestHealthServer.processGetHealthCheckData(
                     pwmRequest.getPwmApplication(),
-                    pwmRequest.getLocale(),
-                    false );
+                    pwmRequest.getLocale() );
             final RestResultBean restResultBean = RestResultBean.withData( jsonOutput );
             pwmRequest.outputJsonResult( restResultBean );
         }

+ 1 - 1
server/src/main/java/password/pwm/http/servlet/DeleteAccountServlet.java

@@ -238,7 +238,7 @@ public class DeleteAccountServlet extends ControlledPwmServlet
         {
             final MacroMachine macroMachine = pwmRequest.getPwmSession().getSessionManager().getMacroMachine( pwmApplication );
             final String macroedUrl = macroMachine.expandMacros( nextUrl );
-            LOGGER.debug( pwmRequest, () -> "settinging forward url to post-delete next url: " + macroedUrl );
+            LOGGER.debug( pwmRequest, () -> "setting forward url to post-delete next url: " + macroedUrl );
             pwmRequest.getPwmSession().getSessionStateBean().setForwardURL( macroedUrl );
         }
 

+ 1 - 6
server/src/main/java/password/pwm/http/servlet/admin/ReportStatusBean.java

@@ -63,7 +63,7 @@ public class ReportStatusBean implements Serializable
             case RollOver:
             {
                 presentableMap.add( new DisplayElement( "usersProcessed", DisplayElement.Type.string, "Users Processed",
-                        numberFormat.format( reportService.getSummaryData().getTotalUsers() )
+                        numberFormat.format( reportService.getSummaryData().getTotalUsers().intValue() )
                                 + " of " + numberFormat.format( reportService.getTotalRecords() ) ) );
                 availableCommands.add( ReportService.ReportCommand.Stop );
             }
@@ -132,11 +132,6 @@ public class ReportStatusBean implements Serializable
             }
             final long totalRecords = reportService.getTotalRecords();
             presentableMap.add( new DisplayElement( "recordsInCache", DisplayElement.Type.string, "Records in Cache", numberFormat.format( totalRecords ) ) );
-            if ( totalRecords > 0 )
-            {
-                presentableMap.add( new DisplayElement( "meanRecordCacheTime", DisplayElement.Type.timestamp, "Mean Record Cache Time",
-                        JavaHelper.toIsoDate( reportService.getSummaryData().getMeanCacheTime() ) ) );
-            }
         }
 
         return ReportStatusBean.builder()

+ 1 - 2
server/src/main/java/password/pwm/http/servlet/configmanager/DebugItemGenerator.java

@@ -33,7 +33,6 @@ import password.pwm.bean.UserIdentity;
 import password.pwm.config.Configuration;
 import password.pwm.config.stored.StoredConfigurationImpl;
 import password.pwm.error.PwmUnrecoverableException;
-import password.pwm.health.HealthMonitor;
 import password.pwm.health.HealthRecord;
 import password.pwm.http.ContextManager;
 import password.pwm.http.PwmRequest;
@@ -349,7 +348,7 @@ public class DebugItemGenerator
         @Override
         public void outputItem( final PwmApplication pwmApplication, final PwmRequest pwmRequest, final OutputStream outputStream ) throws Exception
         {
-            final Set<HealthRecord> records = pwmApplication.getHealthMonitor().getHealthRecords( HealthMonitor.CheckTimeliness.CurrentButNotAncient );
+            final Set<HealthRecord> records = pwmApplication.getHealthMonitor().getHealthRecords();
             final String recordJson = JsonUtil.serializeCollection( records, JsonUtil.Flag.PrettyPrint );
             outputStream.write( recordJson.getBytes( PwmConstants.DEFAULT_CHARSET ) );
         }

+ 7 - 6
server/src/main/java/password/pwm/http/servlet/forgottenpw/ForgottenPasswordServlet.java

@@ -84,6 +84,7 @@ import password.pwm.util.PostChangePasswordAction;
 import password.pwm.util.form.FormUtility;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.JsonUtil;
+import password.pwm.util.java.TimeDuration;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.operations.ActionExecutor;
 import password.pwm.util.operations.PasswordUtility;
@@ -831,8 +832,8 @@ public class ForgottenPasswordServlet extends ControlledPwmServlet
 
         {
             LOGGER.trace( pwmRequest, () -> "preparing to send a new token to user" );
-            final long delayTime = Long.parseLong( pwmRequest.getConfig().readAppProperty( AppProperty.TOKEN_RESEND_DELAY_MS ) );
-            JavaHelper.pause( delayTime );
+            final long delayTimeMs = Long.parseLong( pwmRequest.getConfig().readAppProperty( AppProperty.TOKEN_RESEND_DELAY_MS ) );
+            TimeDuration.of( delayTimeMs, TimeDuration.Unit.MILLISECONDS ).pause();
         }
 
         {
@@ -858,9 +859,6 @@ public class ForgottenPasswordServlet extends ControlledPwmServlet
         {
             final FormConfiguration formConfiguration = forgottenPasswordBean.getAttributeForm().iterator().next();
 
-            // add a bit of jitter to pretend like we're checking a data source
-            JavaHelper.pause( 300 + pwmRequest.getPwmApplication().getSecureService().pwmRandom().nextInt( 700 ) );
-
             if ( forgottenPasswordBean.getUserSearchValues() != null )
             {
                 final List<FormConfiguration> formConfigurations = pwmRequest.getConfig().readSettingAsForm( PwmSetting.FORGOTTEN_PASSWORD_SEARCH_FORM );
@@ -1134,7 +1132,10 @@ public class ForgottenPasswordServlet extends ControlledPwmServlet
             {
                 if ( userInfo.isPasswordLocked() )
                 {
-                    pwmRequest.setAttribute( PwmRequestAttribute.ForgottenPasswordInhibitPasswordReset, Boolean.TRUE );
+                    final boolean inhibitReset = minLifetimeOption != RecoveryMinLifetimeOption.ALLOW
+                            && userInfo.isWithinPasswordMinimumLifetime();
+
+                    pwmRequest.setAttribute( PwmRequestAttribute.ForgottenPasswordInhibitPasswordReset, inhibitReset );
                     pwmRequest.forwardToJsp( JspUrl.RECOVER_PASSWORD_ACTION_CHOICE );
                     return;
                 }

+ 14 - 36
server/src/main/java/password/pwm/http/servlet/helpdesk/HelpdeskServlet.java

@@ -644,8 +644,6 @@ public class HelpdeskServlet extends ControlledPwmServlet
     {
         final HelpdeskProfile helpdeskProfile = getHelpdeskProfile( pwmRequest );
 
-        final Instant startTime = Instant.now();
-
         final HelpdeskVerificationRequestBean helpdeskVerificationRequestBean = JsonUtil.deserialize(
                 pwmRequest.readRequestBodyAsString(),
                 HelpdeskVerificationRequestBean.class
@@ -714,19 +712,7 @@ public class HelpdeskServlet extends ControlledPwmServlet
                 pwmRequest.getPwmApplication().getAuditManager().submit( auditRecord );
             }
 
-            // add a delay to prevent continuous checks
-            final long delayMs = Long.parseLong( pwmRequest.getConfig().readAppProperty( AppProperty.HELPDESK_VERIFICATION_INVALID_DELAY_MS ) );
-            while ( TimeDuration.fromCurrent( startTime ).isShorterThan( delayMs ) )
-            {
-                JavaHelper.pause( 100 );
-            }
-
-            final HelpdeskVerificationResponseBean responseBean = new HelpdeskVerificationResponseBean(
-                    passed,
-                    verificationStateBean.toClientString( pwmRequest.getPwmApplication() )
-            );
-            final RestResultBean restResultBean = RestResultBean.withData( responseBean );
-            pwmRequest.outputJsonResult( restResultBean );
+            return outputVerificationResponseBean( pwmRequest, passed, verificationStateBean );
         }
         catch ( PwmOperationalException e )
         {
@@ -848,7 +834,6 @@ public class HelpdeskServlet extends ControlledPwmServlet
     )
             throws IOException, PwmUnrecoverableException, ServletException
     {
-        final Instant startTime = Instant.now();
         final HelpdeskVerificationRequestBean helpdeskVerificationRequestBean = JsonUtil.deserialize(
                 pwmRequest.readRequestBodyAsString(),
                 HelpdeskVerificationRequestBean.class
@@ -915,20 +900,7 @@ public class HelpdeskServlet extends ControlledPwmServlet
             pwmRequest.getPwmApplication().getAuditManager().submit( auditRecord );
         }
 
-        // add a delay to prevent continuous checks
-        final long delayMs = Long.parseLong( pwmRequest.getConfig().readAppProperty( AppProperty.HELPDESK_VERIFICATION_INVALID_DELAY_MS ) );
-        while ( TimeDuration.fromCurrent( startTime ).isShorterThan( delayMs ) )
-        {
-            JavaHelper.pause( 100 );
-        }
-
-        final HelpdeskVerificationResponseBean responseBean = new HelpdeskVerificationResponseBean(
-                passed,
-                verificationStateBean.toClientString( pwmRequest.getPwmApplication() )
-        );
-        final RestResultBean restResultBean = RestResultBean.withData( responseBean );
-        pwmRequest.outputJsonResult( restResultBean );
-        return ProcessStatus.Halt;
+        return outputVerificationResponseBean( pwmRequest, passed, verificationStateBean );
     }
 
     @ActionHandler( action = "clearOtpSecret" )
@@ -1052,7 +1024,6 @@ public class HelpdeskServlet extends ControlledPwmServlet
             throws IOException, PwmUnrecoverableException, ServletException
     {
         final HelpdeskProfile helpdeskProfile = getHelpdeskProfile( pwmRequest );
-        final Instant startTime = Instant.now();
         final String bodyString = pwmRequest.readRequestBodyAsString();
         final HelpdeskVerificationRequestBean helpdeskVerificationRequestBean = JsonUtil.deserialize(
                 bodyString,
@@ -1132,12 +1103,19 @@ public class HelpdeskServlet extends ControlledPwmServlet
             pwmRequest.getPwmApplication().getAuditManager().submit( auditRecord );
         }
 
+        return outputVerificationResponseBean( pwmRequest, passed, verificationStateBean );
+    }
+
+    private ProcessStatus outputVerificationResponseBean(
+            final PwmRequest pwmRequest,
+            final boolean passed,
+            final HelpdeskVerificationStateBean verificationStateBean
+    )
+            throws IOException, PwmUnrecoverableException
+    {
         // add a delay to prevent continuous checks
-        final long delayMs = Long.parseLong( pwmRequest.getConfig().readAppProperty( AppProperty.HELPDESK_VERIFICATION_INVALID_DELAY_MS ) );
-        while ( TimeDuration.fromCurrent( startTime ).isShorterThan( delayMs ) )
-        {
-            JavaHelper.pause( 100 );
-        }
+        final long delayMs = JavaHelper.silentParseLong( pwmRequest.getConfig().readAppProperty( AppProperty.HELPDESK_VERIFICATION_INVALID_DELAY_MS ), 500 );
+        TimeDuration.of( delayMs, TimeDuration.Unit.MILLISECONDS ).jitterPause( pwmRequest.getPwmApplication().getSecureService(), 0.3f );
 
         final HelpdeskVerificationResponseBean responseBean = new HelpdeskVerificationResponseBean(
                 passed,

+ 22 - 0
server/src/main/java/password/pwm/http/servlet/newuser/NewUserServlet.java

@@ -44,6 +44,7 @@ import password.pwm.http.PwmRequestAttribute;
 import password.pwm.http.PwmSession;
 import password.pwm.http.PwmURL;
 import password.pwm.http.bean.NewUserBean;
+import password.pwm.http.filter.AuthenticationFilter;
 import password.pwm.http.servlet.AbstractPwmServlet;
 import password.pwm.http.servlet.ControlledPwmServlet;
 import password.pwm.http.servlet.PwmServletDefinition;
@@ -195,6 +196,8 @@ public class NewUserServlet extends ControlledPwmServlet
     protected void nextStep( final PwmRequest pwmRequest )
             throws IOException, ServletException, PwmUnrecoverableException, ChaiUnavailableException
     {
+        TimeDuration.of( 8, TimeDuration.Unit.SECONDS ).pause();
+
         final NewUserBean newUserBean = getNewUserBean( pwmRequest );
         final PwmApplication pwmApplication = pwmRequest.getPwmApplication();
         final PwmSession pwmSession = pwmRequest.getPwmSession();
@@ -705,6 +708,25 @@ public class NewUserServlet extends ControlledPwmServlet
         // -- process complete -- \\
         pwmRequest.getPwmApplication().getSessionStateService().clearBean( pwmRequest, NewUserBean.class );
 
+        if ( pwmRequest.isAuthenticated() )
+        {
+            final PwmSession pwmSession = pwmRequest.getPwmSession();
+
+            if ( AuthenticationFilter.forceRequiredRedirects( pwmRequest ) == ProcessStatus.Halt )
+            {
+                return ProcessStatus.Halt;
+            }
+
+        // log the user out if the current profiles states so
+        final boolean forceLogoutOnChange = newUserProfile.readSettingAsBoolean( PwmSetting.NEWUSER_LOGOUT_AFTER_CREATION );
+        if ( forceLogoutOnChange )
+            {
+                LOGGER.trace( pwmSession, "logging out user; account created" );
+                pwmRequest.sendRedirect( PwmServletDefinition.Logout );
+                return ProcessStatus.Halt;
+            }
+        }
+
         final String configuredRedirectUrl = newUserProfile.readSettingAsString( PwmSetting.NEWUSER_REDIRECT_URL );
         if ( !StringUtil.isEmpty( configuredRedirectUrl ) && StringUtil.isEmpty( pwmRequest.getPwmSession().getSessionStateBean().getForwardURL() ) )
         {

+ 1 - 1
server/src/main/java/password/pwm/http/servlet/peoplesearch/PeopleSearchDataReader.java

@@ -998,7 +998,7 @@ class PeopleSearchDataReader
         executor.execute( job );
 
         final TimeDuration maxDuration = peopleSearchConfiguration.getExportCsvMaxDuration();
-        JavaHelper.pause( maxDuration.asMillis(), 1000, o -> ( executor.getQueue().size() + executor.getActiveCount() <= 0 ) );
+        maxDuration.pause( () -> executor.getQueue().size() + executor.getActiveCount() <= 0 );
 
         final TimeDuration timeDuration = TimeDuration.fromCurrent( startTime );
         LOGGER.trace( pwmRequest, () -> "completed csv export of " + rowCounter.get() + " records in " + timeDuration.asCompactString() );

+ 2 - 2
server/src/main/java/password/pwm/http/servlet/peoplesearch/PeopleSearchService.java

@@ -26,7 +26,7 @@ import password.pwm.PwmApplication;
 import password.pwm.error.PwmException;
 import password.pwm.health.HealthRecord;
 import password.pwm.svc.PwmService;
-import password.pwm.util.java.JavaHelper;
+import password.pwm.util.PwmScheduler;
 
 import java.util.List;
 import java.util.concurrent.ArrayBlockingQueue;
@@ -52,7 +52,7 @@ public class PeopleSearchService implements PwmService
 
         final int maxThreadCount = 5;
 
-        final ThreadFactory threadFactory = JavaHelper.makePwmThreadFactory( JavaHelper.makeThreadName( pwmApplication, PeopleSearchService.class ), true );
+        final ThreadFactory threadFactory = PwmScheduler.makePwmThreadFactory( PwmScheduler.makeThreadName( pwmApplication, PeopleSearchService.class ), true );
         threadPoolExecutor = new ThreadPoolExecutor(
                 maxThreadCount,
                 maxThreadCount,

+ 76 - 0
server/src/main/java/password/pwm/http/servlet/resource/ConfigSettingFileResource.java

@@ -0,0 +1,76 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2018 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.http.servlet.resource;
+
+import password.pwm.PwmConstants;
+import password.pwm.config.Configuration;
+import password.pwm.config.PwmSetting;
+import password.pwm.util.java.StringUtil;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+public class ConfigSettingFileResource implements FileResource
+{
+    private final String bodyText;
+    private final String requestURI;
+
+    public ConfigSettingFileResource( final PwmSetting pwmSetting, final Configuration configuration, final String requestURI )
+    {
+        this.bodyText = configuration.readSettingAsString( pwmSetting );
+        this.requestURI = requestURI;
+    }
+
+
+    @Override
+    public InputStream getInputStream()
+            throws IOException
+    {
+        return new ByteArrayInputStream( bodyText.getBytes( PwmConstants.DEFAULT_CHARSET ) );
+    }
+
+    @Override
+    public long length()
+    {
+        return bodyText.length();
+    }
+
+    @Override
+    public long lastModified()
+    {
+        return 0;
+    }
+
+    @Override
+    public boolean exists()
+    {
+        return !StringUtil.isEmpty( bodyText );
+    }
+
+    @Override
+    public String getName()
+    {
+        return requestURI;
+    }
+}

+ 403 - 0
server/src/main/java/password/pwm/http/servlet/resource/ResourceFileRequest.java

@@ -0,0 +1,403 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2018 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.http.servlet.resource;
+
+import org.webjars.WebJarAssetLocator;
+import password.pwm.config.Configuration;
+import password.pwm.config.PwmSetting;
+import password.pwm.error.ErrorInformation;
+import password.pwm.error.PwmError;
+import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.http.HttpHeader;
+import password.pwm.http.PwmHttpRequestWrapper;
+import password.pwm.util.java.StringUtil;
+import password.pwm.util.logging.PwmLogger;
+
+import javax.servlet.ServletContext;
+import javax.servlet.http.HttpServletRequest;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+
+class ResourceFileRequest
+{
+    private static final PwmLogger LOGGER = PwmLogger.forClass( ResourceFileRequest.class );
+
+    private static final Map<String, String> WEB_JAR_VERSION_MAP = Collections.unmodifiableMap( new HashMap<>( new WebJarAssetLocator().getWebJars() ) );
+    private static final Collection<String> WEB_JAR_ASSET_LIST = Collections.unmodifiableCollection( new ArrayList<>( new WebJarAssetLocator().getFullPathIndex().values() ) );
+
+    private final HttpServletRequest httpServletRequest;
+    private final Configuration configuration;
+    private final ResourceServletConfiguration resourceServletConfiguration;
+
+    private FileResource fileResource;
+
+    ResourceFileRequest(
+            final Configuration configuration,
+            final ResourceServletConfiguration resourceServletConfiguration,
+            final HttpServletRequest httpServletRequest
+    )
+    {
+        this.configuration = configuration;
+        this.resourceServletConfiguration = resourceServletConfiguration;
+        this.httpServletRequest = httpServletRequest;
+    }
+
+    HttpServletRequest getHttpServletRequest()
+    {
+        return httpServletRequest;
+    }
+
+    ResourceServletConfiguration getResourceServletConfiguration()
+    {
+        return resourceServletConfiguration;
+    }
+
+    String getRequestURI()
+    {
+        return stripNonceFromURI( figureRequestPathMinusContext() );
+    }
+
+    String getReturnContentType()
+            throws PwmUnrecoverableException
+    {
+        final boolean acceptsCompression = allowsCompression();
+        final String rawContentType = getRawMimeType();
+        return acceptsCompression ? rawContentType : rawContentType + ";charset=UTF-8";
+    }
+
+    FileResource getRequestedFileResource()
+            throws PwmUnrecoverableException
+    {
+        if ( fileResource == null )
+        {
+            final String resourcePathUri = this.getRequestURI();
+            final ServletContext servletContext = this.getHttpServletRequest().getServletContext();
+            fileResource = resolveRequestedResource( configuration, servletContext, resourcePathUri, resourceServletConfiguration );
+        }
+        return fileResource;
+    }
+
+    private String getRawMimeType()
+            throws PwmUnrecoverableException
+    {
+        final String filename = getRequestedFileResource().getName();
+        final String contentType = this.httpServletRequest.getServletContext().getMimeType( filename );
+        if ( contentType == null )
+        {
+            if ( filename.endsWith( ".woff2" ) )
+            {
+                return "font/woff2";
+            }
+        }
+
+        // If content type is unknown, then set the default value.
+        // For all content types, see: http://www.w3schools.com/media/media_mimeref.asp
+        // To add new content types, add new mime-mapping entry in web.xml.
+        return contentType == null ? "application/octet-stream" : contentType;
+    }
+
+    boolean allowsCompression()
+            throws PwmUnrecoverableException
+    {
+        // If content type is text, then determine whether GZIP content encoding is supported by
+        // the browser and expand content type with the one and right character encoding.
+        if ( resourceServletConfiguration.isEnableGzip() )
+        {
+            final String contentType = getRawMimeType();
+            if ( contentType.startsWith( "text" ) || contentType.contains( "javascript" ) )
+            {
+                final PwmHttpRequestWrapper pwmHttpRequestWrapper = new PwmHttpRequestWrapper( httpServletRequest, configuration );
+                final String acceptEncoding = pwmHttpRequestWrapper.readHeaderValueAsString( HttpHeader.AcceptEncoding );
+                return acceptEncoding != null && accepts( acceptEncoding, "gzip" );
+            }
+        }
+        return false;
+    }
+
+    private String stripNonceFromURI(
+            final String uriString
+    )
+    {
+        if ( !resourceServletConfiguration.isEnablePathNonce() )
+        {
+            return uriString;
+        }
+
+        final Matcher theMatcher = resourceServletConfiguration.getNoncePattern().matcher( uriString );
+
+        if ( theMatcher.find() )
+        {
+            return theMatcher.replaceFirst( "" );
+        }
+
+        return uriString;
+    }
+
+    private String figureRequestPathMinusContext()
+    {
+        final String requestURI = httpServletRequest.getRequestURI();
+        return requestURI.substring( httpServletRequest.getContextPath().length() );
+    }
+
+    static FileResource resolveRequestedResource(
+            final Configuration configuration,
+            final ServletContext servletContext,
+            final String resourcePathUri,
+            final ResourceServletConfiguration resourceServletConfiguration
+    )
+            throws PwmUnrecoverableException
+    {
+
+        // URL-decode the file name (might contain spaces and on) and prepare file object.
+        String filename = StringUtil.urlDecode( resourcePathUri );
+
+        // parse out the session key...
+        if ( filename.contains( ";" ) )
+        {
+            filename = filename.substring( 0, filename.indexOf( ";" ) );
+        }
+
+
+        if ( !filename.startsWith( ResourceFileServlet.RESOURCE_PATH ) )
+        {
+            LOGGER.warn( "illegal url request to " + filename );
+            throw new PwmUnrecoverableException( new ErrorInformation( PwmError.ERROR_SERVICE_NOT_AVAILABLE, "illegal url request" ) );
+        }
+
+        {
+            final String embedThemeUrl = ResourceFileServlet.RESOURCE_PATH
+                    + ResourceFileServlet.THEME_CSS_PATH.replace( ResourceFileServlet.TOKEN_THEME, ResourceFileServlet.EMBED_THEME );
+            final String embedThemeMobileUrl = ResourceFileServlet.RESOURCE_PATH
+                    + ResourceFileServlet.THEME_CSS_MOBILE_PATH.replace( ResourceFileServlet.TOKEN_THEME, ResourceFileServlet.EMBED_THEME );
+
+            if ( filename.equalsIgnoreCase( embedThemeUrl ) )
+            {
+                return new ConfigSettingFileResource( PwmSetting.DISPLAY_CSS_EMBED, configuration, filename );
+            }
+            else if ( filename.equalsIgnoreCase( embedThemeMobileUrl ) )
+            {
+                return new ConfigSettingFileResource( PwmSetting.DISPLAY_CSS_MOBILE_EMBED, configuration, filename );
+            }
+        }
+
+
+        {
+            final FileResource resource = handleWebjarURIs( servletContext, resourcePathUri );
+            if ( resource != null )
+            {
+                return resource;
+            }
+        }
+
+        {
+            // check files system zip files.
+            final Map<String, ZipFile> zipResources = resourceServletConfiguration.getZipResources();
+            for ( final Map.Entry<String, ZipFile> entry : zipResources.entrySet() )
+            {
+                final String path = entry.getKey();
+                if ( filename.startsWith( path ) )
+                {
+                    final String zipSubPath = filename.substring( path.length() + 1, filename.length() );
+                    final ZipFile zipFile = entry.getValue();
+                    final ZipEntry zipEntry = zipFile.getEntry( zipSubPath );
+                    if ( zipEntry != null )
+                    {
+                        return new ZipFileResource( zipFile, zipEntry );
+                    }
+                }
+                if ( filename.startsWith( zipResources.get( path ).getName() ) )
+                {
+                    LOGGER.warn( "illegal url request to " + filename + " zip resource" );
+                    throw new PwmUnrecoverableException( new ErrorInformation( PwmError.ERROR_SERVICE_NOT_AVAILABLE, "illegal url request" ) );
+                }
+            }
+        }
+
+        // convert to file.
+        final String filePath = servletContext.getRealPath( filename );
+        final File file = new File( filePath );
+
+        // figure top-most path allowed by request
+        final String parentDirectoryPath = servletContext.getRealPath( ResourceFileServlet.RESOURCE_PATH );
+        final File parentDirectory = new File( parentDirectoryPath );
+
+        FileResource fileSystemResource = null;
+        {
+            //verify the requested page is a child of the servlet resource path.
+            int recursions = 0;
+            File recurseFile = file.getParentFile();
+            while ( recurseFile != null && recursions < 100 )
+            {
+                if ( parentDirectory.equals( recurseFile ) )
+                {
+                    fileSystemResource = new RealFileResource( file );
+                    break;
+                }
+                recurseFile = recurseFile.getParentFile();
+                recursions++;
+            }
+        }
+
+        if ( fileSystemResource == null )
+        {
+            LOGGER.warn( "attempt to access file outside of servlet path " + file.getAbsolutePath() );
+            throw new PwmUnrecoverableException( new ErrorInformation( PwmError.ERROR_SERVICE_NOT_AVAILABLE, "illegal file path request" ) );
+        }
+
+        if ( !fileSystemResource.exists() )
+        {
+            // check custom (configuration defined) zip file bundles
+            final Map<String, FileResource> customResources = resourceServletConfiguration.getCustomFileBundle();
+            for ( final Map.Entry<String, FileResource> entry : customResources.entrySet() )
+            {
+                final String customFileName = entry.getKey();
+                final String testName = ResourceFileServlet.RESOURCE_PATH + "/" + customFileName;
+                if ( testName.equals( resourcePathUri ) )
+                {
+                    return entry.getValue();
+                }
+            }
+        }
+
+        return fileSystemResource;
+    }
+
+    private static FileResource handleWebjarURIs(
+            final ServletContext servletContext,
+            final String resourcePathUri
+    )
+            throws PwmUnrecoverableException
+    {
+        if ( resourcePathUri.startsWith( ResourceFileServlet.WEBJAR_BASE_URL_PATH ) )
+        {
+            // This allows us to override a webjar file, if needed.  Mostly helpful during development.
+            final File file = new File( servletContext.getRealPath( resourcePathUri ) );
+            if ( file.exists() )
+            {
+                return new RealFileResource( file );
+            }
+
+            final String remainingPath = resourcePathUri.substring( ResourceFileServlet.WEBJAR_BASE_URL_PATH.length(), resourcePathUri.length() );
+
+            final String webJarName;
+            final String webJarPath;
+            {
+                final int slashIndex = remainingPath.indexOf( "/" );
+                if ( slashIndex < 0 )
+                {
+                    return null;
+                }
+                webJarName = remainingPath.substring( 0, slashIndex );
+                webJarPath = remainingPath.substring( slashIndex + 1, remainingPath.length() );
+            }
+
+            final String versionString = WEB_JAR_VERSION_MAP.get( webJarName );
+            if ( versionString == null )
+            {
+                return null;
+            }
+
+            final String fullPath = ResourceFileServlet.WEBJAR_BASE_FILE_PATH + "/" + webJarName + "/" + versionString + "/" + webJarPath;
+            if ( WEB_JAR_ASSET_LIST.contains( fullPath ) )
+            {
+                final ClassLoader classLoader = servletContext.getClassLoader();
+                final InputStream inputStream = classLoader.getResourceAsStream( fullPath );
+
+                if ( inputStream != null )
+                {
+                    return new InputStreamFileResource( inputStream, fullPath );
+                }
+            }
+        }
+
+        return null;
+    }
+
+    private static class InputStreamFileResource implements FileResource
+    {
+        private final InputStream inputStream;
+        private final String fullPath;
+
+        InputStreamFileResource( final InputStream inputStream, final String fullPath )
+        {
+            this.inputStream = inputStream;
+            this.fullPath = fullPath;
+        }
+
+        @Override
+        public InputStream getInputStream( ) throws IOException
+        {
+            return inputStream;
+        }
+
+        @Override
+        public long length( )
+        {
+            return 0;
+        }
+
+        @Override
+        public long lastModified( )
+        {
+            return 0;
+        }
+
+        @Override
+        public boolean exists( )
+        {
+            return true;
+        }
+
+        @Override
+        public String getName( )
+        {
+            return fullPath;
+        }
+    }
+
+    /**
+     * Returns true if the given accept header accepts the given value.
+     *
+     * @param acceptHeader The accept header.
+     * @param toAccept     The value to be accepted.
+     * @return True if the given accept header accepts the given value.
+     */
+    private static boolean accepts( final String acceptHeader, final String toAccept )
+    {
+        final String[] acceptValues = acceptHeader.split( "\\s*(,|;)\\s*" );
+        Arrays.sort( acceptValues );
+        return Arrays.binarySearch( acceptValues, toAccept ) > -1
+                || Arrays.binarySearch( acceptValues, toAccept.replaceAll( "/.*$", "/*" ) ) > -1
+                || Arrays.binarySearch( acceptValues, "*/*" ) > -1;
+    }
+}

+ 100 - 489
server/src/main/java/password/pwm/http/servlet/resource/ResourceFileServlet.java

@@ -23,11 +23,8 @@
 package password.pwm.http.servlet.resource;
 
 import com.github.benmanes.caffeine.cache.Cache;
-import org.apache.commons.io.IOUtils;
-import org.webjars.WebJarAssetLocator;
 import password.pwm.PwmApplication;
 import password.pwm.PwmConstants;
-import password.pwm.config.PwmSetting;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmUnrecoverableException;
@@ -38,10 +35,9 @@ import password.pwm.http.servlet.PwmServlet;
 import password.pwm.svc.stats.EventRateMeter;
 import password.pwm.svc.stats.Statistic;
 import password.pwm.svc.stats.StatisticsManager;
-import password.pwm.util.java.StringUtil;
+import password.pwm.util.java.JavaHelper;
 import password.pwm.util.logging.PwmLogger;
 
-import javax.servlet.ServletContext;
 import javax.servlet.ServletException;
 import javax.servlet.annotation.WebServlet;
 import javax.servlet.http.HttpServlet;
@@ -51,21 +47,12 @@ import java.io.BufferedInputStream;
 import java.io.BufferedOutputStream;
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
-import java.io.Closeable;
-import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
-import java.util.regex.Matcher;
 import java.util.zip.GZIPOutputStream;
-import java.util.zip.ZipEntry;
-import java.util.zip.ZipFile;
 
 @WebServlet(
         name = "ResourceFileServlet",
@@ -78,6 +65,9 @@ public class ResourceFileServlet extends HttpServlet implements PwmServlet
     private static final PwmLogger LOGGER = PwmLogger.forClass( ResourceFileServlet.class );
 
     public static final String RESOURCE_PATH = "/public/resources";
+    static final String WEBJAR_BASE_URL_PATH = RESOURCE_PATH + "/webjars/";
+    static final String WEBJAR_BASE_FILE_PATH = "META-INF/resources/webjars";
+
     public static final String THEME_CSS_PATH = "/themes/%THEME%/style.css";
     public static final String THEME_CSS_MOBILE_PATH = "/themes/%THEME%/mobileStyle.css";
     public static final String THEME_CSS_CONFIG_PATH = "/themes/%THEME%/configStyle.css";
@@ -85,13 +75,6 @@ public class ResourceFileServlet extends HttpServlet implements PwmServlet
     public static final String TOKEN_THEME = "%THEME%";
     public static final String EMBED_THEME = "embed";
 
-    private static final String WEBJAR_BASE_FILE_PATH = "META-INF/resources/webjars";
-    private static final String WEBJAR_BASE_URL_PATH = RESOURCE_PATH + "/webjars/";
-
-    private static final Map<String, String> WEB_JAR_VERSION_MAP = Collections.unmodifiableMap( new HashMap<>( new WebJarAssetLocator().getWebJars() ) );
-    private static final Collection<String> WEB_JAR_ASSET_LIST = Collections.unmodifiableCollection( new ArrayList<>( new WebJarAssetLocator().getFullPathIndex().values() ) );
-
-
     @Override
     protected void doGet( final HttpServletRequest req, final HttpServletResponse resp )
             throws ServletException, IOException
@@ -134,11 +117,9 @@ public class ResourceFileServlet extends HttpServlet implements PwmServlet
             throws IOException, PwmUnrecoverableException
     {
 
-        final FileResource file = resolveRequestedFile(
-                req.getServletContext(),
-                figureRequestPathMinusContext( req ),
-                ResourceServletConfiguration.defaultConfiguration()
-        );
+        final ResourceFileRequest resourceFileRequest = new ResourceFileRequest( null, ResourceServletConfiguration.defaultConfiguration(), req );
+
+        final FileResource file = resourceFileRequest.getRequestedFileResource();
 
         if ( file == null || !file.exists() )
         {
@@ -149,9 +130,8 @@ public class ResourceFileServlet extends HttpServlet implements PwmServlet
         handleUncachedResponse( resp, file, false );
     }
 
-    @SuppressWarnings( "checkstyle:MethodLength" )
     protected void processAction( final PwmRequest pwmRequest )
-            throws ServletException, IOException, PwmUnrecoverableException
+            throws IOException, PwmUnrecoverableException
     {
         if ( pwmRequest.getMethod() != HttpMethod.GET )
         {
@@ -164,25 +144,13 @@ public class ResourceFileServlet extends HttpServlet implements PwmServlet
         final ResourceServletService resourceService = pwmApplication.getResourceServletService();
         final ResourceServletConfiguration resourceConfiguration = resourceService.getResourceServletConfiguration();
 
-
-        final String requestURI = stripNonceFromURI( resourceConfiguration, figureRequestPathMinusContext( pwmRequest.getHttpServletRequest() ) );
-
-        try
-        {
-            if ( handleEmbeddedURIs( pwmApplication, requestURI, pwmRequest.getPwmResponse().getHttpServletResponse(), resourceConfiguration ) )
-            {
-                return;
-            }
-        }
-        catch ( Exception e )
-        {
-            LOGGER.error( pwmRequest, "unexpected error detecting/handling special request uri: " + e.getMessage() );
-        }
+        final ResourceFileRequest resourceFileRequest = new ResourceFileRequest( pwmApplication.getConfig(), resourceConfiguration, pwmRequest.getHttpServletRequest() );
+        final String requestURI = resourceFileRequest.getRequestURI();
 
         final FileResource file;
         try
         {
-            file = resolveRequestedFile( this.getServletContext(), requestURI, resourceConfiguration );
+            file = resourceFileRequest.getRequestedFileResource();
         }
         catch ( PwmUnrecoverableException e )
         {
@@ -209,47 +177,14 @@ public class ResourceFileServlet extends HttpServlet implements PwmServlet
         }
 
         // Get content type by file name and set default GZIP support and content disposition.
-        String contentType = getMimeType( file.getName() );
-        boolean acceptsGzip = false;
-
-        // If content type is unknown, then set the default value.
-        // For all content types, see: http://www.w3schools.com/media/media_mimeref.asp
-        // To add new content types, add new mime-mapping entry in web.xml.
-        if ( contentType == null )
-        {
-            contentType = "application/octet-stream";
-        }
-
-        // If content type is text, then determine whether GZIP content encoding is supported by
-        // the browser and expand content type with the one and right character encoding.
-        if ( resourceConfiguration.isEnableGzip() )
-        {
-            if ( contentType.startsWith( "text" ) || contentType.contains( "javascript" ) )
-            {
-                final String acceptEncoding = pwmRequest.readHeaderValueAsString( HttpHeader.AcceptEncoding );
-                acceptsGzip = acceptEncoding != null && accepts( acceptEncoding, "gzip" );
-                contentType += ";charset=UTF-8";
-            }
-        }
+        final String contentType = resourceFileRequest.getReturnContentType();
+        final boolean acceptsGzip = resourceFileRequest.allowsCompression();
 
         final HttpServletResponse response = pwmRequest.getPwmResponse().getHttpServletResponse();
-        final String eTagValue = resourceConfiguration.getNonceValue();
 
+        if ( respondWithNotModified( pwmRequest, resourceConfiguration ) )
         {
-            // reply back with etag.
-            final String ifNoneMatchValue = pwmRequest.readHeaderValueAsString( HttpHeader.If_None_Match );
-            if ( ifNoneMatchValue != null && ifNoneMatchValue.equals( eTagValue ) )
-            {
-                response.reset();
-                response.setStatus( HttpServletResponse.SC_NOT_MODIFIED );
-                try
-                {
-                    pwmRequest.debugHttpRequestToLog( "returning HTTP 304 status" );
-                }
-                catch ( PwmUnrecoverableException e2 )
-                { /* noop */ }
-                return;
-            }
+            return;
         }
 
         // Initialize response.
@@ -260,54 +195,22 @@ public class ResourceFileServlet extends HttpServlet implements PwmServlet
         try
         {
             boolean fromCache = false;
-            StringBuilder debugText = new StringBuilder();
+            String debugText;
             try
             {
-                fromCache = handleCacheableResponse( resourceConfiguration, response, file, acceptsGzip, resourceService.getCacheMap() );
-                if ( fromCache || acceptsGzip )
-                {
-                    debugText.append( "(" );
-                    if ( fromCache )
-                    {
-                        debugText.append( "cached" );
-                    }
-                    if ( fromCache && acceptsGzip )
-                    {
-                        debugText.append( ", " );
-                    }
-                    if ( acceptsGzip )
-                    {
-                        debugText.append( "gzip" );
-                    }
-                    debugText.append( ")" );
-                }
-                else
-                {
-                    debugText = new StringBuilder( "(not cached)" );
-                }
-                StatisticsManager.incrementStat( pwmApplication, Statistic.HTTP_RESOURCE_REQUESTS );
+                fromCache = handleCacheableResponse( resourceFileRequest, response, resourceService.getCacheMap() );
+                debugText = makeDebugText( fromCache, acceptsGzip, false );
             }
             catch ( UncacheableResourceException e )
             {
                 handleUncachedResponse( response, file, acceptsGzip );
-                debugText = new StringBuilder();
-                debugText.append( "(uncacheable" );
-                if ( acceptsGzip )
-                {
-                    debugText.append( ", gzip" );
-                }
-                debugText.append( ")" );
-            }
-            try
-            {
-                pwmRequest.debugHttpRequestToLog( debugText.toString() );
-            }
-            catch ( PwmUnrecoverableException e )
-            {
-                /* noop */
+                debugText = makeDebugText( fromCache, acceptsGzip, true );
             }
 
+            pwmRequest.debugHttpRequestToLog( debugText );
+
             final EventRateMeter.MovingAverage cacheHitRatio = resourceService.getCacheHitRatio();
+            StatisticsManager.incrementStat( pwmApplication, Statistic.HTTP_RESOURCE_REQUESTS );
             cacheHitRatio.update( fromCache ? 1 : 0 );
         }
         catch ( Exception e )
@@ -316,49 +219,82 @@ public class ResourceFileServlet extends HttpServlet implements PwmServlet
         }
     }
 
+    private String makeDebugText( final boolean fromCache, final boolean acceptsGzip, final boolean uncacheable )
+    {
+        if ( uncacheable )
+        {
+            final StringBuilder debugText = new StringBuilder();
+            debugText.append( "(uncacheable" );
+            if ( acceptsGzip )
+            {
+                debugText.append( ", gzip" );
+            }
+            debugText.append( ")" );
+            return debugText.toString();
+        }
+
+        if ( fromCache || acceptsGzip )
+        {
+            final StringBuilder debugText = new StringBuilder();
+            debugText.append( "(" );
+            if ( fromCache )
+            {
+                debugText.append( "cached" );
+            }
+            if ( fromCache && acceptsGzip )
+            {
+                debugText.append( ", " );
+            }
+            if ( acceptsGzip )
+            {
+                debugText.append( "gzip" );
+            }
+            debugText.append( ")" );
+            return debugText.toString();
+        }
+        else
+        {
+            return "(not cached)";
+        }
+    }
+
     private boolean handleCacheableResponse(
-            final ResourceServletConfiguration resourceServletConfiguration,
+            final ResourceFileRequest resourceFileRequest,
             final HttpServletResponse response,
-            final FileResource file,
-            final boolean acceptsGzip,
             final Cache<CacheKey, CacheEntry> responseCache
     )
-            throws UncacheableResourceException, IOException
+            throws UncacheableResourceException, IOException, PwmUnrecoverableException
     {
+        final FileResource file = resourceFileRequest.getRequestedFileResource();
 
-        if ( file.length() > resourceServletConfiguration.getMaxCacheBytes() )
+        if ( file.length() > resourceFileRequest.getResourceServletConfiguration().getMaxCacheBytes() )
         {
             throw new UncacheableResourceException( "file to large to cache" );
         }
 
         boolean fromCache = false;
-        final CacheKey cacheKey = new CacheKey( file, acceptsGzip );
+        final CacheKey cacheKey = new CacheKey( file, resourceFileRequest.allowsCompression() );
         CacheEntry cacheEntry = responseCache.getIfPresent( cacheKey );
         if ( cacheEntry == null )
         {
             final Map<String, String> headers = new HashMap<>();
             final ByteArrayOutputStream tempOutputStream = new ByteArrayOutputStream();
-            final InputStream input = file.getInputStream();
 
-            try
+            try ( InputStream input = resourceFileRequest.getRequestedFileResource().getInputStream() )
             {
-                if ( acceptsGzip )
+                if ( resourceFileRequest.allowsCompression() )
                 {
-                    final GZIPOutputStream gzipOutputStream = new GZIPOutputStream( tempOutputStream );
-                    headers.put( HttpHeader.ContentEncoding.getHttpName(), "gzip" );
-                    copy( input, gzipOutputStream );
-                    close( gzipOutputStream );
+                    try ( GZIPOutputStream gzipOutputStream = new GZIPOutputStream( tempOutputStream ) )
+                    {
+                        headers.put( HttpHeader.ContentEncoding.getHttpName(), "gzip" );
+                        JavaHelper.copy( input, gzipOutputStream );
+                    }
                 }
                 else
                 {
-                    copy( input, tempOutputStream );
+                    JavaHelper.copy( input, tempOutputStream );
                 }
             }
-            finally
-            {
-                close( input );
-                close( tempOutputStream );
-            }
 
             final byte[] entity = tempOutputStream.toByteArray();
             headers.put( HttpHeader.ContentLength.getHttpName(), String.valueOf( entity.length ) );
@@ -375,14 +311,9 @@ public class ResourceFileServlet extends HttpServlet implements PwmServlet
             response.setHeader( key, cacheEntry.getHeaderStrings().get( key ) );
         }
 
-        final OutputStream responseOutputStream = response.getOutputStream();
-        try
-        {
-            copy( new ByteArrayInputStream( cacheEntry.getEntity() ), responseOutputStream );
-        }
-        finally
+        try ( OutputStream responseOutputStream = response.getOutputStream() )
         {
-            close( responseOutputStream );
+            JavaHelper.copy( new ByteArrayInputStream( cacheEntry.getEntity() ), responseOutputStream );
         }
 
         return fromCache;
@@ -392,23 +323,18 @@ public class ResourceFileServlet extends HttpServlet implements PwmServlet
             final HttpServletResponse response,
             final FileResource file,
             final boolean acceptsGzip
-    ) throws IOException
+    )
+            throws IOException
     {
-        // Prepare streams.
-        OutputStream output = null;
-        InputStream input = null;
-
-        try
+        try (
+                OutputStream output = new BufferedOutputStream( response.getOutputStream() );
+                InputStream input = new BufferedInputStream( file.getInputStream() );
+        )
         {
-            // Open streams.
-            input = new BufferedInputStream( file.getInputStream() );
-            output = new BufferedOutputStream( response.getOutputStream() );
-
             if ( acceptsGzip )
             {
-                // The browser accepts GZIP, so GZIP the content.
                 response.setHeader( HttpHeader.ContentEncoding.getHttpName(), "gzip" );
-                output = new GZIPOutputStream( output );
+                JavaHelper.copy( input, new GZIPOutputStream( output ) );
             }
             else
             {
@@ -418,337 +344,12 @@ public class ResourceFileServlet extends HttpServlet implements PwmServlet
                 {
                     response.setHeader( HttpHeader.ContentLength.getHttpName(), String.valueOf( file.length() ) );
                 }
+                JavaHelper.copy( input, output );
             }
-
-            // Copy full range.
-            copy( input, output );
-        }
-        finally
-        {
-            // Gently close streams.
-            close( output );
-            close( input );
         }
 
     }
 
-    /**
-     * Returns true if the given accept header accepts the given value.
-     *
-     * @param acceptHeader The accept header.
-     * @param toAccept     The value to be accepted.
-     * @return True if the given accept header accepts the given value.
-     */
-    private static boolean accepts( final String acceptHeader, final String toAccept )
-    {
-        final String[] acceptValues = acceptHeader.split( "\\s*(,|;)\\s*" );
-        Arrays.sort( acceptValues );
-        return Arrays.binarySearch( acceptValues, toAccept ) > -1
-                || Arrays.binarySearch( acceptValues, toAccept.replaceAll( "/.*$", "/*" ) ) > -1
-                || Arrays.binarySearch( acceptValues, "*/*" ) > -1;
-    }
-
-    /**
-     * Copy the given byte range of the given input to the given output.
-     *
-     * @param input  The input to copy the given range to the given output for.
-     * @param output The output to copy the given range from the given input for.
-     * @throws IOException If something fails at I/O level.
-     */
-    private static void copy( final InputStream input, final OutputStream output )
-            throws IOException
-    {
-        IOUtils.copy( input, output );
-    }
-
-    /**
-     * Close the given resource.
-     *
-     * @param resource The resource to be closed.
-     */
-    private static void close( final Closeable resource )
-    {
-        IOUtils.closeQuietly( resource );
-    }
-
-    static FileResource resolveRequestedFile(
-            final ServletContext servletContext,
-            final String resourcePathUri,
-            final ResourceServletConfiguration resourceServletConfiguration
-    )
-            throws PwmUnrecoverableException
-    {
-        // URL-decode the file name (might contain spaces and on) and prepare file object.
-        String filename = StringUtil.urlDecode( resourcePathUri );
-
-        // parse out the session key...
-        if ( filename.contains( ";" ) )
-        {
-            filename = filename.substring( 0, filename.indexOf( ";" ) );
-        }
-
-
-        if ( !filename.startsWith( RESOURCE_PATH ) )
-        {
-            LOGGER.warn( "illegal url request to " + filename );
-            throw new PwmUnrecoverableException( new ErrorInformation( PwmError.ERROR_SERVICE_NOT_AVAILABLE, "illegal url request" ) );
-        }
-
-        {
-            final FileResource resource = handleWebjarURIs( servletContext, resourcePathUri );
-            if ( resource != null )
-            {
-                return resource;
-            }
-        }
-
-        {
-            // check files system zip files.
-            final Map<String, ZipFile> zipResources = resourceServletConfiguration.getZipResources();
-            for ( final Map.Entry<String, ZipFile> entry : zipResources.entrySet() )
-            {
-                final String path = entry.getKey();
-                if ( filename.startsWith( path ) )
-                {
-                    final String zipSubPath = filename.substring( path.length() + 1, filename.length() );
-                    final ZipFile zipFile = entry.getValue();
-                    final ZipEntry zipEntry = zipFile.getEntry( zipSubPath );
-                    if ( zipEntry != null )
-                    {
-                        return new ZipFileResource( zipFile, zipEntry );
-                    }
-                }
-                if ( filename.startsWith( zipResources.get( path ).getName() ) )
-                {
-                    LOGGER.warn( "illegal url request to " + filename + " zip resource" );
-                    throw new PwmUnrecoverableException( new ErrorInformation( PwmError.ERROR_SERVICE_NOT_AVAILABLE, "illegal url request" ) );
-                }
-            }
-        }
-
-        // convert to file.
-        final String filePath = servletContext.getRealPath( filename );
-        final File file = new File( filePath );
-
-        // figure top-most path allowed by request
-        final String parentDirectoryPath = servletContext.getRealPath( RESOURCE_PATH );
-        final File parentDirectory = new File( parentDirectoryPath );
-
-        FileResource fileSystemResource = null;
-        {
-            //verify the requested page is a child of the servlet resource path.
-            int recursions = 0;
-            File recurseFile = file.getParentFile();
-            while ( recurseFile != null && recursions < 100 )
-            {
-                if ( parentDirectory.equals( recurseFile ) )
-                {
-                    fileSystemResource = new RealFileResource( file );
-                    break;
-                }
-                recurseFile = recurseFile.getParentFile();
-                recursions++;
-            }
-        }
-
-        if ( fileSystemResource == null )
-        {
-            LOGGER.warn( "attempt to access file outside of servlet path " + file.getAbsolutePath() );
-            throw new PwmUnrecoverableException( new ErrorInformation( PwmError.ERROR_SERVICE_NOT_AVAILABLE, "illegal file path request" ) );
-        }
-
-        if ( !fileSystemResource.exists() )
-        {
-            // check custom (configuration defined) zip file bundles
-            final Map<String, FileResource> customResources = resourceServletConfiguration.getCustomFileBundle();
-            for ( final Map.Entry<String, FileResource> entry : customResources.entrySet() )
-            {
-                final String customFileName = entry.getKey();
-                final String testName = RESOURCE_PATH + "/" + customFileName;
-                if ( testName.equals( resourcePathUri ) )
-                {
-                    return entry.getValue();
-                }
-            }
-        }
-
-        return fileSystemResource;
-    }
-
-    private boolean handleEmbeddedURIs(
-            final PwmApplication pwmApplication,
-            final String requestURI,
-            final HttpServletResponse response,
-            final ResourceServletConfiguration resourceServletConfiguration
-    )
-            throws PwmUnrecoverableException, IOException, ServletException
-    {
-        if ( requestURI != null )
-        {
-            final String embedThemeUrl = RESOURCE_PATH + THEME_CSS_PATH.replace( TOKEN_THEME, EMBED_THEME );
-            final String embedThemeMobileUrl = RESOURCE_PATH + THEME_CSS_MOBILE_PATH.replace( TOKEN_THEME, EMBED_THEME );
-            if ( requestURI.equalsIgnoreCase( embedThemeUrl ) )
-            {
-                writeConfigSettingToBody( pwmApplication, PwmSetting.DISPLAY_CSS_EMBED, response, resourceServletConfiguration );
-                return true;
-            }
-            else if ( requestURI.equalsIgnoreCase( embedThemeMobileUrl ) )
-            {
-                writeConfigSettingToBody( pwmApplication, PwmSetting.DISPLAY_CSS_MOBILE_EMBED, response, resourceServletConfiguration );
-                return true;
-            }
-        }
-        return false;
-    }
-
-    private void writeConfigSettingToBody(
-            final PwmApplication pwmApplication,
-            final PwmSetting pwmSetting,
-            final HttpServletResponse response,
-            final ResourceServletConfiguration resourceServletConfiguration
-    )
-            throws PwmUnrecoverableException, IOException
-    {
-        final String bodyText = pwmApplication.getConfig().readSettingAsString( pwmSetting );
-        try
-        {
-            response.setContentType( "text/css" );
-            addExpirationHeaders( resourceServletConfiguration, response );
-            if ( bodyText != null && bodyText.length() > 0 )
-            {
-                response.setIntHeader( "Content-Length", bodyText.length() );
-                copy( new ByteArrayInputStream( bodyText.getBytes( PwmConstants.DEFAULT_CHARSET ) ), response.getOutputStream() );
-            }
-            else
-            {
-                response.setIntHeader( "Content-Length", 0 );
-            }
-        }
-        finally
-        {
-            close( response.getOutputStream() );
-        }
-    }
-
-    private String stripNonceFromURI(
-            final ResourceServletConfiguration resourceServletConfiguration,
-            final String uriString
-    )
-    {
-        if ( !resourceServletConfiguration.isEnablePathNonce() )
-        {
-            return uriString;
-        }
-
-        final Matcher theMatcher = resourceServletConfiguration.getNoncePattern().matcher( uriString );
-
-        if ( theMatcher.find() )
-        {
-            return theMatcher.replaceFirst( "" );
-        }
-
-        return uriString;
-    }
-
-    private String figureRequestPathMinusContext( final HttpServletRequest httpServletRequest )
-    {
-        final String requestURI = httpServletRequest.getRequestURI();
-        return requestURI.substring( httpServletRequest.getContextPath().length(), requestURI.length() );
-    }
-
-    private static FileResource handleWebjarURIs(
-            final ServletContext servletContext,
-            final String resourcePathUri
-    )
-            throws PwmUnrecoverableException
-    {
-        if ( resourcePathUri.startsWith( WEBJAR_BASE_URL_PATH ) )
-        {
-            // This allows us to override a webjar file, if needed.  Mostly helpful during development.
-            final File file = new File( servletContext.getRealPath( resourcePathUri ) );
-            if ( file.exists() )
-            {
-                return new RealFileResource( file );
-            }
-
-            final String remainingPath = resourcePathUri.substring( WEBJAR_BASE_URL_PATH.length(), resourcePathUri.length() );
-
-            final String webJarName;
-            final String webJarPath;
-            {
-                final int slashIndex = remainingPath.indexOf( "/" );
-                if ( slashIndex < 0 )
-                {
-                    return null;
-                }
-                webJarName = remainingPath.substring( 0, slashIndex );
-                webJarPath = remainingPath.substring( slashIndex + 1, remainingPath.length() );
-            }
-
-            final String versionString = WEB_JAR_VERSION_MAP.get( webJarName );
-            if ( versionString == null )
-            {
-                return null;
-            }
-
-            final String fullPath = WEBJAR_BASE_FILE_PATH + "/" + webJarName + "/" + versionString + "/" + webJarPath;
-            if ( WEB_JAR_ASSET_LIST.contains( fullPath ) )
-            {
-                final ClassLoader classLoader = servletContext.getClassLoader();
-                final InputStream inputStream = classLoader.getResourceAsStream( fullPath );
-
-                if ( inputStream != null )
-                {
-                    return new InputStreamFileResource( inputStream, fullPath );
-                }
-            }
-        }
-
-        return null;
-    }
-
-    private static class InputStreamFileResource implements FileResource
-    {
-        private final InputStream inputStream;
-        private final String fullPath;
-
-        InputStreamFileResource( final InputStream inputStream, final String fullPath )
-        {
-            this.inputStream = inputStream;
-            this.fullPath = fullPath;
-        }
-
-        @Override
-        public InputStream getInputStream( ) throws IOException
-        {
-            return inputStream;
-        }
-
-        @Override
-        public long length( )
-        {
-            return 0;
-        }
-
-        @Override
-        public long lastModified( )
-        {
-            return 0;
-        }
-
-        @Override
-        public boolean exists( )
-        {
-            return true;
-        }
-
-        @Override
-        public String getName( )
-        {
-            return fullPath;
-        }
-    }
-
     private void addExpirationHeaders( final ResourceServletConfiguration resourceServletConfiguration, final HttpServletResponse httpResponse )
     {
         httpResponse.setDateHeader( "Expires", System.currentTimeMillis() + ( resourceServletConfiguration.getCacheExpireSeconds() * 1000 ) );
@@ -756,16 +357,26 @@ public class ResourceFileServlet extends HttpServlet implements PwmServlet
         httpResponse.setHeader( "Vary", "Accept-Encoding" );
     }
 
-    private String getMimeType( final String filename )
+    private boolean respondWithNotModified( final PwmRequest pwmRequest, final ResourceServletConfiguration resourceConfiguration )
     {
-        final String contentType = getServletContext().getMimeType( filename );
-        if ( contentType == null )
+        final String eTagValue = resourceConfiguration.getNonceValue();
+        final HttpServletResponse response = pwmRequest.getPwmResponse().getHttpServletResponse();
+
+        final String ifNoneMatchValue = pwmRequest.readHeaderValueAsString( HttpHeader.If_None_Match );
+        if ( ifNoneMatchValue != null && ifNoneMatchValue.equals( eTagValue ) )
         {
-            if ( filename.endsWith( ".woff2" ) )
+            // reply back with etag.
+            response.reset();
+            response.setStatus( HttpServletResponse.SC_NOT_MODIFIED );
+            try
             {
-                return "font/woff2";
+                pwmRequest.debugHttpRequestToLog( "returning HTTP 304 status" );
             }
+            catch ( PwmUnrecoverableException e2 )
+            { /* noop */ }
+            return true;
         }
-        return contentType;
+
+        return false;
     }
 }

+ 5 - 1
server/src/main/java/password/pwm/http/servlet/resource/ResourceServletService.java

@@ -271,7 +271,11 @@ public class ResourceServletService implements PwmService
         for ( final String testUrl : testUrls )
         {
             final String themePathUrl = ResourceFileServlet.RESOURCE_PATH + testUrl.replace( ResourceFileServlet.TOKEN_THEME, themeName );
-            final FileResource resolvedFile = ResourceFileServlet.resolveRequestedFile( servletContext, themePathUrl, getResourceServletConfiguration() );
+            final FileResource resolvedFile = ResourceFileRequest.resolveRequestedResource(
+                    pwmRequest.getConfig(),
+                    servletContext,
+                    themePathUrl,
+                    getResourceServletConfiguration() );
             if ( resolvedFile != null && resolvedFile.exists() )
             {
                 LOGGER.debug( pwmRequest, () -> "check for theme validity of '" + themeName + "' returned true" );

+ 8 - 4
server/src/main/java/password/pwm/http/state/CryptoCookieBeanImpl.java

@@ -30,9 +30,11 @@ import password.pwm.http.PwmRequest;
 import password.pwm.http.PwmRequestAttribute;
 import password.pwm.http.bean.PwmSessionBean;
 import password.pwm.util.PasswordData;
+import password.pwm.util.java.StringUtil;
 import password.pwm.util.java.TimeDuration;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.secure.PwmSecurityKey;
+import password.pwm.util.secure.SecureService;
 
 import java.io.Serializable;
 import java.util.HashMap;
@@ -56,7 +58,7 @@ class CryptoCookieBeanImpl implements SessionBeanProvider
         }
 
         final String sessionGuid = pwmRequest.getPwmSession().getLoginInfoBean().getGuid();
-        final String cookieName = nameForClass( theClass );
+        final String cookieName = nameForClass( pwmRequest, theClass );
 
         try
         {
@@ -139,7 +141,7 @@ class CryptoCookieBeanImpl implements SessionBeanProvider
                     for ( final Map.Entry<Class<? extends PwmSessionBean>, PwmSessionBean> entry : beansInRequest.entrySet() )
                     {
                         final Class<? extends PwmSessionBean> theClass = entry.getKey();
-                        final String cookieName = nameForClass( theClass );
+                        final String cookieName = nameForClass( pwmRequest, theClass );
                         final PwmSessionBean bean = entry.getValue();
                         if ( bean == null )
                         {
@@ -180,9 +182,11 @@ class CryptoCookieBeanImpl implements SessionBeanProvider
         return ( Map<Class<? extends PwmSessionBean>, PwmSessionBean> ) sessionBeans;
     }
 
-    private static String nameForClass( final Class<? extends PwmSessionBean> theClass )
+    private static String nameForClass( final PwmRequest pwmRequest, final Class<? extends PwmSessionBean> theClass )
+            throws PwmUnrecoverableException
     {
-        return theClass.getSimpleName();
+        final SecureService secureService = pwmRequest.getPwmApplication().getSecureService();
+        return "b-" + StringUtil.truncate( secureService.hash( theClass.getName() ), 8 );
     }
 
     @Override

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

@@ -364,7 +364,7 @@ public enum PwmIfTest
                 final HealthMonitor healthMonitor = pwmRequest.getPwmApplication().getHealthMonitor();
                 if ( healthMonitor != null && healthMonitor.status() == PwmService.STATUS.OPEN )
                 {
-                    if ( healthMonitor.getMostSevereHealthStatus( HealthMonitor.CheckTimeliness.NeverBlock ) == HealthStatus.WARN )
+                    if ( healthMonitor.getMostSevereHealthStatus() == HealthStatus.WARN )
                     {
                         return true;
                     }

+ 12 - 1
server/src/main/java/password/pwm/http/tag/value/PwmValue.java

@@ -62,7 +62,8 @@ public enum PwmValue
     localeDir( new LocaleDirOutput() ),
     localeFlagFile( new LocaleFlagFileOutput() ),
     localeName( new LocaleNameOutput() ),
-    inactiveTimeRemaining( new InactiveTimeRemainingOutput() ),;
+    inactiveTimeRemaining( new InactiveTimeRemainingOutput() ),
+    sessionID( new SessionIDValue() ),;
 
     private static final PwmLogger LOGGER = PwmLogger.forClass( PwmValueTag.class );
 
@@ -295,4 +296,14 @@ public enum PwmValue
             return IdleTimeoutCalculator.idleTimeoutForRequest( pwmRequest ).asLongString();
         }
     }
+
+    static class SessionIDValue implements ValueOutput
+    {
+        @Override
+        public String valueOutput( final PwmRequest pwmRequest, final PageContext pageContext )
+                throws PwmUnrecoverableException
+        {
+            return pwmRequest.getPwmSession().getSessionStateBean().getSessionID();
+        }
+    }
 }

+ 2 - 0
server/src/main/java/password/pwm/ldap/UserInfo.java

@@ -109,4 +109,6 @@ public interface UserInfo
     List<String> readMultiStringAttribute( String attribute ) throws PwmUnrecoverableException;
 
     Map<String, String> readStringAttributes( Collection<String> attributes ) throws PwmUnrecoverableException;
+
+    Instant getPasswordExpirationNoticeSendTime( ) throws PwmUnrecoverableException;
 }

+ 1 - 2
server/src/main/java/password/pwm/ldap/UserInfoBean.java

@@ -46,7 +46,6 @@ import java.util.Map;
 @Builder
 public class UserInfoBean implements UserInfo
 {
-
     private final UserIdentity userIdentity;
     private final String username;
     private final String userEmailAddress;
@@ -87,6 +86,7 @@ public class UserInfoBean implements UserInfo
     private final Instant passwordLastModifiedTime;
     private final Instant lastLdapLoginTime;
     private final Instant accountExpirationTime;
+    private final Instant passwordExpirationNoticeSendTime;
 
     private final boolean accountEnabled;
     private final boolean accountExpired;
@@ -148,6 +148,5 @@ public class UserInfoBean implements UserInfo
         }
         return Collections.unmodifiableMap( returnObj );
     }
-
 }
 

+ 18 - 1
server/src/main/java/password/pwm/ldap/UserInfoReader.java

@@ -52,10 +52,11 @@ import password.pwm.error.PwmDataValidationException;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.svc.PwmService;
-import password.pwm.util.i18n.LocaleHelper;
+import password.pwm.svc.pwnotify.PwNotifyUserStatus;
 import password.pwm.util.PasswordData;
 import password.pwm.util.PwmPasswordRuleValidator;
 import password.pwm.util.form.FormUtility;
+import password.pwm.util.i18n.LocaleHelper;
 import password.pwm.util.java.CachingProxyWrapper;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.TimeDuration;
@@ -74,6 +75,7 @@ import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
+import java.util.Optional;
 import java.util.Set;
 
 public class UserInfoReader implements UserInfo
@@ -868,4 +870,19 @@ public class UserInfoReader implements UserInfo
     {
         return locale == null ? null : LocaleHelper.getBrowserLocaleString( locale );
     }
+
+    @Override
+    public Instant getPasswordExpirationNoticeSendTime()
+            throws PwmUnrecoverableException
+    {
+        if ( pwmApplication.getPwNotifyService().status() == PwmService.STATUS.OPEN )
+        {
+            final Optional<PwNotifyUserStatus> optionalState = pwmApplication.getPwNotifyService().readUserNotificationState( userIdentity, sessionLabel );
+            if ( optionalState.isPresent() )
+            {
+                return optionalState.get().getExpireTime();
+            }
+        }
+        return null;
+    }
 }

+ 2 - 1
server/src/main/java/password/pwm/ldap/search/UserSearchEngine.java

@@ -47,6 +47,7 @@ import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.health.HealthRecord;
 import password.pwm.svc.PwmService;
 import password.pwm.svc.stats.Statistic;
+import password.pwm.util.PwmScheduler;
 import password.pwm.util.java.ConditionalTaskExecutor;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.JsonUtil;
@@ -844,7 +845,7 @@ public class UserSearchEngine implements PwmService
             final int factor = Integer.parseInt( configuration.readAppProperty( AppProperty.LDAP_SEARCH_PARALLEL_FACTOR ) );
             final int maxThreads = Integer.parseInt( configuration.readAppProperty( AppProperty.LDAP_SEARCH_PARALLEL_THREAD_MAX ) );
             final int threads = Math.min( maxThreads, ( endPoints ) * factor );
-            final ThreadFactory threadFactory = JavaHelper.makePwmThreadFactory( JavaHelper.makeThreadName( pwmApplication, UserSearchEngine.class ), true );
+            final ThreadFactory threadFactory = PwmScheduler.makePwmThreadFactory( PwmScheduler.makeThreadName( pwmApplication, UserSearchEngine.class ), true );
             return new ThreadPoolExecutor(
                     threads,
                     threads,

+ 6 - 1
server/src/main/java/password/pwm/svc/event/AuditService.java

@@ -309,7 +309,12 @@ public class AuditService implements PwmService
             body = StringUtil.mapToString( mapRecord, "=", "\n" );
         }
 
-        final EmailItemBean emailItem = new EmailItemBean( toAddress, fromAddress, subject, body, null );
+        final EmailItemBean emailItem = EmailItemBean.builder()
+                .to( toAddress )
+                .from( fromAddress )
+                .subject( subject )
+                .bodyPlain( body )
+                .build();
         pwmApplication.getEmailQueue().submitEmail( emailItem, null, macroMachine );
     }
 

+ 3 - 3
server/src/main/java/password/pwm/svc/event/LocalDbAuditVault.java

@@ -26,8 +26,8 @@ import password.pwm.AppProperty;
 import password.pwm.PwmApplication;
 import password.pwm.error.PwmException;
 import password.pwm.svc.PwmService;
+import password.pwm.util.PwmScheduler;
 import password.pwm.util.TransactionSizeCalculator;
-import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.JsonUtil;
 import password.pwm.util.java.Percent;
 import password.pwm.util.java.TimeDuration;
@@ -74,11 +74,11 @@ public class LocalDbAuditVault implements AuditVault
 
         readOldestRecord();
 
-        executorService = JavaHelper.makeBackgroundExecutor( pwmApplication, this.getClass() );
+        executorService = PwmScheduler.makeBackgroundExecutor( pwmApplication, this.getClass() );
 
         status = PwmService.STATUS.OPEN;
         final TimeDuration jobFrequency = TimeDuration.of( 10, TimeDuration.Unit.MINUTES );
-        pwmApplication.scheduleFixedRateJob( new TrimmerThread(), executorService, TimeDuration.SECONDS_10, jobFrequency );
+        pwmApplication.getPwmScheduler().scheduleFixedRateJob( new TrimmerThread(), executorService, TimeDuration.SECONDS_10, jobFrequency );
     }
 
     public void close( )

+ 5 - 4
server/src/main/java/password/pwm/svc/intruder/IntruderManager.java

@@ -53,6 +53,7 @@ import password.pwm.svc.stats.Statistic;
 import password.pwm.svc.stats.StatisticsManager;
 import password.pwm.util.DataStore;
 import password.pwm.util.DataStoreFactory;
+import password.pwm.util.PwmScheduler;
 import password.pwm.util.i18n.LocaleHelper;
 import password.pwm.util.db.DatabaseDataStore;
 import password.pwm.util.db.DatabaseTable;
@@ -171,7 +172,7 @@ public class IntruderManager implements PwmService
         final RecordStore recordStore;
         {
             recordStore = new DataStoreRecordStore( dataStore, this );
-            final String threadName = JavaHelper.makeThreadName( pwmApplication, this.getClass() ) + " timer";
+            final String threadName = PwmScheduler.makeThreadName( pwmApplication, this.getClass() ) + " timer";
             timer = new Timer( threadName, true );
             final long maxRecordAge = Long.parseLong( pwmApplication.getConfig().readAppProperty( AppProperty.INTRUDER_RETENTION_TIME_MS ) );
             final long cleanerRunFrequency = Long.parseLong( pwmApplication.getConfig().readAppProperty( AppProperty.INTRUDER_CLEANUP_FREQUENCY_MS ) );
@@ -444,7 +445,6 @@ public class IntruderManager implements PwmService
         delayPenalty( manager.readIntruderRecord( subject ), sessionLabel == null ? null : sessionLabel );
     }
 
-
     private void delayPenalty( final IntruderRecord intruderRecord, final SessionLabel sessionLabel )
     {
         int points = 0;
@@ -467,7 +467,7 @@ public class IntruderManager implements PwmService
                 LOGGER.trace( sessionLabel, () -> "delaying response " + finalDelay + "ms due to intruder record: " + JsonUtil.serialize( intruderRecord ) );
             }
 
-            JavaHelper.pause( delayPenalty );
+            TimeDuration.of( delayPenalty, TimeDuration.Unit.MILLISECONDS ).pause();
         }
     }
 
@@ -564,7 +564,7 @@ public class IntruderManager implements PwmService
                 final String subject = pwmSession.getSessionStateBean().getSrcAddress();
                 check( RecordType.ADDRESS, subject );
                 final int maxAllowedAttempts = ( int ) pwmApplication.getConfig().readSettingAsLong( PwmSetting.INTRUDER_SESSION_MAX_ATTEMPTS );
-                if ( maxAllowedAttempts != 0 && pwmSession.getSessionStateBean().getIntruderAttempts() > maxAllowedAttempts )
+                if ( maxAllowedAttempts != 0 && pwmSession.getSessionStateBean().getIntruderAttempts().get() > maxAllowedAttempts )
                 {
                     throw new PwmUnrecoverableException( PwmError.ERROR_INTRUDER_SESSION );
                 }
@@ -579,6 +579,7 @@ public class IntruderManager implements PwmService
                 final String subject = pwmSession.getSessionStateBean().getSrcAddress();
                 clear( RecordType.ADDRESS, subject );
                 pwmSession.getSessionStateBean().clearIntruderAttempts();
+                pwmSession.getSessionStateBean().setSessionIdRecycleNeeded( true );
             }
         }
 

+ 3 - 2
server/src/main/java/password/pwm/svc/node/NodeMachine.java

@@ -27,6 +27,7 @@ import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmException;
 import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.util.PwmScheduler;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.TimeDuration;
 import password.pwm.util.logging.PwmLogger;
@@ -65,9 +66,9 @@ class NodeMachine
         this.clusterDataServiceProvider = clusterDataServiceProvider;
         this.settings = nodeServiceSettings;
 
-        this.executorService = JavaHelper.makeBackgroundExecutor( pwmApplication, NodeMachine.class );
+        this.executorService = PwmScheduler.makeBackgroundExecutor( pwmApplication, NodeMachine.class );
 
-        pwmApplication.scheduleFixedRateJob( new HeartbeatProcess(), executorService, settings.getHeartbeatInterval(), settings.getHeartbeatInterval() );
+        pwmApplication.getPwmScheduler().scheduleFixedRateJob( new HeartbeatProcess(), executorService, settings.getHeartbeatInterval(), settings.getHeartbeatInterval() );
     }
 
     public void close( )

+ 0 - 1
server/src/main/java/password/pwm/svc/node/NodeService.java

@@ -44,7 +44,6 @@ import java.util.Map;
 
 public class NodeService implements PwmService
 {
-
     private static final PwmLogger LOGGER = PwmLogger.forClass( NodeService.class );
 
     private PwmApplication pwmApplication;

+ 2 - 1
server/src/main/java/password/pwm/svc/pwnotify/PwNotifyEngine.java

@@ -37,6 +37,7 @@ import password.pwm.ldap.UserInfo;
 import password.pwm.ldap.UserInfoFactory;
 import password.pwm.svc.stats.Statistic;
 import password.pwm.svc.stats.StatisticsManager;
+import password.pwm.util.PwmScheduler;
 import password.pwm.util.i18n.LocaleHelper;
 import password.pwm.util.java.ConditionalTaskExecutor;
 import password.pwm.util.java.JavaHelper;
@@ -377,7 +378,7 @@ public class PwNotifyEngine
 
     private ThreadPoolExecutor createExecutor( final PwmApplication pwmApplication )
     {
-        final ThreadFactory threadFactory = JavaHelper.makePwmThreadFactory( JavaHelper.makeThreadName( pwmApplication, this.getClass() ), true );
+        final ThreadFactory threadFactory = PwmScheduler.makePwmThreadFactory( PwmScheduler.makeThreadName( pwmApplication, this.getClass() ), true );
         final ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
                 1,
                 10,

+ 5 - 4
server/src/main/java/password/pwm/svc/pwnotify/PwNotifyService.java

@@ -37,6 +37,7 @@ import password.pwm.svc.AbstractPwmService;
 import password.pwm.svc.PwmService;
 import password.pwm.svc.stats.Statistic;
 import password.pwm.svc.stats.StatisticsManager;
+import password.pwm.util.PwmScheduler;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.java.TimeDuration;
@@ -139,11 +140,11 @@ public class PwNotifyService extends AbstractPwmService implements PwmService
                     JavaHelper.unhandledSwitchStatement( storageMethod );
             }
 
-            executorService = JavaHelper.makeBackgroundExecutor( pwmApplication, this.getClass() );
+            executorService = PwmScheduler.makeBackgroundExecutor( pwmApplication, this.getClass() );
 
             engine = new PwNotifyEngine( pwmApplication, storageService, () -> status() == STATUS.CLOSED, null );
 
-            pwmApplication.scheduleFixedRateJob( new PwNotifyJob(), executorService, TimeDuration.MINUTE, TimeDuration.MINUTE );
+            pwmApplication.getPwmScheduler().scheduleFixedRateJob( new PwNotifyJob(), executorService, TimeDuration.MINUTE, TimeDuration.MINUTE );
 
             setStatus( STATUS.OPEN );
         }
@@ -193,7 +194,7 @@ public class PwNotifyService extends AbstractPwmService implements PwmService
             }
         }
 
-        final Instant nextZuluZeroTime = JavaHelper.nextZuluZeroTime();
+        final Instant nextZuluZeroTime = PwmScheduler.nextZuluZeroTime();
         final Instant adjustedNextZuluZeroTime = nextZuluZeroTime.plus( settings.getZuluOffset().as( TimeDuration.Unit.SECONDS ), ChronoUnit.SECONDS );
         final Instant previousAdjustedZuluZeroTime = adjustedNextZuluZeroTime.minus( 1, ChronoUnit.DAYS );
 
@@ -258,7 +259,7 @@ public class PwNotifyService extends AbstractPwmService implements PwmService
         if ( !isRunning() )
         {
             nextExecutionTime = Instant.now();
-            pwmApplication.scheduleFutureJob( new PwNotifyJob(), executorService, TimeDuration.ZERO );
+            pwmApplication.getPwmScheduler().scheduleFutureJob( new PwNotifyJob(), executorService, TimeDuration.ZERO );
         }
     }
 

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

@@ -41,6 +41,7 @@ import password.pwm.ldap.UserInfo;
 import password.pwm.ldap.UserInfoFactory;
 import password.pwm.svc.PwmService;
 import password.pwm.svc.stats.EventRateMeter;
+import password.pwm.util.PwmScheduler;
 import password.pwm.util.TransactionSizeCalculator;
 import password.pwm.util.java.BlockingThreadPool;
 import password.pwm.util.java.ClosableIterator;
@@ -75,7 +76,7 @@ public class ReportService implements PwmService
     private PwmApplication pwmApplication;
     private STATUS status = STATUS.NEW;
     private boolean cancelFlag = false;
-    private ReportStatusInfo reportStatus = new ReportStatusInfo( "" );
+    private ReportStatusInfo reportStatus = ReportStatusInfo.builder().build();
     private ReportSummaryData summaryData = ReportSummaryData.newSummaryData( null );
     private ExecutorService executorService;
 
@@ -142,7 +143,7 @@ public class ReportService implements PwmService
 
         dnQueue = LocalDBStoredQueue.createLocalDBStoredQueue( pwmApplication, pwmApplication.getLocalDB(), LocalDB.DB.REPORT_QUEUE );
 
-        executorService = JavaHelper.makeBackgroundExecutor( pwmApplication, this.getClass() );
+        executorService = PwmScheduler.makeBackgroundExecutor( pwmApplication, this.getClass() );
 
         LOGGER.debug( () -> "report service started" );
 
@@ -201,7 +202,7 @@ public class ReportService implements PwmService
             {
                 if ( reportStatus.getCurrentProcess() != ReportStatusInfo.ReportEngineProcess.ReadData
                         && reportStatus.getCurrentProcess() != ReportStatusInfo.ReportEngineProcess.SearchLDAP
-                        )
+                )
                 {
                     executorService.execute( new ClearTask() );
                     executorService.execute( new ReadLDAPTask() );
@@ -292,31 +293,13 @@ public class ReportService implements PwmService
             {
                 try
                 {
-                    UserCacheRecord returnBean = null;
-                    while ( returnBean == null && this.storageKeyIterator.hasNext() )
+                    while ( this.storageKeyIterator.hasNext() )
                     {
                         final UserCacheService.StorageKey key = this.storageKeyIterator.next();
-                        returnBean = userCacheService.readStorageKey( key );
+                        final UserCacheRecord returnBean = userCacheService.readStorageKey( key );
                         if ( returnBean != null )
                         {
-                            if ( returnBean.getCacheTimestamp() == null )
-                            {
-                                final UserCacheRecord finalBean = returnBean;
-                                LOGGER.debug( SessionLabel.REPORTING_SESSION_LABEL, () -> "purging record due to missing cache timestamp: "
-                                        + JsonUtil.serialize( finalBean ) );
-                                userCacheService.removeStorageKey( key );
-                            }
-                            else if ( TimeDuration.fromCurrent( returnBean.getCacheTimestamp() ).isLongerThan( settings.getMaxCacheAge() ) )
-                            {
-                                final UserCacheRecord finalBean = returnBean;
-                                LOGGER.debug( SessionLabel.REPORTING_SESSION_LABEL, () -> "purging record due to old age timestamp: "
-                                        + JsonUtil.serialize( finalBean ) );
-                                userCacheService.removeStorageKey( key );
-                            }
-                            else
-                            {
-                                return returnBean;
-                            }
+                            return returnBean;
                         }
 
                     }
@@ -407,6 +390,7 @@ public class ReportService implements PwmService
             }
             catch ( Exception e )
             {
+                boolean errorProcessed = false;
                 if ( e instanceof PwmException )
                 {
                     if ( ( ( PwmException ) e ).getErrorInformation().getError() == PwmError.ERROR_DIRECTORY_UNAVAILABLE )
@@ -414,13 +398,15 @@ public class ReportService implements PwmService
                         if ( executorService != null )
                         {
                             LOGGER.error( SessionLabel.REPORTING_SESSION_LABEL, "directory unavailable error during background SearchLDAP, will retry; error: " + e.getMessage() );
-                            pwmApplication.scheduleFutureJob( new ReadLDAPTask(), executorService, TimeDuration.of( 10, TimeDuration.Unit.MINUTES ) );
+                            pwmApplication.getPwmScheduler().scheduleFutureJob( new ReadLDAPTask(), executorService, TimeDuration.of( 10, TimeDuration.Unit.MINUTES ) );
+                            errorProcessed = true;
                         }
                     }
-                    else
-                    {
-                        LOGGER.error( SessionLabel.REPORTING_SESSION_LABEL, "error during background ReadData: " + e.getMessage() );
-                    }
+                }
+
+                if ( !errorProcessed )
+                {
+                    LOGGER.error( SessionLabel.REPORTING_SESSION_LABEL, "error during background ReadData: " + e.getMessage() );
                 }
             }
             finally
@@ -491,7 +477,7 @@ public class ReportService implements PwmService
                         if ( executorService != null )
                         {
                             LOGGER.error( SessionLabel.REPORTING_SESSION_LABEL, "directory unavailable error during background ReadData, will retry; error: " + e.getMessage() );
-                            pwmApplication.scheduleFutureJob( new ProcessWorkQueueTask(), executorService, TimeDuration.of( 10, TimeDuration.Unit.MINUTES ) );
+                            pwmApplication.getPwmScheduler().scheduleFutureJob( new ProcessWorkQueueTask(), executorService, TimeDuration.of( 10, TimeDuration.Unit.MINUTES ) );
                         }
                     }
                     else
@@ -571,7 +557,7 @@ public class ReportService implements PwmService
 
                             if ( pauseBetweenIterations )
                             {
-                                JavaHelper.pause( avgTracker.avgAsLong() );
+                                TimeDuration.of( avgTracker.avgAsLong(), TimeDuration.Unit.MILLISECONDS ).pause();
                             }
                         }
                         catch ( Exception e )
@@ -614,20 +600,13 @@ public class ReportService implements PwmService
 
 
         private void updateCachedRecordFromLdap( final UserIdentity userIdentity )
-                throws ChaiUnavailableException, PwmUnrecoverableException, LocalDBException
+                throws PwmUnrecoverableException, LocalDBException
         {
             if ( status != STATUS.OPEN )
             {
                 return;
             }
 
-            final UserCacheService.StorageKey storageKey = UserCacheService.StorageKey.fromUserIdentity( pwmApplication, userIdentity );
-            final UserCacheRecord userCacheRecord = userCacheService.readStorageKey( storageKey );
-
-            if ( userCacheRecord != null )
-            {
-                summaryData.remove( userCacheRecord );
-            }
             final UserInfo userInfo = UserInfoFactory.newUserInfoUsingProxyForOfflineUser(
                     pwmApplication,
                     SessionLabel.REPORTING_SESSION_LABEL,
@@ -642,71 +621,29 @@ public class ReportService implements PwmService
         }
     }
 
-    private class RolloverTask implements Runnable
+    private class DailyJobExecuteTask implements Runnable
     {
         @Override
         public void run( )
         {
-            reportStatus.setCurrentProcess( ReportStatusInfo.ReportEngineProcess.RollOver );
-            try
-            {
-                summaryData = ReportSummaryData.newSummaryData( settings.getTrackDays() );
-                updateRestingCacheData();
-            }
-            finally
+            checkForOutdatedStoreData();
+
+            if ( settings.isDailyJobEnabled() )
             {
-                reportStatus.setCurrentProcess( ReportStatusInfo.ReportEngineProcess.None );
+                executorService.execute( new ClearTask() );
+                executorService.execute( new ReadLDAPTask() );
             }
         }
 
-        private void updateRestingCacheData( )
+        private void checkForOutdatedStoreData()
         {
-            final Instant startTime = Instant.now();
-            int examinedRecords = 0;
-
-            try ( ClosableIterator<UserCacheRecord> iterator = iterator() )
+            final Instant lastFinishDate = reportStatus.getFinishDate();
+            if ( lastFinishDate != null && TimeDuration.fromCurrent( lastFinishDate ).isLongerThan( settings.getMaxCacheAge() ) )
             {
-                final long totalRecords = userCacheService.size();
-                LOGGER.debug( SessionLabel.REPORTING_SESSION_LABEL, () -> "beginning cache review process of " + totalRecords + " records" );
-                Instant lastLogOutputTime = Instant.now();
-
-                while ( !cancelFlag && iterator.hasNext() && status == STATUS.OPEN )
-                {
-                    // (purge routine is embedded in next();
-                    final UserCacheRecord record = iterator.next();
-
-                    if ( summaryData != null && record != null )
-                    {
-                        summaryData.update( record );
-                    }
-
-                    examinedRecords++;
-
-                    if ( TimeDuration.fromCurrent( lastLogOutputTime ).isLongerThan( 30, TimeDuration.Unit.SECONDS ) )
-                    {
-                        final int finalExamined = examinedRecords;
-                        LOGGER.trace( SessionLabel.REPORTING_SESSION_LABEL,
-                                () -> "cache review process in progress, examined " + finalExamined
-                                        + " in " + TimeDuration.compactFromCurrent( startTime ) );
-                        lastLogOutputTime = Instant.now();
-                    }
-                }
-                final int finalExamined = examinedRecords;
-                LOGGER.info( SessionLabel.REPORTING_SESSION_LABEL,
-                        () -> "completed cache review process of " + finalExamined
-                                + " cached report records in " + TimeDuration.compactFromCurrent( startTime ) );
+                executorService.execute( new ClearTask() );
             }
         }
-    }
 
-    private class DailyJobExecuteTask implements Runnable
-    {
-        @Override
-        public void run( )
-        {
-            executorService.execute( new ClearTask() );
-            executorService.execute( new ReadLDAPTask() );
-        }
     }
 
     private class InitializationTask implements Runnable
@@ -725,17 +662,12 @@ public class ReportService implements PwmService
                 return;
             }
 
-            final boolean reportingEnabled = pwmApplication.getConfig().readSettingAsBoolean( PwmSetting.REPORTING_ENABLE );
+            final boolean reportingEnabled = pwmApplication.getConfig().readSettingAsBoolean( PwmSetting.REPORTING_ENABLE_DAILY_JOB );
             if ( reportingEnabled )
             {
-                final Instant nextZuluZeroTime = JavaHelper.nextZuluZeroTime();
-                final long secondsUntilNextDredge = settings.getJobOffsetSeconds() + TimeDuration.fromCurrent( nextZuluZeroTime ).as( TimeDuration.Unit.SECONDS );
-                final TimeDuration initialDelay = TimeDuration.of( secondsUntilNextDredge, TimeDuration.Unit.SECONDS );
-                pwmApplication.scheduleFixedRateJob( new ProcessWorkQueueTask(), executorService, initialDelay, TimeDuration.DAY );
-                LOGGER.debug( () -> "scheduled daily execution, next task will be at " + nextZuluZeroTime.toString() );
+                final TimeDuration jobOffset = TimeDuration.of( settings.getJobOffsetSeconds(), TimeDuration.Unit.SECONDS );
+                pwmApplication.getPwmScheduler().scheduleDailyZuluZeroStartJob( new DailyJobExecuteTask(), executorService, jobOffset );
             }
-            executorService.submit( new RolloverTask() );
-            executorService.submit( new ProcessWorkQueueTask() );
         }
 
 
@@ -769,7 +701,7 @@ public class ReportService implements PwmService
 
             if ( clearFlag )
             {
-                reportStatus = new ReportStatusInfo( settings.getSettingsHash() );
+                reportStatus = ReportStatusInfo.builder().settingsHash( settings.getSettingsHash() ).build();
                 executeCommand( ReportCommand.Clear );
             }
         }
@@ -800,7 +732,7 @@ public class ReportService implements PwmService
                 userCacheService.clear();
             }
             summaryData = ReportSummaryData.newSummaryData( settings.getTrackDays() );
-            reportStatus = new ReportStatusInfo( settings.getSettingsHash() );
+            reportStatus = ReportStatusInfo.builder().settingsHash( settings.getSettingsHash() ).build();
             LOGGER.debug( SessionLabel.REPORTING_SESSION_LABEL, () -> "finished clearing report " + TimeDuration.compactFromCurrent( startTime ) );
         }
     }

+ 5 - 2
server/src/main/java/password/pwm/svc/report/ReportSettings.java

@@ -46,8 +46,10 @@ class ReportSettings implements Serializable
 {
     private static final PwmLogger LOGGER = PwmLogger.forClass( ReportSettings.class );
 
+    private boolean dailyJobEnabled;
+
     @Builder.Default
-    private TimeDuration maxCacheAge = TimeDuration.of( TimeDuration.DAY.asMillis() * 90, TimeDuration.Unit.MILLISECONDS );
+    private TimeDuration maxCacheAge = TimeDuration.of( 10, TimeDuration.Unit.DAYS );
 
     @Builder.Default
     private List<UserPermission> searchFilter = Collections.emptyList();
@@ -77,9 +79,10 @@ class ReportSettings implements Serializable
     public static ReportSettings readSettingsFromConfig( final Configuration config )
     {
         final ReportSettings.ReportSettingsBuilder builder = ReportSettings.builder();
-        builder.maxCacheAge( TimeDuration.of( config.readSettingAsLong( PwmSetting.REPORTING_MAX_CACHE_AGE ), TimeDuration.Unit.SECONDS ) );
+        builder.maxCacheAge( TimeDuration.of( Long.parseLong( config.readAppProperty( AppProperty.REPORTING_MAX_REPORT_AGE_SECONDS ) ), TimeDuration.Unit.SECONDS ) );
         builder.searchFilter( config.readSettingAsUserPermission( PwmSetting.REPORTING_USER_MATCH ) );
         builder.maxSearchSize ( ( int ) config.readSettingAsLong( PwmSetting.REPORTING_MAX_QUERY_SIZE ) );
+        builder.dailyJobEnabled( config.readSettingAsBoolean( PwmSetting.REPORTING_ENABLE_DAILY_JOB ) );
 
         if ( builder.searchFilter == null || builder.searchFilter.isEmpty() )
         {

+ 9 - 72
server/src/main/java/password/pwm/svc/report/ReportStatusInfo.java

@@ -22,20 +22,29 @@
 
 package password.pwm.svc.report;
 
+import lombok.Builder;
+import lombok.Data;
 import password.pwm.error.ErrorInformation;
 import password.pwm.util.java.TimeDuration;
 
 import java.io.Serializable;
 import java.time.Instant;
 
+@Data
+@Builder( toBuilder = true )
 public class ReportStatusInfo implements Serializable
 {
+    @Builder.Default
     private TimeDuration jobDuration = TimeDuration.ZERO;
+
+    private Instant startDate;
     private Instant finishDate;
     private int count;
     private int errors;
     private ErrorInformation lastError;
     private String settingsHash;
+
+    @Builder.Default
     private ReportEngineProcess currentProcess = ReportEngineProcess.None;
 
     public enum ReportEngineProcess
@@ -58,76 +67,4 @@ public class ReportStatusInfo implements Serializable
             return label;
         }
     }
-
-
-    public ReportStatusInfo( final String settingsHash )
-    {
-        this.settingsHash = settingsHash;
-    }
-
-    public String getSettingsHash( )
-    {
-        return settingsHash;
-    }
-
-    public TimeDuration getJobDuration( )
-    {
-        return jobDuration;
-    }
-
-    public void setJobDuration( final TimeDuration jobDuration )
-    {
-        this.jobDuration = jobDuration;
-    }
-
-    public Instant getFinishDate( )
-    {
-        return finishDate;
-    }
-
-    public void setFinishDate( final Instant finishDate )
-    {
-        this.finishDate = finishDate;
-    }
-
-    public int getCount( )
-    {
-        return count;
-    }
-
-    public void setCount( final int count )
-    {
-        this.count = count;
-    }
-
-
-    public int getErrors( )
-    {
-        return errors;
-    }
-
-    public void setErrors( final int errors )
-    {
-        this.errors = errors;
-    }
-
-    public ErrorInformation getLastError( )
-    {
-        return lastError;
-    }
-
-    public void setLastError( final ErrorInformation lastError )
-    {
-        this.lastError = lastError;
-    }
-
-    public ReportEngineProcess getCurrentProcess( )
-    {
-        return currentProcess;
-    }
-
-    public void setCurrentProcess( final ReportEngineProcess currentProcess )
-    {
-        this.currentProcess = currentProcess;
-    }
 }

+ 96 - 227
server/src/main/java/password/pwm/svc/report/ReportSummaryData.java

@@ -23,6 +23,8 @@
 package password.pwm.svc.report;
 
 import com.novell.ldapchai.cr.Answer;
+import lombok.Builder;
+import lombok.Value;
 import password.pwm.config.Configuration;
 import password.pwm.config.option.DataStorageMethod;
 import password.pwm.i18n.Admin;
@@ -44,12 +46,12 @@ import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.stream.Collectors;
 
+@Value
 public class ReportSummaryData
 {
     private static final long MS_DAY = TimeDuration.DAY.asMillis();
     private static final BigInteger TWO = new BigInteger( "2" );
 
-    private Instant meanCacheTime;
     private final AtomicInteger totalUsers = new AtomicInteger( 0 );
     private final AtomicInteger hasResponses = new AtomicInteger( 0 );
     private final AtomicInteger hasResponseSetTime = new AtomicInteger( 0 );
@@ -63,6 +65,7 @@ public class ReportSummaryData
     private final AtomicInteger pwExpired = new AtomicInteger( 0 );
     private final AtomicInteger pwPreExpired = new AtomicInteger( 0 );
     private final AtomicInteger pwWarnPeriod = new AtomicInteger( 0 );
+    private final AtomicInteger hasReceivedPwExpireNotification = new AtomicInteger( 0 );
 
     private final Map<DataStorageMethod, AtomicInteger> responseStorage = new ConcurrentHashMap<>();
     private final Map<Answer.FormatType, AtomicInteger> responseFormatType = new ConcurrentHashMap<>();
@@ -73,6 +76,7 @@ public class ReportSummaryData
     private final Map<Integer, AtomicInteger> responseSetDays = new ConcurrentHashMap<>();
     private final Map<Integer, AtomicInteger> otpSetDays = new ConcurrentHashMap<>();
     private final Map<Integer, AtomicInteger> loginDays = new ConcurrentHashMap<>();
+    private final Map<Integer, AtomicInteger> pwExpireNotificationDays = new ConcurrentHashMap<>();
 
     private ReportSummaryData( )
     {
@@ -92,27 +96,13 @@ public class ReportSummaryData
                 reportSummaryData.responseSetDays.put( day, new AtomicInteger( 0 ) );
                 reportSummaryData.otpSetDays.put( day, new AtomicInteger( 0 ) );
                 reportSummaryData.loginDays.put( day, new AtomicInteger( 0 ) );
+                reportSummaryData.pwExpireNotificationDays.put( day, new AtomicInteger( 0 ) );
             }
         }
 
         return reportSummaryData;
     }
 
-    public int getTotalUsers( )
-    {
-        return totalUsers.get();
-    }
-
-    public int getHasResponses( )
-    {
-        return hasResponses.get();
-    }
-
-    public int getHasPasswordExpirationTime( )
-    {
-        return hasPasswordExpirationTime.get();
-    }
-
     public Map<DataStorageMethod, Integer> getResponseStorage( )
     {
         return Collections.unmodifiableMap( responseStorage.entrySet()
@@ -129,141 +119,77 @@ public class ReportSummaryData
                         e -> e.getValue().get() ) ) );
     }
 
-    public Instant getMeanCacheTime( )
-    {
-        return meanCacheTime;
-    }
-
     void update( final UserCacheRecord userCacheRecord )
     {
-        update( userCacheRecord, true );
-    }
-
-    void remove( final UserCacheRecord userCacheRecord )
-    {
-        update( userCacheRecord, false );
-    }
-
-    @SuppressWarnings( "checkstyle:MethodLength" )
-    private void update( final UserCacheRecord userCacheRecord, final boolean adding )
-    {
-        final int modifier = adding ? 1 : -1;
-
-        totalUsers.addAndGet( modifier );
+        totalUsers.incrementAndGet();
 
-        updateMeanTime( userCacheRecord.cacheTimestamp, adding );
-
-        if ( userCacheRecord.hasResponses )
+        if ( userCacheRecord.isHasResponses() )
         {
-            hasResponses.addAndGet( modifier );
+            hasResponses.incrementAndGet();
         }
 
-        if ( userCacheRecord.hasHelpdeskResponses )
+        if ( userCacheRecord.isHasHelpdeskResponses() )
         {
-            hasHelpdeskResponses.addAndGet( modifier );
+            hasHelpdeskResponses.incrementAndGet();
         }
 
-        if ( userCacheRecord.responseSetTime != null )
+        if ( userCacheRecord.getResponseSetTime() != null )
         {
-            hasResponseSetTime.addAndGet( modifier );
-
-            for ( final Map.Entry<Integer, AtomicInteger> entry : responseSetDays.entrySet() )
-            {
-                final Integer day = entry.getKey();
-                entry.getValue().addAndGet( calcTimeWindow( userCacheRecord.responseSetTime, MS_DAY * day, adding ) );
-            }
+            hasResponseSetTime.incrementAndGet();
+            incrementIfWithinTimeWindow( userCacheRecord, responseSetDays );
         }
 
-        if ( userCacheRecord.passwordExpirationTime != null )
+        if ( userCacheRecord.getPasswordExpirationTime() != null )
         {
-            hasPasswordExpirationTime.addAndGet( modifier );
-
-            for ( final Map.Entry<Integer, AtomicInteger> entry : pwExpireDays.entrySet() )
-            {
-                final Integer day = entry.getKey();
-                entry.getValue().addAndGet( calcTimeWindow( userCacheRecord.passwordExpirationTime, MS_DAY * day, adding ) );
-            }
+            hasPasswordExpirationTime.incrementAndGet();
+            incrementIfWithinTimeWindow( userCacheRecord, pwExpireDays );
         }
 
-        if ( userCacheRecord.accountExpirationTime != null )
+        if ( userCacheRecord.getAccountExpirationTime() != null )
         {
-            hasAccountExpirationTime.addAndGet( modifier );
-
-            for ( final Map.Entry<Integer, AtomicInteger> entry : accountExpireDays.entrySet() )
-            {
-                final Integer day = entry.getKey();
-                entry.getValue().addAndGet( calcTimeWindow( userCacheRecord.accountExpirationTime, MS_DAY * day, adding ) );
-            }
+            hasAccountExpirationTime.incrementAndGet();
+            incrementIfWithinTimeWindow( userCacheRecord, accountExpireDays );
         }
 
-        if ( userCacheRecord.lastLoginTime != null )
+        if ( userCacheRecord.getLastLoginTime() != null )
         {
-            hasLoginTime.addAndGet( modifier );
-
-            for ( final Map.Entry<Integer, AtomicInteger> entry : loginDays.entrySet() )
-            {
-                final Integer day = entry.getKey();
-                entry.getValue().addAndGet( calcTimeWindow( userCacheRecord.lastLoginTime, MS_DAY * day, adding ) );
-            }
+            hasLoginTime.incrementAndGet();
+            incrementIfWithinTimeWindow( userCacheRecord, loginDays );
         }
 
-        if ( userCacheRecord.passwordChangeTime != null )
+        if ( userCacheRecord.getPasswordChangeTime() != null )
         {
-            hasChangePwTime.addAndGet( modifier );
+            hasChangePwTime.incrementAndGet();
+            incrementIfWithinTimeWindow( userCacheRecord, changePwDays );
+        }
 
-            for ( final Map.Entry<Integer, AtomicInteger> entry : changePwDays.entrySet() )
-            {
-                final Integer day = entry.getKey();
-                entry.getValue().addAndGet( calcTimeWindow( userCacheRecord.passwordChangeTime, MS_DAY * day, adding ) );
-            }
+        if ( userCacheRecord.getPasswordExpirationNoticeSendTime() != null )
+        {
+            hasReceivedPwExpireNotification.incrementAndGet();
+            incrementIfWithinTimeWindow( userCacheRecord, pwExpireNotificationDays );
         }
 
-        if ( userCacheRecord.passwordStatus != null )
+        if ( userCacheRecord.getPasswordStatus() != null )
         {
-            if ( adding )
+            if ( userCacheRecord.getPasswordStatus().isExpired() )
             {
-                if ( userCacheRecord.passwordStatus.isExpired() )
-                {
-                    pwExpired.incrementAndGet();
-                }
-                if ( userCacheRecord.passwordStatus.isPreExpired() )
-                {
-                    pwPreExpired.incrementAndGet();
-                }
-                if ( userCacheRecord.passwordStatus.isWarnPeriod() )
-                {
-                    pwWarnPeriod.incrementAndGet();
-                }
+                pwExpired.incrementAndGet();
             }
-            else
+            if ( userCacheRecord.getPasswordStatus().isPreExpired() )
             {
-                if ( userCacheRecord.passwordStatus.isExpired() )
-                {
-                    pwExpired.decrementAndGet();
-                }
-                if ( userCacheRecord.passwordStatus.isPreExpired() )
-                {
-                    pwPreExpired.decrementAndGet();
-                }
-                if ( userCacheRecord.passwordStatus.isWarnPeriod() )
-                {
-                    pwWarnPeriod.decrementAndGet();
-                }
+                pwPreExpired.incrementAndGet();
+            }
+            if ( userCacheRecord.getPasswordStatus().isWarnPeriod() )
+            {
+                pwWarnPeriod.incrementAndGet();
             }
         }
 
-        if ( userCacheRecord.responseStorageMethod != null )
+        if ( userCacheRecord.getResponseStorageMethod() != null )
         {
-            final DataStorageMethod method = userCacheRecord.responseStorageMethod;
+            final DataStorageMethod method = userCacheRecord.getResponseStorageMethod();
             responseStorage.putIfAbsent( method, new AtomicInteger( 0 ) );
-            if ( adding )
-            {
-                responseStorage.get( method ).incrementAndGet();
-            }
-            else
-            {
-                responseStorage.get( method ).decrementAndGet();
-            }
+            responseStorage.get( method ).incrementAndGet();
         }
 
         if ( userCacheRecord.getLdapProfile() != null )
@@ -273,86 +199,54 @@ public class ReportSummaryData
             {
                 ldapProfile.put( userProfile, new AtomicInteger( 0 ) );
             }
-            if ( adding )
-            {
-                ldapProfile.get( userProfile ).incrementAndGet();
-            }
-            else
-            {
-                ldapProfile.get( userProfile ).decrementAndGet();
-            }
+            ldapProfile.get( userProfile ).incrementAndGet();
         }
 
-        if ( userCacheRecord.responseFormatType != null )
+        if ( userCacheRecord.getResponseFormatType() != null )
         {
-            final Answer.FormatType type = userCacheRecord.responseFormatType;
+            final Answer.FormatType type = userCacheRecord.getResponseFormatType();
             responseFormatType.putIfAbsent( type, new AtomicInteger( 0 ) );
-            if ( adding )
-            {
-                responseFormatType.get( type ).incrementAndGet();
-            }
-            else
-            {
-                responseFormatType.get( type ).decrementAndGet();
-            }
+            responseFormatType.get( type ).incrementAndGet();
         }
 
         if ( userCacheRecord.isHasOtpSecret() )
         {
-            hasOtpSecret.addAndGet( modifier );
+            hasOtpSecret.incrementAndGet();
         }
 
         if ( userCacheRecord.getOtpSecretSetTime() != null )
         {
-            hasOtpSecretSetTime.addAndGet( modifier );
-
-            for ( final Map.Entry<Integer, AtomicInteger> entry : otpSetDays.entrySet() )
-            {
-                final int day = entry.getKey();
-                entry.getValue().addAndGet( calcTimeWindow( userCacheRecord.getOtpSecretSetTime(), MS_DAY * day, adding ) );
-            }
+            hasOtpSecretSetTime.incrementAndGet();
+            incrementIfWithinTimeWindow( userCacheRecord, otpSetDays );
         }
     }
 
-    private void updateMeanTime( final Instant newTime, final boolean adding )
+    private void incrementIfWithinTimeWindow(
+            final UserCacheRecord userCacheRecord,
+            final Map<Integer, AtomicInteger> map
+    )
     {
-        if ( meanCacheTime == null )
+        for ( final Map.Entry<Integer, AtomicInteger> entry : map.entrySet() )
         {
-            if ( adding )
-            {
-                meanCacheTime = newTime;
-            }
-            return;
-        }
-
-        final BigInteger currentMillis = BigInteger.valueOf( meanCacheTime.toEpochMilli() );
-        final BigInteger newMillis = BigInteger.valueOf( newTime.toEpochMilli() );
-        final BigInteger combinedMillis = currentMillis.add( newMillis );
-        final BigInteger halvedMillis = combinedMillis.divide( TWO );
-        meanCacheTime = Instant.ofEpochMilli( halvedMillis.longValue() );
-    }
-
-    private int calcTimeWindow( final Instant eventDate, final long timeWindow, final boolean adding )
-    {
-        if ( eventDate == null )
-        {
-            return 0;
-        }
-
-        final TimeDuration timeBoundary = TimeDuration.of( timeWindow, TimeDuration.Unit.MILLISECONDS );
-        final TimeDuration eventDifference = TimeDuration.fromCurrent( eventDate );
+            final int day = entry.getKey();
+            final Instant eventDate = userCacheRecord.getOtpSecretSetTime();
+            final long timeWindow = MS_DAY * day;
+            final AtomicInteger number = entry.getValue();
 
-        if ( timeWindow >= 0 && eventDate.isAfter( Instant.now() ) && eventDifference.isShorterThan( timeBoundary ) )
-        {
-            return adding ? 1 : -1;
-        }
+            if ( eventDate != null )
+            {
+                final TimeDuration timeBoundary = TimeDuration.of( timeWindow, TimeDuration.Unit.MILLISECONDS );
+                final TimeDuration eventDifference = TimeDuration.fromCurrent( eventDate );
 
-        if ( timeWindow < 0 && eventDate.isBefore( Instant.now() ) && eventDifference.isShorterThan( timeBoundary ) )
-        {
-            return adding ? 1 : -1;
+                if (
+                        ( timeWindow >= 0 && eventDate.isAfter( Instant.now() ) && eventDifference.isShorterThan( timeBoundary ) )
+                                || ( timeWindow < 0 && eventDate.isBefore( Instant.now() ) && eventDifference.isShorterThan( timeBoundary ) )
+                )
+                {
+                    number.incrementAndGet();
+                }
+            }
         }
-
-        return 0;
     }
 
 
@@ -450,80 +344,54 @@ public class ReportSummaryData
             }
         }
 
+        if ( this.hasReceivedPwExpireNotification.get() > 0 )
+        {
+            returnCollection.add( new PresentationRow( "Has Received PwExpiry Notice", Integer.toString( this.hasReceivedPwExpireNotification.get() ), null ) );
+            for ( final Integer day : new TreeSet<>( pwExpireNotificationDays.keySet() ) )
+            {
+                if ( day < 0 )
+                {
+                    returnCollection.add( new PresentationRow( "PwExpireNotice " + day, Integer.toString( this.pwExpireNotificationDays.get( day ).get() ), null ) );
+                }
+            }
+        }
+
+
         return returnCollection;
     }
 
+    @Value
+    @Builder( toBuilder = true )
     public static class PresentationRow
     {
         private String label;
         private String count;
         private String pct;
-
-        public PresentationRow(
-                final String label,
-                final String count,
-                final String pct
-        )
-        {
-            this.label = label;
-            this.count = count;
-            this.pct = pct;
-        }
-
-        public String getLabel( )
-        {
-            return label;
-        }
-
-        public String getCount( )
-        {
-            return count;
-        }
-
-        public String getPct( )
-        {
-            return pct;
-        }
     }
 
+    @Value
     public static class PresentationRowBuilder
     {
         private final Configuration config;
         private final int totalUsers;
         private final Locale locale;
 
-        public PresentationRowBuilder(
-                final Configuration config,
-                final int totalUsers,
-                final Locale locale
-        )
+        PresentationRow makeRow( final String labelKey, final int valueCount )
         {
-            this.config = config;
-            this.totalUsers = totalUsers;
-            this.locale = locale;
+            return makeRow( labelKey, valueCount, null );
         }
 
-        public PresentationRow makeRow( final String labelKey, final int valueCount )
+        PresentationRow makeRow( final String labelKey, final int valueCount, final String replacement )
         {
-            return makeRow( labelKey, valueCount, null );
+            return makeRowImpl( labelKey, valueCount, replacement );
         }
 
-        public PresentationRow makeRow( final String labelKey, final int valueCount, final String replacement )
+        PresentationRow makeNoPctRow( final String labelKey, final int valueCount, final String replacement )
         {
-            final String display = replacement == null
-                    ? LocaleHelper.getLocalizedMessage( locale, labelKey, config, Admin.class )
-                    : LocaleHelper.getLocalizedMessage( locale, labelKey, config, Admin.class, new String[]
-                    {
-                            replacement,
-                    }
-            );
-            final String pct = valueCount > 0 ? new Percent( valueCount, totalUsers ).pretty( 2 ) : "";
-            final PwmNumberFormat numberFormat = PwmNumberFormat.forLocale( locale );
-            final String formattedCount = numberFormat.format( valueCount );
-            return new PresentationRow( display, formattedCount, pct );
+            return makeRowImpl( labelKey, valueCount, replacement ).toBuilder().pct( null ).build();
         }
 
-        public PresentationRow makeNoPctRow( final String labelKey, final int valueCount, final String replacement )
+        private PresentationRow makeRowImpl( final String labelKey, final int valueCount, final String replacement )
         {
             final String display = replacement == null
                     ? LocaleHelper.getLocalizedMessage( locale, labelKey, config, Admin.class )
@@ -532,9 +400,10 @@ public class ReportSummaryData
                             replacement,
                     }
             );
+            final String pct = valueCount > 0 ? new Percent( valueCount, totalUsers ).pretty( 2 ) : "";
             final PwmNumberFormat numberFormat = PwmNumberFormat.forLocale( locale );
             final String formattedCount = numberFormat.format( valueCount );
-            return new PresentationRow( display, formattedCount, null );
+            return new PresentationRow( display, formattedCount, pct );
         }
     }
 }

+ 56 - 51
server/src/main/java/password/pwm/svc/report/UserCacheRecord.java

@@ -23,9 +23,8 @@
 package password.pwm.svc.report;
 
 import com.novell.ldapchai.cr.Answer;
-import lombok.AccessLevel;
-import lombok.Getter;
-import lombok.Setter;
+import lombok.Builder;
+import lombok.Value;
 import password.pwm.bean.PasswordStatus;
 import password.pwm.config.option.DataStorageMethod;
 import password.pwm.error.PwmUnrecoverableException;
@@ -34,85 +33,91 @@ import password.pwm.ldap.UserInfo;
 import java.io.Serializable;
 import java.time.Instant;
 
-@Getter
-@Setter( AccessLevel.PRIVATE )
+@Value
+@Builder
 public class UserCacheRecord implements Serializable
 {
-    public String userDN;
-    public String ldapProfile;
-    public String userGUID;
+    private String userDN;
+    private String ldapProfile;
+    private String userGUID;
 
-    public String username;
-    public String email;
+    private String username;
+    private String email;
 
-    public Instant cacheTimestamp = Instant.now();
+    private PasswordStatus passwordStatus;
+    private Instant passwordExpirationTime;
+    private Instant passwordChangeTime;
+    private Instant lastLoginTime;
+    private Instant accountExpirationTime;
+    private Instant passwordExpirationNoticeSendTime;
 
-    public PasswordStatus passwordStatus;
-    public Instant passwordExpirationTime;
-    public Instant passwordChangeTime;
-    public Instant lastLoginTime;
-    public Instant accountExpirationTime;
+    private boolean hasResponses;
+    private boolean hasHelpdeskResponses;
+    private Instant responseSetTime;
+    private DataStorageMethod responseStorageMethod;
+    private Answer.FormatType responseFormatType;
 
-    public boolean hasResponses;
-    public boolean hasHelpdeskResponses;
-    public Instant responseSetTime;
-    public DataStorageMethod responseStorageMethod;
-    public Answer.FormatType responseFormatType;
+    private boolean hasOtpSecret;
+    private Instant otpSecretSetTime;
 
-    public boolean hasOtpSecret;
-    public Instant otpSecretSetTime;
+    private boolean requiresPasswordUpdate;
+    private boolean requiresResponseUpdate;
+    private boolean requiresProfileUpdate;
 
-    public boolean requiresPasswordUpdate;
-    public boolean requiresResponseUpdate;
-    public boolean requiresProfileUpdate;
+    private Instant cacheTimestamp;
 
-    void addUiBeanData(
+    static UserCacheRecord fromUserInfo(
             final UserInfo userInfo
     )
             throws PwmUnrecoverableException
     {
-        this.setUserDN( userInfo.getUserIdentity().getUserDN() );
-        this.setLdapProfile( userInfo.getUserIdentity().getLdapProfileID() );
-        this.setUsername( userInfo.getUsername() );
-        this.setEmail( userInfo.getUserEmailAddress() );
-        this.setUserGUID( userInfo.getUserGuid() );
-
-        this.setPasswordStatus( userInfo.getPasswordStatus() );
-
-        this.setPasswordChangeTime( userInfo.getPasswordLastModifiedTime() );
-        this.setPasswordExpirationTime( userInfo.getPasswordExpirationTime() );
-        this.setLastLoginTime( userInfo.getLastLdapLoginTime() );
-        this.setAccountExpirationTime( userInfo.getAccountExpirationTime() );
-
-        this.setHasResponses( userInfo.getResponseInfoBean() != null );
-        this.setResponseSetTime( userInfo.getResponseInfoBean() != null
+        final UserCacheRecordBuilder builder = new UserCacheRecordBuilder();
+        builder.userDN( userInfo.getUserIdentity().getUserDN() );
+        builder.ldapProfile( userInfo.getUserIdentity().getLdapProfileID() );
+        builder.username( userInfo.getUsername() );
+        builder.email( userInfo.getUserEmailAddress() );
+        builder.userGUID( userInfo.getUserGuid() );
+
+        builder.passwordStatus( userInfo.getPasswordStatus() );
+
+        builder.passwordChangeTime( userInfo.getPasswordLastModifiedTime() );
+        builder.passwordExpirationTime( userInfo.getPasswordExpirationTime() );
+        builder.lastLoginTime( userInfo.getLastLdapLoginTime() );
+        builder.accountExpirationTime( userInfo.getAccountExpirationTime() );
+        builder.passwordExpirationNoticeSendTime( userInfo.getPasswordExpirationNoticeSendTime() );
+
+        builder.hasResponses( userInfo.getResponseInfoBean() != null );
+        builder.responseSetTime( userInfo.getResponseInfoBean() != null
                 ? userInfo.getResponseInfoBean().getTimestamp()
                 : null
         );
-        this.setResponseStorageMethod( userInfo.getResponseInfoBean() != null
+        builder.responseStorageMethod( userInfo.getResponseInfoBean() != null
                 ? userInfo.getResponseInfoBean().getDataStorageMethod()
                 : null
         );
-        this.setResponseFormatType( userInfo.getResponseInfoBean() != null
+        builder.responseFormatType( userInfo.getResponseInfoBean() != null
                 ? userInfo.getResponseInfoBean().getFormatType()
                 : null
         );
 
-        this.setRequiresPasswordUpdate( userInfo.isRequiresNewPassword() );
-        this.setRequiresResponseUpdate( userInfo.isRequiresResponseConfig() );
-        this.setRequiresProfileUpdate( userInfo.isRequiresUpdateProfile() );
-        this.setCacheTimestamp( Instant.now() );
+        builder.requiresPasswordUpdate( userInfo.isRequiresNewPassword() );
+        builder.requiresResponseUpdate( userInfo.isRequiresResponseConfig() );
+        builder.requiresProfileUpdate( userInfo.isRequiresUpdateProfile() );
 
-        this.setHasOtpSecret( userInfo.getOtpUserRecord() != null );
-        this.setOtpSecretSetTime( userInfo.getOtpUserRecord() != null && userInfo.getOtpUserRecord().getTimestamp() != null
+        builder.hasOtpSecret( userInfo.getOtpUserRecord() != null );
+        builder.otpSecretSetTime( userInfo.getOtpUserRecord() != null && userInfo.getOtpUserRecord().getTimestamp() != null
                 ? userInfo.getOtpUserRecord().getTimestamp()
                 : null
         );
 
-        this.setHasHelpdeskResponses( userInfo.getResponseInfoBean() != null
+        builder.hasHelpdeskResponses( userInfo.getResponseInfoBean() != null
                 && userInfo.getResponseInfoBean().getHelpdeskCrMap() != null
                 && !userInfo.getResponseInfoBean().getHelpdeskCrMap().isEmpty()
         );
+
+        builder.cacheTimestamp( Instant.now() );
+
+        return builder.build();
     }
 
 }

+ 6 - 23
server/src/main/java/password/pwm/svc/report/UserCacheService.java

@@ -59,24 +59,14 @@ public class UserCacheService implements PwmService
         return status;
     }
 
-    public UserCacheRecord updateUserCache( final UserInfo userInfo )
+    UserCacheRecord updateUserCache( final UserInfo userInfo )
             throws PwmUnrecoverableException
     {
         final StorageKey storageKey = StorageKey.fromUserInfo( userInfo, pwmApplication );
 
-        boolean preExisting = false;
         try
         {
-            UserCacheRecord userCacheRecord = readStorageKey( storageKey );
-            if ( userCacheRecord == null )
-            {
-                userCacheRecord = new UserCacheRecord();
-            }
-            else
-            {
-                preExisting = true;
-            }
-            userCacheRecord.addUiBeanData( userInfo );
+            final UserCacheRecord userCacheRecord = UserCacheRecord.fromUserInfo( userInfo );
             store( userCacheRecord );
             return userCacheRecord;
         }
@@ -86,24 +76,17 @@ public class UserCacheService implements PwmService
         }
 
         {
-            final boolean finalPreExisting = preExisting;
-            LOGGER.trace( () -> "updateCache: " + ( finalPreExisting ? "updated existing" : "created new" ) + " user cache for "
+            LOGGER.trace( () -> "updateCache: read user cache for "
                     + userInfo.getUserIdentity() + " user key " + storageKey.getKey() );
         }
         return null;
     }
 
-    public UserCacheRecord readStorageKey( final StorageKey storageKey ) throws LocalDBException
+    UserCacheRecord readStorageKey( final StorageKey storageKey ) throws LocalDBException
     {
         return cacheStore.read( storageKey );
     }
 
-    public boolean removeStorageKey( final StorageKey storageKey )
-            throws LocalDBException
-    {
-        return cacheStore.remove( storageKey );
-    }
-
     public void store( final UserCacheRecord userCacheRecord )
             throws LocalDBException, PwmUnrecoverableException
     {
@@ -208,14 +191,14 @@ public class UserCacheService implements PwmService
             return key;
         }
 
-        public static StorageKey fromUserInfo( final UserInfo userInfo, final PwmApplication pwmApplication )
+        static StorageKey fromUserInfo( final UserInfo userInfo, final PwmApplication pwmApplication )
                 throws PwmUnrecoverableException
         {
             final String userGUID = userInfo.getUserGuid();
             return fromUserGUID( userGUID, pwmApplication );
         }
 
-        public static StorageKey fromUserIdentity( final PwmApplication pwmApplication, final UserIdentity userIdentity )
+        static StorageKey fromUserIdentity( final PwmApplication pwmApplication, final UserIdentity userIdentity )
                 throws ChaiUnavailableException, PwmUnrecoverableException
         {
             final String userGUID = LdapOperationsHelper.readLdapGuidValue( pwmApplication, null, userIdentity, true );

+ 18 - 3
server/src/main/java/password/pwm/svc/sessiontrack/SessionTrackService.java

@@ -42,6 +42,7 @@ import password.pwm.util.i18n.LocaleHelper;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.logging.PwmLogger;
+import password.pwm.util.secure.PwmRandom;
 
 import java.io.IOException;
 import java.io.OutputStream;
@@ -61,7 +62,7 @@ public class SessionTrackService implements PwmService
 {
     private static final PwmLogger LOGGER = PwmLogger.forClass( SessionTrackService.class );
 
-    private final transient Map<PwmSession, Boolean> pwmSessions = new ConcurrentHashMap<>();
+    private final Map<PwmSession, String> pwmSessions = new ConcurrentHashMap<>();
 
     private final Cache<UserIdentity, Object> recentLoginCache = Caffeine.newBuilder()
             .maximumSize( 10 )
@@ -108,7 +109,7 @@ public class SessionTrackService implements PwmService
 
     public void addSessionData( final PwmSession pwmSession )
     {
-        pwmSessions.put( pwmSession, Boolean.FALSE );
+        pwmSessions.put( pwmSession, pwmSession.getSessionStateBean().getSessionID() );
     }
 
     public void removeSessionData( final PwmSession pwmSession )
@@ -263,7 +264,7 @@ public class SessionTrackService implements PwmService
         sessionStateInfoBean.setSrcAddress( loopSsBean.getSrcAddress() );
         sessionStateInfoBean.setSrcHost( loopSsBean.getSrcHostname() );
         sessionStateInfoBean.setLastUrl( loopSsBean.getLastRequestURL() );
-        sessionStateInfoBean.setIntruderAttempts( loopSsBean.getIntruderAttempts() );
+        sessionStateInfoBean.setIntruderAttempts( loopSsBean.getIntruderAttempts().get() );
 
         if ( loopSession.isAuthenticated() )
         {
@@ -306,5 +307,19 @@ public class SessionTrackService implements PwmService
         return Collections.unmodifiableList( new ArrayList<>( recentLoginCache.asMap().keySet() ) );
     }
 
+    public String generateNewSessionID()
+    {
+        final PwmRandom pwmRandom = pwmApplication.getSecureService().pwmRandom();
 
+        for ( int safetyCounter = 0; safetyCounter < 1000; safetyCounter++ )
+        {
+            final String newValue = pwmRandom.alphaNumericString( 5 );
+            if ( !pwmSessions.containsValue( newValue ) )
+            {
+                return newValue;
+            }
+        }
+
+        throw new IllegalStateException( "unable to generate unique sessionID value" );
+    }
 }

+ 18 - 23
server/src/main/java/password/pwm/svc/stats/StatisticsManager.java

@@ -30,7 +30,7 @@ import password.pwm.error.PwmException;
 import password.pwm.health.HealthRecord;
 import password.pwm.http.PwmRequest;
 import password.pwm.svc.PwmService;
-import password.pwm.util.AlertHandler;
+import password.pwm.util.PwmScheduler;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.JsonUtil;
 import password.pwm.util.java.TimeDuration;
@@ -329,11 +329,10 @@ public class StatisticsManager implements PwmService
         localDB.put( LocalDB.DB.PWM_STATS, DB_KEY_INITIAL_DAILY_KEY, initialDailyKey.toString() );
 
         {
-            // setup a timer to roll over at 0 Zula and one to write current stats every 10 seconds
-            executorService = JavaHelper.makeBackgroundExecutor( pwmApplication, this.getClass() );
-            pwmApplication.scheduleFixedRateJob( new FlushTask(), executorService, DB_WRITE_FREQUENCY, DB_WRITE_FREQUENCY );
-            final TimeDuration delayTillNextZulu = TimeDuration.fromCurrent( JavaHelper.nextZuluZeroTime() );
-            pwmApplication.scheduleFixedRateJob( new NightlyTask(), executorService, delayTillNextZulu, TimeDuration.DAY );
+            // setup a timer to roll over at 0 Zulu and one to write current stats regularly
+            executorService = PwmScheduler.makeBackgroundExecutor( pwmApplication, this.getClass() );
+            pwmApplication.getPwmScheduler().scheduleFixedRateJob( new FlushTask(), executorService, DB_WRITE_FREQUENCY, DB_WRITE_FREQUENCY );
+            pwmApplication.getPwmScheduler().scheduleDailyZuluZeroStartJob( new NightlyTask(), executorService, TimeDuration.ZERO );
         }
 
         status = STATUS.OPEN;
@@ -367,25 +366,21 @@ public class StatisticsManager implements PwmService
 
     }
 
-    private void resetDailyStats( )
+    public Map<String, String> dailyStatisticsAsLabelValueMap()
     {
-        try
-        {
-            final Map<String, String> emailValues = new LinkedHashMap<>();
-            for ( final Statistic statistic : Statistic.values() )
-            {
-                final String key = statistic.getLabel( PwmConstants.DEFAULT_LOCALE );
-                final String value = statsDaily.getStatistic( statistic );
-                emailValues.put( key, value );
-            }
-
-            AlertHandler.alertDailyStats( pwmApplication, emailValues );
-        }
-        catch ( Exception e )
+        final Map<String, String> emailValues = new LinkedHashMap<>();
+        for ( final Statistic statistic : Statistic.values() )
         {
-            LOGGER.error( "error while generating daily alert statistics: " + e.getMessage() );
+            final String key = statistic.getLabel( PwmConstants.DEFAULT_LOCALE );
+            final String value = statsDaily.getStatistic( statistic );
+            emailValues.put( key, value );
         }
 
+        return Collections.unmodifiableMap( emailValues );
+    }
+
+    private void resetDailyStats( )
+    {
         currentDailyKey = new DailyKey( new Date() );
         statsDaily = new StatisticsBundle();
         LOGGER.debug( () -> "reset daily statistics" );
@@ -452,7 +447,7 @@ public class StatisticsManager implements PwmService
 
         public DailyKey( final String value )
         {
-            final String strippedValue = value.substring( DB_KEY_PREFIX_DAILY.length(), value.length() );
+            final String strippedValue = value.substring( DB_KEY_PREFIX_DAILY.length() );
             final String[] splitValue = strippedValue.split( "_" );
             year = Integer.parseInt( splitValue[ 0 ] );
             day = Integer.parseInt( splitValue[ 1 ] );
@@ -465,7 +460,7 @@ public class StatisticsManager implements PwmService
         @Override
         public String toString( )
         {
-            return DB_KEY_PREFIX_DAILY + String.valueOf( year ) + "_" + String.valueOf( day );
+            return DB_KEY_PREFIX_DAILY + year + "_" + day;
         }
 
         public DailyKey previous( )

+ 3 - 2
server/src/main/java/password/pwm/svc/telemetry/TelemetryService.java

@@ -45,6 +45,7 @@ import password.pwm.svc.PwmService;
 import password.pwm.svc.stats.Statistic;
 import password.pwm.svc.stats.StatisticsBundle;
 import password.pwm.svc.stats.StatisticsManager;
+import password.pwm.util.PwmScheduler;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.JsonUtil;
 import password.pwm.util.java.StringUtil;
@@ -143,7 +144,7 @@ public class TelemetryService implements PwmService
             LOGGER.trace( SessionLabel.TELEMETRY_SESSION_LABEL, () -> "last publish time was " + JavaHelper.toIsoDate( lastPublishTime ) );
         }
 
-        executorService = JavaHelper.makeBackgroundExecutor( pwmApplication, TelemetryService.class );
+        executorService = PwmScheduler.makeBackgroundExecutor( pwmApplication, TelemetryService.class );
 
         scheduleNextJob();
 
@@ -214,7 +215,7 @@ public class TelemetryService implements PwmService
     private void scheduleNextJob( )
     {
         final TimeDuration durationUntilNextPublish = durationUntilNextPublish();
-        pwmApplication.scheduleFutureJob( new PublishJob(), executorService, durationUntilNextPublish );
+        pwmApplication.getPwmScheduler().scheduleFutureJob( new PublishJob(), executorService, durationUntilNextPublish );
         LOGGER.trace( SessionLabel.TELEMETRY_SESSION_LABEL, () -> "next publish time: " + durationUntilNextPublish().asCompactString() );
     }
 

+ 14 - 11
server/src/main/java/password/pwm/svc/token/TokenService.java

@@ -57,6 +57,7 @@ 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.PwmScheduler;
 import password.pwm.util.db.DatabaseDataStore;
 import password.pwm.util.db.DatabaseTable;
 import password.pwm.util.java.JavaHelper;
@@ -194,12 +195,12 @@ public class TokenService implements PwmService
 
         verifyPwModifyTime = Boolean.parseBoolean( configuration.readAppProperty( AppProperty.TOKEN_VERIFY_PW_MODIFY_TIME ) );
 
-        executorService = JavaHelper.makeBackgroundExecutor( pwmApplication, this.getClass() );
+        executorService = PwmScheduler.makeBackgroundExecutor( pwmApplication, this.getClass() );
 
         {
             final int cleanerFrequencySeconds = Integer.parseInt( configuration.readAppProperty( AppProperty.TOKEN_CLEANER_INTERVAL_SECONDS ) );
             final TimeDuration cleanerFrequency = TimeDuration.of( cleanerFrequencySeconds, TimeDuration.Unit.SECONDS );
-            pwmApplication.scheduleFixedRateJob( new CleanerTask(), executorService, TimeDuration.MINUTE, cleanerFrequency );
+            pwmApplication.getPwmScheduler().scheduleFixedRateJob( new CleanerTask(), executorService, TimeDuration.MINUTE, cleanerFrequency );
             LOGGER.trace( () -> "token cleanup will occur every " + cleanerFrequency.asCompactString() );
         }
 
@@ -634,7 +635,7 @@ public class TokenService implements PwmService
                 && tokenPayload.getUserIdentity() != null
                 && tokenPayload.getData() != null
                 && tokenPayload.getData().containsKey( PwmConstants.TOKEN_KEY_PWD_CHG_DATE )
-                )
+        )
         {
             try
             {
@@ -739,13 +740,15 @@ public class TokenService implements PwmService
             pwmApplication.getIntruderManager().mark( RecordType.TOKEN_DEST, toAddress, null );
 
             final EmailItemBean configuredEmailSetting = tokenSendInfo.getConfiguredEmailSetting();
-            pwmApplication.getEmailQueue().submitEmailImmediate( new EmailItemBean(
-                    toAddress,
-                    configuredEmailSetting.getFrom(),
-                    configuredEmailSetting.getSubject(),
-                    configuredEmailSetting.getBodyPlain().replace( "%TOKEN%", tokenSendInfo.getTokenKey() ),
-                    configuredEmailSetting.getBodyHtml().replace( "%TOKEN%", tokenSendInfo.getTokenKey() )
-            ), tokenSendInfo.getUserInfo(), tokenSendInfo.getMacroMachine() );
+            final EmailItemBean tokenizedEmail = configuredEmailSetting.applyBodyReplacement(
+                    "%TOKEN%",
+                    tokenSendInfo.getTokenKey() );
+
+            pwmApplication.getEmailQueue().submitEmailImmediate(
+                    tokenizedEmail,
+                    tokenSendInfo.getUserInfo(),
+                    tokenSendInfo.getMacroMachine() );
+
             LOGGER.debug( () -> "token email added to send queue for " + toAddress );
             return true;
         }
@@ -771,7 +774,7 @@ public class TokenService implements PwmService
             LOGGER.debug( () -> "token SMS added to send queue for " + smsNumber );
             return true;
         }
-        }
+    }
 
     static TimeDuration maxTokenAge( final Configuration configuration )
     {

+ 6 - 8
server/src/main/java/password/pwm/svc/wordlist/AbstractWordlist.java

@@ -35,6 +35,7 @@ import password.pwm.health.HealthRecord;
 import password.pwm.health.HealthStatus;
 import password.pwm.health.HealthTopic;
 import password.pwm.svc.PwmService;
+import password.pwm.util.PwmScheduler;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.TimeDuration;
 import password.pwm.util.localdb.LocalDBException;
@@ -93,7 +94,7 @@ abstract class AbstractWordlist implements Wordlist, PwmService
 
         this.wordklistBucket = new WordlistBucket( pwmApplication, wordlistConfiguration, type );
 
-        executorService = JavaHelper.makeBackgroundExecutor( pwmApplication, this.getClass() );
+        executorService = PwmScheduler.makeBackgroundExecutor( pwmApplication, this.getClass() );
 
         if ( pwmApplication.getLocalDB() != null )
         {
@@ -107,7 +108,7 @@ abstract class AbstractWordlist implements Wordlist, PwmService
             lastError = new ErrorInformation( PwmError.ERROR_SERVICE_NOT_AVAILABLE, errorMsg );
         }
 
-        pwmApplication.scheduleFixedRateJob( new InspectorJob(), executorService, TimeDuration.SECOND, wordlistConfiguration.getInspectorFrequency() );
+        pwmApplication.getPwmScheduler().scheduleFixedRateJob( new InspectorJob(), executorService, TimeDuration.SECOND, wordlistConfiguration.getInspectorFrequency() );
     }
 
     boolean containsWord( final String word ) throws PwmUnrecoverableException
@@ -168,13 +169,10 @@ abstract class AbstractWordlist implements Wordlist, PwmService
         {
             executorService.shutdown();
 
+            JavaHelper.closeAndWaitExecutor( executorService, closeWaitTime );
             if ( backgroundImportRunning.get() )
             {
-                JavaHelper.pause( closeWaitTime.asMillis(), 50, o -> !backgroundImportRunning.get() );
-                if ( backgroundImportRunning.get() )
-                {
-                    getLogger().warn( "background thread still running after waiting " + closeWaitTime.asCompactString() );
-                }
+                getLogger().warn( "background thread still running after waiting " + closeWaitTime.asCompactString() );
             }
         }
     }
@@ -326,7 +324,7 @@ abstract class AbstractWordlist implements Wordlist, PwmService
         inhibitBackgroundImportFlag.set( true );
         try
         {
-            JavaHelper.pause( 10_000, 100, o -> !backgroundImportRunning.get() );
+            TimeDuration.of( 10, TimeDuration.Unit.SECONDS ).pause( () -> !backgroundImportRunning.get() );
             if ( backgroundImportRunning.get() )
             {
                 throw PwmUnrecoverableException.newException( PwmError.ERROR_WORDLIST_IMPORT_ERROR, "unable to cancel background operation in progress" );

+ 4 - 7
server/src/main/java/password/pwm/svc/wordlist/SharedHistoryManager.java

@@ -32,8 +32,8 @@ import password.pwm.config.option.DataStorageMethod;
 import password.pwm.error.PwmException;
 import password.pwm.health.HealthRecord;
 import password.pwm.svc.PwmService;
+import password.pwm.util.PwmScheduler;
 import password.pwm.util.java.JavaHelper;
-import password.pwm.util.java.Sleeper;
 import password.pwm.util.java.TimeDuration;
 import password.pwm.util.localdb.LocalDB;
 import password.pwm.util.localdb.LocalDBException;
@@ -252,8 +252,8 @@ public class SharedHistoryManager implements PwmService
             final TimeDuration frequency = TimeDuration.of( frequencyMs, TimeDuration.Unit.MILLISECONDS );
 
             LOGGER.debug( () -> "scheduling cleaner task to run once every " + frequency.asCompactString() );
-            executorService = JavaHelper.makeBackgroundExecutor( pwmApplication, this.getClass() );
-            pwmApplication.scheduleFixedRateJob( new CleanerTask(), executorService, null, frequency );
+            executorService = PwmScheduler.makeBackgroundExecutor( pwmApplication, this.getClass() );
+            pwmApplication.getPwmScheduler().scheduleFixedRateJob( new CleanerTask(), executorService, null, frequency );
         }
     }
 
@@ -327,8 +327,6 @@ public class SharedHistoryManager implements PwmService
 
     private class CleanerTask extends TimerTask
     {
-        final Sleeper sleeper = new Sleeper( 10 );
-
         private CleanerTask( )
         {
         }
@@ -399,7 +397,6 @@ public class SharedHistoryManager implements PwmService
                     {
                         localOldestEntry = timeStamp < localOldestEntry ? timeStamp : localOldestEntry;
                     }
-                    sleeper.sleep();
                 }
             }
             finally
@@ -497,7 +494,7 @@ public class SharedHistoryManager implements PwmService
                 LOGGER.debug( () -> "starting up in background thread" );
                 init( pwmApplication, settings.maxAgeMs );
             }
-        }, JavaHelper.makeThreadName( pwmApplication, this.getClass() ) + " initializer" ).start();
+        }, PwmScheduler.makeThreadName( pwmApplication, this.getClass() ) + " initializer" ).start();
     }
 
     private static class Settings

+ 3 - 1
server/src/main/java/password/pwm/util/CaptchaUtility.java

@@ -153,6 +153,7 @@ public class CaptchaUtility
                 {
                     writeCaptchaSkipCookie( pwmRequest );
                     LOGGER.trace( pwmRequest, () -> "captcha verification passed" );
+                    StatisticsManager.incrementStat( pwmRequest, Statistic.CAPTCHA_SUCCESSES );
                     return true;
                 }
 
@@ -179,6 +180,7 @@ public class CaptchaUtility
         }
 
         LOGGER.trace( pwmRequest, () -> "captcha verification failed" );
+        StatisticsManager.incrementStat( pwmRequest, Statistic.CAPTCHA_FAILURES );
         return false;
     }
 
@@ -356,7 +358,7 @@ public class CaptchaUtility
             return true;
         }
 
-        final int currentSessionAttempts = pwmRequest.getPwmSession().getSessionStateBean().getIntruderAttempts();
+        final int currentSessionAttempts = pwmRequest.getPwmSession().getSessionStateBean().getIntruderAttempts().get();
         if ( currentSessionAttempts >= maxIntruderCount )
         {
             LOGGER.debug( pwmRequest, () -> "session intruder attempt count '" + currentSessionAttempts + "', therefore captcha will be required" );

+ 41 - 9
server/src/main/java/password/pwm/util/AlertHandler.java → server/src/main/java/password/pwm/util/DailySummaryJob.java

@@ -29,9 +29,9 @@ import password.pwm.PwmConstants;
 import password.pwm.bean.EmailItemBean;
 import password.pwm.config.PwmSetting;
 import password.pwm.error.PwmUnrecoverableException;
-import password.pwm.health.HealthMonitor;
 import password.pwm.health.HealthRecord;
 import password.pwm.i18n.Display;
+import password.pwm.svc.PwmService;
 import password.pwm.svc.report.ReportSummaryData;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.TimeDuration;
@@ -46,21 +46,53 @@ import java.util.Locale;
 import java.util.Map;
 import java.util.TreeMap;
 
-public abstract class AlertHandler
+public class DailySummaryJob implements Runnable
 {
-    private static final PwmLogger LOGGER = PwmLogger.forClass( AlertHandler.class );
+    private static final PwmLogger LOGGER = PwmLogger.forClass( DailySummaryJob.class );
 
+    private final PwmApplication pwmApplication;
 
-    public static void alertDailyStats(
-            final PwmApplication pwmApplication,
-            final Map<String, String> dailyStatistics
-    ) throws PwmUnrecoverableException
+    public DailySummaryJob( final PwmApplication pwmApplication )
+    {
+        this.pwmApplication = pwmApplication;
+    }
+
+    @Override
+    public void run()
+    {
+        try
+        {
+            alertDailyStats();
+        }
+        catch ( Exception e )
+        {
+            LOGGER.error( "error while generating daily alert statistics: " + e.getMessage() );
+        }
+
+
+    }
+
+    private void alertDailyStats(
+
+    )
+            throws PwmUnrecoverableException
     {
         if ( !checkIfEnabled( pwmApplication, PwmSetting.EVENTS_ALERT_DAILY_SUMMARY ) )
         {
+            LOGGER.debug( () -> "skipping daily summary alert job, setting "
+                    + PwmSetting.EVENTS_ALERT_DAILY_SUMMARY.toMenuLocationDebug( null, PwmConstants.DEFAULT_LOCALE )
+                    + " not configured" );
             return;
         }
 
+        if ( pwmApplication.getStatisticsManager().status() != PwmService.STATUS.OPEN )
+        {
+            LOGGER.debug( () -> "skipping daily summary alert job, statistics service is not open" );
+            return;
+        }
+
+        final Map<String, String> dailyStatistics = pwmApplication.getStatisticsManager().dailyStatisticsAsLabelValueMap();
+
         final Locale locale = PwmConstants.DEFAULT_LOCALE;
 
         for ( final String toAddress : pwmApplication.getConfig().readSettingAsStringArray( PwmSetting.AUDIT_EMAIL_SYSTEM_TO ) )
@@ -108,7 +140,7 @@ public abstract class AlertHandler
 
         {
             // health check data
-            final Collection<HealthRecord> healthRecords = pwmApplication.getHealthMonitor().getHealthRecords( HealthMonitor.CheckTimeliness.Immediate );
+            final Collection<HealthRecord> healthRecords = pwmApplication.getHealthMonitor().getHealthRecords();
             textBody.append( "-- Health Check Results --\n" );
             htmlBody.append( "<h2>Health Check Results</h2>" );
 
@@ -157,7 +189,7 @@ public abstract class AlertHandler
         textBody.append( "\n" );
         htmlBody.append( "<br/>" );
 
-        if ( pwmApplication.getConfig().readSettingAsBoolean( PwmSetting.REPORTING_ENABLE ) )
+        if ( pwmApplication.getConfig().readSettingAsBoolean( PwmSetting.REPORTING_ENABLE_DAILY_JOB ) )
         {
             final List<ReportSummaryData.PresentationRow> summaryData = pwmApplication.getReportService()
                     .getSummaryData().asPresentableCollection( pwmApplication.getConfig(), locale );

+ 302 - 0
server/src/main/java/password/pwm/util/PwmScheduler.java

@@ -0,0 +1,302 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2018 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.util;
+
+import org.jetbrains.annotations.NotNull;
+import password.pwm.PwmApplication;
+import password.pwm.PwmConstants;
+import password.pwm.util.java.StringUtil;
+import password.pwm.util.java.TimeDuration;
+import password.pwm.util.logging.PwmLogger;
+
+import java.time.Instant;
+import java.util.Calendar;
+import java.util.GregorianCalendar;
+import java.util.Objects;
+import java.util.TimeZone;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+public class PwmScheduler
+{
+    private static final PwmLogger LOGGER = PwmLogger.forClass( PwmScheduler.class );
+    private final ScheduledExecutorService applicationExecutorService;
+    private final String instanceID;
+
+    public PwmScheduler( final String instanceID )
+    {
+        this.instanceID = instanceID;
+        applicationExecutorService = makeSingleThreadExecutorService( instanceID, this.getClass() );
+    }
+
+    public void shutdown()
+    {
+        applicationExecutorService.shutdown();
+    }
+
+    public Future immediateExecuteInNewThread(
+            final Runnable runnable
+    )
+    {
+        Objects.requireNonNull( runnable );
+
+        final ExecutorService executor = makeSingleThreadExecutorService( instanceID, runnable.getClass() );
+
+        if ( applicationExecutorService.isShutdown() )
+        {
+            return null;
+        }
+
+        final WrappedRunner wrappedRunner = new WrappedRunner( runnable, executor );
+        applicationExecutorService.schedule( wrappedRunner, 0, TimeUnit.MILLISECONDS );
+        executor.shutdown();
+        return wrappedRunner.getFuture();
+    }
+
+    public void scheduleDailyZuluZeroStartJob(
+            final Runnable runnable,
+            final ExecutorService executorService,
+            final TimeDuration offset
+    )
+    {
+        final TimeDuration delayTillNextZulu = TimeDuration.fromCurrent( nextZuluZeroTime() );
+        final TimeDuration delayTillNextOFfiset = delayTillNextZulu.add( offset );
+        scheduleFixedRateJob( runnable, executorService, delayTillNextOFfiset, TimeDuration.DAY );
+    }
+
+    public Future scheduleFutureJob(
+            final Runnable runnable,
+            final ExecutorService executor,
+            final TimeDuration delay
+    )
+    {
+        Objects.requireNonNull( runnable );
+        Objects.requireNonNull( executor );
+        Objects.requireNonNull( delay );
+
+        if ( applicationExecutorService.isShutdown() )
+        {
+            return null;
+        }
+
+        final WrappedRunner wrappedRunner = new WrappedRunner( runnable, executor );
+        applicationExecutorService.schedule( wrappedRunner, delay.asMillis(), TimeUnit.MILLISECONDS );
+        return wrappedRunner.getFuture();
+    }
+
+    public void scheduleFixedRateJob(
+            final Runnable runnable,
+            final ExecutorService executor,
+            final TimeDuration initialDelay,
+            final TimeDuration frequency
+    )
+    {
+        if ( applicationExecutorService.isShutdown() )
+        {
+            return;
+        }
+
+        if ( initialDelay != null )
+        {
+            applicationExecutorService.schedule( new WrappedRunner( runnable, executor ), initialDelay.asMillis(), TimeUnit.MILLISECONDS );
+        }
+
+        final Runnable jobWithNextScheduler = () ->
+        {
+            new WrappedRunner( runnable, executor ).run();
+            scheduleFixedRateJob( runnable, executor, null, frequency );
+        };
+
+        applicationExecutorService.schedule(  jobWithNextScheduler, frequency.asMillis(), TimeUnit.MILLISECONDS );
+    }
+
+    public static ExecutorService makeBackgroundExecutor(
+            final PwmApplication pwmApplication,
+            final Class clazz
+    )
+    {
+        final ThreadPoolExecutor executor = new ThreadPoolExecutor(
+                1,
+                1,
+                10, TimeUnit.SECONDS,
+                new LinkedBlockingQueue<>(),
+                makePwmThreadFactory(
+                        makeThreadName( pwmApplication, clazz ) + "-",
+                        true
+                ) );
+        executor.allowCoreThreadTimeOut( true );
+        return executor;
+    }
+
+
+    public static String makeThreadName( final PwmApplication pwmApplication, final Class theClass )
+    {
+        String instanceName = "-";
+        if ( pwmApplication != null )
+        {
+            instanceName = pwmApplication.getInstanceID();
+        }
+
+        return makeThreadName( instanceName, theClass );
+    }
+
+    public static String makeThreadName( final String instanceID, final Class theClass )
+    {
+        String instanceName = "-";
+        if ( !StringUtil.isEmpty( instanceID ) )
+        {
+            instanceName = instanceID;
+        }
+
+        return PwmConstants.PWM_APP_NAME + "-" + instanceName + "-" + theClass.getSimpleName();
+    }
+
+    public static ThreadFactory makePwmThreadFactory( final String namePrefix, final boolean daemon )
+    {
+        return new ThreadFactory()
+        {
+            private final ThreadFactory realThreadFactory = Executors.defaultThreadFactory();
+
+            @Override
+            public Thread newThread( final Runnable runnable )
+            {
+                final Thread t = realThreadFactory.newThread( runnable );
+                t.setDaemon( daemon );
+                if ( namePrefix != null )
+                {
+                    final String newName = namePrefix + t.getName();
+                    t.setName( newName );
+                }
+                return t;
+            }
+        };
+    }
+
+    public static ScheduledExecutorService makeSingleThreadExecutorService(
+            final PwmApplication pwmApplication,
+            final Class theClass
+    )
+    {
+        return makeSingleThreadExecutorService( pwmApplication.getInstanceID(), theClass );
+    }
+
+    public static ScheduledExecutorService makeSingleThreadExecutorService(
+            final String instanceID,
+            final Class theClass
+    )
+    {
+        return Executors.newSingleThreadScheduledExecutor(
+                makePwmThreadFactory(
+                        makeThreadName( instanceID, theClass ) + "-",
+                        true
+                ) );
+    }
+
+    private static class WrappedRunner implements Runnable
+    {
+        private final Runnable runnable;
+        private final ExecutorService executor;
+        private volatile Future innerFuture;
+        private volatile boolean hasFailed;
+
+        WrappedRunner( final Runnable runnable, final ExecutorService executor )
+        {
+            this.runnable = runnable;
+            this.executor = executor;
+        }
+
+        Future getFuture()
+        {
+            return new Future()
+            {
+                @Override
+                public boolean cancel( final boolean mayInterruptIfRunning )
+                {
+                    return false;
+                }
+
+                @Override
+                public boolean isCancelled()
+                {
+                    return hasFailed;
+                }
+
+                @Override
+                public boolean isDone()
+                {
+                    return hasFailed || innerFuture != null && innerFuture.isDone();
+                }
+
+                @Override
+                public Object get()
+                {
+                    return null;
+                }
+
+                @Override
+                public Object get( final long timeout, @NotNull final TimeUnit unit )
+                {
+                    return null;
+                }
+            };
+        }
+
+        @Override
+        public void run()
+        {
+            try
+            {
+                if ( !executor.isShutdown() )
+                {
+                    innerFuture = executor.submit( runnable );
+                }
+                else
+                {
+                    hasFailed = true;
+                    LOGGER.trace( () -> "skipping scheduled job " + runnable + " on shutdown executor + " + executor );
+                }
+            }
+            catch ( Throwable t )
+            {
+                LOGGER.error( "unexpected error running scheduled job: " + t.getMessage(), t );
+                hasFailed = true;
+            }
+        }
+    }
+
+    public static Instant nextZuluZeroTime( )
+    {
+        final Calendar nextZuluMidnight = GregorianCalendar.getInstance( TimeZone.getTimeZone( "Zulu" ) );
+        nextZuluMidnight.set( Calendar.HOUR_OF_DAY, 0 );
+        nextZuluMidnight.set( Calendar.MINUTE, 0 );
+        nextZuluMidnight.set( Calendar.SECOND, 0 );
+        nextZuluMidnight.add( Calendar.HOUR, 24 );
+        return nextZuluMidnight.getTime().toInstant();
+    }
+}

+ 74 - 0
server/src/main/java/password/pwm/util/ServletUtility.java

@@ -0,0 +1,74 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2018 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.util;
+
+import org.apache.commons.io.IOUtils;
+import password.pwm.PwmConstants;
+import password.pwm.error.ErrorInformation;
+import password.pwm.error.PwmError;
+import password.pwm.error.PwmUnrecoverableException;
+
+import javax.servlet.http.HttpServletRequest;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.io.StringWriter;
+
+public final class ServletUtility
+{
+    private ServletUtility()
+    {
+    }
+
+    public static String readRequestBodyAsString( final HttpServletRequest req, final int maxChars )
+            throws IOException, PwmUnrecoverableException
+    {
+        final StringWriter stringWriter = new StringWriter();
+        final Reader readerStream = new InputStreamReader(
+                req.getInputStream(),
+                PwmConstants.DEFAULT_CHARSET
+        );
+
+        try
+        {
+            IOUtils.copy( readerStream, stringWriter );
+        }
+        catch ( Exception e )
+        {
+            final String errorMsg = "error reading request body stream: " + e.getMessage();
+            throw new PwmUnrecoverableException( new ErrorInformation( PwmError.ERROR_INTERNAL, errorMsg ) );
+        }
+        finally
+        {
+            IOUtils.closeQuietly( readerStream );
+        }
+
+        final String stringValue = stringWriter.toString();
+        if ( stringValue.length() > maxChars )
+        {
+            final String msg = "input request body is to big, size=" + stringValue.length() + ", max=" + maxChars;
+            throw new PwmUnrecoverableException( new ErrorInformation( PwmError.ERROR_INTERNAL, msg ) );
+        }
+        return stringValue;
+    }
+}

+ 2 - 2
server/src/main/java/password/pwm/util/TransactionSizeCalculator.java

@@ -24,7 +24,6 @@ package password.pwm.util;
 
 import lombok.Builder;
 import lombok.Value;
-import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.TimeDuration;
 
 import java.util.Objects;
@@ -56,7 +55,8 @@ public class TransactionSizeCalculator
     @SuppressWarnings( "ResultOfMethodCallIgnored" )
     public void pause( )
     {
-        JavaHelper.pause( Math.min( lastDuration, settings.getDurationGoal().asMillis() * 2 ) );
+        final long pauseTimeMs = Math.min( lastDuration, settings.getDurationGoal().asMillis() * 2 );
+        TimeDuration.of( pauseTimeMs, TimeDuration.Unit.MILLISECONDS ).pause();
     }
 
     public void recordLastTransactionDuration( final TimeDuration duration )

+ 0 - 3
server/src/main/java/password/pwm/util/cli/commands/ExportAuditCommand.java

@@ -26,7 +26,6 @@ import password.pwm.PwmApplication;
 import password.pwm.PwmConstants;
 import password.pwm.svc.event.AuditService;
 import password.pwm.util.cli.CliParameters;
-import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.TimeDuration;
 
 import java.io.File;
@@ -43,7 +42,6 @@ public class ExportAuditCommand extends AbstractCliCommand
         final PwmApplication pwmApplication = cliEnvironment.getPwmApplication();
         final AuditService auditManager = new AuditService();
         auditManager.init( pwmApplication );
-        JavaHelper.pause( 1000 );
 
         final File outputFile = ( File ) cliEnvironment.getOptions().get( CliParameters.REQUIRED_NEW_OUTPUT_FILE.getName() );
 
@@ -53,7 +51,6 @@ public class ExportAuditCommand extends AbstractCliCommand
         try ( FileOutputStream fileOutputStream = new FileOutputStream( outputFile, true ) )
         {
             counter = auditManager.outputVaultToCsv( fileOutputStream, PwmConstants.DEFAULT_LOCALE, false );
-            fileOutputStream.close();
         }
         out( "completed writing " + counter + " rows of audit output in " + TimeDuration.fromCurrent( startTime ).asLongString() );
     }

+ 0 - 2
server/src/main/java/password/pwm/util/cli/commands/ExportResponsesCommand.java

@@ -31,7 +31,6 @@ import password.pwm.bean.UserIdentity;
 import password.pwm.ldap.search.SearchConfiguration;
 import password.pwm.ldap.search.UserSearchEngine;
 import password.pwm.util.cli.CliParameters;
-import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.JsonUtil;
 import password.pwm.util.java.TimeDuration;
 import password.pwm.ws.server.rest.RestChallengesServer;
@@ -53,7 +52,6 @@ public class ExportResponsesCommand extends AbstractCliCommand
         final PwmApplication pwmApplication = cliEnvironment.getPwmApplication();
 
         final File outputFile = ( File ) cliEnvironment.getOptions().get( CliParameters.REQUIRED_NEW_OUTPUT_FILE.getName() );
-        JavaHelper.pause( 2000 );
 
         final long startTime = System.currentTimeMillis();
         final UserSearchEngine userSearchEngine = pwmApplication.getUserSearchEngine();

+ 0 - 2
server/src/main/java/password/pwm/util/cli/commands/ExportStatsCommand.java

@@ -25,7 +25,6 @@ package password.pwm.util.cli.commands;
 import password.pwm.PwmApplication;
 import password.pwm.svc.stats.StatisticsManager;
 import password.pwm.util.cli.CliParameters;
-import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.TimeDuration;
 
 import java.io.File;
@@ -42,7 +41,6 @@ public class ExportStatsCommand extends AbstractCliCommand
     {
         final PwmApplication pwmApplication = cliEnvironment.getPwmApplication();
         final StatisticsManager statsManger = pwmApplication.getStatisticsManager();
-        JavaHelper.pause( 1000 );
 
         final File outputFile = ( File ) cliEnvironment.getOptions().get( CliParameters.REQUIRED_NEW_OUTPUT_FILE.getName() );
         final long startTime = System.currentTimeMillis();

+ 0 - 3
server/src/main/java/password/pwm/util/cli/commands/TokenInfoCommand.java

@@ -75,9 +75,6 @@ public class TokenInfoCommand extends AbstractCliCommand
                 out( "        value: " + value );
             }
         }
-
-        pwmApplication.shutdown();
-        JavaHelper.pause( 1000 );
     }
 
     @Override

+ 3 - 2
server/src/main/java/password/pwm/util/db/DatabaseService.java

@@ -39,6 +39,7 @@ import password.pwm.health.HealthTopic;
 import password.pwm.svc.PwmService;
 import password.pwm.svc.stats.EpsStatistic;
 import password.pwm.svc.stats.StatisticsManager;
+import password.pwm.util.PwmScheduler;
 import password.pwm.util.java.AtomicLoopIntIncrementer;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.JsonUtil;
@@ -110,12 +111,12 @@ public class DatabaseService implements PwmService
         this.pwmApplication = pwmApplication;
         init();
 
-        executorService = JavaHelper.makeBackgroundExecutor( pwmApplication, this.getClass() );
+        executorService = PwmScheduler.makeBackgroundExecutor( pwmApplication, this.getClass() );
 
         final TimeDuration watchdogFrequency = TimeDuration.of(
                 Integer.parseInt( pwmApplication.getConfig().readAppProperty( AppProperty.DB_CONNECTIONS_WATCHDOG_FREQUENCY_SECONDS ) ),
                 TimeDuration.Unit.SECONDS );
-        pwmApplication.scheduleFixedRateJob( new ConnectionMonitor(), executorService, watchdogFrequency, watchdogFrequency );
+        pwmApplication.getPwmScheduler().scheduleFixedRateJob( new ConnectionMonitor(), executorService, watchdogFrequency, watchdogFrequency );
     }
 
     private synchronized void init( )

+ 3 - 1
server/src/main/java/password/pwm/util/java/BlockingThreadPool.java

@@ -22,6 +22,8 @@
 
 package password.pwm.util.java;
 
+import password.pwm.util.PwmScheduler;
+
 import java.util.concurrent.Future;
 import java.util.concurrent.LinkedBlockingDeque;
 import java.util.concurrent.Semaphore;
@@ -35,7 +37,7 @@ public class BlockingThreadPool extends ThreadPoolExecutor
 
     public BlockingThreadPool( final int bound, final String name )
     {
-        super( bound, bound, 0, TimeUnit.MILLISECONDS, new LinkedBlockingDeque<>(), JavaHelper.makePwmThreadFactory( name, true ) );
+        super( bound, bound, 0, TimeUnit.MILLISECONDS, new LinkedBlockingDeque<>(), PwmScheduler.makePwmThreadFactory( name, true ) );
         semaphore = new Semaphore( bound );
     }
 

+ 0 - 129
server/src/main/java/password/pwm/util/java/JavaHelper.java

@@ -26,7 +26,6 @@ import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
 import jetbrains.exodus.core.dataStructures.hash.LinkedHashMap;
 import org.apache.commons.csv.CSVPrinter;
 import org.apache.commons.io.IOUtils;
-import password.pwm.PwmApplication;
 import password.pwm.PwmConstants;
 import password.pwm.config.Configuration;
 import password.pwm.config.PwmSetting;
@@ -54,13 +53,11 @@ import java.time.Instant;
 import java.time.temporal.ChronoUnit;
 import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.Calendar;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.Date;
 import java.util.Enumeration;
-import java.util.GregorianCalendar;
 import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
@@ -68,13 +65,7 @@ import java.util.Objects;
 import java.util.Optional;
 import java.util.Properties;
 import java.util.Set;
-import java.util.TimeZone;
 import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-import java.util.concurrent.LinkedBlockingQueue;
-import java.util.concurrent.ScheduledExecutorService;
-import java.util.concurrent.ThreadFactory;
-import java.util.concurrent.ThreadPoolExecutor;
 import java.util.concurrent.TimeUnit;
 import java.util.function.Predicate;
 import java.util.stream.Collectors;
@@ -148,54 +139,6 @@ public class JavaHelper
         return out.toString();
     }
 
-    /**
-     * Pause the calling thread the specified amount of time.
-     *
-     * @param sleepTimeMS - a time duration in milliseconds
-     * @return time actually spent sleeping
-     */
-    @CheckReturnValue( when = javax.annotation.meta.When.NEVER )
-    public static long pause( final long sleepTimeMS )
-    {
-        final long startTime = System.currentTimeMillis();
-        final long sliceTime = Math.max( 5, sleepTimeMS / 10 );
-        do
-        {
-            try
-            {
-                final long sleepTime = sleepTimeMS - ( System.currentTimeMillis() - startTime );
-                Thread.sleep( Math.min( sleepTime, sliceTime ) );
-            }
-            catch ( InterruptedException e )
-            {
-                // ignore
-            }
-        }
-        while ( ( System.currentTimeMillis() - startTime ) < sleepTimeMS );
-
-        return System.currentTimeMillis() - startTime;
-    }
-
-    public static long pause(
-            final long sleepTimeMS,
-            final long predicateCheckIntervalMS,
-            final Predicate predicate
-    )
-    {
-        final long startTime = System.currentTimeMillis();
-        final long pauseTime = Math.min( sleepTimeMS, predicateCheckIntervalMS );
-        while ( ( System.currentTimeMillis() - startTime ) < sleepTimeMS )
-        {
-            JavaHelper.pause( pauseTime );
-            if ( predicate.test( null ) )
-            {
-                break;
-            }
-        }
-
-        return System.currentTimeMillis() - startTime;
-    }
-
     public static String binaryArrayToHex( final byte[] buf )
     {
         final char[] hexChars = "0123456789ABCDEF".toCharArray();
@@ -208,16 +151,6 @@ public class JavaHelper
         return new String( chars );
     }
 
-    public static Instant nextZuluZeroTime( )
-    {
-        final Calendar nextZuluMidnight = GregorianCalendar.getInstance( TimeZone.getTimeZone( "Zulu" ) );
-        nextZuluMidnight.set( Calendar.HOUR_OF_DAY, 0 );
-        nextZuluMidnight.set( Calendar.MINUTE, 0 );
-        nextZuluMidnight.set( Calendar.SECOND, 0 );
-        nextZuluMidnight.add( Calendar.HOUR, 24 );
-        return nextZuluMidnight.getTime().toInstant();
-    }
-
     public static <E extends Enum<E>> List<E> readEnumListFromStringCollection( final Class<E> enumClass, final Collection<String> inputs )
     {
         final List<E> returnList = new ArrayList<E>();
@@ -420,38 +353,6 @@ public class JavaHelper
         return false;
     }
 
-    public static String makeThreadName( final PwmApplication pwmApplication, final Class theClass )
-    {
-        String instanceName = "-";
-        if ( pwmApplication != null && pwmApplication.getInstanceID() != null )
-        {
-            instanceName = pwmApplication.getInstanceID();
-        }
-
-        return PwmConstants.PWM_APP_NAME + "-" + instanceName + "-" + theClass.getSimpleName();
-    }
-
-    public static ThreadFactory makePwmThreadFactory( final String namePrefix, final boolean daemon )
-    {
-        return new ThreadFactory()
-        {
-            private final ThreadFactory realThreadFactory = Executors.defaultThreadFactory();
-
-            @Override
-            public Thread newThread( final Runnable r )
-            {
-                final Thread t = realThreadFactory.newThread( r );
-                t.setDaemon( daemon );
-                if ( namePrefix != null )
-                {
-                    final String newName = namePrefix + t.getName();
-                    t.setName( newName );
-                }
-                return t;
-            }
-        };
-    }
-
     public static Collection<Method> getAllMethodsForClass( final Class clazz )
     {
         final LinkedHashSet<Method> methods = new LinkedHashSet<>();
@@ -474,36 +375,6 @@ public class JavaHelper
         return new CSVPrinter( new OutputStreamWriter( outputStream, PwmConstants.DEFAULT_CHARSET ), PwmConstants.DEFAULT_CSV_FORMAT );
     }
 
-    public static ScheduledExecutorService makeSingleThreadExecutorService(
-            final PwmApplication pwmApplication,
-            final Class clazz
-    )
-    {
-        return Executors.newSingleThreadScheduledExecutor(
-                makePwmThreadFactory(
-                        JavaHelper.makeThreadName( pwmApplication, clazz ) + "-",
-                        true
-                ) );
-    }
-
-    public static ExecutorService makeBackgroundExecutor(
-            final PwmApplication pwmApplication,
-            final Class clazz
-    )
-    {
-        final ThreadPoolExecutor executor = new ThreadPoolExecutor(
-                1,
-                1,
-                10, TimeUnit.SECONDS,
-                new LinkedBlockingQueue<>(),
-                JavaHelper.makePwmThreadFactory(
-                        JavaHelper.makeThreadName( pwmApplication, clazz ) + "-",
-                        true
-                ) );
-        executor.allowCoreThreadTimeOut( true );
-        return executor;
-    }
-
     /**
      * Copy of {@link ThreadInfo#toString()} but with the MAX_FRAMES changed from 8 to 256.
      * @param threadInfo thread information

+ 0 - 85
server/src/main/java/password/pwm/util/java/Sleeper.java

@@ -1,85 +0,0 @@
-/*
- * Password Management Servlets (PWM)
- * http://www.pwm-project.org
- *
- * Copyright (c) 2006-2009 Novell, Inc.
- * Copyright (c) 2009-2018 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.util.java;
-
-/**
- * Sleep for a percentage of time.  The percentage is determined by the
- * loadFactor, which should be a value from 0-100.  The loadFactor is applied as
- * a percentage of total time that should be spent sleeping.
- *
- * @author Jason D. Rivard
- */
-public class Sleeper
-{
-
-    private static final long MAX_SLEEP_TIME = 500;
-    private static final long STANDARD_SLEEP_TIME = 20;
-
-    private final boolean doSleep;
-    private final int loadFactor;
-
-    private long startTime = System.currentTimeMillis();
-    private long sleepTime = 0;
-
-    public Sleeper( final int loadFactor )
-    {
-        this.loadFactor = loadFactor >= 0 ? loadFactor : 0;
-
-        doSleep = loadFactor > 0;
-    }
-
-    public int getLoadFactor( )
-    {
-        return loadFactor;
-    }
-
-    public void reset( )
-    {
-        startTime = System.currentTimeMillis();
-        sleepTime = 0;
-    }
-
-    public void sleep( )
-    {
-        if ( !doSleep )
-        {
-            return;
-        }
-
-        final long totalRunTime = System.currentTimeMillis() - startTime;
-        final float factor = loadFactor / 100f;
-        final long desiredTotalSleepTime = ( long ) ( totalRunTime * factor );
-
-        final long beginSleepTime = System.currentTimeMillis();
-        while ( sleepTime < desiredTotalSleepTime )
-        {
-            sleepTime += JavaHelper.pause( STANDARD_SLEEP_TIME );
-
-            final long currentSleepTime = System.currentTimeMillis() - beginSleepTime;
-            if ( currentSleepTime > MAX_SLEEP_TIME )
-            {
-                return;
-            }
-        }
-    }
-}

+ 53 - 28
server/src/main/java/password/pwm/util/java/TimeDuration.java

@@ -27,6 +27,8 @@ import lombok.Value;
 import password.pwm.PwmConstants;
 import password.pwm.i18n.Display;
 import password.pwm.util.i18n.LocaleHelper;
+import password.pwm.util.secure.PwmRandom;
+import password.pwm.util.secure.SecureService;
 
 import javax.annotation.CheckReturnValue;
 import javax.annotation.meta.When;
@@ -41,6 +43,7 @@ import java.util.ArrayList;
 import java.util.List;
 import java.util.Locale;
 import java.util.concurrent.TimeUnit;
+import java.util.function.BooleanSupplier;
 
 /**
  * <p>An immutable class representing a time period.  The internal value of the time period is
@@ -329,8 +332,8 @@ public class TimeDuration implements Comparable, Serializable
             segments.add( fractionalTimeDetail.days
                     + " "
                     + ( fractionalTimeDetail.days == 1
-                            ? LocaleHelper.getLocalizedMessage( locale, Display.Display_Day, null )
-                            : LocaleHelper.getLocalizedMessage( locale, Display.Display_Days, null ) )
+                    ? LocaleHelper.getLocalizedMessage( locale, Display.Display_Day, null )
+                    : LocaleHelper.getLocalizedMessage( locale, Display.Display_Days, null ) )
             );
         }
 
@@ -340,8 +343,8 @@ public class TimeDuration implements Comparable, Serializable
             segments.add( fractionalTimeDetail.hours
                     + " "
                     + ( fractionalTimeDetail.hours == 1
-                            ? LocaleHelper.getLocalizedMessage( locale, Display.Display_Hour, null )
-                            : LocaleHelper.getLocalizedMessage( locale, Display.Display_Hours, null ) )
+                    ? LocaleHelper.getLocalizedMessage( locale, Display.Display_Hour, null )
+                    : LocaleHelper.getLocalizedMessage( locale, Display.Display_Hours, null ) )
             );
         }
 
@@ -351,8 +354,8 @@ public class TimeDuration implements Comparable, Serializable
             segments.add( fractionalTimeDetail.minutes
                     + " "
                     + ( fractionalTimeDetail.minutes == 1
-                            ? LocaleHelper.getLocalizedMessage( locale, Display.Display_Minute, null )
-                            : LocaleHelper.getLocalizedMessage( locale, Display.Display_Minutes, null ) )
+                    ? LocaleHelper.getLocalizedMessage( locale, Display.Display_Minute, null )
+                    : LocaleHelper.getLocalizedMessage( locale, Display.Display_Minutes, null ) )
             );
         }
 
@@ -430,48 +433,65 @@ public class TimeDuration implements Comparable, Serializable
         return "TimeDuration[" + this.asCompactString() + "]";
     }
 
+
     /**
      * Pause the calling thread the specified amount of time.
      *
-     * @param sleepTimeMS - a time duration in milliseconds
      * @return time actually spent sleeping
      */
-    public static TimeDuration pause( final long sleepTimeMS )
+    @CheckReturnValue( when = When.NEVER )
+    public TimeDuration pause( )
     {
-        if ( sleepTimeMS < 1 )
-        {
-            return TimeDuration.ZERO;
-        }
+        return pause( this, () -> false );
+    }
+
+    /**
+     * Pause the calling thread the specified amount of time.
+     *
+     * @return time actually spent sleeping
+     */
+    @CheckReturnValue( when = When.NEVER )
+    public TimeDuration jitterPause( final SecureService secureService, final float factor )
+    {
+        final PwmRandom pwmRandom = secureService.pwmRandom();
+        final long jitterMs = (long) ( this.ms * factor );
+        final long deviation = pwmRandom.nextBoolean() ? jitterMs + this.ms : jitterMs - this.ms;
+        return pause( TimeDuration.of( deviation, Unit.MILLISECONDS ), () -> false );
+    }
 
+    @CheckReturnValue( when = When.NEVER )
+    public TimeDuration pause(
+            final BooleanSupplier interruptBoolean
+    )
+    {
+        final long interruptMs = JavaHelper.rangeCheck( 5, 1000, this.asMillis() / 100 );
+        return pause( TimeDuration.of( interruptMs, Unit.MILLISECONDS ), interruptBoolean );
+    }
+
+    @CheckReturnValue( when = When.NEVER )
+    public TimeDuration pause(
+            final TimeDuration interruptCheckInterval,
+            final BooleanSupplier interruptBoolean
+    )
+    {
         final long startTime = System.currentTimeMillis();
-        do
+        final long pauseTime = JavaHelper.rangeCheck( this.asMillis(), this.asMillis(), interruptCheckInterval.asMillis()  );
+
+        while ( ( System.currentTimeMillis() - startTime ) < this.asMillis() && !interruptBoolean.getAsBoolean() )
         {
             try
             {
-                final long sleepTime = sleepTimeMS - ( System.currentTimeMillis() - startTime );
-                Thread.sleep( sleepTime > 0 ? sleepTime : 5 );
+                Thread.sleep( pauseTime );
             }
             catch ( InterruptedException e )
             {
-                //who cares
+                // ignore
             }
         }
-        while ( ( System.currentTimeMillis() - startTime ) < sleepTimeMS );
 
         return TimeDuration.fromCurrent( startTime );
     }
 
-    /**
-     * Pause the calling thread the specified amount of time.
-     *
-     * @return time actually spent sleeping
-     */
-    @CheckReturnValue( when = When.NEVER )
-    public TimeDuration pause( )
-    {
-        return pause( this.as( Unit.MILLISECONDS ) );
-    }
-
     public Duration asDuration()
     {
         return Duration.of( this.ms, ChronoUnit.MILLIS );
@@ -506,5 +526,10 @@ public class TimeDuration implements Comparable, Serializable
             days = ( ( ( totalSeconds / 60 ) / 60 ) / 24 );
         }
     }
+
+    public boolean isZero()
+    {
+        return ms <= 0;
+    }
 }
 

+ 6 - 8
server/src/main/java/password/pwm/util/localdb/WorkQueueProcessor.java

@@ -30,6 +30,7 @@ import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmOperationalException;
 import password.pwm.svc.stats.EventRateMeter;
+import password.pwm.util.PwmScheduler;
 import password.pwm.util.java.AtomicLoopIntIncrementer;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.JsonUtil;
@@ -113,12 +114,12 @@ public final class WorkQueueProcessor<W extends Serializable>
 
         this.workerThread = new WorkerThread();
         workerThread.setDaemon( true );
-        workerThread.setName( JavaHelper.makeThreadName( pwmApplication, sourceClass ) + "-worker-" );
+        workerThread.setName( PwmScheduler.makeThreadName( pwmApplication, sourceClass ) + "-worker-" );
         workerThread.start();
 
         if ( settings.getPreThreads() > 0 )
         {
-            final ThreadFactory threadFactory = JavaHelper.makePwmThreadFactory( JavaHelper.makeThreadName( pwmApplication, sourceClass ), true );
+            final ThreadFactory threadFactory = PwmScheduler.makePwmThreadFactory( PwmScheduler.makeThreadName( pwmApplication, sourceClass ), true );
             executorService = new ThreadPoolExecutor(
                     1,
                     settings.getPreThreads(),
@@ -153,10 +154,7 @@ public final class WorkQueueProcessor<W extends Serializable>
 
         if ( localWorkerThread.isRunning() )
         {
-            JavaHelper.pause(
-                    settings.getMaxShutdownWaitTime().asMillis(),
-                    CLOSE_RETRY_CYCLE_INTERVAL.asMillis(),
-                    o -> !localWorkerThread.isRunning() );
+            settings.getMaxShutdownWaitTime().pause( CLOSE_RETRY_CYCLE_INTERVAL, () -> !localWorkerThread.isRunning() );
         }
 
         final TimeDuration timeDuration = TimeDuration.fromCurrent( startTime );
@@ -256,7 +254,7 @@ public final class WorkQueueProcessor<W extends Serializable>
                             + ", item=" + itemProcessor.convertToDebugString( itemWrapper.getWorkItem() );
                     throw new PwmOperationalException( new ErrorInformation( PwmError.ERROR_INTERNAL, errorMsg ) );
                 }
-                JavaHelper.pause( SUBMIT_QUEUE_FULL_RETRY_CYCLE_INTERVAL.asMillis() );
+                SUBMIT_QUEUE_FULL_RETRY_CYCLE_INTERVAL.pause();
             }
 
             eldestItem = itemWrapper.getDate();
@@ -357,7 +355,7 @@ public final class WorkQueueProcessor<W extends Serializable>
             // rest until not running for up to 3 seconds....
             if ( running.get() )
             {
-                JavaHelper.pause( 3000, 10, o -> !running.get() );
+                TimeDuration.of( 3, TimeDuration.Unit.SECONDS ).pause( TimeDuration.of( 10, TimeDuration.Unit.MILLISECONDS ), () -> !running.get() );
             }
         }
 

+ 6 - 5
server/src/main/java/password/pwm/util/logging/LocalDBLogger.java

@@ -28,6 +28,7 @@ import password.pwm.error.PwmException;
 import password.pwm.health.HealthMessage;
 import password.pwm.health.HealthRecord;
 import password.pwm.svc.PwmService;
+import password.pwm.util.PwmScheduler;
 import password.pwm.util.java.FileSystemUtility;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.PwmNumberFormat;
@@ -124,14 +125,14 @@ public class LocalDBLogger implements PwmService
         status = STATUS.OPEN;
 
         cleanerService = Executors.newSingleThreadScheduledExecutor(
-                JavaHelper.makePwmThreadFactory(
-                        JavaHelper.makeThreadName( pwmApplication, this.getClass() ) + "-cleaner-",
+                PwmScheduler.makePwmThreadFactory(
+                        PwmScheduler.makeThreadName( pwmApplication, this.getClass() ) + "-cleaner-",
                         true
                 ) );
 
         writerService = Executors.newSingleThreadScheduledExecutor(
-                JavaHelper.makePwmThreadFactory(
-                        JavaHelper.makeThreadName( pwmApplication, this.getClass() ) + "-writer-",
+                PwmScheduler.makePwmThreadFactory(
+                        PwmScheduler.makeThreadName( pwmApplication, this.getClass() ) + "-writer-",
                         true
                 ) );
 
@@ -388,7 +389,7 @@ public class LocalDBLogger implements PwmService
                         LOGGER.warn( "discarded event after waiting max buffer wait time of " + settings.getMaxBufferWaitTime().asCompactString() );
                         return;
                     }
-                    JavaHelper.pause( 100 );
+                    TimeDuration.of( 100, TimeDuration.Unit.MILLISECONDS ).pause();
                 }
             }
         }

+ 6 - 10
server/src/main/java/password/pwm/util/operations/PasswordUtility.java

@@ -184,7 +184,7 @@ public class PasswordUtility
             final String toAddress,
             final Locale userLocale
     )
-            throws PwmOperationalException, PwmUnrecoverableException
+            throws PwmUnrecoverableException
     {
         final Configuration config = pwmApplication.getConfig();
         final EmailItemBean configuredEmailSetting = config.readSettingAsEmail( PwmSetting.EMAIL_SENDPASSWORD, userLocale );
@@ -195,18 +195,14 @@ public class PasswordUtility
             return new ErrorInformation( PwmError.ERROR_INTERNAL, errorMsg );
         }
 
-        final EmailItemBean emailItemBean = new EmailItemBean(
-                configuredEmailSetting.getTo(),
-                configuredEmailSetting.getFrom(),
-                configuredEmailSetting.getSubject(),
-                configuredEmailSetting.getBodyPlain().replace( "%TOKEN%", newPassword.getStringValue() ),
-                configuredEmailSetting.getBodyHtml().replace( "%TOKEN%", newPassword.getStringValue() )
-        );
+        final EmailItemBean emailItemBean = configuredEmailSetting.applyBodyReplacement(
+                "%TOKEN%",
+                newPassword.getStringValue() );
+
         pwmApplication.getEmailQueue().submitEmail(
                 emailItemBean,
                 userInfo,
-                macroMachine
-        );
+                macroMachine );
 
 
         LOGGER.debug( () -> "new password email to " + userInfo.getUserIdentity() + " added to send queue for " + toAddress );

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

@@ -77,7 +77,6 @@ import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.ldap.LdapOperationsHelper;
 import password.pwm.util.PasswordData;
 import password.pwm.util.java.AtomicLoopIntIncrementer;
-import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.JsonUtil;
 import password.pwm.util.java.TimeDuration;
 import password.pwm.util.logging.PwmLogger;
@@ -777,7 +776,7 @@ public class NMASCrOperator implements CrOperator
                 while ( !done && TimeDuration.fromCurrent( startTime ).isShorterThan( maxThreadIdleTime ) )
                 {
                     LOGGER.trace( () -> "attempt to read return code, but isNmasDone=false, will await completion" );
-                    JavaHelper.pause( 10 );
+                    TimeDuration.of( 10, TimeDuration.Unit.SECONDS ).pause();
                     if ( completeOnUnsupportedFailure )
                     {
                         done = unsupportedCallbackHasOccurred || this.isNmasDone();

+ 3 - 4
server/src/main/java/password/pwm/ws/client/rest/form/FormDataRequestBean.java

@@ -23,19 +23,18 @@
 package password.pwm.ws.client.rest.form;
 
 import lombok.Builder;
-import lombok.Getter;
+import lombok.Value;
 import password.pwm.config.value.data.FormConfiguration;
 
 import java.io.Serializable;
 import java.util.List;
 import java.util.Map;
 
-@Getter
+@Value
 @Builder
 public class FormDataRequestBean implements Serializable
 {
-
-    @Getter
+    @Value
     @Builder
     public static class FormInfo implements Serializable
     {

+ 2 - 3
server/src/main/java/password/pwm/ws/client/rest/form/FormDataResponseBean.java

@@ -23,12 +23,12 @@
 package password.pwm.ws.client.rest.form;
 
 import lombok.Builder;
-import lombok.Getter;
+import lombok.Value;
 
 import java.io.Serializable;
 import java.util.Map;
 
-@Getter
+@Value
 @Builder
 public class FormDataResponseBean implements Serializable
 {
@@ -36,5 +36,4 @@ public class FormDataResponseBean implements Serializable
     private String errorMessage;
     private String errorDetail;
     private Map<String, String> formValues;
-
 }

+ 5 - 22
server/src/main/java/password/pwm/ws/server/rest/RestHealthServer.java

@@ -70,12 +70,9 @@ public class RestHealthServer extends RestServlet
     private RestResultBean doPwmHealthPlainGet( final RestRequest restRequest )
             throws PwmUnrecoverableException
     {
-        final boolean requestImmediateParam = restRequest.readParameterAsBoolean( PARAM_IMMEDIATE_REFRESH );
-
         try
         {
-            final HealthMonitor.CheckTimeliness timeliness = determineDataTimeliness( requestImmediateParam );
-            final String resultString = restRequest.getPwmApplication().getHealthMonitor().getMostSevereHealthStatus( timeliness ).toString() + "\n";
+            final String resultString = restRequest.getPwmApplication().getHealthMonitor().getMostSevereHealthStatus().toString() + "\n";
             StatisticsManager.incrementStat( restRequest.getPwmApplication(), Statistic.REST_HEALTH );
             return RestResultBean.withData( resultString );
         }
@@ -91,38 +88,24 @@ public class RestHealthServer extends RestServlet
     private RestResultBean doPwmHealthJsonGet( final RestRequest restRequest )
             throws PwmUnrecoverableException, IOException
     {
-        final boolean requestImmediateParam = restRequest.readParameterAsBoolean( PARAM_IMMEDIATE_REFRESH );
-
-        final HealthData jsonOutput = processGetHealthCheckData( restRequest.getPwmApplication(), restRequest.getLocale(), requestImmediateParam );
+        final HealthData jsonOutput = processGetHealthCheckData( restRequest.getPwmApplication(), restRequest.getLocale() );
         StatisticsManager.incrementStat( restRequest.getPwmApplication(), Statistic.REST_HEALTH );
         return RestResultBean.withData( jsonOutput );
     }
 
-    private static HealthMonitor.CheckTimeliness determineDataTimeliness(
-            final boolean refreshImmediate
-    )
-            throws PwmUnrecoverableException
-    {
-        return refreshImmediate
-                ? HealthMonitor.CheckTimeliness.Immediate
-                : HealthMonitor.CheckTimeliness.CurrentButNotAncient;
-    }
-
     public static HealthData processGetHealthCheckData(
             final PwmApplication pwmApplication,
-            final Locale locale,
-            final boolean refreshImmediate
+            final Locale locale
     )
             throws IOException, PwmUnrecoverableException
     {
         final HealthMonitor healthMonitor = pwmApplication.getHealthMonitor();
-        final HealthMonitor.CheckTimeliness timeliness = determineDataTimeliness( refreshImmediate );
-        final List<password.pwm.health.HealthRecord> healthRecords = new ArrayList<>( healthMonitor.getHealthRecords( timeliness ) );
+        final List<password.pwm.health.HealthRecord> healthRecords = new ArrayList<>( healthMonitor.getHealthRecords() );
         final List<HealthRecord> healthRecordBeans = HealthRecord.fromHealthRecords( healthRecords, locale,
                 pwmApplication.getConfig() );
         final HealthData healthData = new HealthData();
         healthData.timestamp = healthMonitor.getLastHealthCheckTime();
-        healthData.overall = healthMonitor.getMostSevereHealthStatus( timeliness ).toString();
+        healthData.overall = healthMonitor.getMostSevereHealthStatus().toString();
         healthData.records = healthRecordBeans;
         return healthData;
     }

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

@@ -137,6 +137,7 @@ http.header.sendXXSSProtection=true
 http.header.noise.length=128
 http.header.csp.nonce.bytes=24
 http.cookie.default.secureFlag=auto
+http.cookie.httponly.enable=true
 http.cookie.theme.name=theme
 http.cookie.theme.age=604800
 http.cookie.locale.name=locale
@@ -148,6 +149,7 @@ http.cookie.captchaSkip.age=86400
 http.cookie.login.name=SESSION
 http.cookie.nonce.name=ID
 http.cookie.nonce.length=32
+http.cookie.sameSite.value=Strict
 http.parameter.forward=forwardURL
 http.parameter.logout=logoutURL
 http.parameter.theme=theme
@@ -170,7 +172,6 @@ http.parameter.oauth.state=state
 http.parameter.oauth.grantType=grant_type
 http.download.buffer.size=102400
 http.session.recycleAtAuth=true
-http.session.validationKeyLength=32
 http.servlet.enablePostRedirectGet=true
 intruder.retentionTimeMS=86400000
 intruder.cleanupFrequencyMS=3603000
@@ -276,6 +277,7 @@ queue.syslog.maxAgeMs=2592000000
 queue.syslog.maxCount=100000
 reporting.ldap.searchTimeoutMs=1800000
 reporting.ldap.searchThreads=8
+reporting.maxReportAgeSeconds=864000
 recaptcha.clientJsUrl=//www.google.com/recaptcha/api.js
 recaptcha.clientIframeUrl=//www.google.com/recaptcha/api/noscript
 recaptcha.validateUrl=https://www.google.com/recaptcha/api/siteverify

+ 6 - 32
server/src/main/resources/password/pwm/config/PwmSetting.xml

@@ -711,12 +711,6 @@
             <property key="Maximum">65535</property>
         </properties>
     </setting>
-    <setting hidden="false" key="email.default.fromAddresses" level="1">
-        <flag>emailSyntax</flag>
-        <default>
-            <value>noreply@example.org</value>
-        </default>
-    </setting>
     <setting hidden="false" key="email.smtp.username" level="1">
         <default>
             <value />
@@ -1636,8 +1630,7 @@
     <setting hidden="false" key="security.cspHeader" level="2">
         <default>
             <!--<value><![CDATA[]]></value>-->
-            <value><![CDATA[default-src 'self'; object-src 'none'; img-src 'self' data:; style-src 'self' 'unsafe-inline'; script-src https://www.google.com/recaptcha/ https://www.gstatic.com/recaptcha/ 'self' 'unsafe-eval' 'unsafe-inline' 'nonce-%NONCE%' ; frame-src https://www.google.com/recaptcha/ https://www.gstatic.com/recaptcha/; report-uri /sspr/public/command/cspReport]]></value>
-            <!-- 'unsafe-inline' on script-src is included for backward compatibility of CSP Level1 browsers.  CSP2 and future ignore it when the nonce is specified -->
+            <value><![CDATA[default-src 'self'; object-src 'none'; img-src 'self' data:; style-src 'self'; script-src https://www.google.com/recaptcha/ https://www.gstatic.com/recaptcha/ 'self' 'nonce-%NONCE%' ; frame-src https://www.google.com/recaptcha/ https://www.gstatic.com/recaptcha/; report-uri /sspr/public/command/cspReport]]></value>
         </default>
     </setting>
     <setting hidden="false" key="email.adminAlert.toAddress" level="1">
@@ -2583,11 +2576,6 @@
         <ldapPermission actor="proxy" access="read"/>
         <default />
     </setting>
-    <setting hidden="false" key="challenge.token.enable" level="1" required="true">
-        <default>
-            <value>false</value>
-        </default>
-    </setting>
     <setting hidden="false" key="challenge.token.sendMethod" level="1" required="true">
         <default>
             <value><![CDATA[EMAILONLY]]></value>
@@ -2763,6 +2751,11 @@
             <value>true</value>
         </default>
     </setting>
+    <setting hidden="false" key="newUser.logoutAfterCreation" level="2">
+        <default>
+            <value>false</value>
+        </default>
+    </setting>
     <setting hidden="false" key="newUser.username.definition" level="1">
         <default>
             <value><![CDATA[@RandomChar:16:ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789@]]></value>
@@ -2893,13 +2886,6 @@
             <option value="checkbox">checkbox</option>
         </options>
     </setting>
-    <setting hidden="false" key="guest.creationUniqueAttributes" level="1">
-        <regex>^[a-zA-Z][a-zA-Z0-9-]*$</regex>
-        <default>
-            <value><![CDATA[cn]]></value>
-            <value><![CDATA[mail]]></value>
-        </default>
-    </setting>
     <setting hidden="false" key="guest.writeAttributes" level="1">
         <ldapPermission actor="guestManager" access="write"/>
         <default />
@@ -2938,11 +2924,6 @@
             <value>true</value>
         </default>
     </setting>
-    <setting hidden="false" key="activateUser.token.verification" level="1" required="true">
-        <default>
-            <value>false</value>
-        </default>
-    </setting>
     <setting hidden="false" key="display.activateUser.agreement" level="1">
         <flag>MacroSupport</flag>
         <default/>
@@ -3880,11 +3861,6 @@
             <value><![CDATA[{"type":"ldapQuery","ldapProfileID":"all","ldapQuery":"(objectClass=*)"}]]></value>
         </default>
     </setting>
-    <setting hidden="false" key="reporting.maxCacheAge" level="1">
-        <default>
-            <value>2592000</value>
-        </default>
-    </setting>
     <setting hidden="false" key="reporting.ldap.maxQuerySize" level="2">
         <default>
             <value>100000</value>
@@ -4173,8 +4149,6 @@
     <category hidden="false" key="EMAIL_SERVERS">
         <profile setting="email.profile.list"/>
     </category>
-    <category hidden="false" key="EMAIL_PROFILE_SETTING">
-    </category>
     <category hidden="false" key="EMAIL">
     </category>
     <category hidden="false" key="EMAIL_SETTINGS">

+ 255 - 125
server/src/main/resources/password/pwm/i18n/Display_nn.properties

@@ -27,141 +27,136 @@ Field_Location=Stad
 Field_NewPassword=Nytt passord
 Field_ConfirmPassword=Stadfest passord
 Field_Confirm_Prefix=Stadfest
-Field_User_Supplied_Question=Sp�rsm�l
-Field_Option_Select=Velg eit sp�rsm�l
-Field_CurrentPassword=Gjeldande passord
+Field_User_Supplied_Question=Sp\u00f8rsm\u00e5l
+Field_Option_Select=Velg eit sp\u00f8rsm\u00e5l
+Field_CurrentPassword=Noverande passord
 Field_Code=Kode
 Field_UserDN=Brukar DN
 Field_UserGUID=Brukar GUID
-Field_AccountEnabled=Brukarkontoen din er aktivert
+Field_AccountEnabled=Kontoen er aktivert
 Field_LastLoginTime=Tidspunkt for siste innlogging
-Field_LastLoginTimeDelta=Tidsintervall for siste innlogging
-Field_PasswordExpired=Passord utg�dd
-Field_PasswordPreExpired=Utg�dd midlertidig passord
-Field_PasswordViolatesPolicy=Bryt med passordpolicy
-Field_PasswordWithinWarningPeriod=Innafor advarselsperiode
+Field_LastLoginTimeDelta=Tidsskilnad for siste innlogging
+Field_PasswordExpired=Passordet er g\u00e5tt ut
+Field_PasswordPreExpired=Passordet har g\u00e5tt ut f\u00f8r tida
+Field_PasswordViolatesPolicy=Bryt med passordretningslinjer
+Field_PasswordWithinWarningPeriod=Innanfor varslingperiode
 Field_PasswordSetTime=Tidspunkt passord er sett
-Field_PasswordExpirationTime=Passordet er forelda
-Field_PasswordLocked=Passordet er sperra (Intruder Detect)
-Field_ResponsesStored=Lagra passordforslag
-Field_ResponsesNeeded=Passord m� oppdaterast
-Field_ResponsesTimestamp=Tidsstempel for lagra passordhint
-
-Value_True=sant
-Value_False=usant
-
+Field_PasswordExpirationTime=Tid n\u00e5r passordet g\u00e5r ut
+Field_PasswordLocked=Passordet er sperra (Inntrengingsfors\u00f8k)
+Field_ResponsesStored=Svara vart lagra
+Field_ResponsesNeeded=Det er naudsynt \u00e5 oppdatera svara
+Field_ResponsesTimestamp=Tidsstempel for lagra svar
+Value_True=Sant
+Value_False=Falskt
 Button_Login=Logg inn
-Button_Reset=Tm
-Button_ChangePassword=Sett passord
-Button_Hide=Skjul Passord
-Button_Show=Vis passord
-Button_Hide_Responses=Skjul passordhint
-Button_Show_Responses=Vis passordhint
-Button_SetResponses=Lagre passordhint
-Button_Search=Sk
-Button_RecoverPassword=Sjekk passordhint
-Button_Create=Lag
+Button_Reset=T\u00f8m
+Button_ChangePassword=Endre passord
+Button_Hide=Skjul
+Button_Show=Vis
+Button_Hide_Responses=Skjul svar
+Button_Show_Responses=Vis svar
+Button_SetResponses=Lagre svara
+Button_Search=S\u00f8k
+Button_RecoverPassword=Sjekk svara
+Button_Create=Opprett
 Button_Activate=Aktiver
-Button_Continue=Fortsett
-Button_CloseWindow=Lat att vindauget
+Button_Continue=Hald fram
+Button_CloseWindow=Lukk vindauge
 Button_Update=Oppdater
 Button_Verify=Stadfest
-Button_UnlockPassword=Opne passord
-Button_ConfirmResponses=Stadfest passordhint
+Button_UnlockPassword=L\u00e5s opp passord
+Button_ConfirmResponses=Stadfest svar p\u00e5 kontrollsp\u00f8rsm\u00e5l
 Button_Confirm=Stadfest
-Button_GoBack=Tilbake
-Button_ChangeResponses=Tilbake
-Button_CheckCode=Sjekk kode
+Button_GoBack=G\u00e5 tilbake
+Button_ChangeResponses=G\u00e5 tilbake
+Button_CheckCode=Kontroll\u00e9r kode
 Button_Logout=Logg ut
 Button_Agree=Eg godtek
 Button_Cancel=Avbryt
 Button_More=Meir
-
 Title_MainPage=Hovudmeny
-Title_Login=Ver venleg logg inn
-Title_ChangePassword=Sett passord
-Title_SetupResponses=Skriv inn passordhint
-Title_SetupRequiredResponses=Obligatoriske sp�rsm�l
-Title_SetupRandomResponses=Tilfeldige sp�rsm�l
-Title_ForgottenPassword=Glymt passord
-Title_RecoverPassword=Glymt passord
-Title_RecoverRequiredResponses=Obligatoriske sp�rsm�l
-Title_RecoverRandomResponses=Tilfeldige sp�rsm�l
+Title_Login=Ver venleg og logg inn
+Title_ChangePassword=Endre passord
+Title_SetupResponses=Angje kontrollsp\u00f8rsm\u00e5l
+Title_SetupRequiredResponses=Obligatoriske sp\u00f8rsm\u00e5l
+Title_SetupRandomResponses=Tilfeldige sp\u00f8rsm\u00e5l
+Title_ForgottenPassword=Gl\u00f8ymt passord
+Title_RecoverPassword=Gl\u00f8ymt passord
+Title_RecoverRequiredResponses=Obligatoriske sp\u00f8rsm\u00e5l
+Title_RecoverRandomResponses=Tilfeldige sp\u00f8rsm\u00e5l
 Title_NewUser=Registrering av ny brukar
-Title_ActivateUser=Aktivering av IT-brukarkonto
+Title_ActivateUser=Aktiver konto
 Title_PleaseWait=Ver venleg og vent
 Title_Error=Feil
-Title_Success=Vellukka
-Title_UserEventHistory=Passordhistorie
+Title_Success=Suksess
+Title_UserEventHistory=Passordhistorikk
 Title_Logout=Logg ut
 Title_UpdateProfile=Oppdater profil
-Title_UpdateProfileConfirm=Stadfest profilinnhald
-Title_PasswordStrength=Passordkvalitet
+Title_UpdateProfileConfirm=Stadfest profildata
+Title_PasswordStrength=Passordstyrke
 Title_Shortcuts=Snarvegar
-Title_PasswordWarning=Passordtvaring
-Title_Captcha=Stadfest
-Title_ConfirmResponses=Stadfest passordhint
-Title_UserInformation=Kontoinformasjon
+Title_PasswordWarning=Passord\u00e5tvaring
+Title_Captcha=Stadfesting
+Title_ConfirmResponses=Stadfest kontrollsp\u00f8rsm\u00e5l
+Title_UserInformation=Min konto
 Title_RandomPasswords=Tilfeldige passord
 Title_PasswordGuide=Passordguide
-Title_ForgottenUsername=Gl�ymt brukarnamn
-Title_GuestRegistration=Registrere gjestebrukar
-Title_GuestUpdate=Oppdatere gjestebrukar
-Title_Admin=Administrator
-Title_PeopleSearch=Persons�k
-Title_Helpdesk=Brukarst�tte
-                     
-Long_Title_Main_Menu=Hovudmeny for passordsj�lvbetening. Her kan du endre passordet ditt, lage nytt n�r du har gl�ymt ditt gamle eller gjere andre passordrelaterte aktivitetar.  
-Long_Title_ChangePassword=Endre noverande passord.
-Long_Title_ForgottenPassword=F� tilgang til brukarkontoen din om du har gl�ymt passordet.
-Long_Title_SetupResponses=Lag passordhint for gl�ymt passord. Desse hemmelege sp�rsm�la og svara vil gjere det mogleg � lage nytt passord om du gl�ymer det gamle.
-Long_Title_ActivateUser=Aktiver brukarkonto og lag eit passord.
+Title_ForgottenUsername=Gl\u00f8ymt brukarnamn
+Title_GuestRegistration=Gjesteregistrering
+Title_GuestUpdate=Oppdater gjestebrukar
+Title_Admin=Administrasjon
+Title_PeopleSearch=Persons\u00f8k
+Title_Helpdesk=Brukarst\u00f8tte
+Long_Title_Main_Menu=Hovudmeny for passordsj\u00f8lvbetening. Herfr\u00e5 kan du endra det noverande passordet ditt, lage nytt n\u00e5r du har gl\u00f8ymt ditt gamle eller gjere andre passordrelaterte aktivitetar.
+Long_Title_ChangePassword=Endre det noverande passordet ditt.
+Long_Title_ForgottenPassword=F\u00e5 tilgjenge til kontoen din igjen viss du har gl\u00f8ymt passordet.
+Long_Title_SetupResponses=Kontrollsp\u00f8rsm\u00e5l og svar l\u00e8t deg retta opp igjen eit gl\u00f8ymt passord.
+Long_Title_ActivateUser=Aktiver ein forh\u00e5ndskonfigurert konto og opprett eit nytt passord.
 Long_Title_NewUser=Opprett ein ny brukarkonto.
-Long_Title_UpdateProfile=Oppdater opplysningane p brukarprofilen din.
-Long_Title_UserEventHistory=Passordhistorikk. Sj� oversikt over n�r du har endra passordet ditt.
-Long_Title_Shortcuts=Personlige snarvegar.
-Long_Title_UserInformation=Informasjon om passordet ditt og passordpolicy (passordkrav).
-Long_Title_GuestRegistration=Registrer ny gjestebrukarkonto.
-Long_Title_GuestUpdate=Oppdatere ein gjestebrukarkonto.
+Long_Title_UpdateProfile=Oppdater opplysningane p\u00e5 brukarprofilen din.
+Long_Title_UserEventHistory=Passordhistorikk. Sj\u00e5 n\u00e5r du har endra passordet ditt tidlegare.
+Long_Title_Shortcuts=Personlege snarvegar.
+Long_Title_UserInformation=Informasjon om passordet ditt og passordretningslinjer.
+Long_Title_GuestRegistration=Registrer ein ny gjestebrukarkonto.
+Long_Title_GuestUpdate=Oppdater ein ny gjestebrukarkonto.
 Long_Title_Admin=Administrative funksjonar
-Long_Title_Logout=Logg ut fr� passordsj�lvbetjeningsportalen.
-Long_Title_ForgottenUsername=Finn gl�ymt brukarnamn.
-Long_Title_PeopleSearch=Finn kontaktinformasjon til dine kollegaer.
-Long_Title_Helpdesk=Brukarst�tteverkty
-
+Long_Title_Logout=Logg ut fr\u00e5 passordsj\u00f8lvbeteningsprogrammet.
+Long_Title_ForgottenUsername=Finn det gl\u00f8ymde brukarnamnet ditt.
+Long_Title_PeopleSearch=S\u00f8k etter kontaktinformasjon til kollegaene dine.
+Long_Title_Helpdesk=Brukarst\u00f8tteverkt\u00f8y
 Display_Login=
-Display_PasswordExpired=Passordet ditt er utg�dd. Du m� sette eit nytt passord no.
-Display_ChangePassword=Ver venleg � lage passordet ditt. <p/>Pass godt p� passordet ditt. N�r du har skrive inn nytt passord, klikkar du p� knappen �Endre passord�. <p/><p/><font color='red'>Du m� hugse passordet du lagar her.</font><p/><p/>Passordet er ditt personlege og skal ikkje delas med andre.</p>Passordet m� tilfredstille krava:
+Display_PasswordExpired=Passordet ditt er g\u00e5tt ut. Du m\u00e5 setja eit nytt passord no.
+Display_ChangePassword=Ver venleg og endra passordet ditt. Hald det nye passordet ditt l\u00f8ynleg. N\u00e5r du har skrive inn det nye passordet ditt, klikkar du p\u00e5 Endre passord-knappen. Viss du m\u00e5 skriva det ned, s\u00f8rg for \u00e5 ta vare p\u00e5 det p\u00e5 ein trygg stad. Det nye passordet ditt m\u00e5 oppfylla f\u00f8lgjande krav\:
 Display_CheckingPassword=Kontrollerer passord ....
 Display_CheckingResponses=Kontrollerer svar ....
 Display_CheckingData=Kontrollerer data ....
-Display_PleaseWait=Ver venleg og vent ....
-Display_SetupResponses=<p>Om du skulle gl�yme passordet, er det mogleg � lage eit nytt passord ved � svare p� sp�rsm�l som berre du veit svaret p�</p><p>Vel sp�rsm�l og svar som du kan nytt til � stadfeste identiteten din. Fordi svara p� desse sp�rsm�la kan nyttast til � f� tilgang til brukarkontoen din, er det viktig at dette er sp�rsm�l som berre du kan svare p�. </p>
-Display_ConfirmResponses=Pass p� at svara og sp�rsm�la er rette. Kontroller staving og teiknsetting. Dersom du gl�ymer passordet, m� du skrive inn dei eksakte svara p� sp�rsm�la nedanfor.
-Display_SetupRequiredResponses=Det er krav om rett svar p� desse sp�rsm�la. Dersom du skulle gl�yme passordet ditt, m� du gje korrekte svar p� alle desse sp�rsm�la for � kunne sette nytt passord.
-Display_SetupRandomResponses= Det er krav om rett svar p� desse sp�rsm�la. Om du skulle gl�yme passordet ditt, m� du gje korrekte svar p� minst% 1% av desse sp�rsm�la for � kunne sette nytt passord.
-Display_ForgottenPassword=Dersom du har gl�ymt passordet ditt, f�lg instruksjonane for � sette nytt passord. Dersom du ikkje har gjeve passordhint tidlegare, vil du ikkje kunne fullf�re denne prosessen.<p/>For � starte, ver venleg � skrive brukarnamnet ditt.
-Display_ForgottenUsername=Ver venleg � skrive inn fylgjande informasjon. Denne informasjonen vert nytta til � finne brukarnamnet ditt.
-Display_RecoverPassword=Ver venleg � svare p� fylgjande sp�rsm�l. Dersom du svarar rett p� alle desse sp�rsm�la, f�r du sette nytt passord.
-Display_RecoverRequiredResponses=Det er eit krav at du svarar p� desse sp�rsm�la.
-Display_RecoverRandomResponses=Du m� svare p� fylgjande sp�rsm�l for � halde fram.
-Display_RecoverPasswordChoices=Brukarkontoen er sperra p� grunn av for mange fors�k p� � logge inn med feil passord. Du kan g� vidare anten med � l�se opp brukarkontoen din, eller ved � endre passordet ditt.
-Display_RecoverChoiceUnlock=L�s opp kontoen din. Dersom du hugsar passordet ditt, kan du l�se opp kontoen din ved � velje dette alternativet. Passordet ditt vert ikkje endra.
-Display_RecoverChoiceReset=Endre passord. Dersom du har gl�ymt passordet ditt , klikk her. Ved � skrive inn nytt passord, vil brukarkontoen din ogs� bli l�st opp.
-Display_RecoverEnterCode=For � bekrefte identiteten din, har du f�tt tilsendt ei eingongskode p� SMS.<p/>Ver venleg og skriv inn den mottekne koda her.<p/><font color='red'>Koden skil mellom sm� og store bokstavar.</font>
-Display_NewUser=For � opprette en ny brukarkonto, m� du fylle ut fylgjande skjema. Passordet m� stette fylgjande krav:
-Display_ActivateUser=For � stadfeste identiteten din, fyll inn felta under. Informasjonen vert brukt til � finne og aktivere brukarkontoen din.<p/>PINkoden har du f�tt tilsendt til din private e-postadresse. Dersom du har endret PINkoden p� StudentWeb m� du kanskje bruke den PINkoden du endret til.<p/><p/><font color='red'>Fullf�r prosessen. Dersom du avbryt, vert ikkje brukarkontoen aktivert p� rette m�ten.</font>
-Display_PleaseWaitPassword=Passordet ditt vert sett. Denne prosessen kan ta litt tid. Vennligst vent p� stadfesting.
-Display_PleaseWaitNewUser=Din nye brukarkonto vert konfigurert. Denne prosessen kan ta litt tid. Vennligst vent p� stadfesting.
-Display_ErrorBody=Ein feil har oppst�tt. Ver venleg lat att nettlesaren din, og pr�v igjen p� eit seinare tidspunkt. Dersom denne feilen oppst�r fleire gongar, m� du kontakte s�rvistorget.
-Display_UserEventHistory=Denne sida viser passordhistorikken din. Berre handlingar utf�rt via denne sj�lvbeteningsportalen er vist her. Alle tidspunkt er oppgjevne i %1% tidssone.
-Display_GuestRegistration=For � opprette ein ny gjestekonto, ver venleg � skriv fylgjande informasjon.
-Display_GuestUpdate=For � oppdatere ein gjestekonto, kan du sjekke og eventuelt endre fylgjande informasjon.
-Display_Logout=<b>Du er no utlogga.</b><p/>Ver venleg lat att alle nettlesarvindauget f�r du pr�var � logge inn igjen.
-Display_PasswordWarn=<b>Passordet ditt g�r snart ut</b>. Ver venleg endre  passordet ditt snarast for � unng� problem med tilgong til alle IT-tenester.  <br/><br/>Passordet g�r ut om  %1%.
-Display_PasswordNoExpire=Passordet ditt har ikkje tidsavgrensa levetid
-Display_ExpirationDate=brukarkontoen sin utl�psdato (maks %1% dagar)
-Display_IdleTimeout=Tidsavbrudd for ingen aktivitet:
+Display_PleaseWait=Laster inn...
+Display_SetupResponses=<p>Viss du gl\u00f8ymer passordet ditt, kan du f\u00e5 tilgjenge til kontoen din ved \u00e5 svara p\u00e5 kontrollsp\u00f8rsm\u00e5la dine.</p><p>Ver venleg og vel kontrollsp\u00f8rsm\u00e5l og svar som kan brukast til \u00e5 stadfesta identiteten din dersom du gl\u00f8ymer passordet ditt. Fordi svara p\u00e5 desse kontrollsp\u00f8rsm\u00e5la kan brukast til \u00e5 f\u00e5 tilgjenge til kontoen din, m\u00e5 du s\u00f8rgja for \u00e5 oppgje svar som ikkje er enkle for andre \u00e5 gjetta eller oppdaga.</p>
+Display_ConfirmResponses=Pass p\u00e5 at svara og sp\u00f8rsm\u00e5la er riktige. Kontroller stavem\u00e5ten og teiknsetting. Om du gl\u00f8ymer passordet ditt, kan du f\u00e5 tilgjenge til kontoen din ved \u00e5 svara p\u00e5 desse kontrollsp\u00f8rsm\u00e5la.
+Display_SetupRequiredResponses=
+Display_SetupRandomResponses=
+Display_ForgottenPassword=Viss du har gl\u00f8ymt passordet ditt, f\u00f8lg instruksjonane for \u00e5 sette nytt passord.
+Display_ForgottenUsername=Ver venleg og skrive inn f\u00f8lgjande informasjon. Denne informasjonen vert nytta til \u00e5 finna det gl\u00f8ymde brukarnamnet ditt.
+Display_RecoverPassword=Ver venleg og svar p\u00e5 f\u00f8lgjande sp\u00f8rsm\u00e5l. Viss du svarar riktig p\u00e5 alle desse sp\u00f8rsm\u00e5la, vil du f\u00e5 lov til \u00e5 sette nytt passord.
+Display_RecoverRequiredResponses=Det er eit krav at du svarar p\u00e5 desse sp\u00f8rsm\u00e5la.
+Display_RecoverRandomResponses=Du m\u00e5 svara p\u00e5 f\u00f8lgjande sp\u00f8rsm\u00e5l for \u00e5 halda fram.
+Display_RecoverPasswordChoices=Kontoen din er vorten sperra p\u00e5 grunn av for mange innloggingsfors\u00f8k med feil passord. Du kan halda fram ved \u00e5 l\u00e5sa opp kontoen din igjen, eller ved \u00e5 endra passordet ditt.
+Display_RecoverChoiceUnlock=L\u00e5s opp kontoen din. Viss du hugsar passordet ditt, kan du l\u00e5sa opp kontoen din ved \u00e5 velja dette alternativet. Passordet ditt vert ikkje endra.
+Display_RecoverChoiceReset=Sett eit nytt passord. Viss du har gl\u00f8ymt passordet ditt og ynskjer \u00e5 setja eit nytt, klikk her. Kontoen din vil \u00f2g l\u00e5sast opp n\u00e5r du set eit nytt passord.
+Display_RecoverEnterCode=For \u00e5 stadfesta identiteten din, er det sendt ein tryggleikskode til deg. Vennlegast klikk lenkja i e-posten eller kopiar og lim inn tryggleikskoden her.
+Display_NewUser=For \u00e5 registrera ein ny konto, ver venleg og fyll ut f\u00f8lgjande skjema.
+Display_ActivateUser=Ver venleg og skriv inn f\u00f8lgjande informasjon for \u00e5 stadfesta identiteten din. Informasjonen vert brukt til \u00e5 finne og aktivere brukarkontoen din.<p/>S\u00f8rg for \u00e5 fullf\u00f8ra prosessen. Dersom du avbryt, vert ikkje brukarkontoen din aktivert p\u00e5 rette m\u00e5ten.
+Display_PleaseWaitPassword=Passordet ditt vert endra. Denne prosessen kan ta fleire minutt, s\u00e5 v\u00ear t\u00e5lmodig og vent p\u00e5 stadfesting.
+Display_PleaseWaitNewUser=Den nye kontoen din vert konfigurert. Denne prosessen kan ta fleire minutt, s\u00e5 v\u00ear t\u00e5lmodig og vent p\u00e5 stadfesting.
+Display_ErrorBody=Det har oppst\u00e5tt ein feil. Ver venleg og lukk nettlesaren din og pr\u00f8v igjen seinare. Viss denne feilen oppst\u00e5r fleire gonger, vennlegast kontakt brukarst\u00f8tta.
+Display_UserEventHistory=Denne sida viser passordhendingsloggen din. Berre handlingar utf\u00f8rt via denne applikasjonen er vist her. Alle tider som er oppf\u00f8rt, er i tidssona %1%.
+Display_GuestRegistration=For \u00e5 registrera ein ny gjestekonto, ver venleg og skriv inn f\u00f8lgjande informasjon.
+Display_GuestUpdate=For \u00e5 oppdatere ein gjestekonto, kan du sjekke og eventuelt endre f\u00f8lgjande informasjon.
+Display_Logout=Du er no logga ut.
+Display_PasswordWarn=<b>Passordet ditt g\u00e5r snart ut</b>. Ver venleg og endra passordet ditt snarast for \u00e5 unng\u00e5 problem med \u00e5 f\u00e5 tilgjenge til denne tenesta.<br/><br/>Passordet ditt g\u00e5r ut p\u00e5 %1%.
+Display_PasswordNoExpire=Passordet ditt g\u00e5r ikkje ut.
+Display_ExpirationDate=Utl\u00f8psdatoen til kontoen (maks. %1% dagar)
+Display_IdleTimeout=Tidsavbrot ved inaktivitet\:
 Display_Day=dag
 Display_Days=dagar
 Display_Hour=time
@@ -170,25 +165,160 @@ Display_Minute=minutt
 Display_Minutes=minutt
 Display_Second=sekund
 Display_Seconds=sekund
-Display_UpdateProfile=Ver venleg oppdater fylgjande inforamasjon:
-Display_UpdateProfileConfirm=Ver venleg � les  informasjonen du har lagt inn, og stadfest denne.
-Display_AutoGeneratedPassword=Auto-generer eit nytt passord
+Display_UpdateProfile=Ver venleg og oppdater f\u00f8lgjande informasjon\:
+Display_UpdateProfileConfirm=Ver venleg og sj\u00e5 gjennom f\u00f8lgjande informasjon du har angjeve og stadfest.
+Display_AutoGeneratedPassword=Automatisk generer eit nytt passord
 Display_ShowPasswordGuide=Passordguide
-Display_Shortcuts=Velg ei av fylgjande lenkjer for � fortsette.
-Display_CommunicationError=Kan ikkje kommunisere med tenaren. Hald fram n�r den er klar.
+Display_Shortcuts=Vel ein av f\u00f8lgjande lenkjer for \u00e5 halda fram.
+Display_CommunicationError=Kan ikkje kommunisere med tenaren. Hald fram n\u00e5r du er kl\u00e5r.
 Display_StrengthMeter=Passordstyrke
-Display_Captcha=Ver venleg � skriv heile stadfestingskoda nedanfor. Hensikta med denne koda er � beskytte brukarkontoen din mot misbruk.  
-Display_CapsLockIsOn= �CAPS LOCK� er p�
-Display_LeaveDirtyPasswordPage=Dersom du forlet denne sida, vil ikkje passordet ditt verte endra.
-Display_PasswordStrengthLow=Strength: <b>Svakt </b>
-Display_PasswordStrengthMedium=Strength: <b>Bra </b>
-Display_PasswordStrengthHigh=Strength: <b>Sterkt </b>
-Display_IdleWarningTitle=Tidsavbrot for ingen aktivitet
-Display_IdleWarningMessage=Nettlesarsesjonen din er i ferd med � utg�. Klikk kvar som helst p� sida for � halde fram.
-Display_PasswordGeneration=Fylgjande passord er tilfeldig generert for deg. Desse passorda er basert p� verkelege ord for � gjere det lettare for deg � hugse, men modifisert for � vere vanskeleg � gisse.
-Display_PasswordPrompt=Ver venleg og skriv inn eit nytt passord
-Display_ResponsesPrompt=Ver venleg og skriv dine svar
-Display_Helpdesk=Skriv inn dine s�kjedata for brukaren, og klikk p� s�k-knappen.
-Display_PeopleSearch=Ver venleg og skriv inn s�kjeorda nedanfor. Du kan s�kje etter ein person dersom du skriv inn namn, e-postadresse eller telefonnummer.
+Display_Captcha=Ver venleg og fullf\u00f8r stadfestingprosessen. Denne prosessen bidreg til \u00e5 verna kontoen din mot misbruk.
+Display_CapsLockIsOn=CAPS LOCK er p\u00e5
+Display_LeaveDirtyPasswordPage=Viss du forl\u00e8t denne sida, vil ikkje passordet ditt verte endra.
+Display_PasswordStrengthLow=Styrke\: <b>Svakt</b>
+Display_PasswordStrengthMedium=Styrke\: <b>Bra</b>
+Display_PasswordStrengthHigh=Styrke\: <b>Sterkt</b>
+Display_IdleWarningTitle=Tidsavbrot ved inaktivitet
+Display_IdleWarningMessage=Nettlesarsesjonen din er i ferd med \u00e5 utg\u00e5. Klikk kvar som helst p\u00e5 sida for \u00e5 halde fram.
+Display_PasswordGeneration=F\u00f8lgjande passord har vorte tilfeldig generert for deg. Desse passorda er basert p\u00e5 verkelege ord for \u00e5 gjera dei lettare \u00e5 hugsa, men har vorte endra for \u00e5 gjera dei vanskelege \u00e5 gjetta.
+Display_PasswordPrompt=Ver venleg og skriv inn det nye passordet ditt
+Display_ResponsesPrompt=Ver venleg og skriv inn svara dine p\u00e5 kontrollsp\u00f8rsm\u00e5la
+Display_Helpdesk=Ver venleg og skriv inn s\u00f8kjedata for brukaren.
+Display_PeopleSearch=Ver venleg og skriv inn s\u00f8kjeorda nedanfor. Du kan s\u00f8kja etter ein person basert p\u00e5 namn, e-postadresse eller telefonnummer.
 Display_FooterInfoText=
-Tooltip_PasswordStrength= Passordkvalitetsm�laren syner kor lett det er � gisse<br/> passordet ditt. Pr�v fylgjande for � gjere passordet ditt sikrare:<ul><li>Skriv eit lengre passord</li><li>Ikkje gjenta bokstavar eller tal</li><li>Bruk b�de store og sm� bokstavar</li><li>Legg til fleire tal</li><li>Legg til fleire spesialteikn</li></ul>
+Tooltip_PasswordStrength=Passordstyrkem\u00e5laren viser kor lett det er \u00e5 gjetta passordet ditt. Pr\u00f8v f\u00f8lgjande for \u00e5 gjera passordet ditt sterkare\: <ul><li>G\u00f8yr passordet lenger</li><li>Ikkje gjenta bokstavar eller tal</li><li> Bruk b\u00e5de store og sm\u00e5 bokstavar</li><li>Legg til fleire tal</li><li>Legg til fleire symbolteikn</li></ul>
+Button_Attributes=Brukardata
+Button_ClearResponses=Fjern svar
+Button_Delete=Slett
+Button_Email=E-post
+Button_Home=Heim
+Button_OrgChart=Organisasjonskart
+Button_OTP=Eingongspassord (OTP)
+Button_SMS=Tekstmelding (SMS)
+Button_TokenResend=Send kode p\u00e5 nytt
+Button_OK=OK
+Display_CaptchaHelp=Hjelp
+Display_CaptchaRefresh=Oppdater
+Display_SetupOtp_Android_Title=Android
+Display_SetupOtp_iPhone_Title=iPhone
+Display_SetupOtp_Other_Title=Andre
+Display_TokenDestination=Token Destinasjon
+Display_UsernameHeader=@User\:ID@
+Display_UsernameFooter=@User\:ID@
+Display_UpdateProfileEnterCode=For \u00e5 stadfesta e-postadressa di, er ein kode sendt til deg p\u00e5 <b>%1%</b>. Ver venleg og skriv inn koden her for \u00e5 halda fram.
+Display_UpdateProfileEnterCodeSMS=For \u00e5 stadfesta telefonnummeret ditt er det sendt ein kode til deg p\u00e5 <b>%1%</b>. Ver venleg og skriv inn koden her for \u00e5 halda fram.
+Field_AccountExpired=Kontoen er g\u00e5tt ut
+Field_AccountExpirationTime=Kontoen sin utl\u00f8pstid
+Field_DateTime=Dato/klokkeslett
+Field_OneTimePassword=Eingongspassord
+Field_Display=Skjerm
+Field_ForwardURL=Vidaresend URL
+Field_LdapProfile=LDAP-profil
+Field_LogoutURL=Logg ut URL
+Field_Method=Metode
+Field_NetworkAddress=Nettverksadresse
+Field_NetworkHost=Nettverksvert
+Field_Policy=Retningslinjer
+Field_Profile=Profil
+Field_UserEmail=E-post
+Field_UserSMS=SMS
+Field_OTP_Identifier=Identifikator
+Field_OTP_Secret=L\u00f8yndom
+Field_OTP_Type=Type
+Field_OTP_RecoveryCodes=Gjenopprettingskoder
+Field_OTP_Stored=OTP vart lagra
+Field_OTP_Timestamp=Tidspunkt OTP vart lagra
+Field_VerificationMethodPreviousAuth=F\u00f8rre autentisering
+Field_VerificationMethodToken=Stadfesting via sms/e-post
+Field_VerificationMethodOTP=Stadfesting via mobil
+Field_VerificationMethodChallengeResponses=L\u00f8ynlege sp\u00f8rsm\u00e5l og svar
+Field_VerificationMethodAttributes=Personopplysningar
+Field_VerificationMethodRemoteResponses=Eksterne svar
+Field_VerificationMethodNAAF=Avansert autentisering
+Field_VerificationMethodOAuth=Ekstern OAuth-autentisering
+Field_Placeholder_Answer=Svar
+Long_Title_DeleteAccount=Fjern kontoen og profilen din fr\u00e5 denne tenesta
+Long_Title_VerificationSend=F\u00f8r denne brukaren kan veljast, m\u00e5 identiteten til brukaren stadfestast. Ver venleg og vel ein stadfestingmetode.
+Title_AnsweredQuestions=Sp\u00f8rsm\u00e5l det er svara p\u00e5
+Title_LocaleSelect=Spr\u00e5kval
+Title_LogoutPublic=Tidsavbrot ved inaktivitet
+Title_OrgChart=Organisasjonskart
+Title_PasswordPolicy=Passordretningslinjer
+Title_RecentVerifications=Nylege stadfestingar
+Title_SecurityResponses=Svar p\u00e5 kontrollsp\u00f8rsm\u00e5l
+Title_SetupOtpSecret=Konfigurer autentisering via mobilapp
+Title_Status=Status
+Title_UserData=Mine data
+Title_ValidateCode=Stadfest kode
+Title_VerificationSend=Vel stadfestingmetode
+Title_DeleteAccount=Slett kontoen min
+Title_Management=Administrasjon
+Title_DirectReports=Underordna
+Title_Organization=Organisasjon
+Confirm_DeleteUser=Er du sikker p\u00e5 at du vil halda fram? Viss du held fram, vert den valde brukaren sletta permanent. Dette kan ikkje angrast.
+Confirm=Er du sikker p\u00e5 at du vil halda fram?
+Value_NotApplicable=n/a
+Value_Default=Standard
+Value_ProgressComplete=Fullf\u00f8rt
+Value_ProgressInProgress=I gang
+Placeholder_Search=S\u00f8k
+Button_Browse=Bla gjennom
+Button_Skip=Hopp over
+Button_Unlock=L\u00e5s opp
+Button_Verificiations=Verifikasjonar
+Display_CaptchaInputWords=Skriv inn teksta som vert vist ovanfor
+Display_CaptchaInputNumbers=Skriv inn tala du h\u00f8yrer
+Display_CaptchaGetAudio=F\u00e5 ein lyd-CAPTCHA
+Display_CaptchaGetImage=F\u00e5 eit bilete-CAPTCHA
+Display_ChangePasswordForm=Ver venleg og skriv inn f\u00f8lgjande data. Dette vert kravt for \u00e5 stadfesta identiteten din f\u00f8r du kan endra passordet ditt.
+Display_DeleteUserConfirm=Er du sikker p\u00e5 at du vil sletta kontoen din? Dette kan ikkje angrast.
+Display_ErrorReference=feilreferanse %1%
+Display_HelpdeskOtpValidation=Instruer brukaren til \u00e5 lasta inn mobilgodkjenningsappen og dela gjeldande passord.
+Display_JavascriptRequired=Javascript vert kravt for \u00e5 visa denne sida.
+Display_LoginPasswordOnly=Ver venleg og skriv inn passordet ditt nedanfor. Det noverande passordet ditt er naudsynt for \u00e5 f\u00e5 tilgjenge til dette programmet.
+Display_LogoutPublic=\u00d8kta di vart tidsavbrote p\u00e5 grunn av inaktivitet.
+Display_NAAF_PASSWORD=Ver venleg og oppgje passordet ditt for NAAF godkjenning.
+Display_NAAF_LDAP_PASSWORD=Oppgje passordet ditt for LDAP godkjenning.
+Display_NAAF_SECURITY_QUESTIONS=Ver venleg og svar p\u00e5 kontrollsp\u00f8rsm\u00e5la dine.
+Display_NAAF_EMAIL_OTP=Ein e-post har vorte send med ditt eingongspassord.
+Display_NAAF_SMS_OTP=Ein SMS har vorte send med ditt eingongspassord.
+Display_NAAF_SMARTPHONE=Verifiseringsprosessen for smarttelefonen har starta. Ver venleg og hald fram n\u00e5r du er ferdig.
+Display_NAAF_RADIUS=Ver venleg og oppgje passordet ditt for RADIUS godkjenning.
+Display_NAAF_TOTP=Ver venleg og skriv inn TOTP-verdien din.
+Display_NAAF_HOTP=Ver venleg og skriv inn HOTP-verdien din.
+Display_NewUserProfile=For \u00e5 registrera ein ny konto, ver venleg og vel en profil.
+Display_Random=Tilfeldig
+Display_RecoverVerificationChoice=Ver venleg og vel ein av f\u00f8lgjande metodar for \u00e5 stadfesta identiteten din. Merk\: Viss administratoren krev fleire former for stadfesting, vert du omdirigert tilbake til denne sida til alle stadfestingkriteria er oppfylt.
+Display_RecoverTokenSendChoices=For \u00e5 stadfesta identiteten din, sendast ein tryggleikskode til deg. Ver venleg og vel kva for ein metode du f\u00f8retrekker \u00e5 motta tryggleikskoden med.
+Display_RecoverTokenSendChoiceEmail=Send koden til din registrerte e-postadresse.
+Display_RecoverTokenSendChoiceSMS=Send koden til mobiltelefonen din som tekstmelding (SMS).
+Display_RecoverEnterCodeSMS=For \u00e5 stadfesta identiteten din, er det sendt ein tryggleikskode til deg via SMS. Ver venleg og skriv inn tryggleikskoden i meldinga her.
+Display_RecoverOTP=For \u00e5 stadfesta identiteten din, bruk mobileininga di til \u00e5 generera ein tryggleikskode.
+Display_RecoverOTPIdentified=For \u00e5 stadfesta identiteten din, bruk mobileininga di til \u00e5 generera ein tryggleikskode. ID-ein for mobileininga er <b>%1%</b>.
+Display_SelectionIndicator=Ver venleg og vel eit sp\u00f8rsm\u00e5l fr\u00e5 lista
+Display_SearchCompleted=S\u00f8ket er fullf\u00f8rt.
+Display_SearchResultsInfo=Returnerte %1% treff p\u00e5 %2%.
+Display_SearchResultsExceeded=Mengd s\u00f8ketreff har overskride maksimal storleik.
+Display_SetRandomPasswordPrompt=Sett eit nytt tilfeldig passord for denne brukaren?
+Display_SearchResultsNone=Ingen treff.
+Display_SetupHelpdeskResponses=<p>Administratoren din krev at du oppgjev f\u00f8lgjande svar. Desse svara vert brukt til \u00e5 stadfesta identiteten din i tilfelle at du kontaktar brukarst\u00f8tta for \u00e5 f\u00e5 hjelp.</p>
+Display_SetupOtpSecret=Viss du gl\u00f8ymer passordet ditt, kan du f\u00e5 tilgjenge til kontoen din ved hjelp av mobilen. F\u00f8lg instruksjonane nedanfor basert p\u00e5 type eining.
+Display_SetupOtp_Android_Steps=<b>Installer Google Authenticator-appen for Android.</b><ol><li>G\u00e5 til Google Play Butikken p\u00e5 telefonen din.</li><li>S\u00f8k etter <b>Google Authenticator</b>.<br/>( <a target="playstore" href="https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2">Last ned fr\u00e5 Google Play Butikk</a> )</li><li>Last ned og installer programmet.</li></ol><b>Deretter opnar og konfigurerer du Google Authenticator.</b><ol><li>I Google Autentisering, trykk p\u00e5 Meny og vel "Konfigurer konto"</li><li>Vel "Skann ein strekkode".</li><li>Bruk kameraet til telefonen for \u00e5 skanna denne strekkoden.</li><li>N\u00e5r du har skanna strekkoden, klikkar du p\u00e5 Hald fram-knappen.</li></ol>
+Display_SetupOtp_iPhone_Steps=<b>P\u00e5 iPhone, trykk p\u00e5 App Store-ikonet.</b> <ol><li>G\u00e5 til App Store p\u00e5 telefonen din.</li><li>S\u00f8k etter <b>Google Authenticator</b>.<br/>( <a target="itunesstore" href="https://itunes.apple.com/us/app/google-authenticator/id388497605?mt=8">Last fr\u00e5 ned App Store</a> )</li><li>Trykk p\u00e5 appen, og klikk deretter p\u00e5 Gratis for \u00e5 lasta ned og installera det.</li></ol><b>Deretter opnar og konfigurerer du Google Authenticator.</b><ol><li>I Google Autentisering klikkar du p\u00e5 "+" og deretter "Scan Barcode."</li><li>Bruk kameraet til telefonen for \u00e5 skanna denne strekkoden.</li><li>N\u00e5r du har skanna strekkoden, klikkar du p\u00e5 Hald fram-knappen.</li></ol>
+Display_SetupOtp_Other_Steps=<b>Finn ein kompatibel tofaktor-app.</b><ul><li>Pr\u00f8v \u00e5 s\u00f8kja i eininga si appbutikk etter <b>Google Authenticator</b>.<br/>Mange einingar har kompatible apper.</li><li>Pr\u00f8v \u00e5 leita etter ein app som st\u00f8ttar "<b>TOTP tryggleik sin tokens</b>" eller "RFC6238"</li><li>Last ned og installer programmet.</li></ul><b>Deretter opnar du og konfigurerer appen.</b><ol><li> Skriv inn dataa nedanfor eller skann koden s\u00e5nn som appen instruerer.</li><li>N\u00e5r du har konfigurert appen, klikkar du Hald fram-knappen.</li></ol>
+Display_TokenResend=Tryggleikskoden din burde koma umiddelbart. Viss du har venta eit bel og enno ikkje har motteke ein kode, klikkar du p\u00e5 knappen "send ny kode" for \u00e5 motta ein ny kode.
+Display_WarnExistingOtpSecretTime=Du har allereie registrert eininga di p\u00e5 <span class="timestamp">%1%</span>. Du kan testa den noverande eininga di ved \u00e5 skriva inn den genererte koden nedanfor. Viss du held fram, kan du konfigurera den noverande eininga di p\u00e5 nytt.
+Display_WarnExistingOtpSecret=Du har allereie registrert eininga di. Du kan testa den noverande eininga di ved \u00e5 skriva inn den genererte koden nedanfor. Viss du held fram, kan du konfigurera den noverande eininga di p\u00e5 nytt.
+Display_WarnExistingResponseTime=Du har allereie sett opp kontrollsp\u00f8rsm\u00e5la dine p\u00e5 <span class="timestamp">%1%</span>. Viss du held fram, kan du oppgje nye svar p\u00e5 kontrollsp\u00f8rsm\u00e5la dine.
+Display_WarnExistingResponse=Du har allereie sett opp kontrollsp\u00f8rsm\u00e5la dine. Viss du held fram, kan du oppgje nye svar p\u00e5 kontrollsp\u00f8rsm\u00e5la dine.
+Display_PleaseVerifyOtp=Ver venleg og skriv inn den sekssifra stadfestingkoden fr\u00e5 eininga di. Viss eininga di ikkje er konfigurert til \u00e5 gje deg ein stadfestingkode, kan du g\u00e5 tilbake til f\u00f8rre side og konfigurera eininga di.
+Display_OtpRecoveryInfo=Kvar av desse gjenopprettingskodene kan brukast n\u00f8yaktig ein gong i tilfelle du ikkje f\u00e5r tilgjenge til telefonen. Pass p\u00e5 \u00e5 <a class="pwm-link-print">skriva ut denne sida</a> eller p\u00e5 annan m\u00e5te skriva ned desse kodane og lagra dei p\u00e5 eit trygt stad.
+Display_OtpClearWarning=Er du sikker p\u00e5 at du vil halda fram? Viss du held fram, vert den eksisterande registreringa di sletta, og du m\u00e5 konfigurera eininga p\u00e5 nytt.
+Display_ResponsesClearWarning=Er du sikker p\u00e5 at du vil halda fram? Viss du held fram, vert det eksisterande svaret dine sletta, og du m\u00e5 svara p\u00e5 kontrollsp\u00f8rsm\u00e5la dine p\u00e5 nytt.
+Display_TypingWait=Ventar p\u00e5 at det vert skrive ferdig...
+Field_PasswordSetTimeDelta=Endringstidspunkt for passord (Delta)
+Long_Title_SetupOtpSecret=Konfigurer mobileining. Viss du gl\u00f8ymer passordet ditt, kan du bruka mobilen din til \u00e5 logga p\u00e5 denne nettstaden.
+Button_ClearOtpReEnroll=Registrer mobileining p\u00e5 nytt
+Button_HelpdeskClearOtpSecret=Fjern OTP l\u00f8yndom
+Display_NAAF_VOICE=Stemmeverifiseringsprosessen har starta. Ver venleg og hald fram n\u00e5r du er ferdig.
+Display_PasswordReplicationStatus=Passordreplikering (framdrift %1%)

+ 150 - 69
server/src/main/resources/password/pwm/i18n/Message_nn.properties

@@ -19,75 +19,156 @@
 # along with this program; if not, write to the Free Software
 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 #
-<
-Requirement_MinLength=M� vere minst %field% teikn langt.
-Requirement_MinLengthPlural=M� vere minst %field% teikn langt.
-Requirement_MaxLength=Kan ikkje vere meir ein %field% teikn langt.
-Requirement_MaxLengthPlural=Kan ikkje vere meir ein %field% teikn langt.
-Requirement_MinAlpha=M� ha med minst %field% bokstavar.
-Requirement_MinAlphaPlural=M� ha med minst %field% bokstavar.
-Requirement_MaxAlpha=Kan ikkje ha med meir enn %field% bokstavar.
-Requirement_MaxAlphaPlural=Kan ikkje ha med meir enn %field% bokstavar.
-Requirement_AllowNumeric=Kan ikkje innehalde numeriske teikn.
-Requirement_MinNumeric=M� ha med minst %field% tal.
-Requirement_MinNumericPlural=M� ha med minst %field% tal.
-Requirement_MaxNumeric=Kan ikkje ha med meir enn %field% tal.
-Requirement_MaxNumericPlural=Kan ikkje ha med meir enn %field% tal.
-Requirement_FirstNumeric=Det fyrste teiknet kan ikkje vere numerisk.
-Requirement_LastNumeric=Det siste teiknet kan ikkje vere numerisk.
-Requirement_AllowSpecial=Kan ikkje innehalde nokon spesialteikn (noko anna ein bokstavar eller tal).
-Requirement_MinSpecial=M� ha minst %field% spesialteikn (noko anna ein bokstavar eller tal).
-Requirement_MinSpecialPlural=M� ha minst %field% spesialteikn (noko anna ein bokstavar eller tal).
-Requirement_MaxSpecial= Kan ikkje ha med meir ein %field% spesialteikn (noko anna ein bokstavar eller tal).
-Requirement_MaxSpecialPlural=Kan ikkje ha med meir ein %field% spesialteikn (noko anna ein bokstavar eller tal).
-Requirement_FirstSpecial=Det fyrste teiknet kan ikkje vere eit spesialteikn (noko anna ein bokstavar eller tal).
-Requirement_LastSpecial=Den siste teiknet kan ikkje vere eit spesialteikn (noko anna ein bokstavar eller tal).
-Requirement_MaxRepeat=Ingen teikn m� gjetakast meir enn %field% gongar.
-Requirement_MaxRepeatPlural=Ingen teikn m� gjetakast meir enn %field% gongar.
-Requirement_MaxSeqRepeat=Ingen teikn m� gjentakast sekvensielt meir enn %field% gongar.
-Requirement_MaxSeqRepeatPlural=Ingen teikn m� gjentakast sekvensielt meir enn %field% gongar.
-Requirement_MinLower=M� ha minst %field% liten bokstav(-ar).
-Requirement_MinLowerPlural=M� ha minst %field% liten bokstav(-ar).
-Requirement_MaxLower=Kan ikkje ha med meir ein %field% liten bokstav(-ar).
-Requirement_MaxLowerPlural=Kan ikkje ha med meir ein %field% liten bokstav(-ar).
-Requirement_MinUpper=M� ha minst %field% stor(e) bokstav(-ar).
-Requirement_MinUpperPlural=M� ha minst %field% stor(e) bokstav(-ar).
-Requirement_MaxUpper=Kan ikkje ha meir enn %field% stor(e) bokstav(-ar).
-Requirement_MaxUpperPlural=Kan ikkje ha meir enn %field% stor(e) bokstav(-ar).
-Requirement_MinUnique=M� ha minst %field% unike karakter.
-Requirement_MinUniquePlural=M� ha minst %field% unike karakter.
-Requirement_RequiredChars=M� inkludere minst eit av de fylgjande teikn: %field%.
-Requirement_DisAllowedValues=Kan ikkje innehalde nokon av fylgjande verdiar: %field%.
-Requirement_DisAllowedAttributes=Kan ikkje inkludere delar av namnet ditt eller brukarnamn.
-Requirement_WordList=Kan ikkje inkludere eit vanlig ord eller ein vanlig sekvens av teikn.
-Requirement_OldChar=Kan ikkje ha meir enn %field% karakter fr� ditt noverande passord.
-Requirement_OldCharPlural=Kan ikkje ha med meir enn %field% teikn fr� ditt noverande passord.
-Requirement_CaseSensitive=Passord krev store og sm� bokstavar.
-Requirement_NotCaseSensitive=Passordet har ikkje store og sm� bokstavar.
-Requirement_MinimumFrequency=Kan ikkje endrast oftare enn ein gang kvar %field%.
-Requirement_ADComplexity=M� ha minst tre typar av fylgjande teikn: <ul><li>store (A-Z)</li><li>sm� bokstavar (a-z)</li><li>tal (0-9)</li><li>Symbol (!, #, $, etc.)</li></ul>
-Requirement_UniqueRequired=Nytt passord kan ikkje ha vore i bruk tidlegare.
 
+<
+Requirement_MinLength=M\u00e5 vera minst %1% teikn langt.
+Requirement_MinLengthPlural=M\u00e5 vera minst %1% teikn langt.
+Requirement_MaxLength=Kan ikkje vera meir enn %1% teikn langt.
+Requirement_MaxLengthPlural=Kan ikkje vera meir enn %1% teikn langt.
+Requirement_MinAlpha=M\u00e5 innehalda minst %1% bokstav.
+Requirement_MinAlphaPlural=M\u00e5 innehalda minst %1% bokstavar.
+Requirement_MaxAlpha=Kan ikkje innehalda meir enn %1% bokstav.
+Requirement_MaxAlphaPlural=Kan ikkje innehalda meir enn %1% bokstavar.
+Requirement_AllowNumeric=Kan ikkje innehalda tal.
+Requirement_MinNumeric=M\u00e5 innehalda minst %1% tal.
+Requirement_MinNumericPlural=M\u00e5 innehalda minst %1% tal.
+Requirement_MaxNumeric=Kan ikkje innehalda meir enn %1% tal.
+Requirement_MaxNumericPlural=Kan ikkje innehalda meir enn %1% tal.
+Requirement_FirstNumeric=Det f\u00f8rste teiknet kan ikkje vera eit tal.
+Requirement_LastNumeric=Det siste teiknet kan ikkje vera eit tal.
+Requirement_AllowSpecial=Kan ikkje innehalda nokon spesialteikn (noko anna enn bokstavar eller tal).
+Requirement_MinSpecial=M\u00e5 ha minst %1% spesialteikn (noko anna enn bokstavar eller tal).
+Requirement_MinSpecialPlural=M\u00e5 ha minst %1% spesialteikn (noko anna enn bokstavar eller tal).
+Requirement_MaxSpecial=Kan ikkje innehalda meir enn %1% spesialteikn (noko anna enn bokstavar eller tal).
+Requirement_MaxSpecialPlural=Kan ikkje innehalda meir enn %1% spesialteikn (noko anna enn bokstavar eller tal).
+Requirement_FirstSpecial=Det f\u00f8rste teiknet kan ikkje vera eit spesialteikn (noko anna enn bokstavar eller tal).
+Requirement_LastSpecial=Den siste teiknet kan ikkje vera eit spesialteikn (noko anna enn bokstavar eller tal).
+Requirement_MaxRepeat=Ingen teikn kan gjentakast meir enn %1% gong.
+Requirement_MaxRepeatPlural=Ingen teikn kan gjentakast meir enn %1% gonger.
+Requirement_MaxSeqRepeat=Ingen teikn kan gjentakast sekvensielt meir enn %1% gong.
+Requirement_MaxSeqRepeatPlural=Ingen teikn kan gjentakast sekvensielt meir enn %1% gonger.
+Requirement_MinLower=M\u00e5 ha minst %1% liten bokstav.
+Requirement_MinLowerPlural=M\u00e5 ha minst %1% sm\u00e5 bokstavar.
+Requirement_MaxLower=Kan ikkje innehalda meir enn %1% liten bokstav.
+Requirement_MaxLowerPlural=Kan ikkje innehalda meir enn %1% sm\u00e5 bokstavar.
+Requirement_MinUpper=M\u00e5 ha minst %1% stor bokstav.
+Requirement_MinUpperPlural=M\u00e5 ha minst %1% store bokstavar.
+Requirement_MaxUpper=Kan ikkje innehalda meir enn %1% stor bokstav.
+Requirement_MaxUpperPlural=Kan ikkje innehalda meir enn %1% store bokstavar.
+Requirement_MinUnique=M\u00e5 ha minst %1% unikt teikn.
+Requirement_MinUniquePlural=M\u00e5 ha minst %1% unike teikn.
+Requirement_RequiredChars=M\u00e5 inkludera minst eit av kvart av dei f\u00f8lgjande teikn\:
+Requirement_DisAllowedValues=Kan ikkje innehalda nokon av f\u00f8lgjande verdiar\: %1%
+Requirement_DisAllowedAttributes=Kan ikkje innehalda delar av namnet eller brukarnamnet ditt.
+Requirement_WordList=Kan ikkje inkludera eit vanleg ord eller ein vanleg sekvens av teikn.
+Requirement_OldChar=Kan ikkje ha meir enn %1% teikn fr\u00e5 det noverande passordet ditt.
+Requirement_OldCharPlural=Kan ikkje ha meir enn %1% teikn fr\u00e5 det noverande passordet ditt.
+Requirement_CaseSensitive=Det skilst p\u00e5 store og sm\u00e5 bokstavar i passordet.
+Requirement_NotCaseSensitive=Passordet skil ikkje p\u00e5 store og sm\u00e5 bokstavar.
+Requirement_MinimumFrequency=Kan ikkje endrast oftare enn ein gang kvar %1%.
+Requirement_ADComplexity=M\u00e5 ha minst tre typar av f\u00f8lgjande teikn\: <ul><li>Store bokstavar (A-Z)</li><li>Sm\u00e5 bokstavar (a-z)</li><li>Tal (0-9)</li><li>Symbol (!, #, $, etc.)</li></ul>
+Requirement_UniqueRequired=Nytt passord kan ikkje ha vore brukt tidlegare.
 Success_PasswordChange=Passordendringa var vellukka.
-Success_SetupResponse=Dine hemmelege sp�rsm�l og svar er lagra. Dersom du gl�ymer passordet ditt, kan du bruke svara p� disse sp�rsm�la for � endre passordet.
-Success_Unknown=Operasjonen er fullf�rt.
-Success_CreateUser=Din nye brukarkonto er oppretta.
-Success_CreateGuest=Den nye gjestekontoen er oppretta. Gjestebrukaren vil f� ei melding til e-postadressa som er lagt inn. Gjestekontoen kan framleis trenge aktivering.
-Success_UpdateGuest=Gjestekontoen er oppdatert. Gjestebrukaren vil f� ei melding til e-postadressa som er lagt inn.
-Success_ActivateUser=Din brukarkonto er aktivert. S�rg for � fullf�re prosessen, ellers vil du ikkje kunne f� tilgong til kontoen din.
-Success_UpdateProfile=Din brukarinformasjon er oppdatert.
-Success_ResponsesMeetRules=Dine svar oppfyller krava. Klikk "Lagre svar" n�r du er klar.
-Success_NewUserForm= Brukarkontoen din er klar til � bli oppretta. Fortsett n�r du er klar.
-Success_UnlockAccount=Brukarkontoen din er l�st opp.
-Success_ConfigFileUpload=Konfigurasjonsfila er lasta opp.  %field%
-Success_PasswordReset=Passordet for %field% er sett.
-
+Success_SetupResponse=Dei l\u00f8ynlege sp\u00f8rsm\u00e5la og svara dine har vorte lagra. Viss du gl\u00f8ymer passordet ditt, kan du bruka svara p\u00e5 desse sp\u00f8rsm\u00e5la for \u00e5 endre passordet.
+Success_Unknown=Handlinga er fullf\u00f8rd.
+Success_CreateUser=Den nye brukarkontoen din er vorten oppretta.
+Success_CreateGuest=Den nye gjestekontoen er oppretta. Gjestebrukaren vil f\u00e5 ei melding viss e-postadressa er angjeven. Gjestekontoen kan enno trenge aktivering.
+Success_UpdateGuest=Gjestekontoen har vorte oppdatert. Gjestebrukaren vil f\u00e5 ei melding til e-postadressa som er angjeve.
+Success_ActivateUser=Brukarkontoen din har vorte aktivert. S\u00f8rg for \u00e5 fullf\u00f8ra prosessen, elles vil du ikkje kunna f\u00e5 tilgjenge til kontoen din.
+Success_UpdateProfile=Brukarinformasjonen din har vorte oppdatert.
+Success_ResponsesMeetRules=Dine svar oppfyller krava. Klikk Lagre svar n\u00e5r du er kl\u00e5r.
+Success_NewUserForm=Kontoen din er kl\u00e5r til \u00e5 opprettast. Hald fram n\u00e5r du er kl\u00e5r.
+Success_UnlockAccount=Kontoen din har vorte l\u00e5st opp.
+Success_ConfigFileUpload=Konfigurasjonsfila har vorte lasta opp.
+Success_PasswordReset=Passordet for %1% er sett.
 EventLog_ChangePassword=Endre passord
-EventLog_RecoverPassword=Gjenopprett gl�ymt passord
-EventLog_SetupResponses=Angi passord responsar
-EventLog_ActivateUser=Aktiver IT-brukarkonto
-EventLog_UpdateProfile=Oppdater attributter
-EventLog_IntruderLockout=Utestenging pga. fors�k p� uautorisert tilgong (feil passord for mange gongar)
-EventLog_HelpdeskSetPassword=Brukarst�tte, angi passord
-EventLog_HelpdeskUnlockPassword=Brukarst�tte, l�se opp passord
-EventLog_HelpdeskClearResponses=Brukarst�tte, slett responsar
+EventLog_RecoverPassword=Gjenopprett gl\u00f8ymt passord
+EventLog_SetupResponses=Angje svar for passord
+EventLog_ActivateUser=Aktiver konto
+EventLog_UpdateProfile=Oppdater attributt
+EventLog_IntruderLockout=Stengt p\u00e5 grunn av inntrengingsfors\u00f8k
+EventLog_HelpdeskSetPassword=Brukarst\u00f8tte - Sett passord
+EventLog_HelpdeskUnlockPassword=Brukarst\u00f8tte - L\u00e5s opp passord
+EventLog_HelpdeskClearResponses=Brukarst\u00f8tte - Fjern svar p\u00e5 kontrollsp\u00f8rsm\u00e5l
+EventLog_Startup=Programoppstart
+EventLog_Shutdown=Programavslutning
+EventLog_FatalEvent=Kritisk hending
+EventLog_ModifyConfiguration=Konfigurasjon endra
+EventLog_IntruderAttempt=Inntrengingsfors\u00f8k
+EventLog_Authenticate=Autentisering
+EventLog_AgreementPassed=Avtale akseptert
+EventLog_UnlockPassword=L\u00e5s opp passord
+EventLog_CreateUser=Opprett konto
+EventLog_HelpdeskAction=Handling utf\u00f8rt av brukarst\u00f8tta
+EventLog_HelpdeskDeleteUser=Brukarst\u00f8tte - Slett brukar
+EventLog_HelpdeskViewDetail=Brukarst\u00f8tte - Vis detaljar
+EventLog_TokenIssued=Token utstedt
+EventLog_ClearResponses=Fjern svar
+EventLog_DeleteAccount=Slett konto
+Rule_ADComplexity=AD-kompleksitet
+Rule_EnableWordlist=Aktiver ordliste
+EventLog_HelpdeskClearOtpSecret=Brukarst\u00f8tte - Fjern OTP l\u00f8yndom
+EventLog_HelpdeskVerifyOtp=Brukarst\u00f8tte - Stadfest OTP
+EventLog_HelpdeskVerifyOtpIncorrect=Brukarst\u00f8tte - Stadfesta OTP er feil
+EventLog_HelpdeskVerifyToken=Brukarst\u00f8tte - Stadfest token
+EventLog_HelpdeskVerifyTokenIncorrect=Brukarst\u00f8tte - Stadfesta token er feil
+EventLog_HelpdeskVerifyAttributes=Brukarst\u00f8tte - Stadfest attributt
+EventLog_HelpdeskVerifyAttributesIncorrect=Brukarst\u00f8tte - Stadfeste attributt er feil
+EventLog_IntruderUserAttempt=Inntrengingsfors\u00f8k
+EventLog_IntruderUserLock=Sperring av brukar p\u00e5 grunn av inntrengingsfors\u00f8k
+EventLog_TokenClaimed=Token reservert
+Eventlog_SetupOtpSecret=Angje OTP l\u00f8yndom
+Requirement_ADComplexity2008=M\u00e5 innehalda minst %1% typar av f\u00f8lgjande teikn\: <ul><li>Store bokstavar (A-Z)</li><li>Sm\u00e5 bokstavar (a-z)</li><li>Tal (0-9)</li><li> Symbol (!, #, $, *etc.)</li><li>Andre teikn som ikkje er oppf\u00f8rt ovanfor</li></ul>
+Rule_PolicyEnabled=Retningslinjer aktivert
+Rule_MinimumLength=Minimum lengd
+Rule_MaximumLength=Maksimal lengd
+Rule_MinimumUpperCase=Minimum mengd store bokstavar
+Rule_MaximumUpperCase=Maksimum mengd store bokstavar
+Rule_MinimumLowerCase=Minimum mengd sm\u00e5 bokstavar
+Rule_MaximumLowerCase=Maksimum mengd sm\u00e5 bokstavar
+Rule_AllowNumeric=Tillat numeriske teikn (tal)
+Rule_MinimumNumeric=Minimum mengd numeriske teikn (tal)
+Rule_MaximumNumeric=Maksimum mengd numeriske teikn (tal)
+Rule_MinimumUnique=Minimum mengd unike teikn
+Rule_MaximumUnique=Maksimum mengd unike teikn
+Rule_AllowFirstCharNumeric=Tillat at f\u00f8rste teikn er eit tal
+Rule_AllowLastCharNumeric=Tillat at siste teikn er eit tal
+Rule_AllowSpecial=Tillat spesialteikn
+Rule_MinimumSpecial=Minimum mengd spesialteikn
+Rule_MaximumSpecial=Maksimum mengd spesialteikn
+Rule_AllowFirstCharSpecial=Tillat at f\u00f8rste teikn er eit spesialteikn
+Rule_AllowLastCharSpecial=Tillat at siste teikn er eit spesialteikn
+Rule_MaximumRepeat=Maksimum gjentaking
+Rule_MaximumSequentialRepeat=Maksimum sekvensiell gjentaking
+Rule_ChangeMessage=Endre melding
+Rule_ExpirationInterval=Utl\u00f8psintervall
+Rule_MinimumLifetime=Minimum levetid
+Rule_CaseSensitive=Skilnad p\u00e5 store og sm\u00e5 bokstavar
+Rule_EnforceAtLogin=H\u00e5ndhev ved innlogging
+Rule_ChallengeResponseEnabled=Kontrollsp\u00f8rsm\u00e5l er aktivert
+Rule_UniqueRequired=Eintydighet er p\u00e5kravt
+Rule_DisallowedValues=Ikkje tillatne verdiar
+Rule_DisallowedAttributes=Ikkje tillatne attributt
+Rule_DisallowCurrent=Ikkje tillat noverande
+Rule_MaximumOldChars=Maksimum gamle teikn
+Rule_RegExMatch=Treff p\u00e5 regul\u00e6rt uttrykk
+Rule_RegExNoMatch=Ingen treff p\u00e5 regul\u00e6rt uttrykk
+Rule_MinimumAlpha=Minimum mengd alfanumeriske teikn
+Rule_MaximumAlpha=Maksimum mengd alfanumeriske teikn
+Rule_MinimumNonAlpha=Minimum mengd ikkje-alfanumeriske teikn
+Rule_MaximumNonAlpha=Maksimum mengd ikkje-alfanumeriske teikn
+Rule_MinimumStrength=Minimum styrke
+Rule_MaximumConsecutive=Maksimum mengd p\u00e5f\u00f8lgjande
+Rule_CharGroupsValues=Verdiar for teikngrupper
+Rule_CharGroupsMinMatch=Minimum mengd obligatoriske teikngrupper
+Rule_AllowUserChange=Tillat at administratoren endrar passordet
+Rule_AllowAdminChange=Tillat at brukaren endrar passordet
+Rule_ADComplexityLevel=AD-kompleksitetsniv\u00e5
+Rule_ADComplexityMaxViolations=Maksimum mengd overtredelser av AD-kompleksitet
+Success_ClearResponse=Dei l\u00f8ynlege sp\u00f8rsm\u00e5la og svara dine har vorte fjerna.
+Success_ChangedHelpdeskPassword=Passordet er endra for brukar
+Success_PasswordSend=Det nye passordet ditt har vorte sendt til %1%. Ver venleg og lukk dette vindauget og logg inn med det nye passordet ditt.
+Success_UpdateForm=Profilen din er kl\u00e5r til \u00e5 oppdaterast. Hald fram n\u00e5r du er kl\u00e5r.
+Success_Action=Handlinga %1% er fullf\u00f8rd.
+Success_OtpSetup=Registrering av eininga di er fullf\u00f8rd.
+Success_TokenResend=Ein ny tryggleikskode er send til deg.

部分文件因为文件数量过多而无法显示