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>
 <FindBugsFilter>
     <Match>
     <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>
     <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>
     <Match>
     <Match>
         <!-- due to bug with java 11 -->
         <!-- due to bug with java 11 -->
+        <!-- https://github.com/spotbugs/spotbugs/issues/756 -->
         <Bug pattern="RCN_REDUNDANT_NULLCHECK_WOULD_HAVE_BEEN_A_NPE"/>
         <Bug pattern="RCN_REDUNDANT_NULLCHECK_WOULD_HAVE_BEEN_A_NPE"/>
     </Match>
     </Match>
 </FindBugsFilter>
 </FindBugsFilter>

+ 2 - 2
client/pom.xml

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

+ 4 - 4
data-service/pom.xml

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

+ 1 - 1
docker/pom.xml

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

+ 2 - 2
onejar/pom.xml

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

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

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

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

@@ -72,6 +72,10 @@ public class TomcatOnejarRunner
         }
         }
         catch ( final Exception e )
         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() );
             throw new OnejarException( "error generating keystore: " + e.getMessage() );
         }
         }
 
 
@@ -160,6 +164,7 @@ public class TomcatOnejarRunner
         connector.setScheme( "https" );
         connector.setScheme( "https" );
         connector.addUpgradeProtocol( new Http2Protocol() );
         connector.addUpgradeProtocol( new Http2Protocol() );
         connector.setAttribute( "SSLEnabled", "true" );
         connector.setAttribute( "SSLEnabled", "true" );
+       // connector.setAttribute( "truststoreType", "PKCS12" );
         connector.setAttribute( "keystoreFile", onejarConfig.getKeystoreFile().getAbsolutePath() );
         connector.setAttribute( "keystoreFile", onejarConfig.getKeystoreFile().getAbsolutePath() );
         connector.setAttribute( "keystorePass", onejarConfig.getKeystorePass() );
         connector.setAttribute( "keystorePass", onejarConfig.getKeystorePass() );
         connector.setAttribute( "keyAlias", OnejarMain.KEYSTORE_ALIAS );
         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 -->
         <build.revision>0</build.revision>  <!-- default in case not set on command line -->
         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
         <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>
         <timestamp.iso>${maven.build.timestamp}</timestamp.iso>
         <maven.compiler.source>1.8</maven.compiler.source>
         <maven.compiler.source>1.8</maven.compiler.source>
         <maven.compiler.target>1.8</maven.compiler.target>
         <maven.compiler.target>1.8</maven.compiler.target>
@@ -60,6 +60,12 @@
                 <checkstyle.skip>true</checkstyle.skip>
                 <checkstyle.skip>true</checkstyle.skip>
             </properties>
             </properties>
         </profile>
         </profile>
+        <profile>
+            <id>skip-spotbugs</id>
+            <properties>
+                <spotbugs.skip>true</spotbugs.skip>
+            </properties>
+        </profile>
         <profile>
         <profile>
             <id>enable-javadoc</id>
             <id>enable-javadoc</id>
             <properties>
             <properties>
@@ -120,7 +126,7 @@
             <plugin>
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-javadoc-plugin</artifactId>
                 <artifactId>maven-javadoc-plugin</artifactId>
-                <version>3.1.1</version>
+                <version>3.2.0</version>
                 <executions>
                 <executions>
                     <execution>
                     <execution>
                         <goals>
                         <goals>
@@ -136,9 +142,6 @@
                                     <Implementation-URL>${project.organization.url}</Implementation-URL>
                                     <Implementation-URL>${project.organization.url}</Implementation-URL>
                                     <Implementation-Build-Java-Vendor>${java.vendor}</Implementation-Build-Java-Vendor>
                                     <Implementation-Build-Java-Vendor>${java.vendor}</Implementation-Build-Java-Vendor>
                                     <Implementation-Build-Java-Version>${java.version}</Implementation-Build-Java-Version>
                                     <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-Branch>${git.branch}</SCM-Git-Branch>
                                     <SCM-Git-Commit-ID>${git.commit.id}</SCM-Git-Commit-ID>
                                     <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>
                                     <SCM-Git-Commit-ID-Abbrev>${git.commit.id.abbrev}</SCM-Git-Commit-ID-Abbrev>
@@ -214,6 +217,7 @@
                 <configuration>
                 <configuration>
                     <source>${maven.compiler.source}</source>
                     <source>${maven.compiler.source}</source>
                     <target>${maven.compiler.target}</target>
                     <target>${maven.compiler.target}</target>
+                    <showWarnings>true</showWarnings>
                 </configuration>
                 </configuration>
             </plugin>
             </plugin>
             <plugin>
             <plugin>
@@ -224,7 +228,7 @@
                     <dependency>
                     <dependency>
                         <groupId>com.puppycrawl.tools</groupId>
                         <groupId>com.puppycrawl.tools</groupId>
                         <artifactId>checkstyle</artifactId>
                         <artifactId>checkstyle</artifactId>
-                        <version>8.30</version>
+                        <version>8.32</version>
                     </dependency>
                     </dependency>
                 </dependencies>
                 </dependencies>
                 <executions>
                 <executions>
@@ -303,15 +307,16 @@
             <plugin>
             <plugin>
                 <groupId>com.github.spotbugs</groupId>
                 <groupId>com.github.spotbugs</groupId>
                 <artifactId>spotbugs-maven-plugin</artifactId>
                 <artifactId>spotbugs-maven-plugin</artifactId>
-                <version>3.1.12.2</version>
+                <version>4.0.0</version>
                 <dependencies>
                 <dependencies>
                     <dependency>
                     <dependency>
                         <groupId>com.github.spotbugs</groupId>
                         <groupId>com.github.spotbugs</groupId>
                         <artifactId>spotbugs</artifactId>
                         <artifactId>spotbugs</artifactId>
-                        <version>4.0.0</version>
+                        <version>4.0.3</version>
                     </dependency>
                     </dependency>
                 </dependencies>
                 </dependencies>
                 <configuration>
                 <configuration>
+                    <skip>${spotbugs.skip}</skip>
                     <fork>false</fork>
                     <fork>false</fork>
                     <excludeFilterFile>${project.root.basedir}/build/spotbugs-exclude.xml</excludeFilterFile>
                     <excludeFilterFile>${project.root.basedir}/build/spotbugs-exclude.xml</excludeFilterFile>
                     <includeTests>false</includeTests>
                     <includeTests>false</includeTests>
@@ -319,7 +324,7 @@
                 </configuration>
                 </configuration>
                 <executions>
                 <executions>
                     <execution>
                     <execution>
-                        <phase>verify</phase>
+                        <phase>test</phase>
                         <goals>
                         <goals>
                             <goal>check</goal>
                             <goal>check</goal>
                         </goals>
                         </goals>
@@ -332,14 +337,17 @@
                 <version>1.8</version>
                 <version>1.8</version>
                 <executions>
                 <executions>
                     <execution>
                     <execution>
+                        <id>output-checksums</id>
                         <goals>
                         <goals>
                             <goal>artifacts</goal>
                             <goal>artifacts</goal>
                         </goals>
                         </goals>
-                        <phase>verify</phase>
+                        <phase>package</phase>
                         <configuration>
                         <configuration>
                             <algorithms>
                             <algorithms>
                                 <algorithm>SHA-1</algorithm>
                                 <algorithm>SHA-1</algorithm>
+                                <algorithm>SHA-256</algorithm>
                             </algorithms>
                             </algorithms>
+                            <failOnError>false</failOnError>
                         </configuration>
                         </configuration>
                     </execution>
                     </execution>
                 </executions>
                 </executions>
@@ -350,7 +358,7 @@
             <plugin> <!-- checks owsp vulnerability database -->
             <plugin> <!-- checks owsp vulnerability database -->
                 <groupId>org.owasp</groupId>
                 <groupId>org.owasp</groupId>
                 <artifactId>dependency-check-maven</artifactId>
                 <artifactId>dependency-check-maven</artifactId>
-                <version>5.3.0</version>
+                <version>5.3.2</version>
                 <executions>
                 <executions>
                     <execution>
                     <execution>
                         <goals>
                         <goals>
@@ -373,7 +381,7 @@
         <dependency>
         <dependency>
             <groupId>com.github.spotbugs</groupId>
             <groupId>com.github.spotbugs</groupId>
             <artifactId>spotbugs-annotations</artifactId>
             <artifactId>spotbugs-annotations</artifactId>
-            <version>4.0.0</version>
+            <version>4.0.3</version>
             <scope>provided</scope>
             <scope>provided</scope>
         </dependency>
         </dependency>
 
 
@@ -387,19 +395,19 @@
         <dependency>
         <dependency>
             <groupId>org.mockito</groupId>
             <groupId>org.mockito</groupId>
             <artifactId>mockito-core</artifactId>
             <artifactId>mockito-core</artifactId>
-            <version>3.3.0</version>
+            <version>3.3.3</version>
             <scope>test</scope>
             <scope>test</scope>
         </dependency>
         </dependency>
         <dependency>
         <dependency>
             <groupId>org.assertj</groupId>
             <groupId>org.assertj</groupId>
             <artifactId>assertj-core</artifactId>
             <artifactId>assertj-core</artifactId>
-            <version>3.15.0</version>
+            <version>3.16.1</version>
             <scope>test</scope>
             <scope>test</scope>
         </dependency>
         </dependency>
         <dependency>
         <dependency>
             <groupId>com.github.tomakehurst</groupId>
             <groupId>com.github.tomakehurst</groupId>
             <artifactId>wiremock</artifactId>
             <artifactId>wiremock</artifactId>
-            <version>2.26.1</version>
+            <version>2.26.3</version>
             <scope>test</scope>
             <scope>test</scope>
         </dependency>
         </dependency>
         <dependency>
         <dependency>

+ 1 - 1
pwm-cr/pom.xml

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

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

@@ -370,17 +370,13 @@ public class ChaiXmlResponseSetSerializer
             return;
             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(
     private static Element challengeToXml(

+ 7 - 10
server/pom.xml

@@ -116,9 +116,6 @@
                             <Implementation-URL>${project.organization.url}</Implementation-URL>
                             <Implementation-URL>${project.organization.url}</Implementation-URL>
                             <Implementation-Build-Java-Vendor>${java.vendor}</Implementation-Build-Java-Vendor>
                             <Implementation-Build-Java-Vendor>${java.vendor}</Implementation-Build-Java-Vendor>
                             <Implementation-Build-Java-Version>${java.version}</Implementation-Build-Java-Version>
                             <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-Branch>${git.branch}</SCM-Git-Branch>
                             <SCM-Git-Commit-ID>${git.commit.id}</SCM-Git-Commit-ID>
                             <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>
                             <SCM-Git-Commit-ID-Abbrev>${git.commit.id.abbrev}</SCM-Git-Commit-ID-Abbrev>
@@ -202,7 +199,7 @@
         <dependency>
         <dependency>
             <groupId>org.apache.directory.api</groupId>
             <groupId>org.apache.directory.api</groupId>
             <artifactId>api-all</artifactId>
             <artifactId>api-all</artifactId>
-            <version>2.0.0</version>
+            <version>2.0.1</version>
         </dependency>
         </dependency>
         <dependency>
         <dependency>
             <groupId>commons-net</groupId>
             <groupId>commons-net</groupId>
@@ -227,7 +224,7 @@
         <dependency>
         <dependency>
             <groupId>org.apache.commons</groupId>
             <groupId>org.apache.commons</groupId>
             <artifactId>commons-lang3</artifactId>
             <artifactId>commons-lang3</artifactId>
-            <version>3.9</version>
+            <version>3.10</version>
         </dependency>
         </dependency>
         <dependency>
         <dependency>
             <groupId>commons-validator</groupId>
             <groupId>commons-validator</groupId>
@@ -242,7 +239,7 @@
         <dependency>
         <dependency>
             <groupId>org.apache.httpcomponents</groupId>
             <groupId>org.apache.httpcomponents</groupId>
             <artifactId>httpclient</artifactId>
             <artifactId>httpclient</artifactId>
-            <version>4.5.11</version>
+            <version>4.5.12</version>
         </dependency>
         </dependency>
         <dependency>
         <dependency>
             <groupId>org.graylog2</groupId>
             <groupId>org.graylog2</groupId>
@@ -302,12 +299,12 @@
         <dependency>
         <dependency>
             <groupId>com.blueconic</groupId>
             <groupId>com.blueconic</groupId>
             <artifactId>browscap-java</artifactId>
             <artifactId>browscap-java</artifactId>
-            <version>1.2.14</version>
+            <version>1.2.16</version>
         </dependency>
         </dependency>
         <dependency>
         <dependency>
             <groupId>org.jetbrains.xodus</groupId>
             <groupId>org.jetbrains.xodus</groupId>
             <artifactId>xodus-environment</artifactId>
             <artifactId>xodus-environment</artifactId>
-            <version>1.3.124</version>
+            <version>1.3.232</version>
         </dependency>
         </dependency>
 
 
         <dependency>
         <dependency>
@@ -318,12 +315,12 @@
         <dependency>
         <dependency>
             <groupId>org.webjars</groupId>
             <groupId>org.webjars</groupId>
             <artifactId>webjars-locator-core</artifactId>
             <artifactId>webjars-locator-core</artifactId>
-            <version>0.44</version>
+            <version>0.45</version>
         </dependency>
         </dependency>
         <dependency>
         <dependency>
             <groupId>com.github.ben-manes.caffeine</groupId>
             <groupId>com.github.ben-manes.caffeine</groupId>
             <artifactId>caffeine</artifactId>
             <artifactId>caffeine</artifactId>
-            <version>2.8.1</version>
+            <version>2.8.4</version>
         </dependency>
         </dependency>
         <dependency>
         <dependency>
             <groupId>com.nulab-inc</groupId>
             <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_MAX_ROLLOVER                       ( "logging.file.maxRollover" ),
     LOGGING_FILE_PATH                               ( "logging.file.path" ),
     LOGGING_FILE_PATH                               ( "logging.file.path" ),
     LOGGING_DEV_OUTPUT                              ( "logging.devOutput.enable" ),
     LOGGING_DEV_OUTPUT                              ( "logging.devOutput.enable" ),
+    LOGGING_LOG_CSP_REPORT                          ( "logging.cspReport.enable" ),
     NEWUSER_LDAP_USE_TEMP_PW                        ( "newUser.ldap.useTempPassword" ),
     NEWUSER_LDAP_USE_TEMP_PW                        ( "newUser.ldap.useTempPassword" ),
     NEWUSER_TOKEN_ALLOW_PLAIN_PW                    ( "newUser.token.allowPlainPassword" ),
     NEWUSER_TOKEN_ALLOW_PLAIN_PW                    ( "newUser.token.allowPlainPassword" ),
     NMAS_THREADS_MAX_COUNT                          ( "nmas.threads.maxCount" ),
     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 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_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_JAVA_VERSION = BUILD_MANIFEST.getOrDefault( "Implementation-Build-Java-Version", "0" );
     public static final String BUILD_VERSION = BUILD_MANIFEST.getOrDefault( "Implementation-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;
     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
 public class UserIdentity implements Serializable, Comparable
 {
 {
+    private static final long serialVersionUID = 1L;
+
     private static final String CRYPO_HEADER = "ui_C-";
     private static final String CRYPO_HEADER = "ui_C-";
     private static final String DELIM_SEPARATOR = "|";
     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.i18n.Config;
 import password.pwm.util.i18n.LocaleHelper;
 import password.pwm.util.i18n.LocaleHelper;
+import password.pwm.util.java.LazySupplier;
 import password.pwm.util.java.XmlElement;
 import password.pwm.util.java.XmlElement;
 
 
 import java.util.ArrayList;
 import java.util.ArrayList;
@@ -189,10 +190,10 @@ public enum PwmSettingCategory
 
 
     private final PwmSettingCategory parent;
     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 )
     PwmSettingCategory( final PwmSettingCategory parent )
@@ -210,28 +211,18 @@ public enum PwmSettingCategory
         return this.toString();
         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();
         return profileSetting.get();
     }
     }
 
 
     public boolean hasProfiles( )
     public boolean hasProfiles( )
     {
     {
-        return getProfileSetting() != null;
+        return getProfileSetting().isPresent();
     }
     }
 
 
     public boolean isTopLevelProfile( )
     public boolean isTopLevelProfile( )
     {
     {
-        if ( isTopLevelProfile == null )
-        {
-            final boolean output = readProfileSettingFromXml( false ) != null;
-            isTopLevelProfile = ( ) -> output;
-        }
         return isTopLevelProfile.get();
         return isTopLevelProfile.get();
     }
     }
 
 
@@ -249,41 +240,12 @@ public enum PwmSettingCategory
 
 
     public int getLevel( )
     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();
         return level.get();
     }
     }
 
 
+
     public boolean isHidden( )
     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();
         return hidden.get();
     }
     }
 
 
@@ -317,34 +279,6 @@ public enum PwmSettingCategory
         return returnObj;
         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( )
     public List<PwmSetting> getSettings( )
     {
     {
         final List<password.pwm.config.PwmSetting> returnList = new ArrayList<>();
         final List<password.pwm.config.PwmSetting> returnList = new ArrayList<>();
@@ -464,4 +398,69 @@ public enum PwmSettingCategory
                 .findFirst()
                 .findFirst()
                 .orElse( null );
                 .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 )
     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 );
         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
 public class PwmPasswordPolicy implements Profile, Serializable
 {
 {
+    private static final long serialVersionUID = 1L;
 
 
     private static final PwmLogger LOGGER = PwmLogger.forClass( PwmPasswordPolicy.class );
     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
             else
             {
             {
-                profileSetting = pwmSetting.getCategory().getProfileSetting();
+                profileSetting = pwmSetting.getCategory().getProfileSetting().orElseThrow( IllegalStateException::new );
             }
             }
 
 
             final StoredValue effectiveValue;
             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.ArrayList;
 import java.util.Collection;
 import java.util.Collection;
 import java.util.HashMap;
 import java.util.HashMap;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.List;
 import java.util.Map;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Objects;
@@ -195,7 +196,8 @@ public class StoredConfigurationModifier
         {
         {
             final StoredConfiguration oldStoredConfiguration = new StoredConfigurationImpl( storedConfigData );
             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 ) )
             if ( !existingProfiles.contains( sourceID ) )
             {
             {
                 throw PwmUnrecoverableException.newException(
                 throw PwmUnrecoverableException.newException(
@@ -209,6 +211,8 @@ public class StoredConfigurationModifier
             }
             }
 
 
             final Collection<PwmSettingCategory> interestedCategories = PwmSettingCategory.associatedProfileCategories( category );
             final Collection<PwmSettingCategory> interestedCategories = PwmSettingCategory.associatedProfileCategories( category );
+            final Map<StoredConfigItemKey, StoredValue> newValues = new LinkedHashMap<>();
+
             for ( final PwmSettingCategory interestedCategory : interestedCategories )
             for ( final PwmSettingCategory interestedCategory : interestedCategories )
             {
             {
                 for ( final PwmSetting pwmSetting : interestedCategory.getSettings() )
                 for ( final PwmSetting pwmSetting : interestedCategory.getSettings() )
@@ -216,19 +220,25 @@ public class StoredConfigurationModifier
                     if ( !oldStoredConfiguration.isDefaultValue( pwmSetting, sourceID ) )
                     if ( !oldStoredConfiguration.isDefaultValue( pwmSetting, sourceID ) )
                     {
                     {
                         final StoredValue value = oldStoredConfiguration.readSetting( 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 );
             final ValueMetaData valueMetaData = new ValueMetaData( Instant.now(), userIdentity );
 
 
             return storedConfigData.toBuilder()
             return storedConfigData.toBuilder()
-                    .storedValue( key, value )
+                    .storedValues( newValues )
                     .metaData( key, valueMetaData )
                     .metaData( key, valueMetaData )
                     .build();
                     .build();
 
 

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

@@ -76,7 +76,7 @@ public abstract class StoredConfigurationUtil
         }
         }
         else
         else
         {
         {
-            profileSetting = pwmSetting.getCategory().getProfileSetting();
+            profileSetting = pwmSetting.getCategory().getProfileSetting().orElseThrow( IllegalStateException::new );
         }
         }
 
 
         return profilesForProfileSetting( profileSetting, storedConfiguration );
         return profilesForProfileSetting( profileSetting, storedConfiguration );
@@ -87,35 +87,20 @@ public abstract class StoredConfigurationUtil
             final StoredConfiguration storedConfiguration
             final StoredConfiguration storedConfiguration
     )
     )
     {
     {
-        final PwmSetting profileSetting = category.getProfileSetting();
+        final PwmSetting profileSetting = category.getProfileSetting().orElseThrow( IllegalStateException::new );
 
 
         return profilesForProfileSetting( profileSetting, storedConfiguration );
         return profilesForProfileSetting( profileSetting, storedConfiguration );
     }
     }
 
 
     private static List<String> profilesForProfileSetting(
     private static List<String> profilesForProfileSetting(
-            final PwmSetting pwmSetting,
+            final PwmSetting profileSetting,
             final StoredConfiguration storedConfiguration
             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 Object nativeObject = storedConfiguration.readSetting( profileSetting, null ).toNativeObject();
         final List<String> settingValues = ( List<String> ) nativeObject;
         final List<String> settingValues = ( List<String> ) nativeObject;
         final LinkedList<String> profiles = new LinkedList<>( settingValues );
         final LinkedList<String> profiles = new LinkedList<>( settingValues );
-        profiles.removeIf( profile -> StringUtil.isEmpty( profile ) );
+        profiles.removeIf( StringUtil::isEmpty );
         return Collections.unmodifiableList( profiles );
         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() ) );
                     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() );
                     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
 public class NamedSecretValue implements StoredValue
 {
 {
+    private static final long serialVersionUID = 1L;
 
 
     private final transient LazySupplier<String> valueHashSupplier = new LazySupplier<>( () -> AbstractValue.valueHashComputer( NamedSecretValue.this ) );
     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
 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 transient LazySupplier<String> valueHashSupplier = new LazySupplier<>( () -> AbstractValue.valueHashComputer( PasswordValue.this ) );
 
 
     private final PasswordData value;
     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.stored.XmlOutputProcessData;
 import password.pwm.config.value.data.FormConfiguration;
 import password.pwm.config.value.data.FormConfiguration;
 import password.pwm.util.java.JsonUtil;
 import password.pwm.util.java.JsonUtil;
+import password.pwm.util.java.StringUtil;
 import password.pwm.util.java.XmlElement;
 import password.pwm.util.java.XmlElement;
 import password.pwm.util.java.XmlFactory;
 import password.pwm.util.java.XmlFactory;
 import password.pwm.util.secure.PwmSecurityKey;
 import password.pwm.util.secure.PwmSecurityKey;
@@ -86,14 +87,14 @@ public class StringValue extends AbstractValue implements StoredValue
     {
     {
         if ( pwmSetting.isRequired() )
         if ( pwmSetting.isRequired() )
         {
         {
-            if ( value == null || value.length() < 1 )
+            if ( StringUtil.isEmpty( value ) )
             {
             {
                 return Collections.singletonList( "required value missing" );
                 return Collections.singletonList( "required value missing" );
             }
             }
         }
         }
 
 
         final Pattern pattern = pwmSetting.getRegExPattern();
         final Pattern pattern = pwmSetting.getRegExPattern();
-        if ( pattern != null && value != null )
+        if ( pattern != null )
         {
         {
             final Matcher matcher = pattern.matcher( value );
             final Matcher matcher = pattern.matcher( value );
             if ( value != null && value.length() > 0 && !matcher.matches() )
             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;
 package password.pwm.config.value.data;
 
 
+import lombok.EqualsAndHashCode;
 import lombok.Value;
 import lombok.Value;
 import password.pwm.util.i18n.LocaleHelper;
 import password.pwm.util.i18n.LocaleHelper;
 import password.pwm.util.java.JsonUtil;
 import password.pwm.util.java.JsonUtil;
@@ -33,6 +34,7 @@ import java.util.Map;
  * @author Richard A. Keil
  * @author Richard A. Keil
  */
  */
 @Value
 @Value
+@EqualsAndHashCode( callSuper = false )
 public class CustomLinkConfiguration implements Serializable
 public class CustomLinkConfiguration implements Serializable
 {
 {
 
 
@@ -41,13 +43,13 @@ public class CustomLinkConfiguration implements Serializable
         text, url, select, checkbox, customLink
         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 )
     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
 public class ContextManager implements Serializable
 {
 {
+    private static final long serialVersionUID = 1L;
+
     private static final PwmLogger LOGGER = PwmLogger.forClass( ContextManager.class );
     private static final PwmLogger LOGGER = PwmLogger.forClass( ContextManager.class );
     private static final SessionLabel SESSION_LABEL = SessionLabel.CONTEXT_SESSION_LABEL;
     private static final SessionLabel SESSION_LABEL = SessionLabel.CONTEXT_SESSION_LABEL;
 
 
@@ -444,7 +446,13 @@ public class ContextManager implements Serializable
                     try
                     try
                     {
                     {
                         final PropertyConfigurationImporter importer = new PropertyConfigurationImporter();
                         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 );
                         configReader.saveConfiguration( storedConfiguration, pwmApplication, SESSION_LABEL );
                         LOGGER.info( SESSION_LABEL, () -> "file " + silentPropertiesFile.getAbsolutePath() + " has been successfully imported and saved as configuration file" );
                         LOGGER.info( SESSION_LABEL, () -> "file " + silentPropertiesFile.getAbsolutePath() + " has been successfully imported and saved as configuration file" );
                         requestPwmApplicationRestart();
                         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
 public class PwmSession implements Serializable
 {
 {
+    private static final long serialVersionUID = 1L;
+
     private static final PwmLogger LOGGER = PwmLogger.forClass( PwmSession.class );
     private static final PwmLogger LOGGER = PwmLogger.forClass( PwmSession.class );
 
 
     private final transient PwmApplication pwmApplication;
     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;
                             return ProcessStatus.Halt;
                         }
                         }
 
 
+                        if ( pwmRequest.isAuthenticated() )
+                        {
+                            return ProcessStatus.Continue;
+                        }
+
                     }
                     }
                     catch ( final Exception e )
                     catch ( final Exception e )
                     {
                     {
@@ -96,13 +101,13 @@ public abstract class HttpAuthenticationUtilities
                         {
                         {
                             final String errorMsg = "error during " + authenticationMethod + " authentication attempt: " + e.getMessage();
                             final String errorMsg = "error during " + authenticationMethod + " authentication attempt: " + e.getMessage();
                             errorInformation = new ErrorInformation( ( ( PwmException ) e ).getError(), errorMsg );
                             errorInformation = new ErrorInformation( ( ( PwmException ) e ).getError(), errorMsg );
+                            LOGGER.error( pwmRequest, errorInformation );
                         }
                         }
                         else
                         else
                         {
                         {
                             errorInformation = new ErrorInformation( PwmError.ERROR_INTERNAL, e.getMessage() );
                             errorInformation = new ErrorInformation( PwmError.ERROR_INTERNAL, e.getMessage() );
-
+                            LOGGER.error( pwmRequest.getLabel(), errorInformation, e );
                         }
                         }
-                        LOGGER.error( pwmRequest, errorInformation );
                         pwmRequest.respondWithError( errorInformation );
                         pwmRequest.respondWithError( errorInformation );
                         return ProcessStatus.Halt;
                         return ProcessStatus.Halt;
                     }
                     }

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

@@ -30,6 +30,8 @@ import java.util.Set;
 @Data
 @Data
 public class ConfigManagerBean extends PwmSessionBean
 public class ConfigManagerBean extends PwmSessionBean
 {
 {
+    private static final long serialVersionUID = 1L;
+
     private transient StoredConfiguration storedConfiguration;
     private transient StoredConfiguration storedConfiguration;
     private boolean passwordVerified;
     private boolean passwordVerified;
     private boolean configUnlockedWarningShown;
     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 )
 @EqualsAndHashCode( callSuper = false )
 public class ForgottenPasswordBean extends PwmSessionBean
 public class ForgottenPasswordBean extends PwmSessionBean
 {
 {
+    private static final long serialVersionUID = 1L;
+
     @SerializedName( "pr" )
     @SerializedName( "pr" )
     private String profile;
     private String profile;
 
 
@@ -82,6 +84,8 @@ public class ForgottenPasswordBean extends PwmSessionBean
     @Data
     @Data
     public static class Progress implements Serializable
     public static class Progress implements Serializable
     {
     {
+        private static final long serialVersionUID = 1L;
+
         @SerializedName( "s" )
         @SerializedName( "s" )
         private boolean tokenSent;
         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() )
             if ( contentPolicy != null && !contentPolicy.isEmpty() )
             {
             {
                 final String nonce = pwmRequest.getCspNonce();
                 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 );
                 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 ),
         strings( HttpMethod.GET ),
         health( HttpMethod.GET ),
         health( HttpMethod.GET ),
         ping( HttpMethod.GET ),
         ping( HttpMethod.GET ),
-        statistics( HttpMethod.GET ),;
+        statistics( HttpMethod.GET ),
+        cspReport( HttpMethod.POST ),;
 
 
 
 
         private final HttpMethod method;
         private final HttpMethod method;
@@ -446,7 +447,6 @@ public class ClientApiServlet extends ControlledPwmServlet
         return displayStrings;
         return displayStrings;
     }
     }
 
 
-
     @ActionHandler( action = "statistics" )
     @ActionHandler( action = "statistics" )
     private ProcessStatus restStatisticsHandler( final PwmRequest pwmRequest )
     private ProcessStatus restStatisticsHandler( final PwmRequest pwmRequest )
             throws ChaiUnavailableException, PwmUnrecoverableException, IOException
             throws ChaiUnavailableException, PwmUnrecoverableException, IOException
@@ -473,7 +473,20 @@ public class ClientApiServlet extends ControlledPwmServlet
         final RestResultBean restResultBean = RestResultBean.withData( jsonOutput );
         final RestResultBean restResultBean = RestResultBean.withData( jsonOutput );
         pwmRequest.outputJsonResult( restResultBean );
         pwmRequest.outputJsonResult( restResultBean );
         return ProcessStatus.Halt;
         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 )
     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 ),
                 l.forKey( "Field_AppVersion", PwmConstants.PWM_APP_NAME ),
                 PwmConstants.SERVLET_VERSION
                 PwmConstants.SERVLET_VERSION
         ) );
         ) );
+        aboutData.add( new DisplayElement(
+                "appBuildTime",
+                DisplayElement.Type.timestamp,
+                l.forKey( "Field_AppBuildTime" ),
+                PwmConstants.BUILD_TIME
+        ) );
         aboutData.add( new DisplayElement(
         aboutData.add( new DisplayElement(
                 "currentTime",
                 "currentTime",
                 DisplayElement.Type.timestamp,
                 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
 @Builder
 public class UserDebugDataBean implements Serializable
 public class UserDebugDataBean implements Serializable
 {
 {
+    private static final long serialVersionUID = 1L;
+
     private transient UserInfo userInfo;
     private transient UserInfo userInfo;
 
 
     private final PublicUserInfoBean publicUserInfoBean;
     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.PwmApplication;
 import password.pwm.PwmApplicationMode;
 import password.pwm.PwmApplicationMode;
 import password.pwm.PwmConstants;
 import password.pwm.PwmConstants;
+import password.pwm.bean.EmailItemBean;
 import password.pwm.bean.SessionLabel;
 import password.pwm.bean.SessionLabel;
 import password.pwm.bean.SmsItemBean;
 import password.pwm.bean.SmsItemBean;
 import password.pwm.bean.UserIdentity;
 import password.pwm.bean.UserIdentity;
@@ -36,6 +37,7 @@ import password.pwm.config.PwmSettingTemplate;
 import password.pwm.config.PwmSettingTemplateSet;
 import password.pwm.config.PwmSettingTemplateSet;
 import password.pwm.config.SettingUIFunction;
 import password.pwm.config.SettingUIFunction;
 import password.pwm.config.StoredValue;
 import password.pwm.config.StoredValue;
+import password.pwm.config.profile.EmailServerProfile;
 import password.pwm.config.profile.PwmPasswordPolicy;
 import password.pwm.config.profile.PwmPasswordPolicy;
 import password.pwm.config.stored.ConfigurationProperty;
 import password.pwm.config.stored.ConfigurationProperty;
 import password.pwm.config.stored.StoredConfigItemKey;
 import password.pwm.config.stored.StoredConfigItemKey;
@@ -72,6 +74,9 @@ import password.pwm.i18n.Config;
 import password.pwm.i18n.Message;
 import password.pwm.i18n.Message;
 import password.pwm.i18n.PwmLocaleBundle;
 import password.pwm.i18n.PwmLocaleBundle;
 import password.pwm.ldap.LdapBrowser;
 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.PasswordData;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.JsonUtil;
 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.RestRandomPasswordServer;
 import password.pwm.ws.server.rest.bean.HealthData;
 import password.pwm.ws.server.rest.bean.HealthData;
 
 
+import javax.mail.MessagingException;
 import javax.servlet.ServletException;
 import javax.servlet.ServletException;
 import javax.servlet.annotation.WebServlet;
 import javax.servlet.annotation.WebServlet;
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayInputStream;
@@ -100,6 +106,7 @@ import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.List;
 import java.util.Locale;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Map;
+import java.util.Optional;
 import java.util.ResourceBundle;
 import java.util.ResourceBundle;
 import java.util.Set;
 import java.util.Set;
 import java.util.StringTokenizer;
 import java.util.StringTokenizer;
@@ -133,6 +140,7 @@ public class ConfigEditorServlet extends ControlledPwmServlet
         ldapHealthCheck( HttpMethod.POST ),
         ldapHealthCheck( HttpMethod.POST ),
         databaseHealthCheck( HttpMethod.POST ),
         databaseHealthCheck( HttpMethod.POST ),
         smsHealthCheck( HttpMethod.POST ),
         smsHealthCheck( HttpMethod.POST ),
+        emailHealthCheck( HttpMethod.POST ),
         finishEditing( HttpMethod.POST ),
         finishEditing( HttpMethod.POST ),
         executeSettingFunction( HttpMethod.POST ),
         executeSettingFunction( HttpMethod.POST ),
         setConfigurationPassword( HttpMethod.POST ),
         setConfigurationPassword( HttpMethod.POST ),
@@ -743,6 +751,57 @@ public class ConfigEditorServlet extends ControlledPwmServlet
         return ProcessStatus.Halt;
         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" )
     @ActionHandler( action = "uploadFile" )
     private ProcessStatus doUploadFile(
     private ProcessStatus doUploadFile(
             final PwmRequest pwmRequest
             final PwmRequest pwmRequest
@@ -1061,7 +1120,8 @@ public class ConfigEditorServlet extends ControlledPwmServlet
         {
         {
             if ( loopCategory.hasProfiles() )
             if ( loopCategory.hasProfiles() )
             {
             {
-                if ( loopCategory.getProfileSetting() == setting )
+                final Optional<PwmSetting> profileSetting = loopCategory.getProfileSetting();
+                if ( profileSetting.isPresent() && profileSetting.get() == setting )
                 {
                 {
                     category = loopCategory;
                     category = loopCategory;
                 }
                 }
@@ -1070,6 +1130,12 @@ public class ConfigEditorServlet extends ControlledPwmServlet
 
 
         final String sourceID = inputMap.get( "sourceID" );
         final String sourceID = inputMap.get( "sourceID" );
         final String destinationID = inputMap.get( "destinationID" );
         final String destinationID = inputMap.get( "destinationID" );
+
+        if ( category == null )
+        {
+            throw new IllegalStateException();
+        }
+
         try
         try
         {
         {
             modifier.copyProfileID( category, sourceID, destinationID, pwmRequest.getUserInfoIfLoggedIn() );
             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() )
         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() )
             if ( profileIDs == null || profileIDs.isEmpty() )
             {
             {
                 return true;
                 return true;
             }
             }
+
             for ( final String profileID : profileIDs )
             for ( final String profileID : profileIDs )
             {
             {
                 for ( final PwmSetting setting : category.getSettings() )
                 for ( final PwmSetting setting : category.getSettings() )
@@ -199,7 +202,8 @@ class NavTreeHelper
                         final String editItemName = LocaleHelper.getLocalizedMessage( locale, Config.Label_ProfileListEditMenuItem, null );
                         final String editItemName = LocaleHelper.getLocalizedMessage( locale, Config.Label_ProfileListEditMenuItem, null );
                         profileEditorInfo.setName( editItemName );
                         profileEditorInfo.setName( editItemName );
                         profileEditorInfo.setType( NavTreeHelper.NavItemType.profileDefinition );
                         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() );
                         profileEditorInfo.setParent( loopCategory.getKey() );
                         navigationData.add( profileEditorInfo );
                         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;
             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;
         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
 class HelpdeskVerificationStateBean implements Serializable
 {
 {
+    private static final long serialVersionUID = 1L;
+
     private static final PwmLogger LOGGER = PwmLogger.forClass( HelpdeskVerificationStateBean.class );
     private static final PwmLogger LOGGER = PwmLogger.forClass( HelpdeskVerificationStateBean.class );
     public static final String PARAMETER_VERIFICATION_STATE_KEY = "verificationState";
     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 )
                 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" );
                             + " and does not contain password, thus ignoring authentication so SSO process can repeat" );
                     return;
                     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(
         return Collections.unmodifiableSet( new HashSet<>( Arrays.asList(
                 this.getTheClass().getSimpleName(),
                 this.getTheClass().getSimpleName(),
                 this.getTheClass().getName(),
                 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 ThreadLocal<ThreadLocalContainer> threadLocalProvider = new ThreadLocal<>();
     private final Set<ThreadLocalContainer> threadLocalContainers = Collections.synchronizedSet( Collections.newSetFromMap( new WeakHashMap<>() ) );
     private final Set<ThreadLocalContainer> threadLocalContainers = Collections.synchronizedSet( Collections.newSetFromMap( new WeakHashMap<>() ) );
     private final ReentrantLock reentrantLock = new ReentrantLock();
     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 ChaiProviderFactory chaiProviderFactory = ChaiProviderFactory.newProviderFactory();
     private final Map<String, Map<Integer, ChaiProvider>> proxyChaiProviders = new HashMap<>();
     private final Map<String, Map<Integer, ChaiProvider>> proxyChaiProviders = new HashMap<>();
 
 
@@ -377,6 +377,14 @@ public class LdapConnectionService implements PwmService
         return chaiProviderFactory;
         return chaiProviderFactory;
     }
     }
 
 
+    private void conditionallyLogDebugInfo()
+    {
+        if ( !chaiProviderFactory.activeProviders().isEmpty() )
+        {
+            logDebugInfo();
+        }
+    }
+
     private void logDebugInfo()
     private void logDebugInfo()
     {
     {
         LOGGER.trace( () -> "status: " + StringUtil.mapToString( connectionDebugInfo() ) );
         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 )
 @Builder( toBuilder = true )
 public class SearchConfiguration implements Serializable
 public class SearchConfiguration implements Serializable
 {
 {
+    private static final long serialVersionUID = 1L;
+
     private String filter;
     private String filter;
     private String ldapProfile;
     private String ldapProfile;
     private String username;
     private String username;
@@ -48,6 +50,9 @@ public class SearchConfiguration implements Serializable
     private transient ChaiProvider chaiProvider;
     private transient ChaiProvider chaiProvider;
     private long searchTimeout;
     private long searchTimeout;
 
 
+    @Builder.Default
+    private boolean ignoreOperationalErrors = false;
+
     @Builder.Default
     @Builder.Default
     private boolean enableValueEscaping = true;
     private boolean enableValueEscaping = true;
 
 
@@ -57,6 +62,27 @@ public class SearchConfiguration implements Serializable
     @Builder.Default
     @Builder.Default
     private boolean enableSplitWhitespace = false;
     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( )
     void validate( )
     {
     {
         if ( this.username != null && this.formValues != null )
         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.
             //see if we need to do a contextless search.
             if ( checkIfStringIsDN( username, sessionLabel ) )
             if ( checkIfStringIsDN( username, sessionLabel ) )
             {
             {
-                return resolveUserDN( username );
+                return resolveUserDN( username, sessionLabel );
             }
             }
             else
             else
             {
             {
@@ -207,10 +207,6 @@ public class UserSearchEngine implements PwmService
                     e.getErrorInformation().getFieldValues() )
                     e.getErrorInformation().getFieldValues() )
             );
             );
         }
         }
-        catch ( final ChaiUnavailableException e )
-        {
-            throw PwmUnrecoverableException.fromChaiException( e );
-        }
     }
     }
 
 
     public UserIdentity performSingleUserSearch(
     public UserIdentity performSingleUserSearch(
@@ -450,6 +446,8 @@ public class UserSearchEngine implements PwmService
                     .sessionLabel( sessionLabel )
                     .sessionLabel( sessionLabel )
                     .searchID( searchID )
                     .searchID( searchID )
                     .jobId( jobIncrementer.next() )
                     .jobId( jobIncrementer.next() )
+                    .searchScope( searchConfiguration.getSearchScope() )
+                    .ignoreOperationalErrors( searchConfiguration.isIgnoreOperationalErrors() )
                     .build();
                     .build();
             final UserSearchJob userSearchJob = new UserSearchJob( pwmApplication, this, userSearchJobParameters );
             final UserSearchJob userSearchJob = new UserSearchJob( pwmApplication, this, userSearchJobParameters );
             returnMap.add( userSearchJob );
             returnMap.add( userSearchJob );
@@ -574,30 +572,38 @@ public class UserSearchEngine implements PwmService
         return false;
         return false;
     }
     }
 
 
-
     private UserIdentity resolveUserDN(
     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(
     private Map<UserIdentity, Map<String, String>> executeSearchJobs(
@@ -671,7 +677,7 @@ public class UserSearchEngine implements PwmService
     }
     }
 
 
     private Map<UserIdentity, Map<String, String>> aggregateJobResults(
     private Map<UserIdentity, Map<String, String>> aggregateJobResults(
-          final Collection<UserSearchJob> userSearchJobs
+            final Collection<UserSearchJob> userSearchJobs
     )
     )
             throws PwmUnrecoverableException
             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 maxThreads = Integer.parseInt( configuration.readAppProperty( AppProperty.LDAP_SEARCH_PARALLEL_THREAD_MAX ) );
             final int threads = Math.min( maxThreads, ( endPoints ) * factor );
             final int threads = Math.min( maxThreads, ( endPoints ) * factor );
             final ThreadFactory threadFactory = PwmScheduler.makePwmThreadFactory( PwmScheduler.makeThreadName( pwmApplication, UserSearchEngine.class ), true );
             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(
             return new ThreadPoolExecutor(
-                    1,
+                    minThreads,
                     threads,
                     threads,
                     1,
                     1,
                     TimeUnit.MINUTES,
                     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.setFilter( userSearchJobParameters.getSearchFilter() );
         searchHelper.setAttributes( userSearchJobParameters.getReturnAttributes() );
         searchHelper.setAttributes( userSearchJobParameters.getReturnAttributes() );
         searchHelper.setTimeLimit( ( int ) userSearchJobParameters.getTimeoutMs() );
         searchHelper.setTimeLimit( ( int ) userSearchJobParameters.getTimeoutMs() );
+        searchHelper.setSearchScope( userSearchJobParameters.getSearchScope().getChaiSearchScope() );
+
 
 
         final String debugInfo;
         final String debugInfo;
         {
         {
@@ -86,10 +88,10 @@ class UserSearchJob implements Callable<Map<UserIdentity, Map<String, String>>>
                         + debugInfo );
                         + debugInfo );
 
 
         final Instant startTime = Instant.now();
         final Instant startTime = Instant.now();
-        final Map<String, Map<String, String>> results;
+        final Map<String, Map<String, String>> results = new LinkedHashMap<>();
         try
         try
         {
         {
-            results = userSearchJobParameters.getChaiProvider().search( userSearchJobParameters.getContext(), searchHelper );
+            results.putAll( userSearchJobParameters.getChaiProvider().search( userSearchJobParameters.getContext(), searchHelper ) );
         }
         }
         catch ( final ChaiUnavailableException e )
         catch ( final ChaiUnavailableException e )
         {
         {
@@ -97,8 +99,11 @@ class UserSearchJob implements Callable<Map<UserIdentity, Map<String, String>>>
         }
         }
         catch ( final ChaiOperationException e )
         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 );
         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 SessionLabel sessionLabel;
     private final int searchID;
     private final int searchID;
     private final int jobId;
     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 ),
     StatisticsManager( password.pwm.svc.stats.StatisticsManager.class, Flag.StartDuringRuntimeInstance ),
     WordlistManager( WordlistService.class, Flag.StartDuringRuntimeInstance ),
     WordlistManager( WordlistService.class, Flag.StartDuringRuntimeInstance ),
     SeedlistManager( SeedlistService.class ),
     SeedlistManager( SeedlistService.class ),
-    EmailQueueManager( EmailService.class ),
+    EmailQueueManager( EmailService.class, Flag.StartDuringRuntimeInstance ),
     SmsQueueManager( password.pwm.util.queue.SmsQueueManager.class ),
     SmsQueueManager( password.pwm.util.queue.SmsQueueManager.class ),
     UrlShortenerService( password.pwm.svc.shorturl.UrlShortenerService.class ),
     UrlShortenerService( password.pwm.svc.shorturl.UrlShortenerService.class ),
     TokenService( password.pwm.svc.token.TokenService.class, Flag.StartDuringRuntimeInstance ),
     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;
         return returnObj;
     }
     }
 
 
-    private static Optional<EmailServer> makeEmailServer(
+    public static Optional<EmailServer> makeEmailServer(
             final Configuration configuration,
             final Configuration configuration,
             final EmailServerProfile profile,
             final EmailServerProfile profile,
             final TrustManager[] trustManagers
             final TrustManager[] trustManagers
@@ -105,7 +105,10 @@ public class EmailServerUtil
                 && port > 0
                 && 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 );
             final javax.mail.Session session = javax.mail.Session.getInstance( properties, null );
             return Optional.of( EmailServer.builder()
             return Optional.of( EmailServer.builder()
                     .id( id )
                     .id( id )
@@ -177,16 +180,20 @@ public class EmailServerUtil
 
 
             final MailSSLSocketFactory mailSSLSocketFactory = new MailSSLSocketFactory();
             final MailSSLSocketFactory mailSSLSocketFactory = new MailSSLSocketFactory();
             mailSSLSocketFactory.setTrustManagers( trustManager );
             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", 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 )
         catch ( final Exception e )
         {
         {
@@ -413,5 +420,4 @@ public class EmailServerUtil
 
 
         return Collections.emptyList();
         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 )
     private boolean determineIfItemCanBeDelivered( final EmailItemBean emailItem )
     {
     {
-
         if ( servers.isEmpty() )
         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;
             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(
     public void submitEmail(
@@ -306,7 +320,7 @@ public class EmailService implements PwmService
                 workingItemBean = EmailServerUtil.applyMacrosToEmail( workingItemBean, macroMachine );
                 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() );
                 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 newThreadLocalTransport = new AtomicInteger();
     private final AtomicInteger useExistingConnection = new AtomicInteger();
     private final AtomicInteger useExistingConnection = new AtomicInteger();
     private final AtomicInteger useExistingTransport = new AtomicInteger();
     private final AtomicInteger useExistingTransport = new AtomicInteger();
@@ -351,12 +395,37 @@ public class EmailService implements PwmService
     }
     }
 
 
     private WorkQueueProcessor.ProcessResult sendItem( final EmailItemBean emailItemBean )
     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;
         EmailConnection serverTransport = null;
 
 
-        // create a new MimeMessage object (using the Session created above)
         try
         try
         {
         {
+            // create a new MimeMessage object (using the Session created above)
             if ( threadLocalTransport.get() == null )
             if ( threadLocalTransport.get() == null )
             {
             {
 
 
@@ -401,11 +470,10 @@ public class EmailService implements PwmService
 
 
             LOGGER.debug( () -> "sent email: " + emailItemBean.toDebugString() );
             LOGGER.debug( () -> "sent email: " + emailItemBean.toDebugString() );
             StatisticsManager.incrementStat( pwmApplication, Statistic.EMAIL_SEND_SUCCESSES );
             StatisticsManager.incrementStat( pwmApplication, Statistic.EMAIL_SEND_SUCCESSES );
-            return WorkQueueProcessor.ProcessResult.SUCCESS;
+
         }
         }
         catch ( final MessagingException | PwmException e )
         catch ( final MessagingException | PwmException e )
         {
         {
-
             final ErrorInformation errorInformation;
             final ErrorInformation errorInformation;
             if ( e instanceof PwmException )
             if ( e instanceof PwmException )
             {
             {
@@ -429,19 +497,7 @@ public class EmailService implements PwmService
                 serverErrors.put( serverTransport.getEmailServer(), errorInformation );
                 serverErrors.put( serverTransport.getEmailServer(), errorInformation );
             }
             }
             LOGGER.error( 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 )
 @Builder( toBuilder = true )
 public class WordlistConfiguration implements Serializable
 public class WordlistConfiguration implements Serializable
 {
 {
+    private static final long serialVersionUID = 1L;
+
     static final int STREAM_BUFFER_SIZE = 1_1024_1024;
     static final int STREAM_BUFFER_SIZE = 1_1024_1024;
     static final PwmHashAlgorithm HASH_ALGORITHM = PwmHashAlgorithm.SHA256;
     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;
         boolean needsAutoImport = false;
         if ( remoteInfo == null )
         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
         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 password.pwm.PwmConstants;
 
 
 import javax.swing.JOptionPane;
 import javax.swing.JOptionPane;
+import java.util.Map;
 
 
 
 
 public class JarMain
 public class JarMain
@@ -47,10 +48,13 @@ public class JarMain
         sb.append( "\n" );
         sb.append( "\n" );
         sb.append( "Build Information: \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( "\n" );
         sb.append( "Reference URL: " + PwmConstants.PWM_URL_HOME + "\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
 public class LDAPPermissionCalculator implements Serializable
 {
 {
+    private static final long serialVersionUID = 1L;
+
     private static final PwmLogger LOGGER = PwmLogger.forClass( LDAPPermissionCalculator.class );
     private static final PwmLogger LOGGER = PwmLogger.forClass( LDAPPermissionCalculator.class );
 
 
     private final transient StoredConfiguration storedConfiguration;
     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
 public class PasswordData implements Serializable
 {
 {
+    private static final long serialVersionUID = 1L;
+
     private static final PwmLogger LOGGER = PwmLogger.forClass( PasswordData.class );
     private static final PwmLogger LOGGER = PwmLogger.forClass( PasswordData.class );
 
 
     private final byte[] passwordData;
     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() );
         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 PropertyConfigurationImporter importer = new PropertyConfigurationImporter();
-            final StoredConfiguration storedConfiguration = importer.readConfiguration( new FileInputStream( inputFile ) );
+            final StoredConfiguration storedConfiguration = importer.readConfiguration( fileInputStream );
 
 
             try ( OutputStream outputStream = new FileOutputStream( configFile ) )
             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 )
     private static String convertErrorInformation( final ErrorInformation info )
     {
     {
         return info.toDebugStr();
         return info.toDebugStr();

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

@@ -20,23 +20,7 @@
 
 
 package password.pwm.util.secure;
 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.PwmApplication;
-import password.pwm.PwmConstants;
 import password.pwm.bean.PrivateKeyCertificate;
 import password.pwm.bean.PrivateKeyCertificate;
 import password.pwm.config.Configuration;
 import password.pwm.config.Configuration;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.PwmSetting;
@@ -48,51 +32,22 @@ import password.pwm.error.PwmError;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.util.PasswordData;
 import password.pwm.util.PasswordData;
 import password.pwm.util.java.JsonUtil;
 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.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.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.KeyStore;
 import java.security.PrivateKey;
 import java.security.PrivateKey;
-import java.security.SecureRandom;
-import java.security.Security;
 import java.security.cert.X509Certificate;
 import java.security.cert.X509Certificate;
-import java.time.Instant;
 import java.util.ArrayList;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Arrays;
-import java.util.Date;
 import java.util.Enumeration;
 import java.util.Enumeration;
 import java.util.List;
 import java.util.List;
-import java.util.concurrent.TimeUnit;
 
 
 public class HttpsServerCertificateManager
 public class HttpsServerCertificateManager
 {
 {
     private static final PwmLogger LOGGER = PwmLogger.forClass( HttpsServerCertificateManager.class );
     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(
     public static KeyStore keyStoreForApplication(
             final PwmApplication pwmApplication,
             final PwmApplication pwmApplication,
             final PasswordData passwordData,
             final PasswordData passwordData,
@@ -151,12 +106,9 @@ public class HttpsServerCertificateManager
     private static KeyStore makeSelfSignedCert( final PwmApplication pwmApplication, final PasswordData password, final String alias )
     private static KeyStore makeSelfSignedCert( final PwmApplication pwmApplication, final PasswordData password, final String alias )
             throws PwmUnrecoverableException
             throws PwmUnrecoverableException
     {
     {
-        final Configuration configuration = pwmApplication.getConfig();
-
         try
         try
         {
         {
-            final SelfCertGenerator selfCertGenerator = new SelfCertGenerator( configuration );
-            return selfCertGenerator.makeSelfSignedCert( pwmApplication, password, alias );
+            return SelfCertFactory.getExistingCertOrGenerateNewCert( pwmApplication, password, alias );
         }
         }
         catch ( final Exception e )
         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
     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
                 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;
                     passed = true;
                 }
                 }
                 catch ( final NoSuchAlgorithmException | SignatureException | NoSuchProviderException | InvalidKeyException | CertificateException e )
                 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
 @Value
 public class RestAuthentication implements Serializable
 public class RestAuthentication implements Serializable
 {
 {
+    private static final long serialVersionUID = 1L;
+
     private RestAuthenticationType type;
     private RestAuthenticationType type;
     private String namedSecretName;
     private String namedSecretName;
     private UserIdentity ldapIdentity;
     private UserIdentity ldapIdentity;

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

@@ -222,6 +222,7 @@ localdb.logWriter.maxTrimSize=5001
 localdb.reloadWhenAppRestarted=false
 localdb.reloadWhenAppRestarted=false
 macro.randomChar.maxLength=100
 macro.randomChar.maxLength=100
 macro.ldapAttr.maxLength=100
 macro.ldapAttr.maxLength=100
+logging.cspReport.enable=true
 logging.devOutput.enable=false
 logging.devOutput.enable=false
 logging.extra.periodicThreadDumpIntervalSeconds=0
 logging.extra.periodicThreadDumpIntervalSeconds=0
 logging.pattern=%d{yyyy-MM-dd'T'HH:mm:ss'Z'}, %-5p, %c{2}, %m%n
 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
 defaultConfigFilename=PwmConfiguration.xml
 defaultPropertiesConfigFilename=silent.properties
 defaultPropertiesConfigFilename=silent.properties
 enableEulaDisplay=false
 enableEulaDisplay=false
-missingVersionString=[Version Missing]
 applicationPathInfoFile=applicationPath.properties
 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"]
 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>
     <setting hidden="false" key="security.cspHeader" level="2">
     <setting hidden="false" key="security.cspHeader" level="2">
         <default>
         <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>
         </default>
     </setting>
     </setting>
     <setting hidden="false" key="email.adminAlert.toAddress" level="1">
     <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_ConfigEditor=Configuration Editor
 MenuItem_ConfigManager=Configuration Manager
 MenuItem_ConfigManager=Configuration Manager
 Field_AppVersion=%1% Version
 Field_AppVersion=%1% Version
+Field_AppBuildTime=Build Time
 Field_CurrentPubVersion=Current Published Version
 Field_CurrentPubVersion=Current Published Version
 Field_UpTime=Up Time
 Field_UpTime=Up Time
 Field_SiteURL=Site URL
 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_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_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_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.
 Warning_NoEndUserModules=End user functionality is not available while the configuration is open.
 Tooltip_ResetButton=Return this setting to its default value.
 Tooltip_ResetButton=Return this setting to its default value.
 Tooltip_HelpButton=Show description for this setting.
 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() )
             if ( category.hasProfiles() )
             {
             {
-                final PwmSetting pwmSetting = category.getProfileSetting();
+                final PwmSetting pwmSetting = category.getProfileSetting().orElseThrow( IllegalStateException::new );
                 Assert.assertEquals( pwmSetting.getSyntax(), PwmSettingSyntax.PROFILE );
                 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>
                     <plugin>
                         <groupId>org.apache.maven.plugins</groupId>
                         <groupId>org.apache.maven.plugins</groupId>
                         <artifactId>maven-assembly-plugin</artifactId>
                         <artifactId>maven-assembly-plugin</artifactId>
-                        <version>3.2.0</version>
+                        <version>3.3.0</version>
                         <configuration>
                         <configuration>
                             <descriptors>
                             <descriptors>
                                 <descriptor>src/build/assembly/release-bundle.xml</descriptor>
                                 <descriptor>src/build/assembly/release-bundle.xml</descriptor>
@@ -56,7 +56,7 @@
             <plugin>
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-assembly-plugin</artifactId>
                 <artifactId>maven-assembly-plugin</artifactId>
-                <version>3.2.0</version>
+                <version>3.3.0</version>
                 <executions>
                 <executions>
                     <execution>
                     <execution>
                         <id>make-ldif-schema-zip</id>
                         <id>make-ldif-schema-zip</id>
@@ -91,7 +91,7 @@
             <plugin>
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-war-plugin</artifactId>
                 <artifactId>maven-war-plugin</artifactId>
-                <version>3.2.2</version>
+                <version>3.2.3</version>
                 <configuration>
                 <configuration>
                     <archiveClasses>false</archiveClasses>
                     <archiveClasses>false</archiveClasses>
                     <packagingExcludes>**/*.jsp</packagingExcludes>
                     <packagingExcludes>**/*.jsp</packagingExcludes>
@@ -182,7 +182,7 @@
                 <!-- builds xml file of dependencies and licenses for use in about page -->
                 <!-- builds xml file of dependencies and licenses for use in about page -->
                 <groupId>com.github.jinnovations</groupId>
                 <groupId>com.github.jinnovations</groupId>
                 <artifactId>attribution-maven-plugin</artifactId>
                 <artifactId>attribution-maven-plugin</artifactId>
-                <version>0.9.7</version>
+                <version>0.9.8</version>
                 <executions>
                 <executions>
                     <execution>
                     <execution>
                         <goals>
                         <goals>
@@ -252,7 +252,7 @@
             <plugin>
             <plugin>
                 <groupId>io.github.zlika</groupId>
                 <groupId>io.github.zlika</groupId>
                 <artifactId>reproducible-build-maven-plugin</artifactId>
                 <artifactId>reproducible-build-maven-plugin</artifactId>
-                <version>0.11</version>
+                <version>0.12</version>
                 <executions>
                 <executions>
                     <execution>
                     <execution>
                         <goals>
                         <goals>
@@ -303,22 +303,22 @@
         <dependency>
         <dependency>
             <groupId>org.webjars.npm</groupId>
             <groupId>org.webjars.npm</groupId>
             <artifactId>dojo</artifactId>
             <artifactId>dojo</artifactId>
-            <version>1.16.0</version>
+            <version>1.16.2</version>
         </dependency>
         </dependency>
         <dependency>
         <dependency>
             <groupId>org.webjars.npm</groupId>
             <groupId>org.webjars.npm</groupId>
             <artifactId>dijit</artifactId>
             <artifactId>dijit</artifactId>
-            <version>1.16.0</version>
+            <version>1.16.2</version>
         </dependency>
         </dependency>
         <dependency>
         <dependency>
             <groupId>org.webjars.npm</groupId>
             <groupId>org.webjars.npm</groupId>
             <artifactId>dojox</artifactId>
             <artifactId>dojox</artifactId>
-            <version>1.16.0</version>
+            <version>1.16.2</version>
         </dependency>
         </dependency>
         <dependency>
         <dependency>
             <groupId>org.webjars.npm</groupId>
             <groupId>org.webjars.npm</groupId>
             <artifactId>dgrid</artifactId>
             <artifactId>dgrid</artifactId>
-            <version>1.3.0</version>
+            <version>1.3.1</version>
         </dependency>
         </dependency>
         <dependency>
         <dependency>
             <groupId>org.webjars.bower</groupId>
             <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_CFGEDIT.selectTemplate = function(newTemplate) {
     PWM_MAIN.showConfirmDialog({
     PWM_MAIN.showConfirmDialog({
         text: PWM_CONFIG.showString('Warning_ChangeTemplate'),
         text: PWM_CONFIG.showString('Warning_ChangeTemplate'),
@@ -838,6 +867,10 @@ PWM_CFGEDIT.displaySettingsCategory = function(category) {
         htmlSettingBody += '<div style="width: 100%; text-align: center">'
         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>'
             + '<button class="btn" id="button-test-SMS"><span class="btn-icon pwm-icon pwm-icon-bolt"></span>Test SMS Settings</button>'
             + '</div>';
             + '</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;
     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();});
         PWM_MAIN.addEventHandler('button-test-DATABASE_SETTINGS', 'click', function(){PWM_CFGEDIT.databaseHealthCheck();});
     } else if (category === 'SMS_GATEWAY') {
     } else if (category === 'SMS_GATEWAY') {
         PWM_MAIN.addEventHandler('button-test-SMS', 'click', function(){PWM_CFGEDIT.smsHealthCheck();});
         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') {
     } else if (category === 'HTTPS_SERVER') {
         PWM_MAIN.addEventHandler('button-test-HTTPS_SERVER', 'click', function(){PWM_CFGEDIT.httpsCertificateView();});
         PWM_MAIN.addEventHandler('button-test-HTTPS_SERVER', 'click', function(){PWM_CFGEDIT.httpsCertificateView();});
     }
     }