Browse Source

Merge remote-tracking branch 'origin'

oddkl 5 years ago
parent
commit
57df1bbe31
74 changed files with 1168 additions and 579 deletions
  1. 3 3
      build/spotbugs-exclude.xml
  2. 2 2
      client/pom.xml
  3. 4 4
      data-service/pom.xml
  4. 1 1
      docker/pom.xml
  5. 2 2
      onejar/pom.xml
  6. 5 1
      onejar/src/main/java/password/pwm/onejar/ArgumentParser.java
  7. 5 0
      onejar/src/main/java/password/pwm/onejar/TomcatOnejarRunner.java
  8. 23 15
      pom.xml
  9. 1 1
      pwm-cr/pom.xml
  10. 5 9
      pwm-cr/src/main/java/password/pwm/cr/ChaiXmlResponseSetSerializer.java
  11. 7 10
      server/pom.xml
  12. 1 0
      server/src/main/java/password/pwm/AppProperty.java
  13. 4 16
      server/src/main/java/password/pwm/PwmConstants.java
  14. 2 0
      server/src/main/java/password/pwm/bean/UserIdentity.java
  15. 73 74
      server/src/main/java/password/pwm/config/PwmSettingCategory.java
  16. 1 1
      server/src/main/java/password/pwm/config/profile/ProfileUtility.java
  17. 1 0
      server/src/main/java/password/pwm/config/profile/PwmPasswordPolicy.java
  18. 1 1
      server/src/main/java/password/pwm/config/stored/StoredConfigurationFactory.java
  19. 17 7
      server/src/main/java/password/pwm/config/stored/StoredConfigurationModifier.java
  20. 4 19
      server/src/main/java/password/pwm/config/stored/StoredConfigurationUtil.java
  21. 1 1
      server/src/main/java/password/pwm/config/value/ActionValue.java
  22. 1 0
      server/src/main/java/password/pwm/config/value/NamedSecretValue.java
  23. 2 0
      server/src/main/java/password/pwm/config/value/PasswordValue.java
  24. 3 2
      server/src/main/java/password/pwm/config/value/StringValue.java
  25. 9 7
      server/src/main/java/password/pwm/config/value/data/CustomLinkConfiguration.java
  26. 9 1
      server/src/main/java/password/pwm/http/ContextManager.java
  27. 2 0
      server/src/main/java/password/pwm/http/PwmSession.java
  28. 7 2
      server/src/main/java/password/pwm/http/auth/HttpAuthenticationUtilities.java
  29. 2 0
      server/src/main/java/password/pwm/http/bean/ConfigManagerBean.java
  30. 4 0
      server/src/main/java/password/pwm/http/bean/ForgottenPasswordBean.java
  31. 2 1
      server/src/main/java/password/pwm/http/filter/RequestInitializationFilter.java
  32. 15 2
      server/src/main/java/password/pwm/http/servlet/ClientApiServlet.java
  33. 6 0
      server/src/main/java/password/pwm/http/servlet/admin/AppDashboardData.java
  34. 2 0
      server/src/main/java/password/pwm/http/servlet/admin/UserDebugDataBean.java
  35. 67 1
      server/src/main/java/password/pwm/http/servlet/configeditor/ConfigEditorServlet.java
  36. 6 2
      server/src/main/java/password/pwm/http/servlet/configeditor/NavTreeHelper.java
  37. 5 0
      server/src/main/java/password/pwm/http/servlet/helpdesk/HelpdeskServlet.java
  38. 2 0
      server/src/main/java/password/pwm/http/servlet/helpdesk/HelpdeskVerificationStateBean.java
  39. 1 1
      server/src/main/java/password/pwm/http/state/CryptoCookieLoginImpl.java
  40. 2 1
      server/src/main/java/password/pwm/i18n/PwmLocaleBundle.java
  41. 9 1
      server/src/main/java/password/pwm/ldap/LdapConnectionService.java
  42. 26 0
      server/src/main/java/password/pwm/ldap/search/SearchConfiguration.java
  43. 34 27
      server/src/main/java/password/pwm/ldap/search/UserSearchEngine.java
  44. 9 4
      server/src/main/java/password/pwm/ldap/search/UserSearchJob.java
  45. 2 0
      server/src/main/java/password/pwm/ldap/search/UserSearchJobParameters.java
  46. 1 1
      server/src/main/java/password/pwm/svc/PwmServiceEnum.java
  47. 17 11
      server/src/main/java/password/pwm/svc/email/EmailServerUtil.java
  48. 88 32
      server/src/main/java/password/pwm/svc/email/EmailService.java
  49. 2 0
      server/src/main/java/password/pwm/svc/wordlist/WordlistConfiguration.java
  50. 1 1
      server/src/main/java/password/pwm/svc/wordlist/WordlistInspector.java
  51. 8 4
      server/src/main/java/password/pwm/util/JarMain.java
  52. 2 0
      server/src/main/java/password/pwm/util/LDAPPermissionCalculator.java
  53. 2 0
      server/src/main/java/password/pwm/util/PasswordData.java
  54. 2 2
      server/src/main/java/password/pwm/util/cli/commands/ImportPropertyConfigCommand.java
  55. 0 1
      server/src/main/java/password/pwm/util/logging/PwmLogger.java
  56. 2 238
      server/src/main/java/password/pwm/util/secure/HttpsServerCertificateManager.java
  57. 5 1
      server/src/main/java/password/pwm/util/secure/PwmTrustManager.java
  58. 151 0
      server/src/main/java/password/pwm/util/secure/self/SelfCertFactory.java
  59. 159 0
      server/src/main/java/password/pwm/util/secure/self/SelfCertGenerator.java
  60. 56 0
      server/src/main/java/password/pwm/util/secure/self/Settings.java
  61. 63 0
      server/src/main/java/password/pwm/util/secure/self/StoredCertData.java
  62. 2 0
      server/src/main/java/password/pwm/ws/server/RestAuthentication.java
  63. 1 0
      server/src/main/resources/password/pwm/AppProperty.properties
  64. 0 1
      server/src/main/resources/password/pwm/PwmConstants.properties
  65. 1 1
      server/src/main/resources/password/pwm/config/PwmSetting.xml
  66. 1 0
      server/src/main/resources/password/pwm/i18n/Admin.properties
  67. 1 0
      server/src/main/resources/password/pwm/i18n/Config.properties
  68. 1 1
      server/src/test/java/password/pwm/config/PwmSettingCategoryTest.java
  69. 68 0
      server/src/test/java/password/pwm/config/stored/StoredConfigurationModifierTest.java
  70. 57 0
      server/src/test/java/password/pwm/config/stored/StoredConfigurationUtilTest.java
  71. 0 57
      server/src/test/java/password/pwm/tests/MakeSelfSignedCertTest.java
  72. 43 0
      server/src/test/java/password/pwm/util/secure/self/SelfCertGeneratorTest.java
  73. 9 9
      webapp/pom.xml
  74. 35 0
      webapp/src/main/webapp/public/resources/js/configeditor.js

+ 3 - 3
build/spotbugs-exclude.xml

@@ -21,14 +21,14 @@
 
 <FindBugsFilter>
     <Match>
-        <Bug pattern="SE_NO_SERIALVERSIONID,RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE,SQL_PREPARED_STATEMENT_GENERATED_FROM_NONCONSTANT_STRING"/>
+        <Bug pattern="RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE"/>
     </Match>
     <Match>
-        <!-- due to bug https://github.com/spotbugs/spotbugs/issues/493 in spotbugs 3.1.3 -->
-        <Bug pattern="OBL_UNSATISFIED_OBLIGATION"/>
+        <Bug pattern="SQL_PREPARED_STATEMENT_GENERATED_FROM_NONCONSTANT_STRING"/>
     </Match>
     <Match>
         <!-- due to bug with java 11 -->
+        <!-- https://github.com/spotbugs/spotbugs/issues/756 -->
         <Bug pattern="RCN_REDUNDANT_NULLCHECK_WOULD_HAVE_BEEN_A_NPE"/>
     </Match>
 </FindBugsFilter>

+ 2 - 2
client/pom.xml

@@ -75,11 +75,11 @@
             <plugin>
                 <groupId>com.github.eirslett</groupId>
                 <artifactId>frontend-maven-plugin</artifactId>
-                <version>1.9.1</version>
+                <version>1.10.0</version>
                 <configuration>
                     <nodeVersion>v12.13.1</nodeVersion>
                     <npmVersion>6.13.4</npmVersion>
-                    <installDirectory>.node</installDirectory>
+                    <installDirectory>target/node-executable</installDirectory>
                 </configuration>
                 <executions>
                     <execution>

+ 4 - 4
data-service/pom.xml

@@ -46,7 +46,7 @@
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-war-plugin</artifactId>
-                <version>3.2.2</version>
+                <version>3.2.3</version>
                 <configuration>
                     <archiveClasses>true</archiveClasses>
                     <packagingExcludes>WEB-INF/classes</packagingExcludes>
@@ -130,7 +130,7 @@
         <dependency>
             <groupId>org.apache.commons</groupId>
             <artifactId>commons-lang3</artifactId>
-            <version>3.9</version>
+            <version>3.10</version>
         </dependency>
         <dependency>
             <groupId>com.sun.mail</groupId>
@@ -140,7 +140,7 @@
         <dependency>
             <groupId>org.apache.httpcomponents</groupId>
             <artifactId>httpclient</artifactId>
-            <version>4.5.11</version>
+            <version>4.5.12</version>
         </dependency>
         <dependency>
             <groupId>log4j</groupId>
@@ -160,7 +160,7 @@
         <dependency>
             <groupId>org.jetbrains.xodus</groupId>
             <artifactId>xodus-environment</artifactId>
-            <version>1.3.124</version>
+            <version>1.3.232</version>
         </dependency>
     </dependencies>
 </project>

+ 1 - 1
docker/pom.xml

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

+ 2 - 2
onejar/pom.xml

@@ -16,7 +16,7 @@
     <name>PWM Password Self Service: Executable Server JAR</name>
 
     <properties>
-        <tomcat.version>9.0.31</tomcat.version>
+        <tomcat.version>9.0.35</tomcat.version>
     </properties>
 
     <build>
@@ -40,7 +40,7 @@
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-assembly-plugin</artifactId>
-                <version>3.2.0</version>
+                <version>3.3.0</version>
                 <configuration>
                     <appendAssemblyId>false</appendAssemblyId>
                     <descriptors>

+ 5 - 1
onejar/src/main/java/password/pwm/onejar/ArgumentParser.java

@@ -187,7 +187,11 @@ public class ArgumentParser
                 System.out.println( msg );
                 throw new IllegalStateException( msg );
             }
-            onejarConfig.war( new FileInputStream( inputWarFile ) );
+
+            try ( InputStream inputStream = new FileInputStream( inputWarFile ) )
+            {
+                onejarConfig.war( inputStream );
+            }
         }
         else
         {

+ 5 - 0
onejar/src/main/java/password/pwm/onejar/TomcatOnejarRunner.java

@@ -72,6 +72,10 @@ public class TomcatOnejarRunner
         }
         catch ( final Exception e )
         {
+            if ( e instanceof InvocationTargetException )
+            {
+                throw new OnejarException( "error generating keystore: " + e.getCause().getMessage() );
+            }
             throw new OnejarException( "error generating keystore: " + e.getMessage() );
         }
 
@@ -160,6 +164,7 @@ public class TomcatOnejarRunner
         connector.setScheme( "https" );
         connector.addUpgradeProtocol( new Http2Protocol() );
         connector.setAttribute( "SSLEnabled", "true" );
+       // connector.setAttribute( "truststoreType", "PKCS12" );
         connector.setAttribute( "keystoreFile", onejarConfig.getKeystoreFile().getAbsolutePath() );
         connector.setAttribute( "keystorePass", onejarConfig.getKeystorePass() );
         connector.setAttribute( "keyAlias", OnejarMain.KEYSTORE_ALIAS );

+ 23 - 15
pom.xml

@@ -30,7 +30,7 @@
         <build.revision>0</build.revision>  <!-- default in case not set on command line -->
         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
 
-        <pwm.minimum.maven.version>3.2</pwm.minimum.maven.version>
+        <pwm.minimum.maven.version>3.5</pwm.minimum.maven.version>
         <timestamp.iso>${maven.build.timestamp}</timestamp.iso>
         <maven.compiler.source>1.8</maven.compiler.source>
         <maven.compiler.target>1.8</maven.compiler.target>
@@ -60,6 +60,12 @@
                 <checkstyle.skip>true</checkstyle.skip>
             </properties>
         </profile>
+        <profile>
+            <id>skip-spotbugs</id>
+            <properties>
+                <spotbugs.skip>true</spotbugs.skip>
+            </properties>
+        </profile>
         <profile>
             <id>enable-javadoc</id>
             <properties>
@@ -120,7 +126,7 @@
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-javadoc-plugin</artifactId>
-                <version>3.1.1</version>
+                <version>3.2.0</version>
                 <executions>
                     <execution>
                         <goals>
@@ -136,9 +142,6 @@
                                     <Implementation-URL>${project.organization.url}</Implementation-URL>
                                     <Implementation-Build-Java-Vendor>${java.vendor}</Implementation-Build-Java-Vendor>
                                     <Implementation-Build-Java-Version>${java.version}</Implementation-Build-Java-Version>
-                                    <Implementation-Build>${build.number}</Implementation-Build>
-                                    <Implementation-Revision>${build.revision}</Implementation-Revision>
-                                    <Implementation-Version-Display>v${project.version} b${build.number} r${build.revision}</Implementation-Version-Display>
                                     <SCM-Git-Branch>${git.branch}</SCM-Git-Branch>
                                     <SCM-Git-Commit-ID>${git.commit.id}</SCM-Git-Commit-ID>
                                     <SCM-Git-Commit-ID-Abbrev>${git.commit.id.abbrev}</SCM-Git-Commit-ID-Abbrev>
@@ -214,6 +217,7 @@
                 <configuration>
                     <source>${maven.compiler.source}</source>
                     <target>${maven.compiler.target}</target>
+                    <showWarnings>true</showWarnings>
                 </configuration>
             </plugin>
             <plugin>
@@ -224,7 +228,7 @@
                     <dependency>
                         <groupId>com.puppycrawl.tools</groupId>
                         <artifactId>checkstyle</artifactId>
-                        <version>8.30</version>
+                        <version>8.32</version>
                     </dependency>
                 </dependencies>
                 <executions>
@@ -303,15 +307,16 @@
             <plugin>
                 <groupId>com.github.spotbugs</groupId>
                 <artifactId>spotbugs-maven-plugin</artifactId>
-                <version>3.1.12.2</version>
+                <version>4.0.0</version>
                 <dependencies>
                     <dependency>
                         <groupId>com.github.spotbugs</groupId>
                         <artifactId>spotbugs</artifactId>
-                        <version>4.0.0</version>
+                        <version>4.0.3</version>
                     </dependency>
                 </dependencies>
                 <configuration>
+                    <skip>${spotbugs.skip}</skip>
                     <fork>false</fork>
                     <excludeFilterFile>${project.root.basedir}/build/spotbugs-exclude.xml</excludeFilterFile>
                     <includeTests>false</includeTests>
@@ -319,7 +324,7 @@
                 </configuration>
                 <executions>
                     <execution>
-                        <phase>verify</phase>
+                        <phase>test</phase>
                         <goals>
                             <goal>check</goal>
                         </goals>
@@ -332,14 +337,17 @@
                 <version>1.8</version>
                 <executions>
                     <execution>
+                        <id>output-checksums</id>
                         <goals>
                             <goal>artifacts</goal>
                         </goals>
-                        <phase>verify</phase>
+                        <phase>package</phase>
                         <configuration>
                             <algorithms>
                                 <algorithm>SHA-1</algorithm>
+                                <algorithm>SHA-256</algorithm>
                             </algorithms>
+                            <failOnError>false</failOnError>
                         </configuration>
                     </execution>
                 </executions>
@@ -350,7 +358,7 @@
             <plugin> <!-- checks owsp vulnerability database -->
                 <groupId>org.owasp</groupId>
                 <artifactId>dependency-check-maven</artifactId>
-                <version>5.3.0</version>
+                <version>5.3.2</version>
                 <executions>
                     <execution>
                         <goals>
@@ -373,7 +381,7 @@
         <dependency>
             <groupId>com.github.spotbugs</groupId>
             <artifactId>spotbugs-annotations</artifactId>
-            <version>4.0.0</version>
+            <version>4.0.3</version>
             <scope>provided</scope>
         </dependency>
 
@@ -387,19 +395,19 @@
         <dependency>
             <groupId>org.mockito</groupId>
             <artifactId>mockito-core</artifactId>
-            <version>3.3.0</version>
+            <version>3.3.3</version>
             <scope>test</scope>
         </dependency>
         <dependency>
             <groupId>org.assertj</groupId>
             <artifactId>assertj-core</artifactId>
-            <version>3.15.0</version>
+            <version>3.16.1</version>
             <scope>test</scope>
         </dependency>
         <dependency>
             <groupId>com.github.tomakehurst</groupId>
             <artifactId>wiremock</artifactId>
-            <version>2.26.1</version>
+            <version>2.26.3</version>
             <scope>test</scope>
         </dependency>
         <dependency>

+ 1 - 1
pwm-cr/pom.xml

@@ -41,7 +41,7 @@
         <dependency>
             <groupId>org.bouncycastle</groupId>
             <artifactId>bcpkix-jdk15on</artifactId>
-            <version>1.64</version>
+            <version>1.65</version>
         </dependency>
     </dependencies>
 

+ 5 - 9
pwm-cr/src/main/java/password/pwm/cr/ChaiXmlResponseSetSerializer.java

@@ -370,17 +370,13 @@ public class ChaiXmlResponseSetSerializer
             return;
         }
 
-        if ( storedChallengeItems != null )
+        for ( final StoredChallengeItem storedChallengeItem : storedChallengeItems )
         {
-            for ( final StoredChallengeItem storedChallengeItem : storedChallengeItems )
-            {
-                final StoredResponseItem storedResponseItem = storedChallengeItem.getAnswer();
-                final String responseElementName = elementNameForType( type );
-                final Element responseElement = challengeToXml( storedChallengeItem, storedResponseItem, responseElementName );
-                parentElement.addContent( responseElement );
-            }
+            final StoredResponseItem storedResponseItem = storedChallengeItem.getAnswer();
+            final String responseElementName = elementNameForType( type );
+            final Element responseElement = challengeToXml( storedChallengeItem, storedResponseItem, responseElementName );
+            parentElement.addContent( responseElement );
         }
-
     }
 
     private static Element challengeToXml(

+ 7 - 10
server/pom.xml

@@ -116,9 +116,6 @@
                             <Implementation-URL>${project.organization.url}</Implementation-URL>
                             <Implementation-Build-Java-Vendor>${java.vendor}</Implementation-Build-Java-Vendor>
                             <Implementation-Build-Java-Version>${java.version}</Implementation-Build-Java-Version>
-                            <Implementation-Build>${build.number}</Implementation-Build>
-                            <Implementation-Revision>${build.revision}</Implementation-Revision>
-                            <Implementation-Version-Display>v${project.version} b${build.number} r${build.revision}</Implementation-Version-Display>
                             <SCM-Git-Branch>${git.branch}</SCM-Git-Branch>
                             <SCM-Git-Commit-ID>${git.commit.id}</SCM-Git-Commit-ID>
                             <SCM-Git-Commit-ID-Abbrev>${git.commit.id.abbrev}</SCM-Git-Commit-ID-Abbrev>
@@ -202,7 +199,7 @@
         <dependency>
             <groupId>org.apache.directory.api</groupId>
             <artifactId>api-all</artifactId>
-            <version>2.0.0</version>
+            <version>2.0.1</version>
         </dependency>
         <dependency>
             <groupId>commons-net</groupId>
@@ -227,7 +224,7 @@
         <dependency>
             <groupId>org.apache.commons</groupId>
             <artifactId>commons-lang3</artifactId>
-            <version>3.9</version>
+            <version>3.10</version>
         </dependency>
         <dependency>
             <groupId>commons-validator</groupId>
@@ -242,7 +239,7 @@
         <dependency>
             <groupId>org.apache.httpcomponents</groupId>
             <artifactId>httpclient</artifactId>
-            <version>4.5.11</version>
+            <version>4.5.12</version>
         </dependency>
         <dependency>
             <groupId>org.graylog2</groupId>
@@ -302,12 +299,12 @@
         <dependency>
             <groupId>com.blueconic</groupId>
             <artifactId>browscap-java</artifactId>
-            <version>1.2.14</version>
+            <version>1.2.16</version>
         </dependency>
         <dependency>
             <groupId>org.jetbrains.xodus</groupId>
             <artifactId>xodus-environment</artifactId>
-            <version>1.3.124</version>
+            <version>1.3.232</version>
         </dependency>
 
         <dependency>
@@ -318,12 +315,12 @@
         <dependency>
             <groupId>org.webjars</groupId>
             <artifactId>webjars-locator-core</artifactId>
-            <version>0.44</version>
+            <version>0.45</version>
         </dependency>
         <dependency>
             <groupId>com.github.ben-manes.caffeine</groupId>
             <artifactId>caffeine</artifactId>
-            <version>2.8.1</version>
+            <version>2.8.4</version>
         </dependency>
         <dependency>
             <groupId>com.nulab-inc</groupId>

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

@@ -241,6 +241,7 @@ public enum AppProperty
     LOGGING_FILE_MAX_ROLLOVER                       ( "logging.file.maxRollover" ),
     LOGGING_FILE_PATH                               ( "logging.file.path" ),
     LOGGING_DEV_OUTPUT                              ( "logging.devOutput.enable" ),
+    LOGGING_LOG_CSP_REPORT                          ( "logging.cspReport.enable" ),
     NEWUSER_LDAP_USE_TEMP_PW                        ( "newUser.ldap.useTempPassword" ),
     NEWUSER_TOKEN_ALLOW_PLAIN_PW                    ( "newUser.token.allowPlainPassword" ),
     NMAS_THREADS_MAX_COUNT                          ( "nmas.threads.maxCount" ),

+ 4 - 16
server/src/main/java/password/pwm/PwmConstants.java

@@ -51,26 +51,14 @@ public abstract class PwmConstants
 {
     public static final Map<String, String> BUILD_MANIFEST = readBuildManifest();
 
-    public static final String BUILD_TIME = BUILD_MANIFEST.getOrDefault( "Implementation-Build-Timestamp", "n/a" );
-    public static final String BUILD_NUMBER = BUILD_MANIFEST.getOrDefault( "Implementation-Build", "0" );
-    public static final String BUILD_REVISION = BUILD_MANIFEST.getOrDefault( "Implementation-Revision", "0" );
+    public static final String BUILD_TIME = BUILD_MANIFEST.getOrDefault( "SCM-Git-Commit-Timestamp", "n/a" );
+    public static final String BUILD_NUMBER = BUILD_MANIFEST.getOrDefault( "SCM-Git-Commit-ID-Abbrev", "0" );
+    public static final String BUILD_REVISION = BUILD_MANIFEST.getOrDefault( "SCM-Git-Commit-ID", "0" );
     public static final String BUILD_JAVA_VENDOR = BUILD_MANIFEST.getOrDefault( "Implementation-Build-Java-Vendor", "0" );
     public static final String BUILD_JAVA_VERSION = BUILD_MANIFEST.getOrDefault( "Implementation-Build-Java-Version", "0" );
     public static final String BUILD_VERSION = BUILD_MANIFEST.getOrDefault( "Implementation-Version", "0" );
 
-    private static final String MISSING_VERSION_STRING = readPwmConstantsBundle( "missingVersionString" );
-    public static final String SERVLET_VERSION;
-
-    static
-    {
-        final String servletVersion = "v" + BUILD_VERSION
-                        + " b" + BUILD_NUMBER
-                        + " r" + BUILD_REVISION;
-
-        SERVLET_VERSION = servletVersion.isEmpty()
-                ? MISSING_VERSION_STRING
-                : servletVersion;
-    }
+    public static final String SERVLET_VERSION = "v" + BUILD_VERSION  + " b" + BUILD_NUMBER;
 
     public static final String CHAI_API_VERSION = com.novell.ldapchai.ChaiConstant.CHAI_API_VERSION;
 

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

@@ -40,6 +40,8 @@ import java.util.StringTokenizer;
 
 public class UserIdentity implements Serializable, Comparable
 {
+    private static final long serialVersionUID = 1L;
+
     private static final String CRYPO_HEADER = "ui_C-";
     private static final String DELIM_SEPARATOR = "|";
 

+ 73 - 74
server/src/main/java/password/pwm/config/PwmSettingCategory.java

@@ -22,6 +22,7 @@ package password.pwm.config;
 
 import password.pwm.i18n.Config;
 import password.pwm.util.i18n.LocaleHelper;
+import password.pwm.util.java.LazySupplier;
 import password.pwm.util.java.XmlElement;
 
 import java.util.ArrayList;
@@ -189,10 +190,10 @@ public enum PwmSettingCategory
 
     private final PwmSettingCategory parent;
 
-    private transient Supplier<PwmSetting> profileSetting;
-    private transient Supplier<Integer> level;
-    private transient Supplier<Boolean> hidden;
-    private transient Supplier<Boolean> isTopLevelProfile;
+    private transient Supplier<Optional<PwmSetting>> profileSetting = new LazySupplier<>( () -> XmlReader.readProfileSettingFromXml( this, true ) );
+    private transient Supplier<Integer> level = new LazySupplier<>( () -> XmlReader.readLevel( this ) );
+    private transient Supplier<Boolean> hidden = new LazySupplier<>( () -> XmlReader.readHidden( this ) );
+    private transient Supplier<Boolean> isTopLevelProfile = new LazySupplier<>( () -> XmlReader.readIsTopLevelProfile( this ) );
 
 
     PwmSettingCategory( final PwmSettingCategory parent )
@@ -210,28 +211,18 @@ public enum PwmSettingCategory
         return this.toString();
     }
 
-    public password.pwm.config.PwmSetting getProfileSetting( )
+    public Optional<PwmSetting> getProfileSetting( )
     {
-        if ( profileSetting == null )
-        {
-            final PwmSetting setting = readProfileSettingFromXml( true );
-            profileSetting = ( ) -> setting;
-        }
         return profileSetting.get();
     }
 
     public boolean hasProfiles( )
     {
-        return getProfileSetting() != null;
+        return getProfileSetting().isPresent();
     }
 
     public boolean isTopLevelProfile( )
     {
-        if ( isTopLevelProfile == null )
-        {
-            final boolean output = readProfileSettingFromXml( false ) != null;
-            isTopLevelProfile = ( ) -> output;
-        }
         return isTopLevelProfile.get();
     }
 
@@ -249,41 +240,12 @@ public enum PwmSettingCategory
 
     public int getLevel( )
     {
-        if ( level == null )
-        {
-            final XmlElement settingElement = PwmSettingXml.readCategoryXml( this );
-            final String levelAttribute = settingElement.getAttributeValue( "level" );
-            final int output = levelAttribute != null ? Integer.parseInt( levelAttribute ) : 0;
-            level = ( ) -> output;
-        }
         return level.get();
     }
 
+
     public boolean isHidden( )
     {
-        if ( hidden == null )
-        {
-            final XmlElement settingElement = PwmSettingXml.readCategoryXml( this );
-            final String hiddenElement = settingElement.getAttributeValue( "hidden" );
-            if ( hiddenElement != null && "true".equalsIgnoreCase( hiddenElement ) )
-            {
-                hidden = () -> true;
-            }
-            else
-            {
-                for ( final PwmSettingCategory parentCategory : getParents() )
-                {
-                    if ( parentCategory.isHidden() )
-                    {
-                        hidden = () -> true;
-                    }
-                }
-            }
-            if ( hidden == null )
-            {
-                hidden = () -> false;
-            }
-        }
         return hidden.get();
     }
 
@@ -317,34 +279,6 @@ public enum PwmSettingCategory
         return returnObj;
     }
 
-    private password.pwm.config.PwmSetting readProfileSettingFromXml( final boolean nested )
-    {
-        PwmSettingCategory nextCategory = this;
-        while ( nextCategory != null )
-        {
-            final XmlElement categoryElement = PwmSettingXml.readCategoryXml( nextCategory );
-            final Optional<XmlElement> profileElement = categoryElement.getChild( "profile" );
-            if ( profileElement.isPresent() )
-            {
-                final String settingKey = profileElement.get().getAttributeValue( "setting" );
-                if ( settingKey != null )
-                {
-                    return password.pwm.config.PwmSetting.forKey( settingKey );
-                }
-            }
-            if ( nested )
-            {
-                nextCategory = nextCategory.getParent();
-            }
-            else
-            {
-                nextCategory = null;
-            }
-        }
-
-        return null;
-    }
-
     public List<PwmSetting> getSettings( )
     {
         final List<password.pwm.config.PwmSetting> returnList = new ArrayList<>();
@@ -464,4 +398,69 @@ public enum PwmSettingCategory
                 .findFirst()
                 .orElse( null );
     }
+
+    private static class XmlReader
+    {
+
+        private static Optional<PwmSetting> readProfileSettingFromXml( final PwmSettingCategory category, final boolean nested )
+        {
+            PwmSettingCategory nextCategory = category;
+            while ( nextCategory != null )
+            {
+                final XmlElement categoryElement = PwmSettingXml.readCategoryXml( nextCategory );
+                final Optional<XmlElement> profileElement = categoryElement.getChild( "profile" );
+                if ( profileElement.isPresent() )
+                {
+                    final String settingKey = profileElement.get().getAttributeValue( "setting" );
+                    if ( settingKey != null )
+                    {
+                        return Optional.of( PwmSetting.forKey( settingKey ) );
+                    }
+                }
+                if ( nested )
+                {
+                    nextCategory = nextCategory.getParent();
+                }
+                else
+                {
+                    nextCategory = null;
+                }
+            }
+
+            return Optional.empty();
+        }
+
+        private static int readLevel( final PwmSettingCategory category )
+        {
+            final XmlElement settingElement = PwmSettingXml.readCategoryXml( category );
+            final String levelAttribute = settingElement.getAttributeValue( "level" );
+            return levelAttribute != null ? Integer.parseInt( levelAttribute ) : 0;
+        }
+
+        private static boolean readHidden( final PwmSettingCategory category )
+        {
+            final XmlElement settingElement = PwmSettingXml.readCategoryXml( category );
+            final String hiddenElement = settingElement.getAttributeValue( "hidden" );
+            if ( hiddenElement != null && "true".equalsIgnoreCase( hiddenElement ) )
+            {
+                return true;
+            }
+            else
+            {
+                for ( final PwmSettingCategory parentCategory : category.getParents() )
+                {
+                    if ( parentCategory.isHidden() )
+                    {
+                        return true;
+                    }
+                }
+            }
+            return false;
+        }
+
+        private static boolean readIsTopLevelProfile( final PwmSettingCategory category )
+        {
+            return readProfileSettingFromXml( category, false ).isPresent();
+        }
+    }
 }

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

@@ -93,7 +93,7 @@ public class ProfileUtility
 
     public static List<String> profileIDsForCategory( final Configuration configuration, final PwmSettingCategory pwmSettingCategory )
     {
-        final PwmSetting profileSetting = pwmSettingCategory.getProfileSetting();
+        final PwmSetting profileSetting = pwmSettingCategory.getProfileSetting().orElseThrow( IllegalStateException::new );
         return configuration.readSettingAsStringArray( profileSetting );
     }
 

+ 1 - 0
server/src/main/java/password/pwm/config/profile/PwmPasswordPolicy.java

@@ -54,6 +54,7 @@ import java.util.regex.Pattern;
  */
 public class PwmPasswordPolicy implements Profile, Serializable
 {
+    private static final long serialVersionUID = 1L;
 
     private static final PwmLogger LOGGER = PwmLogger.forClass( PwmPasswordPolicy.class );
 

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

@@ -250,7 +250,7 @@ public class StoredConfigurationFactory
             }
             else
             {
-                profileSetting = pwmSetting.getCategory().getProfileSetting();
+                profileSetting = pwmSetting.getCategory().getProfileSetting().orElseThrow( IllegalStateException::new );
             }
 
             final StoredValue effectiveValue;

+ 17 - 7
server/src/main/java/password/pwm/config/stored/StoredConfigurationModifier.java

@@ -39,6 +39,7 @@ import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashMap;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
@@ -195,7 +196,8 @@ public class StoredConfigurationModifier
         {
             final StoredConfiguration oldStoredConfiguration = new StoredConfigurationImpl( storedConfigData );
 
-            final List<String> existingProfiles = oldStoredConfiguration.profilesForSetting( category.getProfileSetting() );
+            final PwmSetting profileSetting = category.getProfileSetting().orElseThrow( IllegalStateException::new );
+            final List<String> existingProfiles = StoredConfigurationUtil.profilesForSetting( profileSetting, oldStoredConfiguration );
             if ( !existingProfiles.contains( sourceID ) )
             {
                 throw PwmUnrecoverableException.newException(
@@ -209,6 +211,8 @@ public class StoredConfigurationModifier
             }
 
             final Collection<PwmSettingCategory> interestedCategories = PwmSettingCategory.associatedProfileCategories( category );
+            final Map<StoredConfigItemKey, StoredValue> newValues = new LinkedHashMap<>();
+
             for ( final PwmSettingCategory interestedCategory : interestedCategories )
             {
                 for ( final PwmSetting pwmSetting : interestedCategory.getSettings() )
@@ -216,19 +220,25 @@ public class StoredConfigurationModifier
                     if ( !oldStoredConfiguration.isDefaultValue( pwmSetting, sourceID ) )
                     {
                         final StoredValue value = oldStoredConfiguration.readSetting( pwmSetting, sourceID );
-                        writeSetting( pwmSetting, destinationID, value, userIdentity );
+                        final StoredConfigItemKey key = StoredConfigItemKey.fromSetting( pwmSetting, destinationID );
+                        newValues.put( key, value );
                     }
                 }
             }
-            final List<String> newProfileIDList = new ArrayList<>( existingProfiles );
-            newProfileIDList.add( destinationID );
 
-            final StoredValue value = new StringArrayValue( newProfileIDList );
-            final StoredConfigItemKey key = StoredConfigItemKey.fromSetting( category.getProfileSetting(), null );
+            {
+                final List<String> newProfileIDList = new ArrayList<>( existingProfiles );
+                newProfileIDList.add( destinationID );
+                final StoredConfigItemKey key = StoredConfigItemKey.fromSetting( profileSetting, null );
+                final StoredValue value = new StringArrayValue( newProfileIDList );
+                newValues.put( key, value );
+            }
+
+            final StoredConfigItemKey key = StoredConfigItemKey.fromSetting( category.getProfileSetting().orElseThrow( IllegalStateException::new ), null );
             final ValueMetaData valueMetaData = new ValueMetaData( Instant.now(), userIdentity );
 
             return storedConfigData.toBuilder()
-                    .storedValue( key, value )
+                    .storedValues( newValues )
                     .metaData( key, valueMetaData )
                     .build();
 

+ 4 - 19
server/src/main/java/password/pwm/config/stored/StoredConfigurationUtil.java

@@ -76,7 +76,7 @@ public abstract class StoredConfigurationUtil
         }
         else
         {
-            profileSetting = pwmSetting.getCategory().getProfileSetting();
+            profileSetting = pwmSetting.getCategory().getProfileSetting().orElseThrow( IllegalStateException::new );
         }
 
         return profilesForProfileSetting( profileSetting, storedConfiguration );
@@ -87,35 +87,20 @@ public abstract class StoredConfigurationUtil
             final StoredConfiguration storedConfiguration
     )
     {
-        final PwmSetting profileSetting = category.getProfileSetting();
+        final PwmSetting profileSetting = category.getProfileSetting().orElseThrow( IllegalStateException::new );
 
         return profilesForProfileSetting( profileSetting, storedConfiguration );
     }
 
     private static List<String> profilesForProfileSetting(
-            final PwmSetting pwmSetting,
+            final PwmSetting profileSetting,
             final StoredConfiguration storedConfiguration
     )
     {
-        if ( !pwmSetting.getCategory().hasProfiles() && pwmSetting.getSyntax() != PwmSettingSyntax.PROFILE )
-        {
-            throw new IllegalArgumentException( "cannot build profile list for non-profile setting " + pwmSetting.toString() );
-        }
-
-        final PwmSetting profileSetting;
-        if ( pwmSetting.getSyntax() == PwmSettingSyntax.PROFILE )
-        {
-            profileSetting = pwmSetting;
-        }
-        else
-        {
-            profileSetting = pwmSetting.getCategory().getProfileSetting();
-        }
-
         final Object nativeObject = storedConfiguration.readSetting( profileSetting, null ).toNativeObject();
         final List<String> settingValues = ( List<String> ) nativeObject;
         final LinkedList<String> profiles = new LinkedList<>( settingValues );
-        profiles.removeIf( profile -> StringUtil.isEmpty( profile ) );
+        profiles.removeIf( StringUtil::isEmpty );
         return Collections.unmodifiableList( profiles );
 
     }

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

@@ -313,7 +313,7 @@ public class ActionValue extends AbstractValue implements StoredValue
                 {
                     sb.append( "\n    successStatus=" ).append( StringUtil.collectionToString( webAction.getSuccessStatus() ) );
                 }
-                if ( StringUtil.isEmpty( webAction.getBody() ) )
+                if ( !StringUtil.isEmpty( webAction.getBody() ) )
                 {
                     sb.append( "\n    body=" ).append( webAction.getBody() );
                 }

+ 1 - 0
server/src/main/java/password/pwm/config/value/NamedSecretValue.java

@@ -52,6 +52,7 @@ import java.util.Optional;
 
 public class NamedSecretValue implements StoredValue
 {
+    private static final long serialVersionUID = 1L;
 
     private final transient LazySupplier<String> valueHashSupplier = new LazySupplier<>( () -> AbstractValue.valueHashComputer( NamedSecretValue.this ) );
 

+ 2 - 0
server/src/main/java/password/pwm/config/value/PasswordValue.java

@@ -45,6 +45,8 @@ import java.util.Optional;
 
 public class PasswordValue implements StoredValue
 {
+    private static final long serialVersionUID = 1L;
+
     private final transient LazySupplier<String> valueHashSupplier = new LazySupplier<>( () -> AbstractValue.valueHashComputer( PasswordValue.this ) );
 
     private final PasswordData value;

+ 3 - 2
server/src/main/java/password/pwm/config/value/StringValue.java

@@ -27,6 +27,7 @@ import password.pwm.config.stored.StoredConfigXmlConstants;
 import password.pwm.config.stored.XmlOutputProcessData;
 import password.pwm.config.value.data.FormConfiguration;
 import password.pwm.util.java.JsonUtil;
+import password.pwm.util.java.StringUtil;
 import password.pwm.util.java.XmlElement;
 import password.pwm.util.java.XmlFactory;
 import password.pwm.util.secure.PwmSecurityKey;
@@ -86,14 +87,14 @@ public class StringValue extends AbstractValue implements StoredValue
     {
         if ( pwmSetting.isRequired() )
         {
-            if ( value == null || value.length() < 1 )
+            if ( StringUtil.isEmpty( value ) )
             {
                 return Collections.singletonList( "required value missing" );
             }
         }
 
         final Pattern pattern = pwmSetting.getRegExPattern();
-        if ( pattern != null && value != null )
+        if ( pattern != null )
         {
             final Matcher matcher = pattern.matcher( value );
             if ( value != null && value.length() > 0 && !matcher.matches() )

+ 9 - 7
server/src/main/java/password/pwm/config/value/data/CustomLinkConfiguration.java

@@ -20,6 +20,7 @@
 
 package password.pwm.config.value.data;
 
+import lombok.EqualsAndHashCode;
 import lombok.Value;
 import password.pwm.util.i18n.LocaleHelper;
 import password.pwm.util.java.JsonUtil;
@@ -33,6 +34,7 @@ import java.util.Map;
  * @author Richard A. Keil
  */
 @Value
+@EqualsAndHashCode( callSuper = false )
 public class CustomLinkConfiguration implements Serializable
 {
 
@@ -41,13 +43,13 @@ public class CustomLinkConfiguration implements Serializable
         text, url, select, checkbox, customLink
     }
 
-    private String name;
-    private Type type = Type.customLink;
-    private Map<String, String> labels = Collections.singletonMap( "", "" );
-    private Map<String, String> description = Collections.singletonMap( "", "" );
-    private String customLinkUrl = "";
-    private boolean customLinkNewWindow;
-    private Map<String, String> selectOptions = Collections.emptyMap();
+    private final String name;
+    private final Type type = Type.customLink;
+    private final Map<String, String> labels = Collections.singletonMap( "", "" );
+    private final Map<String, String> description = Collections.singletonMap( "", "" );
+    private final String customLinkUrl = "";
+    private final boolean customLinkNewWindow;
+    private final Map<String, String> selectOptions = Collections.emptyMap();
 
     public String getLabel( final Locale locale )
     {

+ 9 - 1
server/src/main/java/password/pwm/http/ContextManager.java

@@ -77,6 +77,8 @@ import java.util.concurrent.atomic.AtomicInteger;
 
 public class ContextManager implements Serializable
 {
+    private static final long serialVersionUID = 1L;
+
     private static final PwmLogger LOGGER = PwmLogger.forClass( ContextManager.class );
     private static final SessionLabel SESSION_LABEL = SessionLabel.CONTEXT_SESSION_LABEL;
 
@@ -444,7 +446,13 @@ public class ContextManager implements Serializable
                     try
                     {
                         final PropertyConfigurationImporter importer = new PropertyConfigurationImporter();
-                        final StoredConfiguration storedConfiguration = importer.readConfiguration( new FileInputStream( silentPropertiesFile ) );
+
+                        final StoredConfiguration storedConfiguration;
+                        try ( InputStream fileInputStream = new FileInputStream( silentPropertiesFile ) )
+                        {
+                            storedConfiguration = importer.readConfiguration( fileInputStream );
+                        }
+
                         configReader.saveConfiguration( storedConfiguration, pwmApplication, SESSION_LABEL );
                         LOGGER.info( SESSION_LABEL, () -> "file " + silentPropertiesFile.getAbsolutePath() + " has been successfully imported and saved as configuration file" );
                         requestPwmApplicationRestart();

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

@@ -58,6 +58,8 @@ import java.util.Map;
  */
 public class PwmSession implements Serializable
 {
+    private static final long serialVersionUID = 1L;
+
     private static final PwmLogger LOGGER = PwmLogger.forClass( PwmSession.class );
 
     private final transient PwmApplication pwmApplication;

+ 7 - 2
server/src/main/java/password/pwm/http/auth/HttpAuthenticationUtilities.java

@@ -88,6 +88,11 @@ public abstract class HttpAuthenticationUtilities
                             return ProcessStatus.Halt;
                         }
 
+                        if ( pwmRequest.isAuthenticated() )
+                        {
+                            return ProcessStatus.Continue;
+                        }
+
                     }
                     catch ( final Exception e )
                     {
@@ -96,13 +101,13 @@ public abstract class HttpAuthenticationUtilities
                         {
                             final String errorMsg = "error during " + authenticationMethod + " authentication attempt: " + e.getMessage();
                             errorInformation = new ErrorInformation( ( ( PwmException ) e ).getError(), errorMsg );
+                            LOGGER.error( pwmRequest, errorInformation );
                         }
                         else
                         {
                             errorInformation = new ErrorInformation( PwmError.ERROR_INTERNAL, e.getMessage() );
-
+                            LOGGER.error( pwmRequest.getLabel(), errorInformation, e );
                         }
-                        LOGGER.error( pwmRequest, errorInformation );
                         pwmRequest.respondWithError( errorInformation );
                         return ProcessStatus.Halt;
                     }

+ 2 - 0
server/src/main/java/password/pwm/http/bean/ConfigManagerBean.java

@@ -30,6 +30,8 @@ import java.util.Set;
 @Data
 public class ConfigManagerBean extends PwmSessionBean
 {
+    private static final long serialVersionUID = 1L;
+
     private transient StoredConfiguration storedConfiguration;
     private boolean passwordVerified;
     private boolean configUnlockedWarningShown;

+ 4 - 0
server/src/main/java/password/pwm/http/bean/ForgottenPasswordBean.java

@@ -49,6 +49,8 @@ import java.util.Set;
 @EqualsAndHashCode( callSuper = false )
 public class ForgottenPasswordBean extends PwmSessionBean
 {
+    private static final long serialVersionUID = 1L;
+
     @SerializedName( "pr" )
     private String profile;
 
@@ -82,6 +84,8 @@ public class ForgottenPasswordBean extends PwmSessionBean
     @Data
     public static class Progress implements Serializable
     {
+        private static final long serialVersionUID = 1L;
+
         @SerializedName( "s" )
         private boolean tokenSent;
 

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

@@ -357,7 +357,8 @@ public class RequestInitializationFilter implements Filter
             if ( contentPolicy != null && !contentPolicy.isEmpty() )
             {
                 final String nonce = pwmRequest.getCspNonce();
-                final String expandedPolicy = contentPolicy.replace( "%NONCE%", nonce );
+                final String replacedPolicy = contentPolicy.replace( "%NONCE%", nonce );
+                final String expandedPolicy = MacroMachine.forNonUserSpecific( pwmRequest.getPwmApplication(), null ).expandMacros( replacedPolicy );
                 resp.setHeader( HttpHeader.ContentSecurityPolicy, expandedPolicy );
             }
         }

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

@@ -106,7 +106,8 @@ public class ClientApiServlet extends ControlledPwmServlet
         strings( HttpMethod.GET ),
         health( HttpMethod.GET ),
         ping( HttpMethod.GET ),
-        statistics( HttpMethod.GET ),;
+        statistics( HttpMethod.GET ),
+        cspReport( HttpMethod.POST ),;
 
 
         private final HttpMethod method;
@@ -446,7 +447,6 @@ public class ClientApiServlet extends ControlledPwmServlet
         return displayStrings;
     }
 
-
     @ActionHandler( action = "statistics" )
     private ProcessStatus restStatisticsHandler( final PwmRequest pwmRequest )
             throws ChaiUnavailableException, PwmUnrecoverableException, IOException
@@ -473,7 +473,20 @@ public class ClientApiServlet extends ControlledPwmServlet
         final RestResultBean restResultBean = RestResultBean.withData( jsonOutput );
         pwmRequest.outputJsonResult( restResultBean );
         return ProcessStatus.Halt;
+    }
+
+    @ActionHandler( action = "cspReport" )
+    private ProcessStatus restCspReportHandler( final PwmRequest pwmRequest )
+            throws PwmUnrecoverableException, IOException
+    {
+        if ( !Boolean.parseBoolean( pwmRequest.getConfig().readAppProperty( AppProperty.LOGGING_LOG_CSP_REPORT ) ) )
+        {
+            return ProcessStatus.Halt;
+        }
 
+        final String body = pwmRequest.readRequestBodyAsString();
+        LOGGER.trace( () -> body );
+        return ProcessStatus.Halt;
     }
 
     private void precheckPublicHealthAndStats( final PwmRequest pwmRequest )

+ 6 - 0
server/src/main/java/password/pwm/http/servlet/admin/AppDashboardData.java

@@ -193,6 +193,12 @@ public class AppDashboardData implements Serializable
                 l.forKey( "Field_AppVersion", PwmConstants.PWM_APP_NAME ),
                 PwmConstants.SERVLET_VERSION
         ) );
+        aboutData.add( new DisplayElement(
+                "appBuildTime",
+                DisplayElement.Type.timestamp,
+                l.forKey( "Field_AppBuildTime" ),
+                PwmConstants.BUILD_TIME
+        ) );
         aboutData.add( new DisplayElement(
                 "currentTime",
                 DisplayElement.Type.timestamp,

+ 2 - 0
server/src/main/java/password/pwm/http/servlet/admin/UserDebugDataBean.java

@@ -36,6 +36,8 @@ import java.util.Map;
 @Builder
 public class UserDebugDataBean implements Serializable
 {
+    private static final long serialVersionUID = 1L;
+
     private transient UserInfo userInfo;
 
     private final PublicUserInfoBean publicUserInfoBean;

+ 67 - 1
server/src/main/java/password/pwm/http/servlet/configeditor/ConfigEditorServlet.java

@@ -25,6 +25,7 @@ import password.pwm.AppProperty;
 import password.pwm.PwmApplication;
 import password.pwm.PwmApplicationMode;
 import password.pwm.PwmConstants;
+import password.pwm.bean.EmailItemBean;
 import password.pwm.bean.SessionLabel;
 import password.pwm.bean.SmsItemBean;
 import password.pwm.bean.UserIdentity;
@@ -36,6 +37,7 @@ import password.pwm.config.PwmSettingTemplate;
 import password.pwm.config.PwmSettingTemplateSet;
 import password.pwm.config.SettingUIFunction;
 import password.pwm.config.StoredValue;
+import password.pwm.config.profile.EmailServerProfile;
 import password.pwm.config.profile.PwmPasswordPolicy;
 import password.pwm.config.stored.ConfigurationProperty;
 import password.pwm.config.stored.StoredConfigItemKey;
@@ -72,6 +74,9 @@ import password.pwm.i18n.Config;
 import password.pwm.i18n.Message;
 import password.pwm.i18n.PwmLocaleBundle;
 import password.pwm.ldap.LdapBrowser;
+import password.pwm.svc.email.EmailServer;
+import password.pwm.svc.email.EmailServerUtil;
+import password.pwm.svc.email.EmailService;
 import password.pwm.util.PasswordData;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.JsonUtil;
@@ -86,6 +91,7 @@ import password.pwm.ws.server.RestResultBean;
 import password.pwm.ws.server.rest.RestRandomPasswordServer;
 import password.pwm.ws.server.rest.bean.HealthData;
 
+import javax.mail.MessagingException;
 import javax.servlet.ServletException;
 import javax.servlet.annotation.WebServlet;
 import java.io.ByteArrayInputStream;
@@ -100,6 +106,7 @@ import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
+import java.util.Optional;
 import java.util.ResourceBundle;
 import java.util.Set;
 import java.util.StringTokenizer;
@@ -133,6 +140,7 @@ public class ConfigEditorServlet extends ControlledPwmServlet
         ldapHealthCheck( HttpMethod.POST ),
         databaseHealthCheck( HttpMethod.POST ),
         smsHealthCheck( HttpMethod.POST ),
+        emailHealthCheck( HttpMethod.POST ),
         finishEditing( HttpMethod.POST ),
         executeSettingFunction( HttpMethod.POST ),
         setConfigurationPassword( HttpMethod.POST ),
@@ -743,6 +751,57 @@ public class ConfigEditorServlet extends ControlledPwmServlet
         return ProcessStatus.Halt;
     }
 
+    @ActionHandler( action = "emailHealthCheck" )
+    private ProcessStatus restEmailHealthCheck(
+            final PwmRequest pwmRequest
+    )
+            throws IOException, PwmUnrecoverableException
+    {
+        final Instant startTime = Instant.now();
+        final ConfigManagerBean configManagerBean = getBean( pwmRequest );
+        final String profileID = pwmRequest.readParameterAsString( "profile" );
+
+        LOGGER.debug( pwmRequest, () -> "beginning restEmailHealthCheck" );
+
+        final Map<String, String> params = pwmRequest.readBodyAsJsonStringMap();
+        final EmailItemBean testEmailItem = new EmailItemBean( params.get( "to" ), params.get( "from" ), params.get( "subject" ), params.get( "body" ), null );
+
+        final List<HealthRecord> returnRecords = new ArrayList<>();
+
+        final Configuration testConfiguration = new Configuration( configManagerBean.getStoredConfiguration() );
+
+        final EmailServerProfile emailServerProfile = testConfiguration.getEmailServerProfiles().get( profileID );
+        if ( emailServerProfile != null )
+        {
+            final Optional<EmailServer> emailServer = EmailServerUtil.makeEmailServer( testConfiguration, emailServerProfile, null );
+            if ( emailServer.isPresent() )
+            {
+                final MacroMachine macroMachine = MacroMachine.forUser( pwmRequest, pwmRequest.getUserInfoIfLoggedIn() );
+
+                try
+                {
+                    EmailService.sendEmailSynchronous( emailServer.get(), testConfiguration, testEmailItem, macroMachine );
+                    returnRecords.add( new HealthRecord( HealthStatus.INFO, HealthTopic.Email, "message sent" ) );
+                }
+                catch ( final MessagingException | PwmException e )
+                {
+                    returnRecords.add( new HealthRecord( HealthStatus.WARN, HealthTopic.Email, JavaHelper.readHostileExceptionMessage( e ) ) );
+                }
+            }
+        }
+
+        if ( returnRecords.isEmpty() )
+        {
+            returnRecords.add( new HealthRecord( HealthStatus.WARN, HealthTopic.Email, "smtp service is not configured." ) );
+        }
+
+        final HealthData healthData = HealthRecord.asHealthDataBean( testConfiguration, pwmRequest.getLocale(), returnRecords );
+        final RestResultBean restResultBean = RestResultBean.withData( healthData );
+        pwmRequest.outputJsonResult( restResultBean );
+        LOGGER.debug( pwmRequest, () -> "completed restEmailHealthCheck in " + TimeDuration.fromCurrent( startTime ).asCompactString() );
+        return ProcessStatus.Halt;
+    }
+
     @ActionHandler( action = "uploadFile" )
     private ProcessStatus doUploadFile(
             final PwmRequest pwmRequest
@@ -1061,7 +1120,8 @@ public class ConfigEditorServlet extends ControlledPwmServlet
         {
             if ( loopCategory.hasProfiles() )
             {
-                if ( loopCategory.getProfileSetting() == setting )
+                final Optional<PwmSetting> profileSetting = loopCategory.getProfileSetting();
+                if ( profileSetting.isPresent() && profileSetting.get() == setting )
                 {
                     category = loopCategory;
                 }
@@ -1070,6 +1130,12 @@ public class ConfigEditorServlet extends ControlledPwmServlet
 
         final String sourceID = inputMap.get( "sourceID" );
         final String destinationID = inputMap.get( "destinationID" );
+
+        if ( category == null )
+        {
+            throw new IllegalStateException();
+        }
+
         try
         {
             modifier.copyProfileID( category, sourceID, destinationID, pwmRequest.getUserInfoIfLoggedIn() );

+ 6 - 2
server/src/main/java/password/pwm/http/servlet/configeditor/NavTreeHelper.java

@@ -106,11 +106,14 @@ class NavTreeHelper
 
         if ( category.hasProfiles() )
         {
-            final List<String> profileIDs = storedConfiguration.profilesForSetting( category.getProfileSetting() );
+            final List<String> profileIDs = storedConfiguration.profilesForSetting(
+                    category.getProfileSetting().orElseThrow( IllegalStateException::new ) );
+
             if ( profileIDs == null || profileIDs.isEmpty() )
             {
                 return true;
             }
+
             for ( final String profileID : profileIDs )
             {
                 for ( final PwmSetting setting : category.getSettings() )
@@ -199,7 +202,8 @@ class NavTreeHelper
                         final String editItemName = LocaleHelper.getLocalizedMessage( locale, Config.Label_ProfileListEditMenuItem, null );
                         profileEditorInfo.setName( editItemName );
                         profileEditorInfo.setType( NavTreeHelper.NavItemType.profileDefinition );
-                        profileEditorInfo.setProfileSetting( loopCategory.getProfileSetting().getKey() );
+                        final PwmSetting profileSetting = loopCategory.getProfileSetting().orElseThrow( IllegalStateException::new );
+                        profileEditorInfo.setProfileSetting( profileSetting.getKey() );
                         profileEditorInfo.setParent( loopCategory.getKey() );
                         navigationData.add( profileEditorInfo );
                     }

+ 5 - 0
server/src/main/java/password/pwm/http/servlet/helpdesk/HelpdeskServlet.java

@@ -208,6 +208,11 @@ public class HelpdeskServlet extends ControlledPwmServlet
             return ProcessStatus.Halt;
         }
 
+        // verify the chaiProvider is available - ie, password is supplied, proxy available etc.
+        // we do this now so redirects can handle properly instead of during a later rest request.
+        final UserIdentity loggedInUser = pwmRequest.getPwmSession().getUserInfo().getUserIdentity();
+        getChaiUser( pwmRequest, helpdeskProfile, loggedInUser ).getChaiProvider();
+
         return ProcessStatus.Continue;
     }
 

+ 2 - 0
server/src/main/java/password/pwm/http/servlet/helpdesk/HelpdeskVerificationStateBean.java

@@ -49,6 +49,8 @@ import java.util.TreeMap;
 
 class HelpdeskVerificationStateBean implements Serializable
 {
+    private static final long serialVersionUID = 1L;
+
     private static final PwmLogger LOGGER = PwmLogger.forClass( HelpdeskVerificationStateBean.class );
     public static final String PARAMETER_VERIFICATION_STATE_KEY = "verificationState";
 

+ 1 - 1
server/src/main/java/password/pwm/http/state/CryptoCookieLoginImpl.java

@@ -128,7 +128,7 @@ class CryptoCookieLoginImpl implements SessionLoginProvider
 
                 if ( remoteLoginCookie.getType() == AuthenticationType.AUTH_WITHOUT_PASSWORD && remoteLoginCookie.getUserCurrentPassword() == null )
                 {
-                    LOGGER.debug( () -> "remote session has authType " + AuthenticationType.AUTH_WITHOUT_PASSWORD.name()
+                    LOGGER.debug( pwmRequest, () -> "remote session has authType " + AuthenticationType.AUTH_WITHOUT_PASSWORD.name()
                             + " and does not contain password, thus ignoring authentication so SSO process can repeat" );
                     return;
                 }

+ 2 - 1
server/src/main/java/password/pwm/i18n/PwmLocaleBundle.java

@@ -93,7 +93,8 @@ public enum PwmLocaleBundle
         return Collections.unmodifiableSet( new HashSet<>( Arrays.asList(
                 this.getTheClass().getSimpleName(),
                 this.getTheClass().getName(),
-                "password.pwm." + this.getTheClass().getSimpleName()
+                "password.pwm." + this.getTheClass().getSimpleName(),
+                this.name()
         ) ) );
     }
 

+ 9 - 1
server/src/main/java/password/pwm/ldap/LdapConnectionService.java

@@ -75,7 +75,7 @@ public class LdapConnectionService implements PwmService
     private final ThreadLocal<ThreadLocalContainer> threadLocalProvider = new ThreadLocal<>();
     private final Set<ThreadLocalContainer> threadLocalContainers = Collections.synchronizedSet( Collections.newSetFromMap( new WeakHashMap<>() ) );
     private final ReentrantLock reentrantLock = new ReentrantLock();
-    private final ConditionalTaskExecutor debugLogger = ConditionalTaskExecutor.forPeriodicTask( this::logDebugInfo, TimeDuration.MINUTE );
+    private final ConditionalTaskExecutor debugLogger = ConditionalTaskExecutor.forPeriodicTask( this::conditionallyLogDebugInfo, TimeDuration.MINUTE );
     private final ChaiProviderFactory chaiProviderFactory = ChaiProviderFactory.newProviderFactory();
     private final Map<String, Map<Integer, ChaiProvider>> proxyChaiProviders = new HashMap<>();
 
@@ -377,6 +377,14 @@ public class LdapConnectionService implements PwmService
         return chaiProviderFactory;
     }
 
+    private void conditionallyLogDebugInfo()
+    {
+        if ( !chaiProviderFactory.activeProviders().isEmpty() )
+        {
+            logDebugInfo();
+        }
+    }
+
     private void logDebugInfo()
     {
         LOGGER.trace( () -> "status: " + StringUtil.mapToString( connectionDebugInfo() ) );

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

@@ -39,6 +39,8 @@ import java.util.Map;
 @Builder( toBuilder = true )
 public class SearchConfiguration implements Serializable
 {
+    private static final long serialVersionUID = 1L;
+
     private String filter;
     private String ldapProfile;
     private String username;
@@ -48,6 +50,9 @@ public class SearchConfiguration implements Serializable
     private transient ChaiProvider chaiProvider;
     private long searchTimeout;
 
+    @Builder.Default
+    private boolean ignoreOperationalErrors = false;
+
     @Builder.Default
     private boolean enableValueEscaping = true;
 
@@ -57,6 +62,27 @@ public class SearchConfiguration implements Serializable
     @Builder.Default
     private boolean enableSplitWhitespace = false;
 
+    @Builder.Default
+    private SearchScope searchScope = SearchScope.subtree;
+
+    public enum SearchScope
+    {
+        base( com.novell.ldapchai.provider.SearchScope.BASE ),
+        subtree( com.novell.ldapchai.provider.SearchScope.SUBTREE ),;
+
+        private final com.novell.ldapchai.provider.SearchScope chaiSearchScope;
+
+        SearchScope( final com.novell.ldapchai.provider.SearchScope chaiSearchScope )
+        {
+            this.chaiSearchScope = chaiSearchScope;
+        }
+
+        public com.novell.ldapchai.provider.SearchScope getChaiSearchScope()
+        {
+            return chaiSearchScope;
+        }
+    }
+
     void validate( )
     {
         if ( this.username != null && this.formValues != null )

+ 34 - 27
server/src/main/java/password/pwm/ldap/search/UserSearchEngine.java

@@ -181,7 +181,7 @@ public class UserSearchEngine implements PwmService
             //see if we need to do a contextless search.
             if ( checkIfStringIsDN( username, sessionLabel ) )
             {
-                return resolveUserDN( username );
+                return resolveUserDN( username, sessionLabel );
             }
             else
             {
@@ -207,10 +207,6 @@ public class UserSearchEngine implements PwmService
                     e.getErrorInformation().getFieldValues() )
             );
         }
-        catch ( final ChaiUnavailableException e )
-        {
-            throw PwmUnrecoverableException.fromChaiException( e );
-        }
     }
 
     public UserIdentity performSingleUserSearch(
@@ -450,6 +446,8 @@ public class UserSearchEngine implements PwmService
                     .sessionLabel( sessionLabel )
                     .searchID( searchID )
                     .jobId( jobIncrementer.next() )
+                    .searchScope( searchConfiguration.getSearchScope() )
+                    .ignoreOperationalErrors( searchConfiguration.isIgnoreOperationalErrors() )
                     .build();
             final UserSearchJob userSearchJob = new UserSearchJob( pwmApplication, this, userSearchJobParameters );
             returnMap.add( userSearchJob );
@@ -574,30 +572,38 @@ public class UserSearchEngine implements PwmService
         return false;
     }
 
-
     private UserIdentity resolveUserDN(
-            final String userDN
+            final String userDN,
+            final SessionLabel sessionLabel
     )
-            throws PwmUnrecoverableException, ChaiUnavailableException, PwmOperationalException
+            throws PwmUnrecoverableException, PwmOperationalException
     {
-        final Collection<LdapProfile> ldapProfiles = pwmApplication.getConfig().getLdapProfiles().values();
-        for ( final LdapProfile ldapProfile : ldapProfiles )
+        LOGGER.trace( sessionLabel, () -> "finding profile for userDN " + userDN );
+        final SearchConfiguration searchConfiguration = SearchConfiguration.builder()
+                .filter( "(objectClass=*)" )
+                .enableContextValidation( false )
+                .contexts( Collections.singletonList( userDN ) )
+                .searchScope( SearchConfiguration.SearchScope.base )
+                .ignoreOperationalErrors( true )
+                .build();
+        final Map<UserIdentity, Map<String, String>> results = performMultiUserSearch(
+                searchConfiguration,
+                1,
+                Collections.singleton( "objectClass" ),
+                sessionLabel );
+
+        if ( results.size() < 1 )
         {
-            final ChaiProvider provider = pwmApplication.getProxyChaiProvider( ldapProfile.getIdentifier() );
-            final ChaiUser user = provider.getEntryFactory().newChaiUser( userDN );
-            if ( user.exists() )
-            {
-                try
-                {
-                    return new UserIdentity( user.readCanonicalDN(), ldapProfile.getIdentifier() );
-                }
-                catch ( final ChaiOperationException e )
-                {
-                    LOGGER.error( () -> "unexpected error reading canonical userDN for '" + userDN + "', error: " + e.getMessage() );
-                }
-            }
+            throw new PwmOperationalException( new ErrorInformation( PwmError.ERROR_CANT_MATCH_USER ) );
         }
-        throw new PwmOperationalException( new ErrorInformation( PwmError.ERROR_CANT_MATCH_USER ) );
+        else if ( results.size() > 1 )
+        {
+            throw new PwmOperationalException( new ErrorInformation( PwmError.ERROR_CANT_MATCH_USER, "duplicate DN matches discovered" ) );
+        }
+
+        final UserIdentity userIdentity = results.keySet().iterator().next();
+        validateSpecifiedContext( userIdentity.getLdapProfile( pwmApplication.getConfig() ), userIdentity.getUserDN() );
+        return userIdentity;
     }
 
     private Map<UserIdentity, Map<String, String>> executeSearchJobs(
@@ -671,7 +677,7 @@ public class UserSearchEngine implements PwmService
     }
 
     private Map<UserIdentity, Map<String, String>> aggregateJobResults(
-          final Collection<UserSearchJob> userSearchJobs
+            final Collection<UserSearchJob> userSearchJobs
     )
             throws PwmUnrecoverableException
     {
@@ -800,11 +806,12 @@ public class UserSearchEngine implements PwmService
             final int maxThreads = Integer.parseInt( configuration.readAppProperty( AppProperty.LDAP_SEARCH_PARALLEL_THREAD_MAX ) );
             final int threads = Math.min( maxThreads, ( endPoints ) * factor );
             final ThreadFactory threadFactory = PwmScheduler.makePwmThreadFactory( PwmScheduler.makeThreadName( pwmApplication, UserSearchEngine.class ), true );
+            final int minThreads = JavaHelper.rangeCheck( 1, 10, endPoints );
 
-            LOGGER.trace( () -> "initialized with " + threads + " max threads" );
+            LOGGER.trace( () -> "initialized with threads min=" + minThreads + " max=" + threads );
 
             return new ThreadPoolExecutor(
-                    1,
+                    minThreads,
                     threads,
                     1,
                     TimeUnit.MINUTES,

+ 9 - 4
server/src/main/java/password/pwm/ldap/search/UserSearchJob.java

@@ -69,6 +69,8 @@ class UserSearchJob implements Callable<Map<UserIdentity, Map<String, String>>>
         searchHelper.setFilter( userSearchJobParameters.getSearchFilter() );
         searchHelper.setAttributes( userSearchJobParameters.getReturnAttributes() );
         searchHelper.setTimeLimit( ( int ) userSearchJobParameters.getTimeoutMs() );
+        searchHelper.setSearchScope( userSearchJobParameters.getSearchScope().getChaiSearchScope() );
+
 
         final String debugInfo;
         {
@@ -86,10 +88,10 @@ class UserSearchJob implements Callable<Map<UserIdentity, Map<String, String>>>
                         + debugInfo );
 
         final Instant startTime = Instant.now();
-        final Map<String, Map<String, String>> results;
+        final Map<String, Map<String, String>> results = new LinkedHashMap<>();
         try
         {
-            results = userSearchJobParameters.getChaiProvider().search( userSearchJobParameters.getContext(), searchHelper );
+            results.putAll( userSearchJobParameters.getChaiProvider().search( userSearchJobParameters.getContext(), searchHelper ) );
         }
         catch ( final ChaiUnavailableException e )
         {
@@ -97,8 +99,11 @@ class UserSearchJob implements Callable<Map<UserIdentity, Map<String, String>>>
         }
         catch ( final ChaiOperationException e )
         {
-            throw new PwmOperationalException( PwmError.forChaiError( e.getErrorCode() ), "ldap error during searchID="
-                    + userSearchJobParameters.getSearchID() + ", context=" + userSearchJobParameters.getContext() + ", error=" + e.getMessage() );
+            if ( !userSearchJobParameters.isIgnoreOperationalErrors() )
+            {
+                throw new PwmOperationalException( PwmError.forChaiError( e.getErrorCode() ), "ldap error during searchID="
+                        + userSearchJobParameters.getSearchID() + ", context=" + userSearchJobParameters.getContext() + ", error=" + e.getMessage() );
+            }
         }
 
         final TimeDuration searchDuration = TimeDuration.fromCurrent( startTime );

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

@@ -42,4 +42,6 @@ public class UserSearchJobParameters
     private final SessionLabel sessionLabel;
     private final int searchID;
     private final int jobId;
+    private final SearchConfiguration.SearchScope searchScope;
+    private final boolean ignoreOperationalErrors;
 }

+ 1 - 1
server/src/main/java/password/pwm/svc/PwmServiceEnum.java

@@ -43,7 +43,7 @@ public enum PwmServiceEnum
     StatisticsManager( password.pwm.svc.stats.StatisticsManager.class, Flag.StartDuringRuntimeInstance ),
     WordlistManager( WordlistService.class, Flag.StartDuringRuntimeInstance ),
     SeedlistManager( SeedlistService.class ),
-    EmailQueueManager( EmailService.class ),
+    EmailQueueManager( EmailService.class, Flag.StartDuringRuntimeInstance ),
     SmsQueueManager( password.pwm.util.queue.SmsQueueManager.class ),
     UrlShortenerService( password.pwm.svc.shorturl.UrlShortenerService.class ),
     TokenService( password.pwm.svc.token.TokenService.class, Flag.StartDuringRuntimeInstance ),

+ 17 - 11
server/src/main/java/password/pwm/svc/email/EmailServerUtil.java

@@ -87,7 +87,7 @@ public class EmailServerUtil
         return returnObj;
     }
 
-    private static Optional<EmailServer> makeEmailServer(
+    public static Optional<EmailServer> makeEmailServer(
             final Configuration configuration,
             final EmailServerProfile profile,
             final TrustManager[] trustManagers
@@ -105,7 +105,10 @@ public class EmailServerUtil
                 && port > 0
         )
         {
-            final Properties properties = makeJavaMailProps( configuration, profile, trustManagers );
+            final TrustManager[] effectiveTrustManagers = trustManagers == null
+                    ? trustManagerForProfile( configuration, profile )
+                    : trustManagers;
+            final Properties properties = makeJavaMailProps( configuration, profile, effectiveTrustManagers );
             final javax.mail.Session session = javax.mail.Session.getInstance( properties, null );
             return Optional.of( EmailServer.builder()
                     .id( id )
@@ -177,16 +180,20 @@ public class EmailServerUtil
 
             final MailSSLSocketFactory mailSSLSocketFactory = new MailSSLSocketFactory();
             mailSSLSocketFactory.setTrustManagers( trustManager );
-
-            properties.put( "mail.smtp.ssl.enable", true );
-            properties.put( "mail.smtp.ssl.checkserveridentity", true );
-            properties.put( "mail.smtp.socketFactory.fallback", false );
             properties.put( "mail.smtp.ssl.socketFactory", mailSSLSocketFactory );
-            properties.put( "mail.smtp.ssl.socketFactory.port", port );
 
-            final boolean useStartTls = smtpServerType == SmtpServerType.START_TLS;
-            properties.put( "mail.smtp.starttls.enable", useStartTls );
-            properties.put( "mail.smtp.starttls.required", useStartTls );
+            if ( smtpServerType == SmtpServerType.SMTPS )
+            {
+                properties.put( "mail.smtp.ssl.enable", true );
+                properties.put( "mail.smtp.ssl.checkserveridentity", true );
+                properties.put( "mail.smtp.socketFactory.fallback", false );
+                properties.put( "mail.smtp.ssl.socketFactory.port", port );
+            }
+            else if ( smtpServerType == SmtpServerType.START_TLS )
+            {
+                properties.put( "mail.smtp.starttls.enable", true );
+                properties.put( "mail.smtp.starttls.required", true );
+            }
         }
         catch ( final Exception e )
         {
@@ -413,5 +420,4 @@ public class EmailServerUtil
 
         return Collections.emptyList();
     }
-
 }

+ 88 - 32
server/src/main/java/password/pwm/svc/email/EmailService.java

@@ -225,38 +225,52 @@ public class EmailService implements PwmService
 
     private boolean determineIfItemCanBeDelivered( final EmailItemBean emailItem )
     {
-
         if ( servers.isEmpty() )
         {
-            LOGGER.debug( () -> "discarding email send event (no SMTP server address configured) " + emailItem.toDebugString() );
+            LOGGER.debug( () -> "discarding email send event, no email servers configured" );
             return false;
         }
 
-        if ( emailItem.getFrom() == null || emailItem.getFrom().length() < 1 )
+        try
         {
-            LOGGER.error( () -> "discarding email event (no from address): " + emailItem.toDebugString() );
-            return false;
+            validateEmailItem( emailItem );
+            return true;
+        }
+        catch ( final PwmOperationalException e )
+        {
+            LOGGER.debug( () -> "discarding email send event: " + e.getMessage() );
         }
+        return false;
+    }
+
+    private static void validateEmailItem( final EmailItemBean emailItem )
+            throws PwmOperationalException
+    {
+
 
-        if ( emailItem.getTo() == null || emailItem.getTo().length() < 1 )
+        if ( StringUtil.isEmpty( emailItem.getFrom() ) )
         {
-            LOGGER.error( () -> "discarding email event (no to address): " + emailItem.toDebugString() );
-            return false;
+            throw new PwmOperationalException( new ErrorInformation( PwmError.ERROR_EMAIL_SEND_FAILURE,
+                    "missing from address in email item" ) );
         }
 
-        if ( emailItem.getSubject() == null || emailItem.getSubject().length() < 1 )
+        if ( StringUtil.isEmpty( emailItem.getTo() ) )
         {
-            LOGGER.error( () -> "discarding email event (no subject): " + emailItem.toDebugString() );
-            return false;
+            throw new PwmOperationalException( new ErrorInformation( PwmError.ERROR_EMAIL_SEND_FAILURE,
+                    "missing to address in email item" ) );
         }
 
-        if ( ( emailItem.getBodyPlain() == null || emailItem.getBodyPlain().length() < 1 ) && ( emailItem.getBodyHtml() == null || emailItem.getBodyHtml().length() < 1 ) )
+        if ( StringUtil.isEmpty( emailItem.getSubject() ) )
         {
-            LOGGER.error( () -> "discarding email event (no body): " + emailItem.toDebugString() );
-            return false;
+            throw new PwmOperationalException( new ErrorInformation( PwmError.ERROR_EMAIL_SEND_FAILURE,
+                    "missing subject in email item" ) );
         }
 
-        return true;
+        if ( StringUtil.isEmpty( emailItem.getBodyPlain() ) && StringUtil.isEmpty( emailItem.getBodyHtml() ) )
+        {
+            throw new PwmOperationalException( new ErrorInformation( PwmError.ERROR_EMAIL_SEND_FAILURE,
+                    "missing body in email item" ) );
+        }
     }
 
     public void submitEmail(
@@ -306,7 +320,7 @@ public class EmailService implements PwmService
                 workingItemBean = EmailServerUtil.applyMacrosToEmail( workingItemBean, macroMachine );
             }
 
-            if ( workingItemBean.getTo() == null || workingItemBean.getTo().length() < 1 )
+            if ( StringUtil.isEmpty( workingItemBean.getTo() ) )
             {
                 LOGGER.error( () -> "no destination address available for email, skipping; email: " + emailItem.toDebugString() );
             }
@@ -335,6 +349,36 @@ public class EmailService implements PwmService
         }
     }
 
+    public static void sendEmailSynchronous(
+            final EmailServer emailServer,
+            final Configuration configuration,
+            final EmailItemBean emailItem,
+            final MacroMachine macroMachine
+    )
+            throws PwmOperationalException, PwmUnrecoverableException, MessagingException
+
+    {
+        validateEmailItem( emailItem );
+        EmailItemBean workingItemBean = emailItem;
+        if ( macroMachine != null )
+        {
+            workingItemBean = EmailServerUtil.applyMacrosToEmail( workingItemBean, macroMachine );
+        }
+        final Transport transport = EmailServerUtil.makeSmtpTransport( emailServer );
+        final List<Message> messages = EmailServerUtil.convertEmailItemToMessages(
+                workingItemBean,
+                configuration,
+                emailServer
+        );
+
+        for ( final Message message : messages )
+        {
+            message.saveChanges();
+            transport.sendMessage( message, message.getAllRecipients() );
+        }
+        transport.close();
+    }
+
     private final AtomicInteger newThreadLocalTransport = new AtomicInteger();
     private final AtomicInteger useExistingConnection = new AtomicInteger();
     private final AtomicInteger useExistingTransport = new AtomicInteger();
@@ -351,12 +395,37 @@ public class EmailService implements PwmService
     }
 
     private WorkQueueProcessor.ProcessResult sendItem( final EmailItemBean emailItemBean )
+    {
+        try
+        {
+            executeEmailSend( emailItemBean );
+            return WorkQueueProcessor.ProcessResult.SUCCESS;
+        }
+        catch ( final MessagingException | PwmException e )
+        {
+            if ( EmailServerUtil.examineSendFailure( e, retryableStatusResponses ) )
+            {
+                LOGGER.error( () -> "error sending email (" + e.getMessage() + ") " + emailItemBean.toDebugString() + ", will retry" );
+                StatisticsManager.incrementStat( pwmApplication, Statistic.EMAIL_SEND_FAILURES );
+                return WorkQueueProcessor.ProcessResult.RETRY;
+            }
+            else
+            {
+                LOGGER.error( () -> "error sending email (" + e.getMessage() + ") " + emailItemBean.toDebugString() + ", permanent failure, discarding message" );
+                StatisticsManager.incrementStat( pwmApplication, Statistic.EMAIL_SEND_DISCARDS );
+                return WorkQueueProcessor.ProcessResult.FAILED;
+            }
+        }
+    }
+
+    private void executeEmailSend( final EmailItemBean emailItemBean )
+            throws PwmUnrecoverableException, MessagingException
     {
         EmailConnection serverTransport = null;
 
-        // create a new MimeMessage object (using the Session created above)
         try
         {
+            // create a new MimeMessage object (using the Session created above)
             if ( threadLocalTransport.get() == null )
             {
 
@@ -401,11 +470,10 @@ public class EmailService implements PwmService
 
             LOGGER.debug( () -> "sent email: " + emailItemBean.toDebugString() );
             StatisticsManager.incrementStat( pwmApplication, Statistic.EMAIL_SEND_SUCCESSES );
-            return WorkQueueProcessor.ProcessResult.SUCCESS;
+
         }
         catch ( final MessagingException | PwmException e )
         {
-
             final ErrorInformation errorInformation;
             if ( e instanceof PwmException )
             {
@@ -429,19 +497,7 @@ public class EmailService implements PwmService
                 serverErrors.put( serverTransport.getEmailServer(), errorInformation );
             }
             LOGGER.error( errorInformation );
-
-            if ( EmailServerUtil.examineSendFailure( e, retryableStatusResponses ) )
-            {
-                LOGGER.error( () -> "error sending email (" + e.getMessage() + ") " + emailItemBean.toDebugString() + ", will retry" );
-                StatisticsManager.incrementStat( pwmApplication, Statistic.EMAIL_SEND_FAILURES );
-                return WorkQueueProcessor.ProcessResult.RETRY;
-            }
-            else
-            {
-                LOGGER.error( () -> "error sending email (" + e.getMessage() + ") " + emailItemBean.toDebugString() + ", permanent failure, discarding message" );
-                StatisticsManager.incrementStat( pwmApplication, Statistic.EMAIL_SEND_DISCARDS );
-                return WorkQueueProcessor.ProcessResult.FAILED;
-            }
+            throw e;
         }
     }
 

+ 2 - 0
server/src/main/java/password/pwm/svc/wordlist/WordlistConfiguration.java

@@ -47,6 +47,8 @@ import java.util.function.Supplier;
 @Builder( toBuilder = true )
 public class WordlistConfiguration implements Serializable
 {
+    private static final long serialVersionUID = 1L;
+
     static final int STREAM_BUFFER_SIZE = 1_1024_1024;
     static final PwmHashAlgorithm HASH_ALGORITHM = PwmHashAlgorithm.SHA256;
 

+ 1 - 1
server/src/main/java/password/pwm/svc/wordlist/WordlistInspector.java

@@ -321,7 +321,7 @@ class WordlistInspector implements Runnable
         boolean needsAutoImport = false;
         if ( remoteInfo == null )
         {
-            getLogger().warn( () -> "can't read remote wordlist data from url " + rootWordlist.getConfiguration().getAutoImportUrl() );
+            getLogger().warn( () -> "can not read remote wordlist data from url " + rootWordlist.getConfiguration().getAutoImportUrl() );
         }
         else
         {

+ 8 - 4
server/src/main/java/password/pwm/util/JarMain.java

@@ -23,6 +23,7 @@ package password.pwm.util;
 import password.pwm.PwmConstants;
 
 import javax.swing.JOptionPane;
+import java.util.Map;
 
 
 public class JarMain
@@ -47,10 +48,13 @@ public class JarMain
         sb.append( "\n" );
         sb.append( "Build Information: \n" );
 
-        sb.append( "build.time=" + PwmConstants.BUILD_TIME + "\n" );
-        sb.append( "build.number=" + PwmConstants.BUILD_NUMBER + "\n" );
-        sb.append( "build.java.version=" + PwmConstants.BUILD_JAVA_VERSION + "\n" );
-        sb.append( "build.java.vendor=" + PwmConstants.BUILD_JAVA_VENDOR + "\n" );
+        for ( final Map.Entry<String, String> entry : PwmConstants.BUILD_MANIFEST.entrySet() )
+        {
+            sb.append( entry.getKey() );
+            sb.append( "=" );
+            sb.append( entry.getValue() );
+            sb.append( "\n" );
+        }
 
         sb.append( "\n" );
         sb.append( "Reference URL: " + PwmConstants.PWM_URL_HOME + "\n" );

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

@@ -57,6 +57,8 @@ import java.util.TreeMap;
 
 public class LDAPPermissionCalculator implements Serializable
 {
+    private static final long serialVersionUID = 1L;
+
     private static final PwmLogger LOGGER = PwmLogger.forClass( LDAPPermissionCalculator.class );
 
     private final transient StoredConfiguration storedConfiguration;

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

@@ -43,6 +43,8 @@ import java.util.Arrays;
  */
 public class PasswordData implements Serializable
 {
+    private static final long serialVersionUID = 1L;
+
     private static final PwmLogger LOGGER = PwmLogger.forClass( PasswordData.class );
 
     private final byte[] passwordData;

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

@@ -50,10 +50,10 @@ public class ImportPropertyConfigCommand extends AbstractCliCommand
 
         final File inputFile = ( File ) cliEnvironment.getOptions().get( CliParameters.REQUIRED_EXISTING_INPUT_FILE.getName() );
 
-        try
+        try ( FileInputStream fileInputStream = new FileInputStream( inputFile ) )
         {
             final PropertyConfigurationImporter importer = new PropertyConfigurationImporter();
-            final StoredConfiguration storedConfiguration = importer.readConfiguration( new FileInputStream( inputFile ) );
+            final StoredConfiguration storedConfiguration = importer.readConfiguration( fileInputStream );
 
             try ( OutputStream outputStream = new FileOutputStream( configFile ) )
             {

+ 0 - 1
server/src/main/java/password/pwm/util/logging/PwmLogger.java

@@ -244,7 +244,6 @@ public class PwmLogger
         }
     }
 
-
     private static String convertErrorInformation( final ErrorInformation info )
     {
         return info.toDebugStr();

+ 2 - 238
server/src/main/java/password/pwm/util/secure/HttpsServerCertificateManager.java

@@ -20,23 +20,7 @@
 
 package password.pwm.util.secure;
 
-import org.bouncycastle.asn1.x500.X500NameBuilder;
-import org.bouncycastle.asn1.x500.style.BCStyle;
-import org.bouncycastle.asn1.x509.BasicConstraints;
-import org.bouncycastle.asn1.x509.ExtendedKeyUsage;
-import org.bouncycastle.asn1.x509.Extension;
-import org.bouncycastle.asn1.x509.KeyPurposeId;
-import org.bouncycastle.asn1.x509.KeyUsage;
-import org.bouncycastle.cert.X509v3CertificateBuilder;
-import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
-import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
-import org.bouncycastle.jce.provider.BouncyCastleProvider;
-import org.bouncycastle.operator.ContentSigner;
-import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
-import password.pwm.AppAttribute;
-import password.pwm.AppProperty;
 import password.pwm.PwmApplication;
-import password.pwm.PwmConstants;
 import password.pwm.bean.PrivateKeyCertificate;
 import password.pwm.config.Configuration;
 import password.pwm.config.PwmSetting;
@@ -48,51 +32,22 @@ import password.pwm.error.PwmError;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.util.PasswordData;
 import password.pwm.util.java.JsonUtil;
-import password.pwm.util.java.PwmDateFormat;
-import password.pwm.util.java.StringUtil;
 import password.pwm.util.logging.PwmLogger;
+import password.pwm.util.secure.self.SelfCertFactory;
 
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
 import java.io.InputStream;
-import java.io.ObjectInputStream;
-import java.io.ObjectOutputStream;
-import java.io.Serializable;
-import java.math.BigInteger;
-import java.net.URI;
-import java.net.URISyntaxException;
-import java.security.KeyPair;
-import java.security.KeyPairGenerator;
 import java.security.KeyStore;
 import java.security.PrivateKey;
-import java.security.SecureRandom;
-import java.security.Security;
 import java.security.cert.X509Certificate;
-import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.Date;
 import java.util.Enumeration;
 import java.util.List;
-import java.util.concurrent.TimeUnit;
 
 public class HttpsServerCertificateManager
 {
     private static final PwmLogger LOGGER = PwmLogger.forClass( HttpsServerCertificateManager.class );
 
-    private static volatile boolean bouncyCastleInitialized;
-
-    private static synchronized void initBouncyCastleProvider( )
-    {
-        if ( !bouncyCastleInitialized )
-        {
-            Security.addProvider( new BouncyCastleProvider() );
-            bouncyCastleInitialized = true;
-        }
-    }
-
-
     public static KeyStore keyStoreForApplication(
             final PwmApplication pwmApplication,
             final PasswordData passwordData,
@@ -151,12 +106,9 @@ public class HttpsServerCertificateManager
     private static KeyStore makeSelfSignedCert( final PwmApplication pwmApplication, final PasswordData password, final String alias )
             throws PwmUnrecoverableException
     {
-        final Configuration configuration = pwmApplication.getConfig();
-
         try
         {
-            final SelfCertGenerator selfCertGenerator = new SelfCertGenerator( configuration );
-            return selfCertGenerator.makeSelfSignedCert( pwmApplication, password, alias );
+            return SelfCertFactory.getExistingCertOrGenerateNewCert( pwmApplication, password, alias );
         }
         catch ( final Exception e )
         {
@@ -164,194 +116,6 @@ public class HttpsServerCertificateManager
         }
     }
 
-    public static class StoredCertData implements Serializable
-    {
-        private final X509Certificate x509Certificate;
-        private String keypairb64;
-
-        public StoredCertData( final X509Certificate x509Certificate, final KeyPair keypair )
-                throws IOException
-        {
-            this.x509Certificate = x509Certificate;
-            final ByteArrayOutputStream baos = new ByteArrayOutputStream();
-            final ObjectOutputStream oos = new ObjectOutputStream( baos );
-            oos.writeObject( keypair );
-            final byte[] ba = baos.toByteArray();
-            keypairb64 = StringUtil.base64Encode( ba );
-        }
-
-        public X509Certificate getX509Certificate( )
-        {
-            return x509Certificate;
-        }
-
-        public KeyPair getKeypair( )
-                throws IOException, ClassNotFoundException
-        {
-            final byte[] ba = StringUtil.base64Decode( keypairb64 );
-            final ByteArrayInputStream bais = new ByteArrayInputStream( ba );
-            final ObjectInputStream ois = new ObjectInputStream( bais );
-            return ( KeyPair ) ois.readObject();
-        }
-    }
-
-
-    public static class SelfCertGenerator
-    {
-        private final Configuration config;
-
-        public SelfCertGenerator( final Configuration config )
-        {
-            this.config = config;
-        }
-
-        public KeyStore makeSelfSignedCert( final PwmApplication pwmApplication, final PasswordData password, final String alias )
-                throws Exception
-        {
-            final String cnName = makeSubjectName();
-            final KeyStore keyStore = KeyStore.getInstance( "jks" );
-            keyStore.load( null, password.getStringValue().toCharArray() );
-            StoredCertData storedCertData = pwmApplication.readAppAttribute( AppAttribute.HTTPS_SELF_CERT, StoredCertData.class );
-            if ( storedCertData != null )
-            {
-                if ( !cnName.equals( storedCertData.getX509Certificate().getSubjectDN().getName() ) )
-                {
-                    LOGGER.info( () -> "replacing stored self cert, subject name does not match configured site url" );
-                    storedCertData = null;
-                }
-                else if ( storedCertData.getX509Certificate().getNotBefore().after( new Date() ) )
-                {
-                    LOGGER.info( () -> "replacing stored self cert, not-before date is in the future" );
-                    storedCertData = null;
-                }
-                else if ( storedCertData.getX509Certificate().getNotAfter().before( new Date() ) )
-                {
-                    LOGGER.info( () -> "replacing stored self cert, not-after date is in the past" );
-                    storedCertData = null;
-                }
-            }
-
-            if ( storedCertData == null )
-            {
-                storedCertData = makeSelfSignedCert( cnName );
-                pwmApplication.writeAppAttribute( AppAttribute.HTTPS_SELF_CERT, storedCertData );
-            }
-
-            keyStore.setKeyEntry(
-                    alias,
-                    storedCertData.getKeypair().getPrivate(),
-                    password.getStringValue().toCharArray(),
-                    new X509Certificate[]
-                            {
-                                    storedCertData.getX509Certificate(),
-                            }
-            );
-            return keyStore;
-        }
-
-        public String makeSubjectName( )
-                throws Exception
-        {
-            String cnName = PwmConstants.PWM_APP_NAME.toLowerCase() + ".example.com";
-            {
-                final String siteURL = config.readSettingAsString( PwmSetting.PWM_SITE_URL );
-                if ( siteURL != null && !siteURL.isEmpty() )
-                {
-                    try
-                    {
-                        final URI uri = new URI( siteURL );
-                        if ( uri.getHost() != null && !uri.getHost().isEmpty() )
-                        {
-                            cnName = uri.getHost();
-                        }
-                    }
-                    catch ( final URISyntaxException e )
-                    {
-                        // disregard
-                    }
-                }
-            }
-            return cnName;
-        }
-
-
-        public StoredCertData makeSelfSignedCert( final String cnName )
-                throws Exception
-        {
-            initBouncyCastleProvider();
-
-            LOGGER.debug( () -> "creating self-signed certificate with cn of " + cnName );
-            final KeyPair keyPair = generateRSAKeyPair( config );
-            final long futureSeconds = Long.parseLong( config.readAppProperty( AppProperty.SECURITY_HTTPSSERVER_SELF_FUTURESECONDS ) );
-            final X509Certificate certificate = generateV3Certificate( keyPair, cnName, futureSeconds );
-            return new StoredCertData( certificate, keyPair );
-        }
-
-
-        public static X509Certificate generateV3Certificate( final KeyPair pair, final String cnValue, final long futureSeconds )
-                throws Exception
-        {
-            final X500NameBuilder subjectName = new X500NameBuilder( BCStyle.INSTANCE );
-            subjectName.addRDN( BCStyle.CN, cnValue );
-
-            final BigInteger serialNumber = makeSerialNumber();
-
-
-            // 2 days in the past
-            final Date notBefore = new Date( System.currentTimeMillis() - TimeUnit.DAYS.toMillis( 2 ) );
-
-            final Date notAfter = new Date( System.currentTimeMillis() + ( futureSeconds * 1000 ) );
-
-            final X509v3CertificateBuilder certGen = new JcaX509v3CertificateBuilder(
-                    subjectName.build(),
-                    serialNumber,
-                    notBefore,
-                    notAfter,
-                    subjectName.build(),
-                    pair.getPublic()
-            );
-
-            // false == not a CA
-            final BasicConstraints basic = new BasicConstraints( false );
-
-            // OID, critical, ASN.1 encoded value
-            certGen.addExtension( Extension.basicConstraints, true, basic.getEncoded() );
-
-            // sign and key encipher
-            final KeyUsage keyUsage = new KeyUsage( KeyUsage.digitalSignature | KeyUsage.keyEncipherment );
-
-            // OID, critical, ASN.1 encoded value
-            certGen.addExtension( Extension.keyUsage, true, keyUsage.getEncoded() );
-
-            // server authentication
-            final ExtendedKeyUsage extKeyUsage = new ExtendedKeyUsage( KeyPurposeId.id_kp_serverAuth );
-
-            // OID, critical, ASN.1 encoded value
-            certGen.addExtension( Extension.extendedKeyUsage, true, extKeyUsage.getEncoded() );
-
-            final ContentSigner sigGen = new JcaContentSignerBuilder( "SHA256WithRSAEncryption" ).setProvider( "BC" ).build( pair.getPrivate() );
-
-            return new JcaX509CertificateConverter().setProvider( "BC" ).getCertificate( certGen.build( sigGen ) );
-        }
-
-        private static BigInteger makeSerialNumber()
-        {
-            final PwmDateFormat formatter = PwmDateFormat.newPwmDateFormat( "yyyyMMddhhmmss" );
-            final String serNumStr = formatter.format( Instant.now() );
-            return new BigInteger( serNumStr );
-        }
-
-        static KeyPair generateRSAKeyPair( final Configuration config )
-                throws Exception
-        {
-            final int keySize = Integer.parseInt( config.readAppProperty( AppProperty.SECURITY_HTTPSSERVER_SELF_KEY_SIZE ) );
-            final String keyAlg = config.readAppProperty( AppProperty.SECURITY_HTTPSSERVER_SELF_ALG );
-            final KeyPairGenerator kpGen = KeyPairGenerator.getInstance( keyAlg, "BC" );
-            kpGen.initialize( keySize, new SecureRandom() );
-            return kpGen.generateKeyPair();
-        }
-    }
-
 
     public enum KeyStoreFormat
     {

+ 5 - 1
server/src/main/java/password/pwm/util/secure/PwmTrustManager.java

@@ -135,7 +135,11 @@ public class PwmTrustManager implements X509TrustManager
             {
                 try
                 {
-                    testCertificate.verify( rootCA.getPublicKey() );
+                    // first check certificate equality.  if certificate is same, we don't need to verify it signed itself
+                    if ( !testCertificate.equals( rootCA ) )
+                    {
+                        testCertificate.verify( rootCA.getPublicKey() );
+                    }
                     passed = true;
                 }
                 catch ( final NoSuchAlgorithmException | SignatureException | NoSuchProviderException | InvalidKeyException | CertificateException e )

+ 151 - 0
server/src/main/java/password/pwm/util/secure/self/SelfCertFactory.java

@@ -0,0 +1,151 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2019 The PWM Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package password.pwm.util.secure.self;
+
+import password.pwm.AppAttribute;
+import password.pwm.PwmApplication;
+import password.pwm.PwmConstants;
+import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.util.PasswordData;
+import password.pwm.util.java.StringUtil;
+import password.pwm.util.logging.PwmLogger;
+import password.pwm.util.secure.SecureService;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+import java.util.Date;
+import java.util.Optional;
+
+public class SelfCertFactory
+{
+    private static final PwmLogger LOGGER = PwmLogger.forClass( SelfCertFactory.class );
+
+    public static KeyStore getExistingCertOrGenerateNewCert( final PwmApplication pwmApplication, final PasswordData password, final String alias )
+        throws Exception
+    {
+        final Settings settings = Settings.fromConfiguration( pwmApplication.getConfig() );
+
+        final Optional<StoredCertData> existingCert = loadExistingStoredCert( pwmApplication );
+        if ( existingCert.isPresent() )
+        {
+            if ( evaluateExistingStoredCert( existingCert.get(), settings ) )
+            {
+                return storedCertToKeyStore( existingCert.get(), alias, password );
+            }
+        }
+
+        return generateNewCert(
+            settings,
+            pwmApplication.getSecureService(),
+            password,
+            alias );
+    }
+
+    public static KeyStore generateNewCert(
+        final Settings settings,
+        final SecureService secureService,
+        final PasswordData password,
+        final String alias
+    )
+        throws Exception
+    {
+        final SelfCertGenerator selfCertGenerator = new SelfCertGenerator(
+            settings,
+            secureService );
+        final StoredCertData storedCertData = selfCertGenerator.generateNewCertificate( makeSubjectName( settings ) );
+        return storedCertToKeyStore( storedCertData, alias, password );
+    }
+
+    private static Optional<StoredCertData> loadExistingStoredCert( final PwmApplication pwmApplication )
+    {
+        final StoredCertData storedCertData = pwmApplication.readAppAttribute( AppAttribute.HTTPS_SELF_CERT, StoredCertData.class );
+        return Optional.ofNullable( storedCertData );
+    }
+
+    private static boolean evaluateExistingStoredCert( final StoredCertData storedCertData, final Settings settings )
+    {
+        final String cnName = makeSubjectName( settings );
+        if ( !cnName.equals( storedCertData.getX509Certificate().getSubjectDN().getName() ) )
+        {
+            LOGGER.info( () -> "replacing stored self cert, subject name does not match configured site url" );
+            return false;
+        }
+        else if ( storedCertData.getX509Certificate().getNotBefore().after( new Date() ) )
+        {
+            LOGGER.info( () -> "replacing stored self cert, not-before date is in the future" );
+            return false;
+        }
+        else if ( storedCertData.getX509Certificate().getNotAfter().before( new Date() ) )
+        {
+            LOGGER.info( () -> "replacing stored self cert, not-after date is in the past" );
+            return false;
+        }
+
+        return true;
+    }
+
+    private static String makeSubjectName( final Settings settings )
+    {
+        String cnName = PwmConstants.PWM_APP_NAME.toLowerCase() + ".example.com";
+        {
+            final String siteURL = settings.getSiteUrl();
+            if ( !StringUtil.isEmpty( siteURL ) )
+            {
+                try
+                {
+                    final URI uri = new URI( siteURL );
+                    if ( uri.getHost() != null && !uri.getHost().isEmpty() )
+                    {
+                        cnName = uri.getHost();
+                    }
+                }
+                catch ( final URISyntaxException e )
+                {
+                    // disregard
+                }
+            }
+        }
+        return cnName;
+    }
+
+    static KeyStore storedCertToKeyStore( final StoredCertData storedCertData, final String alias, final PasswordData password )
+        throws KeyStoreException, IOException, ClassNotFoundException, PwmUnrecoverableException, CertificateException, NoSuchAlgorithmException
+    {
+        final KeyStore keyStore = KeyStore.getInstance( "jks" );
+        keyStore.load( null, password.getStringValue().toCharArray() );
+        keyStore.setKeyEntry(
+            alias,
+            storedCertData.getKeypair().getPrivate(),
+            password.getStringValue().toCharArray(),
+            new X509Certificate[]
+                {
+                    storedCertData.getX509Certificate(),
+                    }
+        );
+        return keyStore;
+    }
+}

+ 159 - 0
server/src/main/java/password/pwm/util/secure/self/SelfCertGenerator.java

@@ -0,0 +1,159 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2019 The PWM Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package password.pwm.util.secure.self;
+
+import org.bouncycastle.asn1.x500.X500NameBuilder;
+import org.bouncycastle.asn1.x500.style.BCStyle;
+import org.bouncycastle.asn1.x509.BasicConstraints;
+import org.bouncycastle.asn1.x509.ExtendedKeyUsage;
+import org.bouncycastle.asn1.x509.Extension;
+import org.bouncycastle.asn1.x509.KeyPurposeId;
+import org.bouncycastle.asn1.x509.KeyUsage;
+import org.bouncycastle.cert.X509v3CertificateBuilder;
+import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
+import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.bouncycastle.operator.ContentSigner;
+import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
+import password.pwm.util.java.PwmDateFormat;
+import password.pwm.util.logging.PwmLogger;
+import password.pwm.util.secure.SecureService;
+
+import java.math.BigInteger;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.SecureRandom;
+import java.security.Security;
+import java.security.cert.X509Certificate;
+import java.time.Instant;
+import java.util.Date;
+import java.util.concurrent.TimeUnit;
+
+class SelfCertGenerator
+{
+    private static final PwmLogger LOGGER = PwmLogger.forClass( SelfCertGenerator.class );
+
+    private static volatile boolean bouncyCastleInitialized;
+
+    private final Settings settings;
+    private final SecureService secureService;
+
+    SelfCertGenerator( final Settings settings,  final SecureService secureService )
+    {
+        this.secureService = secureService;
+        this.settings = settings;
+    }
+
+    StoredCertData generateNewCertificate( final String cnName )
+        throws Exception
+    {
+        initBouncyCastleProvider();
+
+        LOGGER.debug( () -> "creating self-signed certificate with cn of " + cnName );
+        final KeyPair keyPair = generateRSAKeyPair( );
+        final X509Certificate certificate = generateV3Certificate( keyPair, cnName );
+        return new StoredCertData( certificate, keyPair );
+    }
+
+
+    private X509Certificate generateV3Certificate( final KeyPair pair, final String cnValue )
+        throws Exception
+    {
+        final X500NameBuilder subjectName = new X500NameBuilder( BCStyle.INSTANCE );
+        subjectName.addRDN( BCStyle.CN, cnValue );
+
+        final BigInteger serialNumber = makeSerialNumber();
+
+        // 2 days in the past
+        final Date notBefore = new Date( System.currentTimeMillis() - TimeUnit.DAYS.toMillis( 2 ) );
+
+        final long futureSeconds = settings.getFutureSeconds();
+        final Date notAfter = new Date( System.currentTimeMillis() + ( futureSeconds * 1000 ) );
+
+        final X509v3CertificateBuilder certGen = new JcaX509v3CertificateBuilder(
+            subjectName.build(),
+            serialNumber,
+            notBefore,
+            notAfter,
+            subjectName.build(),
+            pair.getPublic()
+        );
+
+        // false == not a CA
+        final BasicConstraints basic = new BasicConstraints( false );
+
+        // OID, critical, ASN.1 encoded value
+        certGen.addExtension( Extension.basicConstraints, true, basic.getEncoded() );
+
+        // add subject alternate name
+        /*
+        {
+            final ASN1Encodable[] subjectAlternativeNames = new ASN1Encodable[]
+                {
+                    new GeneralName( GeneralName.dNSName, cnValue ),
+                    };
+            final DERSequence subjectAlternativeNamesExtension = new DERSequence( subjectAlternativeNames );
+            certGen.addExtension( Extension.subjectAlternativeName, false, subjectAlternativeNamesExtension );
+        }
+        */
+
+
+        // sign and key encipher
+        final KeyUsage keyUsage = new KeyUsage( KeyUsage.digitalSignature | KeyUsage.keyEncipherment );
+
+        // OID, critical, ASN.1 encoded value
+        certGen.addExtension( Extension.keyUsage, true, keyUsage.getEncoded() );
+
+        // server authentication
+        final ExtendedKeyUsage extKeyUsage = new ExtendedKeyUsage( KeyPurposeId.id_kp_serverAuth );
+
+        // OID, critical, ASN.1 encoded value
+        certGen.addExtension( Extension.extendedKeyUsage, true, extKeyUsage.getEncoded() );
+
+        final ContentSigner sigGen = new JcaContentSignerBuilder( "SHA256WithRSAEncryption" ).setProvider( "BC" ).build( pair.getPrivate() );
+
+        return new JcaX509CertificateConverter().setProvider( "BC" ).getCertificate( certGen.build( sigGen ) );
+    }
+
+    private BigInteger makeSerialNumber()
+    {
+        final PwmDateFormat formatter = PwmDateFormat.newPwmDateFormat( "yyyyMMddhhmmss" );
+        final String serNumStr = formatter.format( Instant.now() );
+        return new BigInteger( serNumStr );
+    }
+
+    private KeyPair generateRSAKeyPair( )
+        throws Exception
+    {
+        final KeyPairGenerator kpGen = KeyPairGenerator.getInstance( settings.getKeyAlg(), "BC" );
+        kpGen.initialize( settings.getKeySize(), secureService == null ? new SecureRandom() : secureService.pwmRandom() );
+        return kpGen.generateKeyPair();
+    }
+
+    private static synchronized void initBouncyCastleProvider( )
+    {
+        if ( !bouncyCastleInitialized )
+        {
+            Security.addProvider( new BouncyCastleProvider() );
+            bouncyCastleInitialized = true;
+        }
+    }
+}

+ 56 - 0
server/src/main/java/password/pwm/util/secure/self/Settings.java

@@ -0,0 +1,56 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2019 The PWM Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package password.pwm.util.secure.self;
+
+import lombok.Builder;
+import lombok.Value;
+import password.pwm.AppProperty;
+import password.pwm.PwmConstants;
+import password.pwm.config.Configuration;
+import password.pwm.config.PwmSetting;
+import password.pwm.util.java.TimeDuration;
+
+@Value
+@Builder
+public class Settings
+{
+    @Builder.Default
+    private int keySize = 1024;
+
+    @Builder.Default
+    private String keyAlg = "RSA";
+
+    @Builder.Default
+    private long futureSeconds = TimeDuration.of( 30, TimeDuration.Unit.DAYS ).as( TimeDuration.Unit.SECONDS );
+
+    @Builder.Default
+    private String siteUrl = "http://" + PwmConstants.PWM_APP_NAME.toLowerCase() + ".example.com";
+
+    public static Settings fromConfiguration ( final Configuration config )
+    {
+        return Settings.builder()
+            .keySize( Integer.parseInt( config.readAppProperty( AppProperty.SECURITY_HTTPSSERVER_SELF_KEY_SIZE )  ) )
+            .keyAlg( config.readAppProperty( AppProperty.SECURITY_HTTPSSERVER_SELF_ALG ) )
+            .futureSeconds( Long.parseLong( config.readAppProperty( AppProperty.SECURITY_HTTPSSERVER_SELF_FUTURESECONDS ) ) )
+            .siteUrl( config.readSettingAsString( PwmSetting.PWM_SITE_URL ) )
+            .build();
+    }
+}

+ 63 - 0
server/src/main/java/password/pwm/util/secure/self/StoredCertData.java

@@ -0,0 +1,63 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2019 The PWM Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package password.pwm.util.secure.self;
+
+import password.pwm.util.java.StringUtil;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.Serializable;
+import java.security.KeyPair;
+import java.security.cert.X509Certificate;
+
+public class StoredCertData implements Serializable
+{
+    private final X509Certificate x509Certificate;
+    private String keypairb64;
+
+    public StoredCertData( final X509Certificate x509Certificate, final KeyPair keypair )
+        throws IOException
+    {
+        this.x509Certificate = x509Certificate;
+        final ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        final ObjectOutputStream oos = new ObjectOutputStream( baos );
+        oos.writeObject( keypair );
+        final byte[] ba = baos.toByteArray();
+        keypairb64 = StringUtil.base64Encode( ba );
+    }
+
+    public X509Certificate getX509Certificate( )
+    {
+        return x509Certificate;
+    }
+
+    public KeyPair getKeypair( )
+        throws IOException, ClassNotFoundException
+    {
+        final byte[] ba = StringUtil.base64Decode( keypairb64 );
+        final ByteArrayInputStream bais = new ByteArrayInputStream( ba );
+        final ObjectInputStream ois = new ObjectInputStream( bais );
+        return ( KeyPair ) ois.readObject();
+    }
+}

+ 2 - 0
server/src/main/java/password/pwm/ws/server/RestAuthentication.java

@@ -32,6 +32,8 @@ import java.util.Set;
 @Value
 public class RestAuthentication implements Serializable
 {
+    private static final long serialVersionUID = 1L;
+
     private RestAuthenticationType type;
     private String namedSecretName;
     private UserIdentity ldapIdentity;

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

@@ -222,6 +222,7 @@ localdb.logWriter.maxTrimSize=5001
 localdb.reloadWhenAppRestarted=false
 macro.randomChar.maxLength=100
 macro.ldapAttr.maxLength=100
+logging.cspReport.enable=true
 logging.devOutput.enable=false
 logging.extra.periodicThreadDumpIntervalSeconds=0
 logging.pattern=%d{yyyy-MM-dd'T'HH:mm:ss'Z'}, %-5p, %c{2}, %m%n

+ 0 - 1
server/src/main/resources/password/pwm/PwmConstants.properties

@@ -38,6 +38,5 @@ paramName.token=token
 defaultConfigFilename=PwmConfiguration.xml
 defaultPropertiesConfigFilename=silent.properties
 enableEulaDisplay=false
-missingVersionString=[Version Missing]
 applicationPathInfoFile=applicationPath.properties
 includedLocales=["","en_CA","ca","cs","da","de","el","es","fi","fr","fr_CA","hu","it","iw","ja","ko","nl","nn","no","pl","pt","pt_BR","sk","sv","th","tr","zh_CN","zh_TW"]

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

@@ -1613,7 +1613,7 @@
     </setting>
     <setting hidden="false" key="security.cspHeader" level="2">
         <default>
-            <value><![CDATA[default-src 'self'; object-src 'none'; img-src 'self' data:; style-src 'self' 'unsafe-inline'; script-src https://www.recaptcha.net/recaptcha/ https://www.gstatic.cn/recaptcha/  https://www.gstatic.com/recaptcha/ https://www.google.com/recaptcha/ 'self' 'unsafe-eval' 'nonce-%NONCE%'; frame-src https://www.recaptcha.net/recaptcha/ https://www.gstatic.cn/recaptcha/ https://www.gstatic.com/recaptcha/ https://www.google.com/recaptcha/ ; report-uri /sspr/public/command/cspReport]]></value>
+            <value><![CDATA[default-src 'self'; object-src 'none'; img-src 'self' data:; style-src 'self' 'unsafe-inline'; script-src https://www.recaptcha.net/recaptcha/ https://www.gstatic.cn/recaptcha/  https://www.gstatic.com/recaptcha/ https://www.google.com/recaptcha/ 'self' 'unsafe-eval' 'nonce-%NONCE%'; frame-src https://www.recaptcha.net/recaptcha/ https://www.gstatic.cn/recaptcha/ https://www.gstatic.com/recaptcha/ https://www.google.com/recaptcha/ ; report-uri @PwmContextPath@/public/api?processAction=cspReport]]></value>
         </default>
     </setting>
     <setting hidden="false" key="email.adminAlert.toAddress" level="1">

+ 1 - 0
server/src/main/resources/password/pwm/i18n/Admin.properties

@@ -334,6 +334,7 @@ Title_URLReference=URL Reference
 MenuItem_ConfigEditor=Configuration Editor
 MenuItem_ConfigManager=Configuration Manager
 Field_AppVersion=%1% Version
+Field_AppBuildTime=Build Time
 Field_CurrentPubVersion=Current Published Version
 Field_UpTime=Up Time
 Field_SiteURL=Site URL

+ 1 - 0
server/src/main/resources/password/pwm/i18n/Config.properties

@@ -133,6 +133,7 @@ Warning_InvalidFormat=The value does not have the correct format.
 Warning_UploadIE9=This feature is not available when using Internet Explorer 9 and earlier.  Please use a different browser or a newer version of Internet Explorer.
 Warning_ValueIncorrectFormat=The value does not have the correct format.
 Warning_SmsTestData=The test that will be performed will include resolving configured macros (if any).  The macros will be resolved using data of the logged in user, and thus may include sensitive data.  The message should be formatted as required by the SMS gateway service.
+Warning_EmailTestData=Email Test Data
 Warning_NoEndUserModules=End user functionality is not available while the configuration is open.
 Tooltip_ResetButton=Return this setting to its default value.
 Tooltip_HelpButton=Show description for this setting.

+ 1 - 1
server/src/test/java/password/pwm/config/PwmSettingCategoryTest.java

@@ -66,7 +66,7 @@ public class PwmSettingCategoryTest
         {
             if ( category.hasProfiles() )
             {
-                final PwmSetting pwmSetting = category.getProfileSetting();
+                final PwmSetting pwmSetting = category.getProfileSetting().orElseThrow( IllegalStateException::new );
                 Assert.assertEquals( pwmSetting.getSyntax(), PwmSettingSyntax.PROFILE );
             }
         }

+ 68 - 0
server/src/test/java/password/pwm/config/stored/StoredConfigurationModifierTest.java

@@ -0,0 +1,68 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2019 The PWM Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package password.pwm.config.stored;
+
+import org.junit.Assert;
+import org.junit.Test;
+import password.pwm.config.PwmSetting;
+import password.pwm.config.value.NumericValue;
+import password.pwm.config.value.StringValue;
+import password.pwm.error.PwmUnrecoverableException;
+
+import java.util.List;
+
+public class StoredConfigurationModifierTest
+{
+
+    @Test
+    public void testWriteSetting() throws PwmUnrecoverableException
+    {
+        final StoredConfiguration storedConfiguration = StoredConfigurationFactory.newConfig();
+        final StoredConfigurationModifier modifier = StoredConfigurationModifier.newModifier( storedConfiguration );
+
+        modifier.writeSetting( PwmSetting.NOTES, null, new StringValue( "notes test" ), null );
+
+        final StoredConfiguration newConfig = modifier.newStoredConfiguration();
+
+        final String notesText = ( ( String ) newConfig.readSetting( PwmSetting.NOTES, null ).toNativeObject() );
+        Assert.assertEquals( notesText, "notes test" );
+    }
+
+    @Test
+    public void testCopyProfileID() throws PwmUnrecoverableException
+    {
+        final StoredConfiguration storedConfiguration = StoredConfigurationFactory.newConfig();
+        final StoredConfigurationModifier modifier = StoredConfigurationModifier.newModifier( storedConfiguration );
+
+        modifier.writeSetting( PwmSetting.HELPDESK_RESULT_LIMIT, "default", new NumericValue( 19 ), null );
+        modifier.copyProfileID( PwmSetting.HELPDESK_RESULT_LIMIT.getCategory(), "default", "newProfile", null );
+
+        final StoredConfiguration newConfig = modifier.newStoredConfiguration();
+
+        final List<String> profileNames = newConfig.profilesForSetting( PwmSetting.HELPDESK_RESULT_LIMIT );
+        Assert.assertEquals( profileNames.size(), 2 );
+        Assert.assertTrue( profileNames.contains( "default" ) );
+        Assert.assertTrue( profileNames.contains( "newProfile" ) );
+
+        final long copiedResultLimit = ( ( long ) newConfig.readSetting( PwmSetting.HELPDESK_RESULT_LIMIT, "default" ).toNativeObject() );
+        Assert.assertEquals( copiedResultLimit, 19 );
+    }
+}

+ 57 - 0
server/src/test/java/password/pwm/config/stored/StoredConfigurationUtilTest.java

@@ -0,0 +1,57 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2019 The PWM Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package password.pwm.config.stored;
+
+import org.junit.Assert;
+import org.junit.Test;
+import password.pwm.config.PwmSetting;
+import password.pwm.config.value.StringValue;
+import password.pwm.error.PwmUnrecoverableException;
+
+import java.util.Set;
+
+public class StoredConfigurationUtilTest
+{
+
+    @Test
+    public void testChangedValues() throws PwmUnrecoverableException
+    {
+        final StoredConfiguration storedConfiguration = StoredConfigurationFactory.newConfig();
+        final StoredConfigurationModifier modifier = StoredConfigurationModifier.newModifier( storedConfiguration );
+
+        modifier.writeSetting( PwmSetting.NOTES, null, new StringValue( "notes test" ), null );
+
+        final StoredConfiguration newConfig = modifier.newStoredConfiguration();
+
+        final Set<StoredConfigItemKey> modifiedKeys = StoredConfigurationUtil.changedValues( storedConfiguration, newConfig );
+        Assert.assertEquals( modifiedKeys.size(), 1 );
+        Assert.assertEquals( modifiedKeys.iterator().next(), StoredConfigItemKey.fromSetting( PwmSetting.NOTES, null ) );
+
+
+        final StoredConfigurationModifier modifier2 = StoredConfigurationModifier.newModifier( newConfig );
+        modifier2.resetSetting( PwmSetting.NOTES, null, null );
+        final StoredConfiguration resetConfig = modifier2.newStoredConfiguration();
+        final Set<StoredConfigItemKey> resetKeys = StoredConfigurationUtil.changedValues( newConfig, resetConfig );
+        Assert.assertEquals( resetKeys.size(), 1 );
+        Assert.assertEquals( resetKeys.iterator().next(), StoredConfigItemKey.fromSetting( PwmSetting.NOTES, null ) );
+
+    }
+}

+ 0 - 57
server/src/test/java/password/pwm/tests/MakeSelfSignedCertTest.java

@@ -1,57 +0,0 @@
-/*
- * Password Management Servlets (PWM)
- * http://www.pwm-project.org
- *
- * Copyright (c) 2006-2009 Novell, Inc.
- * Copyright (c) 2009-2019 The PWM Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package password.pwm.tests;
-
-import org.bouncycastle.jce.provider.BouncyCastleProvider;
-import org.junit.Assert;
-import org.junit.Test;
-import password.pwm.util.secure.HttpsServerCertificateManager;
-
-import java.security.KeyPair;
-import java.security.KeyPairGenerator;
-import java.security.Provider;
-import java.security.SecureRandom;
-import java.security.Security;
-import java.security.cert.X509Certificate;
-import java.util.concurrent.TimeUnit;
-
-public class MakeSelfSignedCertTest
-{
-    private static final Provider BC_PROVIDER = new BouncyCastleProvider();
-
-    @Test
-    public void testSelfSignedCert() throws Exception
-    {
-        Security.addProvider( BC_PROVIDER );
-
-        final KeyPairGenerator kpGen = KeyPairGenerator.getInstance( "RSA", "BC" );
-        kpGen.initialize( 2048, new SecureRandom() );
-        final KeyPair keyPair = kpGen.generateKeyPair();
-
-
-        final String cnName = "test.myname.com";
-        final long futureSeconds = ( TimeUnit.DAYS.toMillis( 2 * 365 ) ) / 1000;
-
-        final X509Certificate storedCertData = HttpsServerCertificateManager.SelfCertGenerator.generateV3Certificate( keyPair, cnName, futureSeconds );
-        Assert.assertNotNull( storedCertData );
-        Assert.assertEquals( storedCertData.getSubjectDN().getName(), storedCertData.getIssuerDN().getName() );
-    }
-}

+ 43 - 0
server/src/test/java/password/pwm/util/secure/self/SelfCertGeneratorTest.java

@@ -0,0 +1,43 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2019 The PWM Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package password.pwm.util.secure.self;
+
+import org.junit.Assert;
+import org.junit.Test;
+import password.pwm.PwmConstants;
+import password.pwm.util.PasswordData;
+
+import java.security.KeyStore;
+import java.security.cert.Certificate;
+import java.security.cert.X509Certificate;
+
+public class SelfCertGeneratorTest
+{
+
+    @Test
+    public void doSelfCertGeneratorTest() throws Exception
+    {
+        final KeyStore keyStore = SelfCertFactory.generateNewCert( Settings.builder().build(), null, new PasswordData( "password" ), "alias" );
+        final Certificate certificate = keyStore.getCertificate( "alias" );
+        final String subjectDN = ( ( X509Certificate) certificate ).getSubjectDN().getName();
+        Assert.assertEquals( "CN=" + PwmConstants.PWM_APP_NAME.toLowerCase() + ".example.com", subjectDN );
+    }
+}

+ 9 - 9
webapp/pom.xml

@@ -29,7 +29,7 @@
                     <plugin>
                         <groupId>org.apache.maven.plugins</groupId>
                         <artifactId>maven-assembly-plugin</artifactId>
-                        <version>3.2.0</version>
+                        <version>3.3.0</version>
                         <configuration>
                             <descriptors>
                                 <descriptor>src/build/assembly/release-bundle.xml</descriptor>
@@ -56,7 +56,7 @@
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-assembly-plugin</artifactId>
-                <version>3.2.0</version>
+                <version>3.3.0</version>
                 <executions>
                     <execution>
                         <id>make-ldif-schema-zip</id>
@@ -91,7 +91,7 @@
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-war-plugin</artifactId>
-                <version>3.2.2</version>
+                <version>3.2.3</version>
                 <configuration>
                     <archiveClasses>false</archiveClasses>
                     <packagingExcludes>**/*.jsp</packagingExcludes>
@@ -182,7 +182,7 @@
                 <!-- builds xml file of dependencies and licenses for use in about page -->
                 <groupId>com.github.jinnovations</groupId>
                 <artifactId>attribution-maven-plugin</artifactId>
-                <version>0.9.7</version>
+                <version>0.9.8</version>
                 <executions>
                     <execution>
                         <goals>
@@ -252,7 +252,7 @@
             <plugin>
                 <groupId>io.github.zlika</groupId>
                 <artifactId>reproducible-build-maven-plugin</artifactId>
-                <version>0.11</version>
+                <version>0.12</version>
                 <executions>
                     <execution>
                         <goals>
@@ -303,22 +303,22 @@
         <dependency>
             <groupId>org.webjars.npm</groupId>
             <artifactId>dojo</artifactId>
-            <version>1.16.0</version>
+            <version>1.16.2</version>
         </dependency>
         <dependency>
             <groupId>org.webjars.npm</groupId>
             <artifactId>dijit</artifactId>
-            <version>1.16.0</version>
+            <version>1.16.2</version>
         </dependency>
         <dependency>
             <groupId>org.webjars.npm</groupId>
             <artifactId>dojox</artifactId>
-            <version>1.16.0</version>
+            <version>1.16.2</version>
         </dependency>
         <dependency>
             <groupId>org.webjars.npm</groupId>
             <artifactId>dgrid</artifactId>
-            <version>1.3.0</version>
+            <version>1.3.1</version>
         </dependency>
         <dependency>
             <groupId>org.webjars.bower</groupId>

+ 35 - 0
webapp/src/main/webapp/public/resources/js/configeditor.js

@@ -780,6 +780,35 @@ PWM_CFGEDIT.smsHealthCheck = function() {
     });
 };
 
+PWM_CFGEDIT.emailHealthCheck = function() {
+    require(["dojo/dom-form"], function(domForm){
+        var dialogBody = '<p>' + PWM_CONFIG.showString('Warning_EmailTestData') + '</p><form id="emailCheckParametersForm"><table>';
+        dialogBody += '<tr><td>To</td><td><input name="to" type="text" value="test@example.com"/></td></tr>';
+        dialogBody += '<tr><td>From</td><td><input name="from" type="text" value="@DefaultEmailFromAddress@"/></td></tr>';
+        dialogBody += '<tr><td>Subject</td><td><input name="subject" type="text" value="Test Email"/></td></tr>';
+        dialogBody += '<tr><td>Body</td><td><input name="body" type="text" value="Test Email""/></td></tr>';
+        dialogBody += '</table></form>';
+        PWM_MAIN.showDialog({text:dialogBody,showCancel:true,title:'Test Email Connection',closeOnOk:false,okAction:function(){
+                var formElement = PWM_MAIN.getObject("emailCheckParametersForm");
+                var formData = domForm.toObject(formElement);
+                var url =  "editor?processAction=emailHealthCheck";
+                url = PWM_MAIN.addParamToUrl(url,'profile',PWM_CFGEDIT.readCurrentProfile());
+                PWM_MAIN.showWaitDialog({loadFunction:function(){
+                        var loadFunction = function(data) {
+                            if (data['error']) {
+                                PWM_MAIN.showErrorDialog(data);
+                            } else {
+                                var bodyText = PWM_ADMIN.makeHealthHtml(data['data'],false,false);
+                                var titleText = 'Email Send Message Status';
+                                PWM_MAIN.showDialog({text:bodyText,title:titleText,showCancel:true});
+                            }
+                        };
+                        PWM_MAIN.ajaxRequest(url,loadFunction,{content:formData});
+                    }});
+            }});
+    });
+};
+
 PWM_CFGEDIT.selectTemplate = function(newTemplate) {
     PWM_MAIN.showConfirmDialog({
         text: PWM_CONFIG.showString('Warning_ChangeTemplate'),
@@ -838,6 +867,10 @@ PWM_CFGEDIT.displaySettingsCategory = function(category) {
         htmlSettingBody += '<div style="width: 100%; text-align: center">'
             + '<button class="btn" id="button-test-SMS"><span class="btn-icon pwm-icon pwm-icon-bolt"></span>Test SMS Settings</button>'
             + '</div>';
+    } else if (category === 'EMAIL_SERVERS') {
+        htmlSettingBody += '<div style="width: 100%; text-align: center">'
+            + '<button class="btn" id="button-test-EMAIL"><span class="btn-icon pwm-icon pwm-icon-bolt"></span>Test Email Settings</button>'
+            + '</div>';
     }
 
     PWM_VAR['skippedSettingCount'] = 0;
@@ -866,6 +899,8 @@ PWM_CFGEDIT.displaySettingsCategory = function(category) {
         PWM_MAIN.addEventHandler('button-test-DATABASE_SETTINGS', 'click', function(){PWM_CFGEDIT.databaseHealthCheck();});
     } else if (category === 'SMS_GATEWAY') {
         PWM_MAIN.addEventHandler('button-test-SMS', 'click', function(){PWM_CFGEDIT.smsHealthCheck();});
+    } else if (category === 'EMAIL_SERVERS') {
+        PWM_MAIN.addEventHandler('button-test-EMAIL', 'click', function(){PWM_CFGEDIT.emailHealthCheck();});
     } else if (category === 'HTTPS_SERVER') {
         PWM_MAIN.addEventHandler('button-test-HTTPS_SERVER', 'click', function(){PWM_CFGEDIT.httpsCertificateView();});
     }