浏览代码

Merge branch 'master' into xml-abstraction

# Conflicts:
#	server/src/test/java/password/pwm/tests/PwmPasswordJudgeTest.java
#	server/src/test/java/password/pwm/util/queue/EmailQueueManagerTest.java
jrivard@gmail.com 6 年之前
父节点
当前提交
00b13175f0
共有 100 个文件被更改,包括 2255 次插入1287 次删除
  1. 1 0
      .gitignore
  2. 8 1
      build/checkstyle-import.xml
  3. 2 1
      client/package.json
  4. 2 2
      client/src/modules/helpdesk/helpdesk-detail.component.html
  5. 1 1
      client/src/modules/helpdesk/helpdesk-detail.component.scss
  6. 5 1
      client/src/modules/helpdesk/helpdesk-search-base.component.ts
  7. 1 1
      client/src/modules/helpdesk/helpdesk-search-table.component.html
  8. 1 0
      client/src/modules/helpdesk/helpdesk.module.ts
  9. 1 0
      client/src/modules/helpdesk/main.ts
  10. 5 6
      client/src/modules/helpdesk/verifications-dialog.component.scss
  11. 4 4
      client/src/modules/helpdesk/verifications-dialog.template.html
  12. 5 1
      client/src/modules/peoplesearch/peoplesearch-base.component.ts
  13. 1 1
      client/src/modules/peoplesearch/peoplesearch-table.component.html
  14. 37 0
      client/src/styles.scss
  15. 2 2
      data-service/pom.xml
  16. 2 2
      docker/pom.xml
  17. 1 1
      onejar/pom.xml
  18. 10 8
      pom.xml
  19. 1 1
      pwm-cr/pom.xml
  20. 43 28
      pwm-cr/src/test/java/password/pwm/cr/ChaiXmlResponseSet1Test.java
  21. 14 11
      pwm-cr/src/test/java/password/pwm/cr/ChaiXmlResponseSetReaderTest.java
  22. 1 1
      pwm-cr/src/test/resources/password/pwm/cr/ChaiXmlResponseSet1.xml
  23. 47 13
      server/pom.xml
  24. 4 1
      server/src/main/java/password/pwm/AppProperty.java
  25. 1 0
      server/src/main/java/password/pwm/PwmAboutProperty.java
  26. 2 0
      server/src/main/java/password/pwm/PwmConstants.java
  27. 12 4
      server/src/main/java/password/pwm/config/PwmSetting.java
  28. 3 2
      server/src/main/java/password/pwm/config/profile/NewUserProfile.java
  29. 1 1
      server/src/main/java/password/pwm/health/ApplianceStatusChecker.java
  30. 1 0
      server/src/main/java/password/pwm/health/HealthMessage.java
  31. 1 1
      server/src/main/java/password/pwm/health/HealthMonitor.java
  32. 83 11
      server/src/main/java/password/pwm/health/LDAPHealthChecker.java
  33. 69 17
      server/src/main/java/password/pwm/http/ContextManager.java
  34. 2 1
      server/src/main/java/password/pwm/http/JspUrl.java
  35. 1 1
      server/src/main/java/password/pwm/http/auth/OAuthFilterAuthenticationProvider.java
  36. 186 0
      server/src/main/java/password/pwm/http/client/HttpTrustManagerHelper.java
  37. 33 54
      server/src/main/java/password/pwm/http/client/PwmHttpClient.java
  38. 13 2
      server/src/main/java/password/pwm/http/client/PwmHttpClientRequest.java
  39. 1 1
      server/src/main/java/password/pwm/http/filter/AuthenticationFilter.java
  40. 64 22
      server/src/main/java/password/pwm/http/servlet/ClientApiServlet.java
  41. 123 0
      server/src/main/java/password/pwm/http/servlet/FullPageHealthServlet.java
  42. 1 0
      server/src/main/java/password/pwm/http/servlet/PwmServletDefinition.java
  43. 1 29
      server/src/main/java/password/pwm/http/servlet/admin/AdminServlet.java
  44. 1 1
      server/src/main/java/password/pwm/http/servlet/changepw/ChangePasswordServlet.java
  45. 2 2
      server/src/main/java/password/pwm/http/servlet/configeditor/ConfigEditorServlet.java
  46. 6 6
      server/src/main/java/password/pwm/http/servlet/configguide/ConfigGuideServlet.java
  47. 61 25
      server/src/main/java/password/pwm/http/servlet/configmanager/DebugItemGenerator.java
  48. 1 1
      server/src/main/java/password/pwm/http/servlet/forgottenpw/ForgottenPasswordServlet.java
  49. 3 3
      server/src/main/java/password/pwm/http/servlet/helpdesk/HelpdeskClientDataBean.java
  50. 1 1
      server/src/main/java/password/pwm/http/servlet/oauth/OAuthConsumerServlet.java
  51. 91 51
      server/src/main/java/password/pwm/http/servlet/oauth/OAuthMachine.java
  52. 1 1
      server/src/main/java/password/pwm/http/servlet/oauth/OAuthResolveResults.java
  53. 15 13
      server/src/main/java/password/pwm/http/servlet/peoplesearch/PeopleSearchConfiguration.java
  54. 18 1
      server/src/main/java/password/pwm/http/servlet/peoplesearch/PeopleSearchDataReader.java
  55. 4 0
      server/src/main/java/password/pwm/http/servlet/peoplesearch/PeopleSearchServlet.java
  56. 0 87
      server/src/main/java/password/pwm/http/servlet/peoplesearch/UserDetailBean.java
  57. 3 63
      server/src/main/java/password/pwm/http/servlet/peoplesearch/bean/AttributeDetailBean.java
  58. 5 22
      server/src/main/java/password/pwm/http/servlet/peoplesearch/bean/LinkReferenceBean.java
  59. 4 6
      server/src/main/java/password/pwm/http/servlet/peoplesearch/bean/OrgChartDataBean.java
  60. 4 6
      server/src/main/java/password/pwm/http/servlet/peoplesearch/bean/OrgChartReferenceBean.java
  61. 8 43
      server/src/main/java/password/pwm/http/servlet/peoplesearch/bean/PeopleSearchClientConfigBean.java
  62. 68 0
      server/src/main/java/password/pwm/http/servlet/peoplesearch/bean/SearchAttributeBean.java
  63. 5 2
      server/src/main/java/password/pwm/http/servlet/peoplesearch/bean/SearchResultBean.java
  64. 39 0
      server/src/main/java/password/pwm/http/servlet/peoplesearch/bean/UserDetailBean.java
  65. 5 22
      server/src/main/java/password/pwm/http/servlet/peoplesearch/bean/UserReferenceBean.java
  66. 12 9
      server/src/main/java/password/pwm/http/tag/value/PwmValue.java
  67. 38 0
      server/src/main/java/password/pwm/svc/event/AuditField.java
  68. 36 0
      server/src/main/java/password/pwm/svc/event/AuditFormatter.java
  69. 197 0
      server/src/main/java/password/pwm/svc/event/CEFAuditFormatter.java
  70. 95 0
      server/src/main/java/password/pwm/svc/event/JsonAuditFormatter.java
  71. 23 198
      server/src/main/java/password/pwm/svc/event/SyslogAuditService.java
  72. 3 0
      server/src/main/java/password/pwm/svc/event/UserAuditRecord.java
  73. 1 1
      server/src/main/java/password/pwm/svc/telemetry/HttpTelemetrySender.java
  74. 46 18
      server/src/main/java/password/pwm/svc/wordlist/WordlistBucket.java
  75. 14 13
      server/src/main/java/password/pwm/svc/wordlist/WordlistImporter.java
  76. 1 1
      server/src/main/java/password/pwm/svc/wordlist/WordlistSource.java
  77. 3 1
      server/src/main/java/password/pwm/svc/wordlist/WordlistStatus.java
  78. 278 0
      server/src/main/java/password/pwm/util/PropertyConfigurationImporter.java
  79. 2 2
      server/src/main/java/password/pwm/util/cli/MainClass.java
  80. 0 248
      server/src/main/java/password/pwm/util/cli/commands/ImportIDMConfigCommand.java
  81. 87 0
      server/src/main/java/password/pwm/util/cli/commands/ImportPropertyConfigCommand.java
  82. 18 0
      server/src/main/java/password/pwm/util/i18n/LocaleHelper.java
  83. 83 13
      server/src/main/java/password/pwm/util/java/JavaHelper.java
  84. 2 39
      server/src/main/java/password/pwm/util/localdb/XodusLocalDB.java
  85. 15 14
      server/src/main/java/password/pwm/util/logging/LocalDBLogger.java
  86. 34 105
      server/src/main/java/password/pwm/util/logging/LocalDBLoggerSettings.java
  87. 17 0
      server/src/main/java/password/pwm/util/macro/InternalMacros.java
  88. 15 4
      server/src/main/java/password/pwm/util/queue/SmsQueueManager.java
  89. 49 12
      server/src/main/java/password/pwm/util/secure/X509Utils.java
  90. 6 3
      server/src/main/resources/password/pwm/AppProperty.properties
  91. 2 0
      server/src/main/resources/password/pwm/PwmConstants.properties
  92. 7 2
      server/src/main/resources/password/pwm/config/PwmSetting.xml
  93. 8 1
      server/src/main/resources/password/pwm/i18n/Display.properties
  94. 3 2
      server/src/main/resources/password/pwm/i18n/Display_ca.properties
  95. 3 2
      server/src/main/resources/password/pwm/i18n/Display_da.properties
  96. 3 2
      server/src/main/resources/password/pwm/i18n/Display_de.properties
  97. 3 2
      server/src/main/resources/password/pwm/i18n/Display_en_CA.properties
  98. 3 2
      server/src/main/resources/password/pwm/i18n/Display_es.properties
  99. 3 2
      server/src/main/resources/password/pwm/i18n/Display_fr.properties
  100. 3 2
      server/src/main/resources/password/pwm/i18n/Display_fr_CA.properties

+ 1 - 0
.gitignore

@@ -25,3 +25,4 @@
 /webapp/src/main/webapp/public/resources/themes/netiq32
 /webapp/src/main/webapp/public/resources/themes/idm
 /webapp/src/main/webapp/public/resources/themes/mdefault
+/webapp/src/main/webapp/public/resources/themes/mdefault44

+ 8 - 1
build/checkstyle-import.xml

@@ -45,7 +45,7 @@
     <!-- commons ftp client -->
     <allow pkg="org.apache.commons.net.ftp"/>
 
-    <!-- xml  -->
+    <!-- xml -->
     <allow pkg="org.jdom2"/>
     <allow pkg="javax.xml"/>
     <allow pkg="org.w3c"/>
@@ -57,6 +57,13 @@
     <!-- log4j -->
     <allow pkg="org.apache.log4j"/>
 
+    <!-- testing -->
+    <allow pkg="org.junit"/>
+    <allow pkg="org.mockito"/>
+    <allow pkg="com.github.tomakehurst.wiremock"/>
+    <allow pkg="org.reflections"/>
+    <allow pkg="org.bouncycastle.jce.provider"/>
+
     <!-- gson -->
     <allow pkg="com.google.gson"/>
 

+ 2 - 1
client/package.json

@@ -24,6 +24,7 @@
         "@uirouter/angularjs": "1.0.15",
         "angular": "1.6.9",
         "angular-aria": "1.6.9",
+        "angular-sanitize": "1.6.9",
         "angular-translate": "2.17.0",
         "textangular": "1.5.16"
     },
@@ -76,7 +77,7 @@
         "url-loader": "1.0.1",
         "webpack": "4.1.1",
         "webpack-cli": "2.0.12",
-        "webpack-dev-server": "3.1.1",
+        "webpack-dev-server": "3.1.14",
         "webpack-merge": "4.1.2",
         "write-file-webpack-plugin": "4.2.0"
     }

+ 2 - 2
client/src/modules/helpdesk/helpdesk-detail.component.html

@@ -166,7 +166,7 @@
                         <td ng-bind="'Field_Display' | translate"></td>
                         <td>
                             <ul>
-                                <li ng-repeat="item in $ctrl.person.passwordRequirements" ng-bind="item"></li>
+                                <li ng-repeat="item in $ctrl.person.passwordRequirements" ng-bind-html="item"></li>
                             </ul>
                         </td>
                     </tr>
@@ -193,4 +193,4 @@
             </ias-tab-pane>
         </div>
     </div>
-</div>
+</div>

+ 1 - 1
client/src/modules/helpdesk/helpdesk-detail.component.scss

@@ -98,7 +98,7 @@
             padding: 3px 15px;
           }
 
-          ul {
+          > ul {
             list-style: none;
             margin: 0;
             padding: 0;

+ 5 - 1
client/src/modules/helpdesk/helpdesk-search-base.component.ts

@@ -166,7 +166,11 @@ export default abstract class HelpDeskSearchBaseComponent {
             }
 
             if (keys.size < this.queries.length) {
-                this.searchMessage = 'Search attributes must be unique';
+                this.$translate('Display_SearchAttrsUnique')
+                    .then((translation: string) => {
+                        this.searchMessage = translation;
+                    });
+
                 return null;
             }
 

+ 1 - 1
client/src/modules/helpdesk/helpdesk-search-table.component.html

@@ -79,7 +79,7 @@
         <ias-button class="ias-icon-button table-configuration-menu-toggle" ias-toggle="menu1">
             <ias-icon icon="configure_thick"></ias-icon>
         </ias-button>
-        <ias-menu name="menu1" ias-align="end end">
+        <ias-menu name="menu1" ias-align="end end" class="ias-styles-root">
             <div class="ias-input-container">
                 <div class="checkbox-button" ng-repeat="(key, value) in $ctrl.columnConfiguration">
                     <input type="checkbox" ng-checked="value.visible" aria-label="Toggle column visibility" disabled/>

+ 1 - 0
client/src/modules/helpdesk/helpdesk.module.ts

@@ -48,6 +48,7 @@ const moduleName = 'help-desk';
 
 module(moduleName, [
     'ngAria',
+    'ngSanitize',
     uxModule
 ])
 

+ 1 - 0
client/src/modules/helpdesk/main.ts

@@ -22,6 +22,7 @@
 
 import 'angular';
 import 'angular-translate';
+import 'angular-sanitize';
 import '@microfocus/ng-ias/dist/ng-ias';
 
 import { bootstrap, module } from 'angular';

+ 5 - 6
client/src/modules/helpdesk/verifications-dialog.component.scss

@@ -4,13 +4,12 @@
 }
 
 .token-destination-submitter {
-    display: grid;
-    grid-template-columns: max-content max-content max-content;
-    grid-column-gap: 10px;
-    grid-row-gap: 5px;
+    display: flex;
+    flex-wrap: wrap;
     align-items: center;
 
-    select {
-        grid-column-start: 1;
+    > * {
+        margin-right: 10px !important;
+        margin-top: 4px !important;
     }
 }

+ 4 - 4
client/src/modules/helpdesk/verifications-dialog.template.html

@@ -48,7 +48,7 @@
                 </ias-button>
             </div>
 
-            <div class="ias-dialog-container" ng-switch-when="verify">
+            <div class="ias-dialog-container grow-with-content" ng-switch-when="verify">
                 <div class="ias-dialog-label">
                     <div class="ias-title" ng-bind="'Title_ValidateCode' | translate"></div>
                 </div>
@@ -66,9 +66,8 @@
                             </div>
                         </div>
                         <div ng-switch-when="TOKEN" ng-switch-when-separator="|">
+                            <label ng-bind="'Display_TokenDestination' | translate"></label>
                             <div class="ias-input-container token-destination-submitter">
-                                <label ng-bind="'Display_TokenDestination' | translate"></label>
-
                                 <select id="verification-token-destination" ng-model="$ctrl.tokenDestinationID"
                                         ng-change="$ctrl.onTokenDestinationChanged()">
                                     <option ng-repeat="tokenDestination in $ctrl.verificationOptions.tokenDestinations"
@@ -78,7 +77,8 @@
                                     </option>
                                 </select>
                                 <ias-button ng-click="$ctrl.sendVerificationTokenToDestination()"
-                                            ng-disabled="$ctrl.sendingVerificationToken">
+                                            ng-disabled="$ctrl.sendingVerificationToken"
+                                            title="{{ 'Button_SendToken' | translate }}">
                                     {{ 'Button_SendToken' | translate }}
                                 </ias-button>
                                 <div class="loading-gif-25" ng-if="$ctrl.sendingVerificationToken"></div>

+ 5 - 1
client/src/modules/peoplesearch/peoplesearch-base.component.ts

@@ -223,7 +223,11 @@ abstract class PeopleSearchBaseComponent {
             }
 
             if (keys.size < this.queries.length) {
-                this.searchMessage = 'Search attributes must be unique';
+                this.$translate('Display_SearchAttrsUnique')
+                    .then((translation: string) => {
+                        this.searchMessage = translation;
+                    });
+
                 return null;
             }
 

+ 1 - 1
client/src/modules/peoplesearch/peoplesearch-table.component.html

@@ -83,7 +83,7 @@
         <ias-button class="ias-icon-button table-configuration-menu-toggle" ias-toggle="menu1">
             <ias-icon icon="configure_thick"></ias-icon>
         </ias-button>
-        <ias-menu name="menu1" ias-align="end end">
+        <ias-menu name="menu1" ias-align="end end" class="ias-styles-root">
             <div class="ias-input-container">
                 <div class="checkbox-button" ng-repeat="(key, value) in $ctrl.columnConfiguration">
                     <input type="checkbox" ng-checked="value.visible" aria-label="Toggle column visibility" disabled/>

+ 37 - 0
client/src/styles.scss

@@ -6,6 +6,43 @@
   .ias-dialog {
     // Need to fix this minor css problem in ux-ias until a version > 1.0.0-rc comes out.
     position: fixed;
+
+    > .ias-dialog-container {
+      &.grow-with-content {
+        @media (min-width: 768px) {
+          max-width: 100%;
+          min-width: 375px;
+          width: max-content;
+        }
+      }
+
+      > .ias-dialog-label > .ias-title {
+        overflow: hidden;
+        text-overflow: ellipsis;
+      }
+    }
+  }
+
+  // The ias-menu is added at the root of the document.  This fixes when we have to add the ias-styles-root class
+  // to the ias-menu itself to get the IAS styles applied.
+  &.ias-menu {
+    background-color: transparent;
+    color: inherit;
+    font-family: inherit;
+    font-size: inherit;
+    font-weight: inherit;
+    line-height: inherit;
+
+    bottom: 0;
+    display: none;
+    position: fixed;
+    left: 0;
+    right: 0;
+    top: 0;
+
+    &.ias-open {
+      display: inline-block;
+    }
   }
 
   // ux-ias normally applies the following to the body tag, but the way we are including ux-ias under .ias-styles-root

+ 2 - 2
data-service/pom.xml

@@ -150,7 +150,7 @@
         <dependency>
             <groupId>org.apache.httpcomponents</groupId>
             <artifactId>httpclient</artifactId>
-            <version>4.5.6</version>
+            <version>4.5.7</version>
         </dependency>
         <dependency>
             <groupId>log4j</groupId>
@@ -170,7 +170,7 @@
         <dependency>
             <groupId>org.jetbrains.xodus</groupId>
             <artifactId>xodus-environment</artifactId>
-            <version>1.2.3</version>
+            <version>1.3.0</version>
         </dependency>
     </dependencies>
 </project>

+ 2 - 2
docker/pom.xml

@@ -34,7 +34,7 @@
             <plugin>
                 <groupId>com.google.cloud.tools</groupId>
                 <artifactId>jib-maven-plugin</artifactId>
-                <version>0.10.1</version>
+                <version>1.0.0</version>
                 <executions>
                     <execution>
                         <id>make-docker-image</id>
@@ -45,7 +45,7 @@
                         <configuration>
                             <skip>${skipDocker}</skip>
                             <from>
-                                <image>adoptopenjdk/openjdk11</image>
+                                <image>adoptopenjdk/openjdk11:slim</image>
                             </from>
                             <to>
                                 <image>${dockerImageTag}</image>

+ 1 - 1
onejar/pom.xml

@@ -17,7 +17,7 @@
 
     <properties>
         <project.root.basedir>${project.basedir}/..</project.root.basedir>
-        <tomcat.version>9.0.14</tomcat.version>
+        <tomcat.version>9.0.16</tomcat.version>
     </properties>
 
     <build>

+ 10 - 8
pom.xml

@@ -41,9 +41,9 @@
     </properties>
 
     <modules>
-        <module>client</module>
         <module>pwm-cr</module>
         <module>server</module>
+        <module>client</module>
         <module>webapp</module>
         <module>onejar</module>
         <module>data-service</module>
@@ -171,7 +171,7 @@
                     <dependency>
                         <groupId>com.puppycrawl.tools</groupId>
                         <artifactId>checkstyle</artifactId>
-                        <version>8.16</version>
+                        <version>8.18</version>
                     </dependency>
                 </dependencies>
                 <executions>
@@ -183,11 +183,12 @@
                             <configLocation>${project.root.basedir}/build/checkstyle.xml</configLocation>
                             <encoding>UTF-8</encoding>
                             <consoleOutput>true</consoleOutput>
-                            <includeTestResources>false</includeTestResources>
+                            <includeTestResources>true</includeTestResources>
                             <failsOnError>true</failsOnError>
                             <includes>**/*.java,**/*.jsp,**/*.properties,**/*.xml,**/*.css,**/*.svg</includes>
                             <sourceDirectories>
                                 <directory>src/main</directory>
+                                <directory>src/test</directory>
                             </sourceDirectories>
                         </configuration>
                         <goals>
@@ -202,11 +203,12 @@
                             <configLocation>${project.root.basedir}/build/checkstyle-header.xml</configLocation>
                             <encoding>UTF-8</encoding>
                             <consoleOutput>true</consoleOutput>
-                            <includeTestResources>false</includeTestResources>
+                            <includeTestResources>true</includeTestResources>
                             <failsOnError>true</failsOnError>
                             <includes>**/**</includes>
                             <sourceDirectories>
                                 <directory>src</directory>
+                                <directory>src/test</directory>
                             </sourceDirectories>
                         </configuration>
                         <goals>
@@ -218,12 +220,12 @@
             <plugin>
                 <groupId>com.github.spotbugs</groupId>
                 <artifactId>spotbugs-maven-plugin</artifactId>
-                <version>3.1.10</version>
+                <version>3.1.11</version>
                 <dependencies>
                     <dependency>
                         <groupId>com.github.spotbugs</groupId>
                         <artifactId>spotbugs</artifactId>
-                        <version>3.1.10</version>
+                        <version>3.1.12</version>
                     </dependency>
                 </dependencies>
                 <configuration>
@@ -266,13 +268,13 @@
         <dependency>
             <groupId>org.projectlombok</groupId>
             <artifactId>lombok</artifactId>
-            <version>1.18.4</version>
+            <version>1.18.6</version>
             <scope>provided</scope>
         </dependency>
         <dependency>
             <groupId>com.github.spotbugs</groupId>
             <artifactId>spotbugs-annotations</artifactId>
-            <version>3.1.10</version>
+            <version>3.1.12</version>
             <scope>provided</scope>
         </dependency>
     </dependencies>

+ 1 - 1
pwm-cr/pom.xml

@@ -45,7 +45,7 @@
         <dependency>
             <groupId>org.bouncycastle</groupId>
             <artifactId>bcpkix-jdk15on</artifactId>
-            <version>1.60</version>
+            <version>1.61</version>
         </dependency>
 
         <!-- Test dependencies -->

+ 43 - 28
pwm-cr/src/test/java/password/pwm/cr/ChaiXmlResponseSet1Test.java

@@ -35,10 +35,13 @@ import java.io.InputStreamReader;
 import java.io.Reader;
 import java.nio.charset.Charset;
 
-public class ChaiXmlResponseSet1Test {
+public class ChaiXmlResponseSet1Test
+{
 
     @Test
-    public void testReadingStoredChaiXmlChallengeSet() throws IOException {
+    public void testReadingStoredChaiXmlChallengeSet()
+            throws IOException
+    {
         /*
         final Reader reader = readInputXmlFile();
         StoredResponseSet storedResponseSet = new ChaiXmlResponseSetSerializer().read(reader, ChaiXmlResponseSetSerializer.Type.USER);
@@ -49,15 +52,17 @@ public class ChaiXmlResponseSet1Test {
 
 
     @Test
-    public void testReadingStoredChaiHelpdeskXmlChallengeSet() throws IOException {
+    public void testReadingStoredChaiHelpdeskXmlChallengeSet() throws IOException
+    {
         final Reader reader = readInputXmlFile();
-        StoredResponseSet storedResponseSet = new ChaiXmlResponseSetSerializer().read(reader, ChaiXmlResponseSetSerializer.Type.HELPDESK);
+        final StoredResponseSet storedResponseSet = new ChaiXmlResponseSetSerializer().read( reader, ChaiXmlResponseSetSerializer.Type.HELPDESK );
 
-        testHelpdeskResponseSetValidity(storedResponseSet);
+        testHelpdeskResponseSetValidity( storedResponseSet );
     }
 
     @Test
-    public void testReadWriteRead() throws IOException {
+    public void testReadWriteRead() throws IOException
+    {
         /*
         final ChaiXmlResponseSetSerializer chaiXmlResponseSetSerializer = new ChaiXmlResponseSetSerializer();
 
@@ -86,50 +91,60 @@ public class ChaiXmlResponseSet1Test {
         */
     }
 
-    private static Reader readInputXmlFile() {
-        return new InputStreamReader(ChaiXmlResponseSet1Test.class.getResourceAsStream("ChaiXmlResponseSet1.xml"), Charset.forName("UTF8"));
+    private static Reader readInputXmlFile()
+    {
+        return new InputStreamReader( ChaiXmlResponseSet1Test.class.getResourceAsStream( "ChaiXmlResponseSet1.xml" ), Charset.forName( "UTF8" ) );
     }
 
 
-    private void testUserResponseSetValidity(final StoredResponseSet storedResponseSet) {
-        Assert.assertEquals(4, storedResponseSet.getStoredChallengeItems().size());
-        Assert.assertEquals(4, StoredItemUtils.filterStoredChallenges(storedResponseSet.getStoredChallengeItems(), ResponseLevel.RANDOM).size());
+    private void testUserResponseSetValidity( final StoredResponseSet storedResponseSet )
+    {
+        Assert.assertEquals( 4, storedResponseSet.getStoredChallengeItems().size() );
+        Assert.assertEquals( 4, StoredItemUtils.filterStoredChallenges( storedResponseSet.getStoredChallengeItems(), ResponseLevel.RANDOM ).size() );
 
-        for (final StoredChallengeItem storedChallengeItem : storedResponseSet.getStoredChallengeItems()) {
+        for ( final StoredChallengeItem storedChallengeItem : storedResponseSet.getStoredChallengeItems() )
+        {
             final String questionText = storedChallengeItem.getQuestionText();
-            if ("What is the name of the main character in your favorite book?".equals(questionText)) {
+            if ( "What is the name of the main character in your favorite book?".equals( questionText ) )
+            {
                 final StoredResponseItem storedResponseItem = storedChallengeItem.getAnswer();
-                Assert.assertTrue(HashFactory.testResponseItem(storedResponseItem, "book"));
-                Assert.assertFalse(HashFactory.testResponseItem(storedResponseItem, "wrong answer"));
+                Assert.assertTrue( HashFactory.testResponseItem( storedResponseItem, "book" ) );
+                Assert.assertFalse( HashFactory.testResponseItem( storedResponseItem, "wrong answer" ) );
             }
 
-            if ("What is the name of your favorite teacher?".equals(questionText)) {
+            if ( "What is the name of your favorite teacher?".equals( questionText ) )
+            {
                 final StoredResponseItem storedResponseItem = storedChallengeItem.getAnswer();
-                Assert.assertTrue(HashFactory.testResponseItem(storedResponseItem, "teacher"));
-                Assert.assertFalse(HashFactory.testResponseItem(storedResponseItem, "wrong answer"));
+                Assert.assertTrue( HashFactory.testResponseItem( storedResponseItem, "teacher" ) );
+                Assert.assertFalse( HashFactory.testResponseItem( storedResponseItem, "wrong answer" ) );
             }
 
-            if ("What was the name of your childhood best friend?".equals(questionText)) {
+            if ( "What was the name of your childhood best friend?".equals( questionText ) )
+            {
                 final StoredResponseItem storedResponseItem = storedChallengeItem.getAnswer();
-                Assert.assertTrue(HashFactory.testResponseItem(storedResponseItem, "friend"));
-                Assert.assertFalse(HashFactory.testResponseItem(storedResponseItem, "wrong answer"));
+                Assert.assertTrue( HashFactory.testResponseItem( storedResponseItem, "friend" ) );
+                Assert.assertFalse( HashFactory.testResponseItem( storedResponseItem, "wrong answer" ) );
             }
 
-            if ("What was your favorite show as a child?".equals(questionText)) {
+            if ( "What was your favorite show as a child?".equals( questionText ) )
+            {
                 final StoredResponseItem storedResponseItem = storedChallengeItem.getAnswer();
-                Assert.assertTrue(HashFactory.testResponseItem(storedResponseItem, "child"));
-                Assert.assertFalse(HashFactory.testResponseItem(storedResponseItem, "wrong answer"));
+                Assert.assertTrue( HashFactory.testResponseItem( storedResponseItem, "child" ) );
+                Assert.assertFalse( HashFactory.testResponseItem( storedResponseItem, "wrong answer" ) );
             }
         }
 
     }
 
-    private void testHelpdeskResponseSetValidity(final StoredResponseSet storedResponseSet) {
-        Assert.assertEquals(2, storedResponseSet.getStoredChallengeItems().size());
+    private void testHelpdeskResponseSetValidity( final StoredResponseSet storedResponseSet )
+    {
+        Assert.assertEquals( 2, storedResponseSet.getStoredChallengeItems().size() );
 
-        for (final StoredChallengeItem storedChallengeItem : storedResponseSet.getStoredChallengeItems()) {
+        for ( final StoredChallengeItem storedChallengeItem : storedResponseSet.getStoredChallengeItems() )
+        {
             final String questionText = storedChallengeItem.getQuestionText();
-            if ("What is the name of the main character in your favorite book?".equals(questionText)) {
+            if ( "What is the name of the main character in your favorite book?".equals( questionText ) )
+            {
                 final StoredResponseItem storedResponseItem = storedChallengeItem.getAnswer();
 
             }

+ 14 - 11
pwm-cr/src/test/java/password/pwm/cr/ChaiXmlResponseSetReaderTest.java

@@ -28,25 +28,28 @@ import password.pwm.cr.api.QuestionSource;
 import password.pwm.cr.api.ResponseLevel;
 
 
-public class ChaiXmlResponseSetReaderTest {
+public class ChaiXmlResponseSetReaderTest
+{
 
-    @Test(expected=IllegalArgumentException.class)
-    public void testBogusMaxLength() throws Exception {
+    @Test( expected = IllegalArgumentException.class )
+    public void testBogusMaxLength() throws Exception
+    {
 
         ChallengeItemPolicy.builder()
-                .questionText("question 1!")
-                .maxLength(-3)
+                .questionText( "question 1!" )
+                .maxLength( -3 )
                 .build().validate();
     }
 
     @Test
-    public void testValidChallengeItemCreations() {
+    public void testValidChallengeItemCreations()
+    {
         ChallengeItemPolicy.builder()
-                .questionText("question 1!")
-                .minLength(1)
-                .maxLength(10)
-                .questionSource(QuestionSource.ADMIN_DEFINED)
-                .responseLevel(ResponseLevel.REQUIRED)
+                .questionText( "question 1!" )
+                .minLength( 1 )
+                .maxLength( 10 )
+                .questionSource( QuestionSource.ADMIN_DEFINED )
+                .responseLevel( ResponseLevel.REQUIRED )
                 .build();
 
     }

+ 1 - 1
pwm-cr/src/test/resources/password/pwm/cr/ChaiXmlResponseSet1.xml

@@ -44,4 +44,4 @@
         <challenge>Question 2</challenge>
         <answer format="HELPDESK">H4sIAAAAAAAAAIuQYQ59M3HZ5VvFk6_dNZjsAAAQvpfpEAAAAA==</answer>
     </helpdesk-response>
-</ResponseSet>
+</ResponseSet>

+ 47 - 13
server/pom.xml

@@ -17,6 +17,7 @@
     <properties>
         <project.root.basedir>${project.basedir}/..</project.root.basedir>
         <skipTests>false</skipTests>
+        <skipExtendedTests>true</skipExtendedTests>
     </properties>
 
     <profiles>
@@ -26,6 +27,15 @@
                 <skipTests>true</skipTests>
             </properties>
         </profile>
+        <profile>
+            <id>enable-extended-tests</id>
+            <properties>
+                <skipExtendedTests>false</skipExtendedTests>
+            </properties>
+            <activation>
+                <activeByDefault>false</activeByDefault>
+            </activation>
+        </profile>
     </profiles>
 
     <build>
@@ -33,13 +43,37 @@
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-surefire-plugin</artifactId>
-                <version>2.22.0</version>
-                <configuration>
-                    <skipTests>${skipTests}</skipTests>
-                    <excludes>
-                        <exclude>**/password.pwm.manual.*</exclude>
-                    </excludes>
-                </configuration>
+                <version>3.0.0-M3</version>
+                <executions>
+                    <execution>
+                        <id>default-test</id>
+                        <goals>
+                            <goal>test</goal>
+                        </goals>
+                        <phase>test</phase>
+                        <configuration>
+                            <skipTests>${skipTests}</skipTests>
+                            <excludes>
+                                <exclude>**/ExtendedTest*.java</exclude>
+                                <exclude>**/*ExtendedTest*.java</exclude>
+                            </excludes>
+                        </configuration>
+                    </execution>
+                    <execution>
+                        <id>extended-test</id>
+                        <goals>
+                            <goal>test</goal>
+                        </goals>
+                        <phase>test</phase>
+                        <configuration>
+                            <skipTests>${skipExtendedTests}</skipTests>
+                            <includes>
+                                <include>**/ExtendedTest*.java</include>
+                                <include>**/*ExtendedTest*.java</include>
+                            </includes>
+                        </configuration>
+                    </execution>
+                </executions>
             </plugin>
             <plugin>
                 <artifactId>maven-resources-plugin</artifactId>
@@ -210,7 +244,7 @@
         <dependency>
             <groupId>commons-fileupload</groupId>
             <artifactId>commons-fileupload</artifactId>
-            <version>1.3.3</version>
+            <version>1.4</version>
         </dependency>
         <dependency>
             <groupId>org.apache.commons</groupId>
@@ -230,7 +264,7 @@
         <dependency>
             <groupId>org.apache.httpcomponents</groupId>
             <artifactId>httpclient</artifactId>
-            <version>4.5.6</version>
+            <version>4.5.7</version>
         </dependency>
         <dependency>
             <groupId>org.graylog2</groupId>
@@ -255,12 +289,12 @@
         <dependency>
             <groupId>org.bouncycastle</groupId>
             <artifactId>bcprov-jdk15on</artifactId>
-            <version>1.60</version>
+            <version>1.61</version>
         </dependency>
         <dependency>
             <groupId>org.bouncycastle</groupId>
             <artifactId>bcpkix-jdk15on</artifactId>
-            <version>1.60</version>
+            <version>1.61</version>
         </dependency>
         <dependency>
             <groupId>jaxen</groupId>
@@ -295,7 +329,7 @@
         <dependency>
             <groupId>org.jetbrains.xodus</groupId>
             <artifactId>xodus-environment</artifactId>
-            <version>1.2.3</version>
+            <version>1.3.0</version>
         </dependency>
         <dependency>
             <groupId>org.slf4j</groupId>
@@ -305,7 +339,7 @@
         <dependency>
             <groupId>org.webjars</groupId>
             <artifactId>webjars-locator-core</artifactId>
-            <version>0.36</version>
+            <version>0.37</version>
         </dependency>
         <dependency>
             <groupId>com.github.ben-manes.caffeine</groupId>

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

@@ -41,7 +41,7 @@ public enum AppProperty
     AUDIT_EVENTS_EMAILFROM                          ( "audit.events.emailFrom" ),
     AUDIT_EVENTS_EMAILSUBJECT                       ( "audit.events.emailSubject" ),
     AUDIT_EVENTS_LOCALDB_MAX_BULK_REMOVALS          ( "audit.events.localdb.maxBulkRemovals" ),
-    AUDIT_SYSLOG_CEF_EXTENSIONS                     ( "audit.syslog.cef.extensions" ),
+    AUDIT_SYSLOG_CEF_TIMEZONE                       ( "audit.syslog.cef.timezone" ),
     AUDIT_SYSLOG_CEF_HEADER_PRODUCT                 ( "audit.syslog.cef.header.product" ),
     AUDIT_SYSLOG_CEF_HEADER_SEVERITY                ( "audit.syslog.cef.header.severity" ),
     AUDIT_SYSLOG_CEF_HEADER_VENDOR                  ( "audit.syslog.cef.header.vendor" ),
@@ -128,6 +128,7 @@ public enum AppProperty
     HTTP_CLIENT_SOCKET_TIMEOUT_MS                   ( "http.client.socketTimeoutMs" ),
     HTTP_CLIENT_CONNECT_TIMEOUT_MS                  ( "http.client.connectTimeoutMs" ),
     HTTP_CLIENT_REQUEST_TIMEOUT_MS                  ( "http.client.requestTimeoutMs" ),
+    HTTP_CLIENT_ENABLE_HOSTNAME_VERIFICATION        ( "http.client.enableHostnameVerification" ),
     HTTP_CLIENT_PROMISCUOUS_WORDLIST_ENABLE         ( "http.client.promiscuous.wordlist.enable" ),
     HTTP_ENABLE_GZIP                                ( "http.gzip.enable" ),
     HTTP_ERRORS_ALLOW_HTML                          ( "http.errors.allowHtml" ),
@@ -153,6 +154,7 @@ public enum AppProperty
     HTTP_PARAM_OAUTH_ACCESS_TOKEN                   ( "http.parameter.oauth.accessToken" ),
     HTTP_PARAM_OAUTH_ATTRIBUTES                     ( "http.parameter.oauth.attributes" ),
     HTTP_PARAM_OAUTH_CLIENT_ID                      ( "http.parameter.oauth.clientID" ),
+    HTTP_PARAM_OAUTH_CLIENT_SECRET                  ( "http.parameter.oauth.clientSecret" ),
     HTTP_PARAM_OAUTH_CODE                           ( "http.parameter.oauth.code" ),
     HTTP_PARAM_OAUTH_EXPIRES                        ( "http.parameter.oauth.expires" ),
     HTTP_PARAM_OAUTH_RESPONSE_TYPE                  ( "http.parameter.oauth.responseType" ),
@@ -165,6 +167,7 @@ public enum AppProperty
     HTTP_SESSION_RECYCLE_AT_AUTH                    ( "http.session.recycleAtAuth" ),
     HTTP_SESSION_VALIDATION_KEY_LENGTH              ( "http.session.validationKeyLength" ),
     HTTP_SERVLET_ENABLE_POST_REDIRECT_GET           ( "http.servlet.enablePostRedirectGet" ),
+    L10N_RTL_REGEX                                  ( "l10n.rtl.regex" ),
     LOCALDB_AGGRESSIVE_COMPACT_ENABLED              ( "localdb.aggressiveCompact.enabled" ),
     LOCALDB_IMPLEMENTATION                          ( "localdb.implementation" ),
     LOCALDB_INIT_STRING                             ( "localdb.initParameters" ),

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

@@ -82,6 +82,7 @@ public enum PwmAboutProperty
     java_memoryFree( "Java Memory Free", pwmApplication -> Long.toString( Runtime.getRuntime().freeMemory() ) ),
     java_memoryAllocated( "Java Memory Allocated", pwmApplication -> Long.toString( Runtime.getRuntime().totalMemory() ) ),
     java_memoryMax( "Java Memory Max", pwmApplication -> Long.toString( Runtime.getRuntime().maxMemory() ) ),
+    java_processors( "Java Available Processors", pwmApplication -> Integer.toString( Runtime.getRuntime().availableProcessors() ) ),
     java_threadCount( "Java Thread Count", pwmApplication -> Integer.toString( Thread.activeCount() ) ),
     java_runtimeVersion( "Java Runtime Version", pwmApplication -> System.getProperty( "java.runtime.version" ) ),
     java_vmName( "Java VM Name", pwmApplication -> System.getProperty( "java.vm.name" ) ),

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

@@ -74,8 +74,10 @@ public abstract class PwmConstants
     public static final String CHAI_API_VERSION = com.novell.ldapchai.ChaiConstant.CHAI_API_VERSION;
 
     public static final String DEFAULT_CONFIG_FILE_FILENAME = readPwmConstantsBundle( "defaultConfigFilename" );
+    public static final String DEFAULT_PROPERTIES_CONFIG_FILE_FILENAME = readPwmConstantsBundle( "defaultPropertiesConfigFilename" );
 
     public static final String PWM_APP_NAME = readPwmConstantsBundle( "pwm.appName" );
+    public static final String PWM_VENDOR_NAME = readPwmConstantsBundle( "pwm.vendorName" );
     public static final String PWM_URL_HOME = readPwmConstantsBundle( "url.pwm-home" );
 
     public static final String PWM_APP_NAME_VERSION = PWM_APP_NAME + " " + SERVLET_VERSION;

+ 12 - 4
server/src/main/java/password/pwm/config/PwmSetting.java

@@ -960,12 +960,12 @@ public enum PwmSetting
             "peopleSearch.result.limit", PwmSettingSyntax.NUMERIC, PwmSettingCategory.PEOPLE_SEARCH ),
     PEOPLE_SEARCH_USE_PROXY(
             "peopleSearch.useProxy", PwmSettingSyntax.BOOLEAN, PwmSettingCategory.PEOPLE_SEARCH ),
-    PEOPLE_SEARCH_DISPLAY_NAME(
-            "peopleSearch.displayName.user", PwmSettingSyntax.STRING, PwmSettingCategory.PEOPLE_SEARCH ),
     PEOPLE_SEARCH_DISPLAY_NAMES_CARD_LABELS(
             "peopleSearch.displayName.cardLabels", PwmSettingSyntax.STRING_ARRAY, PwmSettingCategory.PEOPLE_SEARCH ),
     PEOPLE_SEARCH_MAX_CACHE_SECONDS(
             "peopleSearch.maxCacheSeconds", PwmSettingSyntax.DURATION, PwmSettingCategory.PEOPLE_SEARCH ),
+    PEOPLE_SEARCH_ENABLE_PHOTO(
+            "peopleSearch.enablePhoto", PwmSettingSyntax.BOOLEAN, PwmSettingCategory.PEOPLE_SEARCH ),
     PEOPLE_SEARCH_PHOTO_QUERY_FILTER(
             "peopleSearch.photo.queryFilter", PwmSettingSyntax.USER_PERMISSION, PwmSettingCategory.PEOPLE_SEARCH ),
     PEOPLE_SEARCH_SEARCH_FILTER(
@@ -1061,8 +1061,6 @@ public enum PwmSetting
             "helpdesk.forcePwExpiration", PwmSettingSyntax.BOOLEAN, PwmSettingCategory.HELPDESK_BASE ),
     HELPDESK_USE_PROXY(
             "helpdesk.useProxy", PwmSettingSyntax.BOOLEAN, PwmSettingCategory.HELPDESK_BASE ),
-    HELPDESK_DETAIL_DISPLAY_NAME(
-            "helpdesk.displayName", PwmSettingSyntax.STRING, PwmSettingCategory.HELPDESK_BASE ),
     HELPDESK_DISPLAY_NAMES_CARD_LABELS(
             "helpdesk.displayName.cardLabels", PwmSettingSyntax.STRING_ARRAY, PwmSettingCategory.HELPDESK_BASE ),
     HELPDESK_TOKEN_SEND_METHOD(
@@ -1224,6 +1222,16 @@ public enum PwmSetting
 
 
     // deprecated.
+
+    // deprecated 2019-01-20
+    PEOPLE_SEARCH_DISPLAY_NAME(
+            "peopleSearch.displayName.user", PwmSettingSyntax.STRING, PwmSettingCategory.PEOPLE_SEARCH ),
+
+    // deprecated 2019-01-20
+    HELPDESK_DETAIL_DISPLAY_NAME(
+            "helpdesk.displayName", PwmSettingSyntax.STRING, PwmSettingCategory.HELPDESK_BASE ),
+
+
     // deprecated 2018-12-05
     REPORTING_SEARCH_FILTER(
             "reporting.ldap.searchFilter", PwmSettingSyntax.STRING, PwmSettingCategory.REPORTING ),

+ 3 - 2
server/src/main/java/password/pwm/config/profile/NewUserProfile.java

@@ -48,6 +48,7 @@ public class NewUserProfile extends AbstractProfile
 {
 
     private static final ProfileType PROFILE_TYPE = ProfileType.NewUser;
+    public static final String TEST_USER_CONFIG_VALUE = "TESTUSER";
 
     private Instant newUserPasswordPolicyCacheTime;
     private final Map<Locale, PwmPasswordPolicy> newUserPasswordPolicyCache = new HashMap<>();
@@ -107,7 +108,7 @@ public class NewUserProfile extends AbstractProfile
         {
 
             final String lookupDN;
-            if ( "TESTUSER".equalsIgnoreCase( configuredNewUserPasswordDN ) )
+            if ( TEST_USER_CONFIG_VALUE.equalsIgnoreCase( configuredNewUserPasswordDN ) )
             {
                 lookupDN = defaultLdapProfile.readSettingAsString( PwmSetting.LDAP_TEST_USER_DN );
                 if ( lookupDN == null || lookupDN.isEmpty() )
@@ -116,7 +117,7 @@ public class NewUserProfile extends AbstractProfile
                             + PwmSetting.LDAP_TEST_USER_DN.toMenuLocationDebug( defaultLdapProfile.getIdentifier(), PwmConstants.DEFAULT_LOCALE )
                             + " must be configured since setting "
                             + PwmSetting.NEWUSER_PASSWORD_POLICY_USER.toMenuLocationDebug( this.getIdentifier(), PwmConstants.DEFAULT_LOCALE )
-                            + " is set to TESTUSER";
+                            + " is set to " + TEST_USER_CONFIG_VALUE;
                     throw new PwmUnrecoverableException( new ErrorInformation( PwmError.ERROR_INVALID_CONFIG, errorMsg ) );
                 }
             }

+ 1 - 1
server/src/main/java/password/pwm/health/ApplianceStatusChecker.java

@@ -91,7 +91,7 @@ public class ApplianceStatusChecker implements HealthChecker
         final Map<String, String> requestHeaders = Collections.singletonMap( "sspr-authorization-token", getApplianceAccessToken( pwmApplication ) );
 
         final PwmHttpClientConfiguration pwmHttpClientConfiguration = PwmHttpClientConfiguration.builder()
-                .trustManager( new X509Utils.PromiscuousTrustManager() )
+                .trustManager( new X509Utils.PromiscuousTrustManager( SessionLabel.HEALTH_SESSION_LABEL ) )
                 .build();
 
         final PwmHttpClient pwmHttpClient = new PwmHttpClient( pwmApplication, SessionLabel.HEALTH_SESSION_LABEL, pwmHttpClientConfiguration );

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

@@ -88,6 +88,7 @@ public enum HealthMessage
     LocalDBLogger_NOTOPEN( HealthStatus.CAUTION, HealthTopic.LocalDB ),
     LocalDBLogger_HighRecordCount( HealthStatus.CAUTION, HealthTopic.LocalDB ),
     LocalDBLogger_OldRecordPresent( HealthStatus.CAUTION, HealthTopic.LocalDB ),
+    NewUser_PwTemplateBad( HealthStatus.CAUTION, HealthTopic.Configuration ),
     ServiceClosed( HealthStatus.CAUTION, HealthTopic.Application ),
     ServiceClosed_LocalDBUnavail( HealthStatus.CAUTION, HealthTopic.Application ),
     ServiceClosed_AppReadOnly( HealthStatus.CAUTION, HealthTopic.Application ),

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

@@ -56,7 +56,7 @@ public class HealthMonitor implements PwmService
     static
     {
         final List<HealthChecker> records = new ArrayList<>();
-        records.add( new LDAPStatusChecker() );
+        records.add( new LDAPHealthChecker() );
         records.add( new JavaChecker() );
         records.add( new ConfigurationChecker() );
         records.add( new LocalDBHealthChecker() );

+ 83 - 11
server/src/main/java/password/pwm/health/LDAPStatusChecker.java → server/src/main/java/password/pwm/health/LDAPHealthChecker.java

@@ -45,6 +45,7 @@ import password.pwm.config.PwmSettingCategory;
 import password.pwm.config.PwmSettingFlag;
 import password.pwm.config.PwmSettingSyntax;
 import password.pwm.config.profile.LdapProfile;
+import password.pwm.config.profile.NewUserProfile;
 import password.pwm.config.profile.PwmPasswordPolicy;
 import password.pwm.config.profile.PwmPasswordRule;
 import password.pwm.config.value.data.UserPermission;
@@ -56,6 +57,7 @@ import password.pwm.ldap.UserInfo;
 import password.pwm.ldap.UserInfoFactory;
 import password.pwm.util.PasswordData;
 import password.pwm.util.RandomPasswordGenerator;
+import password.pwm.util.i18n.LocaleHelper;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.java.TimeDuration;
@@ -80,10 +82,10 @@ import java.util.Locale;
 import java.util.Map;
 import java.util.Set;
 
-public class LDAPStatusChecker implements HealthChecker
+public class LDAPHealthChecker implements HealthChecker
 {
 
-    private static final PwmLogger LOGGER = PwmLogger.forClass( LDAPStatusChecker.class );
+    private static final PwmLogger LOGGER = PwmLogger.forClass( LDAPHealthChecker.class );
     private static final String TOPIC = "LDAP";
 
     public List<HealthRecord> doHealthCheck( final PwmApplication pwmApplication )
@@ -95,9 +97,9 @@ public class LDAPStatusChecker implements HealthChecker
         for ( final Map.Entry<String, LdapProfile> entry : ldapProfiles.entrySet() )
         {
             final String profileID = entry.getKey();
-            final List<HealthRecord> profileRecords = new ArrayList<>();
-            profileRecords.addAll(
-                    checkBasicLdapConnectivity( pwmApplication, config, entry.getValue(), true ) );
+            final List<HealthRecord> profileRecords = new ArrayList<>(
+                    checkBasicLdapConnectivity( pwmApplication, config, entry.getValue(), true )
+            );
 
             if ( profileRecords.isEmpty() )
             {
@@ -108,7 +110,6 @@ public class LDAPStatusChecker implements HealthChecker
             {
                 profileRecords.add( HealthRecord.forMessage( HealthMessage.LDAP_OK ) );
                 profileRecords.addAll( doLdapTestUserCheck( config, ldapProfiles.get( profileID ), pwmApplication ) );
-
             }
             returnRecords.addAll( profileRecords );
         }
@@ -147,6 +148,8 @@ public class LDAPStatusChecker implements HealthChecker
                 returnRecords.addAll( checkUserPermissionValues( pwmApplication ) );
 
                 returnRecords.addAll( checkLdapDNSyntaxValues( pwmApplication ) );
+
+                returnRecords.addAll( checkNewUserPasswordTemplateSetting( pwmApplication, config ) );
             }
         }
 
@@ -842,7 +845,7 @@ public class LDAPStatusChecker implements HealthChecker
                 if ( !pwmSetting.isHidden()
                         && pwmSetting.getCategory() == PwmSettingCategory.LDAP_PROFILE
                         && pwmSetting.getFlags().contains( PwmSettingFlag.ldapDNsyntax )
-                        )
+                )
                 {
                     for ( final String profile : config.getLdapProfiles().keySet() )
                     {
@@ -891,6 +894,75 @@ public class LDAPStatusChecker implements HealthChecker
         return returnList;
     }
 
+    private static List<HealthRecord> checkNewUserPasswordTemplateSetting(
+            final PwmApplication pwmApplication,
+            final Configuration configuration
+    )
+    {
+        final Locale locale = PwmConstants.DEFAULT_LOCALE;
+        if ( !configuration.readSettingAsBoolean( PwmSetting.NEWUSER_ENABLE ) )
+        {
+            return Collections.emptyList();
+        }
+
+        for ( final NewUserProfile newUserProfile : configuration.getNewUserProfiles().values() )
+        {
+            final String policyUserStr = newUserProfile.readSettingAsString( PwmSetting.NEWUSER_PASSWORD_POLICY_USER );
+
+            if ( StringUtil.isEmpty( policyUserStr ) )
+            {
+                return Collections.singletonList(
+                        HealthRecord.forMessage(
+                                HealthMessage.NewUser_PwTemplateBad,
+                                PwmSetting.NEWUSER_PASSWORD_POLICY_USER.toMenuLocationDebug( newUserProfile.getIdentifier(), locale ),
+                                LocaleHelper.valueNotApplicable( locale )
+                        )
+                );
+            }
+
+            try
+            {
+                final LdapProfile ldapProfile = configuration.getDefaultLdapProfile();
+                if ( NewUserProfile.TEST_USER_CONFIG_VALUE.equals( policyUserStr ) )
+                {
+                    final UserIdentity testUser = ldapProfile.getTestUser( pwmApplication );
+                    if ( testUser != null )
+                    {
+                        return Collections.emptyList();
+                    }
+                }
+
+                final UserIdentity newUserTemplateIdentity = new UserIdentity( policyUserStr, ldapProfile.getIdentifier() );
+
+                final ChaiUser chaiUser = pwmApplication.getProxiedChaiUser( newUserTemplateIdentity );
+
+                try
+                {
+                    if ( !chaiUser.exists() )
+                    {
+                        return Collections.singletonList(
+                                HealthRecord.forMessage(
+                                        HealthMessage.NewUser_PwTemplateBad,
+                                        PwmSetting.NEWUSER_PASSWORD_POLICY_USER.toMenuLocationDebug( newUserProfile.getIdentifier(), locale )
+                                )
+                        );
+                    }
+                }
+                catch ( ChaiUnavailableException e )
+                {
+                    throw PwmUnrecoverableException.fromChaiException( e );
+                }
+            }
+            catch ( PwmUnrecoverableException e )
+            {
+                LOGGER.error( "error checking new user password policy user settings:" + e.getMessage() );
+            }
+        }
+
+        return Collections.emptyList();
+    }
+
+
     private static List<HealthRecord> checkUserPermission(
             final PwmApplication pwmApplication,
             final UserPermission userPermission,
@@ -1031,15 +1103,15 @@ public class LDAPStatusChecker implements HealthChecker
             throws PwmUnrecoverableException
     {
         final PwmApplication tempApplication = new PwmApplication( pwmApplication.getPwmEnvironment().makeRuntimeInstance( config ) );
-        final LDAPStatusChecker ldapStatusChecker = new LDAPStatusChecker();
+        final LDAPHealthChecker ldapHealthChecker = new LDAPHealthChecker();
         final List<HealthRecord> profileRecords = new ArrayList<>();
 
         final LdapProfile ldapProfile = config.getLdapProfiles().get( profileID );
-        profileRecords.addAll( ldapStatusChecker.checkBasicLdapConnectivity( tempApplication, config, ldapProfile,
+        profileRecords.addAll( ldapHealthChecker.checkBasicLdapConnectivity( tempApplication, config, ldapProfile,
                 testContextless ) );
         if ( fullTest )
         {
-            profileRecords.addAll( ldapStatusChecker.checkLdapServerUrls( pwmApplication, config, ldapProfile ) );
+            profileRecords.addAll( ldapHealthChecker.checkLdapServerUrls( pwmApplication, config, ldapProfile ) );
         }
 
         if ( profileRecords.isEmpty() )
@@ -1049,7 +1121,7 @@ public class LDAPStatusChecker implements HealthChecker
 
         if ( fullTest )
         {
-            profileRecords.addAll( ldapStatusChecker.doLdapTestUserCheck( config, ldapProfile, tempApplication ) );
+            profileRecords.addAll( ldapHealthChecker.doLdapTestUserCheck( config, ldapProfile, tempApplication ) );
         }
 
         return HealthRecord.asHealthDataBean( config, locale, profileRecords );

+ 69 - 17
server/src/main/java/password/pwm/http/ContextManager.java

@@ -35,23 +35,26 @@ import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmException;
 import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.util.PropertyConfigurationImporter;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.TimeDuration;
 import password.pwm.util.logging.PwmLogger;
-import password.pwm.util.secure.PwmRandom;
 
 import javax.servlet.ServletContext;
 import javax.servlet.ServletRequest;
 import javax.servlet.http.HttpSession;
 import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
 import java.io.InputStream;
 import java.io.Serializable;
+import java.nio.file.Files;
+import java.nio.file.Path;
 import java.time.Instant;
 import java.util.Collection;
 import java.util.Date;
 import java.util.Locale;
 import java.util.Map;
-import java.util.TimerTask;
 import java.util.concurrent.Executors;
 import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.TimeUnit;
@@ -73,17 +76,16 @@ public class ContextManager implements Serializable
 
     private final AtomicInteger restartCount = new AtomicInteger( 0 );
     private TimeDuration readApplicationLockMaxWait = TimeDuration.SECONDS_30;
-    private final String instanceGuid;
     private final Lock restartLock = new ReentrantLock();
 
     private String contextPath;
+    private File applicationPath;
 
     private static final String UNSPECIFIED_VALUE = "unspecified";
 
     public ContextManager( final ServletContext servletContext )
     {
         this.servletContext = servletContext;
-        this.instanceGuid = PwmRandom.getInstance().randomUUID().toString();
         this.contextPath = servletContext.getContextPath();
     }
 
@@ -192,6 +194,7 @@ public class ContextManager implements Serializable
 
     public void initialize( )
     {
+        final Instant startTime = Instant.now();
 
         try
         {
@@ -207,7 +210,6 @@ public class ContextManager implements Serializable
 
 
         final ParameterReader parameterReader = new ParameterReader( servletContext );
-        final File applicationPath;
         {
             final String applicationPathStr = parameterReader.readApplicationPath();
             if ( applicationPathStr == null || applicationPathStr.isEmpty() )
@@ -224,7 +226,7 @@ public class ContextManager implements Serializable
         File configurationFile = null;
         try
         {
-            configurationFile = locateConfigurationFile( applicationPath );
+            configurationFile = locateConfigurationFile( applicationPath, PwmConstants.DEFAULT_CONFIG_FILE_FILENAME );
 
             configReader = new ConfigurationReader( configurationFile );
             configReader.getStoredConfiguration().lock();
@@ -297,6 +299,13 @@ public class ContextManager implements Serializable
 
             checkConfigForSaveOnRestart( configReader, pwmApplication );
         }
+
+        if ( pwmApplication == null || pwmApplication.getApplicationMode() == PwmApplicationMode.NEW )
+        {
+            taskMaster.scheduleWithFixedDelay( new SilentPropertiesFileWatcher(), fileScanFrequencyMs, fileScanFrequencyMs, TimeUnit.MILLISECONDS );
+        }
+
+        LOGGER.trace( () -> "initialization complete (" + TimeDuration.compactFromCurrent( startTime ) + ")" );
     }
 
     private void checkConfigForSaveOnRestart(
@@ -396,7 +405,7 @@ public class ContextManager implements Serializable
         return configReader;
     }
 
-    private class ConfigFileWatcher extends TimerTask
+    private class ConfigFileWatcher implements Runnable
     {
         @Override
         public void run( )
@@ -412,6 +421,56 @@ public class ContextManager implements Serializable
         }
     }
 
+    private class SilentPropertiesFileWatcher implements Runnable
+    {
+        private final File silentPropertiesFile;
+
+        SilentPropertiesFileWatcher()
+        {
+            silentPropertiesFile = locateConfigurationFile( applicationPath, PwmConstants.DEFAULT_PROPERTIES_CONFIG_FILE_FILENAME );
+        }
+
+        @Override
+        public void run()
+        {
+            if ( pwmApplication == null || pwmApplication.getApplicationMode() == PwmApplicationMode.NEW )
+            {
+                if ( silentPropertiesFile.exists() )
+                {
+                    boolean success = false;
+                    LOGGER.info( () -> "file " + silentPropertiesFile.getAbsolutePath() + " has appeared, will import as configuration" );
+                    try
+                    {
+                        final PropertyConfigurationImporter importer = new PropertyConfigurationImporter();
+                        final StoredConfigurationImpl storedConfiguration = importer.readConfiguration( new FileInputStream( silentPropertiesFile ) );
+                        configReader.saveConfiguration( storedConfiguration, pwmApplication, null );
+                        LOGGER.info( () -> "file " + silentPropertiesFile.getAbsolutePath() + " has been successfully imported and saved as configuration file" );
+                        requestPwmApplicationRestart();
+                        success = true;
+                    }
+                    catch ( Exception e )
+                    {
+                        LOGGER.error( "error importing " + silentPropertiesFile.getAbsolutePath() + ", error: " + e.getMessage() );
+                    }
+
+                    final String appendValue = success ? ".imported" : ".error";
+                    final Path source = silentPropertiesFile.toPath();
+                    final Path dest = source.resolveSibling( "silent.properties" + appendValue );
+
+                    try
+                    {
+                        Files.move( source, dest );
+                        LOGGER.info( () -> "file " + source.toString() + " has been renamed to " + dest.toString() );
+                    }
+                    catch ( IOException e )
+                    {
+                        LOGGER.error( "error renaming file " + source.toString() + " to " + dest.toString() + ", error: " + e.getMessage() );
+                    }
+                }
+            }
+        }
+    }
+
     private class RestartFlagWatcher implements Runnable
     {
 
@@ -524,10 +583,9 @@ public class ContextManager implements Serializable
         return restartCount.get();
     }
 
-    public File locateConfigurationFile( final File applicationPath )
-            throws Exception
+    private File locateConfigurationFile( final File applicationPath, final String filename )
     {
-        return new File( applicationPath.getAbsolutePath() + File.separator + PwmConstants.DEFAULT_CONFIG_FILE_FILENAME );
+        return new File( applicationPath.getAbsolutePath() + File.separator + filename );
     }
 
     public File locateWebInfFilePath( )
@@ -546,18 +604,13 @@ public class ContextManager implements Serializable
         return null;
     }
 
-    static void outputError( final String outputText )
+    private static void outputError( final String outputText )
     {
         final String msg = PwmConstants.PWM_APP_NAME + " " + JavaHelper.toIsoDate( new Date() ) + " " + outputText;
         System.out.println( msg );
         System.out.println( msg );
     }
 
-    public String getInstanceGuid( )
-    {
-        return instanceGuid;
-    }
-
     public InputStream getResourceAsStream( final String path )
     {
         return servletContext.getResourceAsStream( path );
@@ -640,5 +693,4 @@ public class ContextManager implements Serializable
     {
         return contextPath;
     }
-
 }

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

@@ -99,7 +99,8 @@ public enum JspUrl
     CONFIG_MANAGER_CERTIFICATES( "configmanager-certificates.jsp" ),
     CONFIG_MANAGER_LOCALDB( "configmanager-localdb.jsp" ),
     CONFIG_MANAGER_LOGIN( "configmanager-login.jsp" ),
-    HELPDESK_SEARCH( "helpdesk.jsp" ),;
+    HELPDESK_SEARCH( "helpdesk.jsp" ),
+    FULL_PAGE_HEALTH( "fullpagehealth.jsp" ),;
 
     private String path;
     private static final String JSP_ROOT_URL = "/WEB-INF/jsp/";

+ 1 - 1
server/src/main/java/password/pwm/http/auth/OAuthFilterAuthenticationProvider.java

@@ -46,7 +46,7 @@ public class OAuthFilterAuthenticationProvider implements PwmHttpFilterAuthentic
         }
 
         final String originalURL = pwmRequest.getURLwithQueryString();
-        final OAuthMachine oAuthMachine = new OAuthMachine( oauthSettings );
+        final OAuthMachine oAuthMachine = new OAuthMachine( pwmRequest.getSessionLabel(), oauthSettings );
         oAuthMachine.redirectUserToOAuthServer( pwmRequest, originalURL, null, null );
         redirected = true;
     }

+ 186 - 0
server/src/main/java/password/pwm/http/client/HttpTrustManagerHelper.java

@@ -0,0 +1,186 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2018 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+package password.pwm.http.client;
+
+import org.apache.http.conn.ssl.DefaultHostnameVerifier;
+import org.apache.http.conn.ssl.NoopHostnameVerifier;
+import password.pwm.AppProperty;
+import password.pwm.bean.SessionLabel;
+import password.pwm.config.Configuration;
+import password.pwm.error.PwmError;
+import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.util.java.JavaHelper;
+import password.pwm.util.secure.PwmHashAlgorithm;
+import password.pwm.util.secure.X509Utils;
+
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.TrustManager;
+import java.security.cert.X509Certificate;
+import java.util.Iterator;
+
+class HttpTrustManagerHelper
+{
+    private final Configuration configuration;
+    private final SessionLabel sessionLabel;
+    private final PwmHttpClientConfiguration pwmHttpClientConfiguration;
+    private final TrustManagerType trustManagerType;
+
+    enum TrustManagerType
+    {
+        promiscuous,
+        supplied,
+        configuredCertificates,
+        defaultJava,
+    }
+
+    HttpTrustManagerHelper(
+            final Configuration configuration,
+            final SessionLabel sessionLabel,
+            final PwmHttpClientConfiguration pwmHttpClientConfiguration
+    )
+    {
+        this.configuration = configuration;
+        this.sessionLabel = sessionLabel;
+        this.pwmHttpClientConfiguration = pwmHttpClientConfiguration;
+        this.trustManagerType = figureType();
+    }
+
+    TrustManagerType getTrustManagerType()
+    {
+        return trustManagerType;
+    }
+
+    private TrustManagerType figureType()
+    {
+
+        final boolean configPromiscuousEnabled = Boolean.parseBoolean( configuration.readAppProperty( AppProperty.SECURITY_HTTP_PROMISCUOUS_ENABLE ) );
+        final boolean promiscuousTrustMgrSet = pwmHttpClientConfiguration != null
+                && pwmHttpClientConfiguration.getTrustManager() != null
+                && X509Utils.PromiscuousTrustManager.class.equals( pwmHttpClientConfiguration.getTrustManager().getClass() );
+
+        if ( configPromiscuousEnabled || promiscuousTrustMgrSet )
+        {
+            return TrustManagerType.promiscuous;
+        }
+
+        // use the client supplied TrustManager
+        if ( pwmHttpClientConfiguration.getTrustManager() != null )
+        {
+            return TrustManagerType.supplied;
+        }
+
+        // using configured certificates
+        if ( !JavaHelper.isEmpty( pwmHttpClientConfiguration.getCertificates() ) )
+        {
+            return TrustManagerType.configuredCertificates;
+        }
+
+        // use default trust manager
+        return TrustManagerType.defaultJava;
+    }
+
+    HostnameVerifier hostnameVerifier()
+    {
+        final TrustManagerType trustManagerType = getTrustManagerType();
+        if ( trustManagerType == TrustManagerType.promiscuous )
+        {
+            return NoopHostnameVerifier.INSTANCE;
+        }
+
+        if ( !Boolean.parseBoolean( configuration.readAppProperty( AppProperty.HTTP_CLIENT_ENABLE_HOSTNAME_VERIFICATION ) ) )
+        {
+            return NoopHostnameVerifier.INSTANCE;
+        }
+
+        return new DefaultHostnameVerifier();
+    }
+
+    TrustManager[] makeTrustManager(
+    )
+            throws PwmUnrecoverableException
+    {
+        final TrustManagerType trustManagerType = getTrustManagerType();
+
+        switch ( trustManagerType )
+        {
+            case promiscuous:
+                return new TrustManager[]
+                        {
+                                new X509Utils.PromiscuousTrustManager( sessionLabel ),
+                        };
+
+            case supplied:
+            {
+                return new TrustManager[]
+                        {
+                                pwmHttpClientConfiguration.getTrustManager(),
+                        };
+            }
+
+            case configuredCertificates:
+            {
+                return new TrustManager[]
+                        {
+                                new X509Utils.CertMatchingTrustManager( configuration, pwmHttpClientConfiguration.getCertificates() ),
+                        };
+            }
+
+            case defaultJava:
+            {
+                return X509Utils.getDefaultJavaTrustManager( configuration );
+            }
+
+            default:
+                JavaHelper.unhandledSwitchStatement( trustManagerType );
+
+        }
+
+        throw PwmUnrecoverableException.newException( PwmError.ERROR_INTERNAL, "unknown trust manager type" );
+    }
+
+    String debugText() throws PwmUnrecoverableException
+    {
+        final TrustManagerType type = getTrustManagerType();
+        final StringBuilder value = new StringBuilder( "trust manager [" + type );
+        if ( TrustManagerType.supplied == type )
+        {
+            value.append( "=" );
+            value.append( pwmHttpClientConfiguration.getTrustManager().getClass().getSimpleName() );
+        }
+        else if ( TrustManagerType.configuredCertificates == type )
+        {
+            value.append( "=" );
+            for ( final Iterator<X509Certificate> iterator = pwmHttpClientConfiguration.getCertificates().iterator(); iterator.hasNext(); )
+            {
+                final X509Certificate certificate = iterator.next();
+                value.append( X509Utils.hash( certificate, PwmHashAlgorithm.SHA1 ) );
+                if ( iterator.hasNext() )
+                {
+                    value.append( "," );
+                }
+            }
+        }
+        value.append( "]" );
+        return value.toString();
+    }
+}

+ 33 - 54
server/src/main/java/password/pwm/http/client/PwmHttpClient.java

@@ -45,7 +45,6 @@ import org.apache.http.conn.routing.HttpRoute;
 import org.apache.http.conn.routing.HttpRoutePlanner;
 import org.apache.http.conn.socket.ConnectionSocketFactory;
 import org.apache.http.conn.socket.PlainConnectionSocketFactory;
-import org.apache.http.conn.ssl.NoopHostnameVerifier;
 import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
 import org.apache.http.entity.StringEntity;
 import org.apache.http.impl.client.BasicCredentialsProvider;
@@ -53,8 +52,6 @@ import org.apache.http.impl.client.HttpClientBuilder;
 import org.apache.http.impl.client.ProxyAuthenticationStrategy;
 import org.apache.http.impl.conn.BasicHttpClientConnectionManager;
 import org.apache.http.protocol.HttpContext;
-import org.apache.http.ssl.SSLContextBuilder;
-import org.apache.http.ssl.TrustStrategy;
 import org.apache.http.util.EntityUtils;
 import password.pwm.AppProperty;
 import password.pwm.PwmApplication;
@@ -70,22 +67,16 @@ import password.pwm.http.HttpMethod;
 import password.pwm.http.PwmURL;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.java.TimeDuration;
+import password.pwm.util.logging.PwmLogLevel;
 import password.pwm.util.logging.PwmLogger;
-import password.pwm.util.secure.X509Utils;
 
 import javax.net.ssl.SSLContext;
-import javax.net.ssl.TrustManager;
 import java.io.IOException;
 import java.io.InputStream;
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.net.URL;
-import java.security.KeyManagementException;
-import java.security.KeyStoreException;
-import java.security.NoSuchAlgorithmException;
 import java.security.SecureRandom;
-import java.security.cert.CertificateException;
-import java.security.cert.X509Certificate;
 import java.time.Instant;
 import java.util.LinkedHashMap;
 import java.util.List;
@@ -131,40 +122,25 @@ public class PwmHttpClient
     {
         final HttpClientBuilder clientBuilder = HttpClientBuilder.create();
         clientBuilder.setUserAgent( PwmConstants.PWM_APP_NAME + " " + PwmConstants.SERVLET_VERSION );
-        final boolean configPromiscuousEnabled = Boolean.parseBoolean( configuration.readAppProperty( AppProperty.SECURITY_HTTP_PROMISCUOUS_ENABLE ) );
-        final boolean promiscuousTrustMgrSet = pwmHttpClientConfiguration != null
-                && pwmHttpClientConfiguration.getTrustManager() != null
-                && X509Utils.PromiscuousTrustManager.class.equals( pwmHttpClientConfiguration.getTrustManager().getClass() );
 
         try
         {
-            if ( configPromiscuousEnabled || promiscuousTrustMgrSet )
-            {
-                clientBuilder.setSSLContext( promiscuousSSLContext() );
-                clientBuilder.setSSLHostnameVerifier( NoopHostnameVerifier.INSTANCE );
-            }
-            else if ( pwmHttpClientConfiguration != null && ( pwmHttpClientConfiguration.getCertificates() != null || pwmHttpClientConfiguration.getTrustManager() != null ) )
-            {
-                final SSLContext sslContext = SSLContext.getInstance( "TLS" );
-                final TrustManager trustManager = pwmHttpClientConfiguration.getTrustManager() != null
-                        ? pwmHttpClientConfiguration.getTrustManager()
-                        : new X509Utils.CertMatchingTrustManager( configuration, pwmHttpClientConfiguration.getCertificates() );
-                sslContext.init( null, new TrustManager[]
-                                {
-                                        trustManager,
-                                },
-                        new SecureRandom() );
-
-                final SSLConnectionSocketFactory sslConnectionFactory = new SSLConnectionSocketFactory( sslContext, NoopHostnameVerifier.INSTANCE );
-                final Registry<ConnectionSocketFactory> registry = RegistryBuilder.<ConnectionSocketFactory>create()
-                        .register( "https", sslConnectionFactory )
-                        .register( "http", PlainConnectionSocketFactory.INSTANCE )
-                        .build();
-                final HttpClientConnectionManager ccm = new BasicHttpClientConnectionManager( registry );
-                clientBuilder.setSSLContext( sslContext );
-                clientBuilder.setSSLSocketFactory( sslConnectionFactory );
-                clientBuilder.setConnectionManager( ccm );
-            }
+            final SSLContext sslContext = SSLContext.getInstance( "TLS" );
+            final HttpTrustManagerHelper httpTrustManagerHelper = new HttpTrustManagerHelper( configuration, sessionLabel, pwmHttpClientConfiguration );
+            sslContext.init(
+                    null,
+                    httpTrustManagerHelper.makeTrustManager(),
+                    new SecureRandom() );
+            final SSLConnectionSocketFactory sslConnectionFactory = new SSLConnectionSocketFactory( sslContext, httpTrustManagerHelper.hostnameVerifier() );
+            final Registry<ConnectionSocketFactory> registry = RegistryBuilder.<ConnectionSocketFactory>create()
+                    .register( "https", sslConnectionFactory )
+                    .register( "http", PlainConnectionSocketFactory.INSTANCE )
+                    .build();
+            final HttpClientConnectionManager ccm = new BasicHttpClientConnectionManager( registry );
+            clientBuilder.setSSLHostnameVerifier( httpTrustManagerHelper.hostnameVerifier() );
+            clientBuilder.setSSLContext( sslContext );
+            clientBuilder.setSSLSocketFactory( sslConnectionFactory );
+            clientBuilder.setConnectionManager( ccm );
         }
         catch ( Exception e )
         {
@@ -281,8 +257,22 @@ public class PwmHttpClient
         final Instant startTime = Instant.now();
         final int counter = REQUEST_COUNTER.getAndIncrement();
 
-        LOGGER.trace( sessionLabel, () -> "preparing to send (id=" + counter + ") "
-                + clientRequest.toDebugString( this ) );
+        if ( LOGGER.isEnabled( PwmLogLevel.TRACE ) )
+        {
+            final String sslDebugText;
+            if ( clientRequest.isHttps() )
+            {
+                final HttpTrustManagerHelper httpTrustManagerHelper = new HttpTrustManagerHelper( pwmApplication.getConfig(), sessionLabel, pwmHttpClientConfiguration );
+                sslDebugText = "using " + httpTrustManagerHelper.debugText();
+            }
+            else
+            {
+                sslDebugText = "";
+            }
+
+            LOGGER.trace( sessionLabel, () -> "preparing to send (id=" + counter + ") "
+                    + clientRequest.toDebugString( this, sslDebugText ) );
+        }
 
         final HttpResponse httpResponse = executeRequest( clientRequest );
         final String responseBody = EntityUtils.toString( httpResponse.getEntity() );
@@ -375,17 +365,6 @@ public class PwmHttpClient
         return httpClient.execute( httpRequest );
     }
 
-    private static SSLContext promiscuousSSLContext( ) throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException
-    {
-        return new SSLContextBuilder().loadTrustMaterial( null, new TrustStrategy()
-        {
-            public boolean isTrusted( final X509Certificate[] arg0, final String arg1 ) throws CertificateException
-            {
-                return true;
-            }
-        } ).build();
-    }
-
     public InputStream streamForUrl( final String inputUrl )
             throws IOException, PwmUnrecoverableException
     {

+ 13 - 2
server/src/main/java/password/pwm/http/client/PwmHttpClientRequest.java

@@ -24,8 +24,10 @@ package password.pwm.http.client;
 
 import lombok.Value;
 import password.pwm.http.HttpMethod;
+import password.pwm.util.java.StringUtil;
 
 import java.io.Serializable;
+import java.net.URI;
 import java.util.Map;
 
 @Value
@@ -36,8 +38,17 @@ public class PwmHttpClientRequest implements Serializable
     private final String body;
     private final Map<String, String> headers;
 
-    public String toDebugString( final PwmHttpClient pwmHttpClient )
+    public String toDebugString( final PwmHttpClient pwmHttpClient, final String additionalText )
     {
-        return pwmHttpClient.entityToDebugString( "HTTP " + method + " request to " + url, headers, body );
+        final String topLine = "HTTP " + method + " request to " + url
+                + ( StringUtil.isEmpty( additionalText )
+                ? ""
+                : " " + additionalText );
+        return pwmHttpClient.entityToDebugString( topLine, headers, body );
+    }
+
+    public boolean isHttps()
+    {
+        return "https".equals( URI.create( getUrl() ).getScheme() );
     }
 }

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

@@ -171,7 +171,7 @@ public class AuthenticationFilter extends AbstractPwmFilter
         if ( pwmSession.getLoginInfoBean().getOauthExp() != null )
         {
             final OAuthSettings oauthSettings = OAuthSettings.forSSOAuthentication( pwmRequest.getConfig() );
-            final OAuthMachine oAuthMachine = new OAuthMachine( oauthSettings );
+            final OAuthMachine oAuthMachine = new OAuthMachine( pwmRequest.getSessionLabel(), oauthSettings );
             if ( oAuthMachine.checkOAuthExpiration( pwmRequest ) )
             {
                 pwmRequest.respondWithError( new ErrorInformation( PwmError.ERROR_OAUTH_ERROR, "oauth access token has expired" ) );

+ 64 - 22
server/src/main/java/password/pwm/http/servlet/ClientApiServlet.java

@@ -47,6 +47,7 @@ import password.pwm.http.PwmURL;
 import password.pwm.i18n.Display;
 import password.pwm.svc.stats.EpsStatistic;
 import password.pwm.svc.stats.Statistic;
+import password.pwm.svc.stats.StatisticsManager;
 import password.pwm.util.i18n.LocaleHelper;
 import password.pwm.util.java.TimeDuration;
 import password.pwm.util.logging.PwmLogger;
@@ -55,6 +56,7 @@ import password.pwm.util.secure.PwmHashAlgorithm;
 import password.pwm.util.secure.SecureEngine;
 import password.pwm.ws.server.RestResultBean;
 import password.pwm.ws.server.rest.RestHealthServer;
+import password.pwm.ws.server.rest.RestStatisticsServer;
 import password.pwm.ws.server.rest.bean.HealthData;
 
 import javax.servlet.ServletException;
@@ -106,7 +108,9 @@ public class ClientApiServlet extends ControlledPwmServlet
         clientData( HttpMethod.GET ),
         strings( HttpMethod.GET ),
         health( HttpMethod.GET ),
-        ping( HttpMethod.GET ),;
+        ping( HttpMethod.GET ),
+        statistics( HttpMethod.GET ),;
+
 
         private final HttpMethod method;
 
@@ -211,27 +215,7 @@ public class ClientApiServlet extends ControlledPwmServlet
     public ProcessStatus restHealthProcessor( final PwmRequest pwmRequest )
             throws IOException, ServletException, PwmUnrecoverableException
     {
-        if ( pwmRequest.getPwmApplication().getApplicationMode() == PwmApplicationMode.RUNNING )
-        {
-            if ( !pwmRequest.getConfig().readSettingAsBoolean( PwmSetting.PUBLIC_HEALTH_STATS_WEBSERVICES ) )
-            {
-                if ( !pwmRequest.isAuthenticated() )
-                {
-                    final ErrorInformation errorInformation = new ErrorInformation( PwmError.ERROR_AUTHENTICATION_REQUIRED );
-                    LOGGER.debug( pwmRequest, errorInformation );
-                    pwmRequest.respondWithError( errorInformation );
-                    return ProcessStatus.Halt;
-                }
-
-                if ( !pwmRequest.getPwmSession().getSessionManager().checkPermission( pwmRequest.getPwmApplication(), Permission.PWMADMIN ) )
-                {
-                    final ErrorInformation errorInformation = new ErrorInformation( PwmError.ERROR_UNAUTHORIZED, "admin privileges required" );
-                    LOGGER.debug( pwmRequest, errorInformation );
-                    pwmRequest.respondWithError( errorInformation );
-                    return ProcessStatus.Halt;
-                }
-            }
-        }
+        precheckPublicHealthAndStats( pwmRequest );
 
         try
         {
@@ -470,4 +454,62 @@ public class ClientApiServlet extends ControlledPwmServlet
         return displayStrings;
     }
 
+
+    @ActionHandler( action = "statistics" )
+    private ProcessStatus restStatisticsHandler( final PwmRequest pwmRequest )
+            throws ChaiUnavailableException, PwmUnrecoverableException, IOException
+    {
+        precheckPublicHealthAndStats( pwmRequest );
+
+        final String statKey = pwmRequest.readParameterAsString( "statKey" );
+        final String statName = pwmRequest.readParameterAsString( "statName" );
+        final String days = pwmRequest.readParameterAsString( "days" );
+
+        final StatisticsManager statisticsManager = pwmRequest.getPwmApplication().getStatisticsManager();
+        final RestStatisticsServer.OutputVersion1.JsonOutput jsonOutput = new RestStatisticsServer.OutputVersion1.JsonOutput();
+        jsonOutput.EPS = RestStatisticsServer.OutputVersion1.addEpsStats( statisticsManager );
+
+        if ( statName != null && statName.length() > 0 )
+        {
+            jsonOutput.nameData = RestStatisticsServer.OutputVersion1.doNameStat( statisticsManager, statName, days );
+        }
+        else
+        {
+            jsonOutput.keyData = RestStatisticsServer.OutputVersion1.doKeyStat( statisticsManager, statKey );
+        }
+
+        final RestResultBean restResultBean = RestResultBean.withData( jsonOutput );
+        pwmRequest.outputJsonResult( restResultBean );
+        return ProcessStatus.Halt;
+
+    }
+
+    private void precheckPublicHealthAndStats( final PwmRequest pwmRequest )
+            throws PwmUnrecoverableException
+    {
+        if (
+                pwmRequest.getPwmApplication().getApplicationMode() != PwmApplicationMode.RUNNING
+                        && pwmRequest.getPwmApplication().getApplicationMode() != PwmApplicationMode.CONFIGURATION
+        )
+        {
+            final ErrorInformation errorInformation = new ErrorInformation( PwmError.ERROR_SERVICE_NOT_AVAILABLE );
+            throw new PwmUnrecoverableException( errorInformation );
+        }
+
+        if ( !pwmRequest.getConfig().readSettingAsBoolean( PwmSetting.PUBLIC_HEALTH_STATS_WEBSERVICES ) )
+        {
+            if ( !pwmRequest.isAuthenticated() )
+            {
+                final ErrorInformation errorInformation = new ErrorInformation( PwmError.ERROR_AUTHENTICATION_REQUIRED );
+                throw new PwmUnrecoverableException( errorInformation );
+            }
+
+            if ( !pwmRequest.getPwmSession().getSessionManager().checkPermission( pwmRequest.getPwmApplication(), Permission.PWMADMIN ) )
+            {
+                final ErrorInformation errorInformation = new ErrorInformation( PwmError.ERROR_UNAUTHORIZED, "admin privileges required" );
+                throw new PwmUnrecoverableException( errorInformation );
+            }
+        }
+    }
 }
+

+ 123 - 0
server/src/main/java/password/pwm/http/servlet/FullPageHealthServlet.java

@@ -0,0 +1,123 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2018 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+package password.pwm.http.servlet;
+
+import com.novell.ldapchai.exception.ChaiUnavailableException;
+import password.pwm.PwmConstants;
+import password.pwm.config.PwmSetting;
+import password.pwm.error.ErrorInformation;
+import password.pwm.error.PwmError;
+import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.http.HttpMethod;
+import password.pwm.http.JspUrl;
+import password.pwm.http.ProcessStatus;
+import password.pwm.http.PwmRequest;
+import password.pwm.util.logging.PwmLogger;
+
+import javax.servlet.ServletException;
+import javax.servlet.annotation.WebServlet;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Locale;
+
+@WebServlet(
+        name = "FullPageHealthServlet",
+        urlPatterns = {
+                PwmConstants.URL_PREFIX_PUBLIC + "/health",
+        }
+)
+public class FullPageHealthServlet extends ControlledPwmServlet
+{
+    private static final PwmLogger LOGGER = PwmLogger.forClass( FullPageHealthServlet.class );
+
+    public enum FullPageHealthAction implements AbstractPwmServlet.ProcessAction
+    {
+        value( HttpMethod.GET ),;
+
+        private final HttpMethod method;
+
+        FullPageHealthAction( final HttpMethod method )
+        {
+            this.method = method;
+        }
+
+        public Collection<HttpMethod> permittedMethods( )
+        {
+            return Collections.singletonList( method );
+        }
+    }
+
+    @Override
+    public Class<? extends ProcessAction> getProcessActionsClass( )
+    {
+        return FullPageHealthAction.class;
+    }
+
+    @Override
+    protected void processAction( final PwmRequest pwmRequest )
+            throws ServletException, IOException, ChaiUnavailableException, PwmUnrecoverableException
+    {
+        super.processAction( pwmRequest );
+    }
+
+    @Override
+    protected void nextStep( final PwmRequest pwmRequest )
+            throws PwmUnrecoverableException, IOException, ServletException
+    {
+        forwardToJSP( pwmRequest );
+        pwmRequest.getPwmSession().unauthenticateUser( pwmRequest );
+    }
+
+    @Override
+    public ProcessStatus preProcessCheck( final PwmRequest pwmRequest )
+            throws PwmUnrecoverableException, IOException, ServletException
+    {
+        if ( !pwmRequest.getConfig().readSettingAsBoolean( PwmSetting.PUBLIC_HEALTH_STATS_WEBSERVICES ) )
+        {
+            final Locale locale = pwmRequest.getLocale();
+            final String errorMsg = "configuration setting "
+                    + PwmSetting.PUBLIC_HEALTH_STATS_WEBSERVICES.toMenuLocationDebug( null, locale )
+                    + " must be enabled for this page to function.";
+            final ErrorInformation errorInformation = new ErrorInformation( PwmError.ERROR_SERVICE_NOT_AVAILABLE, errorMsg );
+            throw new PwmUnrecoverableException( errorInformation );
+        }
+
+        return ProcessStatus.Continue;
+    }
+
+    private void forwardToJSP(
+            final PwmRequest pwmRequest
+    )
+            throws IOException, ServletException, PwmUnrecoverableException
+    {
+        pwmRequest.forwardToJsp( JspUrl.FULL_PAGE_HEALTH );
+    }
+
+    @ActionHandler( action = "value" )
+    private ProcessStatus bogusValueHandler( final PwmRequest pwmRequest )
+    {
+        // bogus method to satisfy test case
+        return ProcessStatus.Continue;
+    }
+}

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

@@ -95,6 +95,7 @@ public enum PwmServletDefinition
     ConfigManager_Wordlists( ConfigManagerWordlistServlet.class, ConfigManagerBean.class ),
     ConfigManager_LocalDB( ConfigManagerLocalDBServlet.class, ConfigManagerBean.class ),
     ConfigManager_Certificates( ConfigManagerCertificatesServlet.class, ConfigManagerBean.class ),
+    FullPageHealth( FullPageHealthServlet.class, null ),
 
     NewUser( NewUserServlet.class, NewUserBean.class ),
     ActivateUser( ActivateUserServlet.class, ActivateUserBean.class ),

+ 1 - 29
server/src/main/java/password/pwm/http/servlet/admin/AdminServlet.java

@@ -61,8 +61,8 @@ import password.pwm.svc.report.ReportCsvUtility;
 import password.pwm.svc.report.ReportService;
 import password.pwm.svc.report.UserCacheRecord;
 import password.pwm.svc.stats.StatisticsManager;
-import password.pwm.util.i18n.LocaleHelper;
 import password.pwm.util.db.DatabaseException;
+import password.pwm.util.i18n.LocaleHelper;
 import password.pwm.util.java.ClosableIterator;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.JsonUtil;
@@ -75,7 +75,6 @@ import password.pwm.util.logging.PwmLogEvent;
 import password.pwm.util.logging.PwmLogLevel;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.ws.server.RestResultBean;
-import password.pwm.ws.server.rest.RestStatisticsServer;
 
 import javax.servlet.ServletException;
 import javax.servlet.annotation.WebServlet;
@@ -132,7 +131,6 @@ public class AdminServlet extends ControlledPwmServlet
         auditData( HttpMethod.GET ),
         sessionData( HttpMethod.GET ),
         intruderData( HttpMethod.GET ),
-        statistics( HttpMethod.GET ),
         startPwNotifyJob( HttpMethod.POST ),
         readPwNotifyStatus( HttpMethod.POST ),
         readPwNotifyLog( HttpMethod.POST ),
@@ -545,32 +543,6 @@ public class AdminServlet extends ControlledPwmServlet
         return ProcessStatus.Halt;
     }
 
-    @ActionHandler( action = "statistics" )
-    private ProcessStatus restStatisticsHandler( final PwmRequest pwmRequest )
-            throws ChaiUnavailableException, PwmUnrecoverableException, IOException
-    {
-        final String statKey = pwmRequest.readParameterAsString( "statKey" );
-        final String statName = pwmRequest.readParameterAsString( "statName" );
-        final String days = pwmRequest.readParameterAsString( "days" );
-
-        final StatisticsManager statisticsManager = pwmRequest.getPwmApplication().getStatisticsManager();
-        final RestStatisticsServer.OutputVersion1.JsonOutput jsonOutput = new RestStatisticsServer.OutputVersion1.JsonOutput();
-        jsonOutput.EPS = RestStatisticsServer.OutputVersion1.addEpsStats( statisticsManager );
-
-        if ( statName != null && statName.length() > 0 )
-        {
-            jsonOutput.nameData = RestStatisticsServer.OutputVersion1.doNameStat( statisticsManager, statName, days );
-        }
-        else
-        {
-            jsonOutput.keyData = RestStatisticsServer.OutputVersion1.doKeyStat( statisticsManager, statKey );
-        }
-
-        final RestResultBean restResultBean = RestResultBean.withData( jsonOutput );
-        pwmRequest.outputJsonResult( restResultBean );
-        return ProcessStatus.Halt;
-    }
-
     private void processDebugUserSearch( final PwmRequest pwmRequest )
             throws PwmUnrecoverableException
     {

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

@@ -88,7 +88,7 @@ public abstract class ChangePasswordServlet extends ControlledPwmServlet
 
     private static final PwmLogger LOGGER = PwmLogger.forClass( ChangePasswordServlet.class );
 
-    private enum ChangePasswordAction implements ControlledPwmServlet.ProcessAction
+    public enum ChangePasswordAction implements ControlledPwmServlet.ProcessAction
     {
         checkProgress( HttpMethod.POST ),
         complete( HttpMethod.GET ),

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

@@ -58,7 +58,7 @@ import password.pwm.health.DatabaseStatusChecker;
 import password.pwm.health.HealthRecord;
 import password.pwm.health.HealthStatus;
 import password.pwm.health.HealthTopic;
-import password.pwm.health.LDAPStatusChecker;
+import password.pwm.health.LDAPHealthChecker;
 import password.pwm.http.HttpMethod;
 import password.pwm.http.JspUrl;
 import password.pwm.http.ProcessStatus;
@@ -672,7 +672,7 @@ public class ConfigEditorServlet extends ControlledPwmServlet
         LOGGER.debug( pwmRequest, () -> "beginning restLdapHealthCheck" );
         final String profileID = pwmRequest.readParameterAsString( "profile" );
         final Configuration config = new Configuration( configManagerBean.getStoredConfiguration() );
-        final HealthData healthData = LDAPStatusChecker.healthForNewConfiguration( pwmRequest.getPwmApplication(), config, pwmRequest.getLocale(), profileID, true, true );
+        final HealthData healthData = LDAPHealthChecker.healthForNewConfiguration( pwmRequest.getPwmApplication(), config, pwmRequest.getLocale(), profileID, true, true );
         final RestResultBean restResultBean = RestResultBean.withData( healthData );
 
         pwmRequest.outputJsonResult( restResultBean );

+ 6 - 6
server/src/main/java/password/pwm/http/servlet/configguide/ConfigGuideServlet.java

@@ -49,7 +49,7 @@ import password.pwm.health.HealthMonitor;
 import password.pwm.health.HealthRecord;
 import password.pwm.health.HealthStatus;
 import password.pwm.health.HealthTopic;
-import password.pwm.health.LDAPStatusChecker;
+import password.pwm.health.LDAPHealthChecker;
 import password.pwm.http.ContextManager;
 import password.pwm.http.HttpMethod;
 import password.pwm.http.ProcessStatus;
@@ -238,7 +238,7 @@ public class ConfigGuideServlet extends ControlledPwmServlet
                 .getPwmEnvironment()
                 .makeRuntimeInstance( tempConfiguration ) );
 
-        final LDAPStatusChecker ldapStatusChecker = new LDAPStatusChecker();
+        final LDAPHealthChecker ldapHealthChecker = new LDAPHealthChecker();
         final List<HealthRecord> records = new ArrayList<>();
         final LdapProfile ldapProfile = tempConfiguration.getDefaultLdapProfile();
 
@@ -261,7 +261,7 @@ public class ConfigGuideServlet extends ControlledPwmServlet
 
             case LDAP_PROXY:
             {
-                records.addAll( ldapStatusChecker.checkBasicLdapConnectivity( tempApplication, tempConfiguration, ldapProfile, false ) );
+                records.addAll( ldapHealthChecker.checkBasicLdapConnectivity( tempApplication, tempConfiguration, ldapProfile, false ) );
                 if ( records.isEmpty() )
                 {
                     records.add( password.pwm.health.HealthRecord.forMessage( HealthMessage.LDAP_OK ) );
@@ -271,7 +271,7 @@ public class ConfigGuideServlet extends ControlledPwmServlet
 
             case LDAP_CONTEXT:
             {
-                records.addAll( ldapStatusChecker.checkBasicLdapConnectivity( tempApplication, tempConfiguration, ldapProfile, true ) );
+                records.addAll( ldapHealthChecker.checkBasicLdapConnectivity( tempApplication, tempConfiguration, ldapProfile, true ) );
                 if ( records.isEmpty() )
                 {
                     records.add( new HealthRecord( HealthStatus.GOOD, HealthTopic.LDAP, "LDAP Contextless Login Root validated" ) );
@@ -317,8 +317,8 @@ public class ConfigGuideServlet extends ControlledPwmServlet
                 final String testUserValue = configGuideBean.getFormData().get( ConfigGuideFormField.PARAM_LDAP_TEST_USER );
                 if ( testUserValue != null && !testUserValue.isEmpty() )
                 {
-                    records.addAll( ldapStatusChecker.checkBasicLdapConnectivity( tempApplication, tempConfiguration, ldapProfile, false ) );
-                    records.addAll( ldapStatusChecker.doLdapTestUserCheck( tempConfiguration, ldapProfile, tempApplication ) );
+                    records.addAll( ldapHealthChecker.checkBasicLdapConnectivity( tempApplication, tempConfiguration, ldapProfile, false ) );
+                    records.addAll( ldapHealthChecker.doLdapTestUserCheck( tempConfiguration, ldapProfile, tempApplication ) );
                 }
                 else
                 {

+ 61 - 25
server/src/main/java/password/pwm/http/servlet/configmanager/DebugItemGenerator.java

@@ -22,6 +22,8 @@
 
 package password.pwm.http.servlet.configmanager;
 
+import lombok.Builder;
+import lombok.Value;
 import org.apache.commons.csv.CSVPrinter;
 import password.pwm.AppProperty;
 import password.pwm.PwmAboutProperty;
@@ -69,15 +71,15 @@ import java.lang.management.ThreadInfo;
 import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collection;
 import java.util.Collections;
-import java.util.Enumeration;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Properties;
 import java.util.Set;
 import java.util.TreeMap;
-import java.util.TreeSet;
+import java.util.stream.Collectors;
 import java.util.zip.ZipEntry;
 import java.util.zip.ZipOutputStream;
 
@@ -104,7 +106,8 @@ public class DebugItemGenerator
             SessionDataGenerator.class,
             LdapRecentUserDebugGenerator.class,
             ClusterInfoDebugGenerator.class,
-            CacheServiceDebugItemGenerator.class
+            CacheServiceDebugItemGenerator.class,
+            RootFileSystemDebugItemGenerator.class
     ) );
 
     static void outputZipDebugFile(
@@ -252,21 +255,10 @@ public class DebugItemGenerator
         @Override
         public void outputItem( final PwmApplication pwmApplication, final PwmRequest pwmRequest, final OutputStream outputStream ) throws Exception
         {
-            final Properties outputProps = new Properties()
-            {
-                public synchronized Enumeration<Object> keys( )
-                {
-                    return Collections.enumeration( new TreeSet<>( super.keySet() ) );
-                }
-            };
-
+            final Properties outputProps = new JavaHelper.SortedProperties();
             final Map<PwmAboutProperty, String> infoBean = PwmAboutProperty.makeInfoBean( pwmApplication );
             outputProps.putAll( PwmAboutProperty.toStringMap( infoBean ) );
-            try ( ByteArrayOutputStream baos = new ByteArrayOutputStream() )
-            {
-                outputProps.store( baos, JavaHelper.toIsoDate( Instant.now() ) );
-                outputStream.write( baos.toByteArray() );
-            }
+            outputProps.store( outputStream, JavaHelper.toIsoDate( Instant.now() ) );
         }
     }
 
@@ -281,11 +273,9 @@ public class DebugItemGenerator
         @Override
         public void outputItem( final PwmApplication pwmApplication, final PwmRequest pwmRequest, final OutputStream outputStream ) throws Exception
         {
-            final Properties outputProps = JavaHelper.newSortedProperties();
+            final Properties outputProps = new JavaHelper.SortedProperties();
             outputProps.putAll( System.getenv() );
-            final ByteArrayOutputStream baos = new ByteArrayOutputStream();
-            outputProps.store( baos, JavaHelper.toIsoDate( Instant.now() ) );
-            outputStream.write( baos.toByteArray() );
+            outputProps.store( outputStream, JavaHelper.toIsoDate( Instant.now() ) );
         }
     }
 
@@ -302,16 +292,14 @@ public class DebugItemGenerator
         {
 
             final Configuration config = pwmRequest.getConfig();
-            final Properties outputProps = JavaHelper.newSortedProperties();
+            final Properties outputProps = new JavaHelper.SortedProperties();
 
             for ( final AppProperty appProperty : AppProperty.values() )
             {
-                outputProps.setProperty( appProperty.getKey(), config.readAppProperty( appProperty ) );
+                outputProps.put( appProperty.getKey(), config.readAppProperty( appProperty ) );
             }
 
-            final ByteArrayOutputStream baos = new ByteArrayOutputStream();
-            outputProps.store( baos, JavaHelper.toIsoDate( Instant.now() ) );
-            outputStream.write( baos.toByteArray() );
+            outputStream.write( JsonUtil.serializeMap( outputProps ).getBytes( PwmConstants.DEFAULT_CHARSET ) );
         }
     }
 
@@ -741,6 +729,54 @@ public class DebugItemGenerator
         }
     }
 
+    static class RootFileSystemDebugItemGenerator implements Generator
+    {
+        @Override
+        public String getFilename( )
+        {
+            return "filesystem-data.json";
+        }
+
+        @Override
+        public void outputItem(
+                final PwmApplication pwmApplication,
+                final PwmRequest pwmRequest,
+                final OutputStream outputStream
+        )
+                throws Exception
+        {
+            final Collection<RootFileSystemInfo> rootInfos = RootFileSystemInfo.forAllRootFileSystems();
+            outputStream.write( JsonUtil.serializeCollection( rootInfos, JsonUtil.Flag.PrettyPrint ).getBytes( PwmConstants.DEFAULT_CHARSET ) );
+        }
+
+        @Value
+        @Builder
+        private static class RootFileSystemInfo implements Serializable
+        {
+            private String rootPath;
+            private long totalSpace;
+            private long freeSpace;
+            private long usableSpace;
+
+            static Collection<RootFileSystemInfo> forAllRootFileSystems()
+            {
+                return Arrays.stream( File.listRoots() )
+                        .map( RootFileSystemInfo::forRoot )
+                        .collect( Collectors.toList() );
+            }
+
+            static RootFileSystemInfo forRoot( final File fileRoot )
+            {
+                return RootFileSystemInfo.builder()
+                        .rootPath( fileRoot.getAbsolutePath() )
+                        .totalSpace( fileRoot.getTotalSpace() )
+                        .freeSpace( fileRoot.getFreeSpace() )
+                        .usableSpace( fileRoot.getUsableSpace() )
+                        .build();
+            }
+        }
+    }
+
     interface Generator
     {
 

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

@@ -1501,7 +1501,7 @@ public class ForgottenPasswordServlet extends ControlledPwmServlet
                 forgottenPasswordBean.getProgress().setInProgressVerificationMethod( IdentityVerificationMethod.OAUTH );
                 final ForgottenPasswordProfile forgottenPasswordProfile = ForgottenPasswordUtil.forgottenPasswordProfile( pwmRequest.getPwmApplication(), forgottenPasswordBean );
                 final OAuthSettings oAuthSettings = OAuthSettings.forForgottenPassword( forgottenPasswordProfile );
-                final OAuthMachine oAuthMachine = new OAuthMachine( oAuthSettings );
+                final OAuthMachine oAuthMachine = new OAuthMachine( pwmRequest.getSessionLabel(), oAuthSettings );
                 pwmRequest.getPwmApplication().getSessionStateService().saveSessionBeans( pwmRequest );
                 final UserIdentity userIdentity = forgottenPasswordBean.getUserIdentity();
                 oAuthMachine.redirectUserToOAuthServer( pwmRequest, null, userIdentity, forgottenPasswordProfile.getIdentifier() );

+ 3 - 3
server/src/main/java/password/pwm/http/servlet/helpdesk/HelpdeskClientDataBean.java

@@ -32,7 +32,7 @@ import password.pwm.config.option.MessageSendMethod;
 import password.pwm.config.profile.HelpdeskProfile;
 import password.pwm.config.value.data.ActionConfiguration;
 import password.pwm.config.value.data.FormConfiguration;
-import password.pwm.http.servlet.peoplesearch.PeopleSearchClientConfigBean;
+import password.pwm.http.servlet.peoplesearch.bean.SearchAttributeBean;
 
 import java.io.Serializable;
 import java.util.ArrayList;
@@ -57,7 +57,7 @@ public class HelpdeskClientDataBean implements Serializable
     private Map<String, Collection<IdentityVerificationMethod>> verificationMethods;
     private List<FormInformation> verificationForm;
     private int maxAdvancedSearchAttributes;
-    private List<PeopleSearchClientConfigBean.SearchAttribute> advancedSearchAttributes;
+    private List<SearchAttributeBean> advancedSearchAttributes;
     private boolean enableAdvancedSearch;
 
 
@@ -138,7 +138,7 @@ public class HelpdeskClientDataBean implements Serializable
             builder.verificationForm( formInformations );
         }
         {
-            final List<PeopleSearchClientConfigBean.SearchAttribute> searchAttributes = PeopleSearchClientConfigBean.SearchAttribute.searchAttributesFromForm(
+            final List<SearchAttributeBean> searchAttributes = SearchAttributeBean.searchAttributesFromForm(
                     locale,
                     helpdeskProfile.readSettingAsForm( PwmSetting.HELPDESK_SEARCH_FORM ) );
 

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

@@ -163,7 +163,7 @@ public class OAuthConsumerServlet extends AbstractPwmServlet
 
         final OAuthState oauthState = oAuthRequestState.get().getOAuthState();
         final OAuthSettings oAuthSettings = makeOAuthSettings( pwmRequest, oauthState );
-        final OAuthMachine oAuthMachine = new OAuthMachine( oAuthSettings );
+        final OAuthMachine oAuthMachine = new OAuthMachine( pwmRequest.getSessionLabel(), oAuthSettings );
 
         // make sure request was initiated in users current session
         if ( !oAuthRequestState.get().isSessionMatch() )

+ 91 - 51
server/src/main/java/password/pwm/http/servlet/oauth/OAuthMachine.java

@@ -56,6 +56,7 @@ import java.net.URISyntaxException;
 import java.security.cert.X509Certificate;
 import java.time.Instant;
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.HashMap;
 import java.util.LinkedHashMap;
 import java.util.List;
@@ -66,10 +67,15 @@ public class OAuthMachine
 {
     private static final PwmLogger LOGGER = PwmLogger.forClass( OAuthMachine.class );
 
+    private final SessionLabel sessionLabel;
     private final OAuthSettings settings;
 
-    public OAuthMachine( final OAuthSettings settings )
+    public OAuthMachine(
+            final SessionLabel sessionLabel,
+            final OAuthSettings settings
+    )
     {
+        this.sessionLabel = sessionLabel;
         this.settings = settings;
     }
 
@@ -104,7 +110,7 @@ public class OAuthMachine
             throws PwmUnrecoverableException, IOException
     {
 
-        LOGGER.trace( pwmRequest, () -> "preparing to redirect user to oauth authentication service, setting nextUrl to " + nextUrl );
+        LOGGER.trace( sessionLabel, () -> "preparing to redirect user to oauth authentication service, setting nextUrl to " + nextUrl );
         pwmRequest.getPwmSession().getSessionStateBean().setOauthInProgress( true );
 
         final Configuration config = pwmRequest.getConfig();
@@ -138,7 +144,7 @@ public class OAuthMachine
         {
             pwmRequest.sendRedirect( redirectUrl );
             pwmRequest.getPwmSession().getSessionStateBean().setOauthInProgress( true );
-            LOGGER.debug( pwmRequest, () -> "redirecting user to oauth id server, url: " + redirectUrl );
+            LOGGER.debug( sessionLabel, () -> "redirecting user to oauth id server, url: " + redirectUrl );
         }
         catch ( PwmUnrecoverableException e )
         {
@@ -165,36 +171,34 @@ public class OAuthMachine
         requestParams.put( config.readAppProperty( AppProperty.HTTP_PARAM_OAUTH_GRANT_TYPE ), grantType );
         requestParams.put( config.readAppProperty( AppProperty.HTTP_PARAM_OAUTH_REDIRECT_URI ), redirectUri );
         requestParams.put( config.readAppProperty( AppProperty.HTTP_PARAM_OAUTH_CLIENT_ID ), clientID );
-        requestParams.put( "client_secret", settings.getSecret().getStringValue() );
+        requestParams.put( config.readAppProperty( AppProperty.HTTP_PARAM_OAUTH_CLIENT_SECRET ), settings.getSecret().getStringValue() );
 
         final PwmHttpClientResponse restResults = makeHttpRequest( pwmRequest, "oauth code resolver", settings, requestUrl, requestParams, null );
 
-        return resolveResultsFromResponseBody( pwmRequest.getSessionLabel(), pwmRequest.getConfig(), restResults.getBody() );
+        final OAuthResolveResults results = resolveResultsFromResponseBody( pwmRequest, restResults.getBody() );
+
+        LOGGER.trace( sessionLabel, () -> "successfully received access token" );
+
+        return results;
     }
 
     private OAuthResolveResults resolveResultsFromResponseBody(
-            final SessionLabel sessionLabel,
-            final Configuration config,
+            final PwmRequest pwmRequest,
             final String resolveResponseBodyStr
-            )
+    )
     {
-        final Map<String, String> resolveResultValues = JsonUtil.deserializeStringMap( resolveResponseBodyStr );
-
-        int expireSeconds = 0;
-        try
-        {
-            expireSeconds = Integer.parseInt( resolveResultValues.get( config.readAppProperty( AppProperty.HTTP_PARAM_OAUTH_EXPIRES ) ) );
-        }
-        catch ( Exception e )
-        {
-            LOGGER.warn( sessionLabel, "error parsing oauth expires value in code resolver response from server, error: " + e.getMessage() );
-        }
+        final Configuration config = pwmRequest.getConfig();
+        final String oauthExpiresParam = config.readAppProperty( AppProperty.HTTP_PARAM_OAUTH_EXPIRES );
+        final String oauthAccessTokenParam = config.readAppProperty( AppProperty.HTTP_PARAM_OAUTH_ACCESS_TOKEN );
+        final String refreshTokenParam = config.readAppProperty( AppProperty.HTTP_PARAM_OAUTH_REFRESH_TOKEN );
 
-        final String accessToken = readAttributeFromBodyMap( resolveResultValues, config.readAppProperty( AppProperty.HTTP_PARAM_OAUTH_ACCESS_TOKEN ) );
+        final long expireSeconds = JavaHelper.silentParseLong( readAttributeFromBodyMap( resolveResponseBodyStr, oauthExpiresParam ), 0 );
+        final String accessToken = readAttributeFromBodyMap( resolveResponseBodyStr, oauthAccessTokenParam );
+        final String refreshToken = readAttributeFromBodyMap( resolveResponseBodyStr, refreshTokenParam );
 
         return OAuthResolveResults.builder()
                 .accessToken( accessToken )
-                .refreshToken( resolveResultValues.get( config.readAppProperty( AppProperty.HTTP_PARAM_OAUTH_REFRESH_TOKEN ) ) )
+                .refreshToken( refreshToken  )
                 .expiresSeconds( expireSeconds )
                 .build();
     }
@@ -215,7 +219,7 @@ public class OAuthMachine
 
         final PwmHttpClientResponse restResults = makeHttpRequest( pwmRequest, "OAuth refresh resolver", settings, requestUrl, requestParams, null );
 
-        return resolveResultsFromResponseBody( pwmRequest.getSessionLabel(), pwmRequest.getConfig(), restResults.getBody() );
+        return resolveResultsFromResponseBody( pwmRequest, restResults.getBody() );
     }
 
     String makeOAuthGetUserInfoRequest(
@@ -224,31 +228,31 @@ public class OAuthMachine
     )
             throws PwmUnrecoverableException
     {
-        final Configuration config = pwmRequest.getConfig();
-        final String requestUrl = settings.getAttributesUrl();
-        final Map<String, String> requestParams = new HashMap<>();
-        requestParams.put( config.readAppProperty( AppProperty.HTTP_PARAM_OAUTH_ACCESS_TOKEN ), accessToken );
-        requestParams.put( config.readAppProperty( AppProperty.HTTP_PARAM_OAUTH_ATTRIBUTES ), settings.getDnAttributeName() );
-
-        final PwmHttpClientResponse restResults = makeHttpRequest( pwmRequest, "OAuth userinfo", settings, requestUrl, requestParams, accessToken );
+        final PwmHttpClientResponse restResults;
+        {
+            final Configuration config = pwmRequest.getConfig();
+            final String requestUrl = settings.getAttributesUrl();
+            final Map<String, String> requestParams = new HashMap<>();
+            requestParams.put( config.readAppProperty( AppProperty.HTTP_PARAM_OAUTH_ACCESS_TOKEN ), accessToken );
+            requestParams.put( config.readAppProperty( AppProperty.HTTP_PARAM_OAUTH_ATTRIBUTES ), settings.getDnAttributeName() );
+            restResults = makeHttpRequest( pwmRequest, "OAuth userinfo", settings, requestUrl, requestParams, accessToken );
+        }
 
         final String resultBody = restResults.getBody();
-        final Map<String, String> getAttributeResultValues = JsonUtil.deserializeStringMap( resultBody );
 
-        LOGGER.debug( pwmRequest, () -> "received attribute values from OAuth IdP for attributes: "
-                + StringUtil.collectionToString( getAttributeResultValues.keySet() ) );
+        LOGGER.trace( sessionLabel, () -> "received attribute values from OAuth IdP for attributes: " );
 
-        final String oauthSuppliedUsername = getAttributeResultValues.get( settings.getDnAttributeName() );
+        final String oauthSuppliedUsername = readAttributeFromBodyMap( resultBody, settings.getDnAttributeName() );
 
         if ( StringUtil.isEmpty( oauthSuppliedUsername ) )
         {
             final String msg = "OAuth server did not respond with an username value for configured attribute '" + settings.getDnAttributeName() + "'";
             final ErrorInformation errorInformation = new ErrorInformation( PwmError.ERROR_OAUTH_ERROR, msg );
-            LOGGER.error( pwmRequest, errorInformation );
+            LOGGER.error( sessionLabel, errorInformation );
             throw new PwmUnrecoverableException( errorInformation );
         }
 
-        LOGGER.debug( pwmRequest, () -> "received user login id value from OAuth server: " + oauthSuppliedUsername );
+        LOGGER.debug( sessionLabel, () -> "received user login id value from OAuth server: " + oauthSuppliedUsername );
 
         return oauthSuppliedUsername;
     }
@@ -352,7 +356,7 @@ public class OAuthMachine
             }
         }
 
-        LOGGER.trace( () -> "calculated oauth self end point URI as '" + redirectUri + "' using method " + debugSource );
+        LOGGER.trace( pwmRequest, () -> "calculated oauth self end point URI as '" + redirectUri + "' using method " + debugSource );
         return redirectUri;
     }
 
@@ -374,7 +378,7 @@ public class OAuthMachine
             return false;
         }
 
-        LOGGER.trace( pwmRequest, () -> "oauth access token has expired, attempting to refresh" );
+        LOGGER.trace( sessionLabel, () -> "oauth access token has expired, attempting to refresh" );
 
         try
         {
@@ -385,7 +389,7 @@ public class OAuthMachine
                 if ( resolveResults.getExpiresSeconds() > 0 )
                 {
                     final Instant accessTokenExpirationDate = Instant.ofEpochMilli( System.currentTimeMillis() + 1000 * resolveResults.getExpiresSeconds() );
-                    LOGGER.trace( pwmRequest, () -> "noted oauth access token expiration at timestamp " + JavaHelper.toIsoDate( accessTokenExpirationDate ) );
+                    LOGGER.trace( sessionLabel, () -> "noted oauth access token expiration at timestamp " + JavaHelper.toIsoDate( accessTokenExpirationDate ) );
                     loginInfoBean.setOauthExp( accessTokenExpirationDate );
                     loginInfoBean.setOauthRefToken( resolveResults.getRefreshToken() );
                     return false;
@@ -394,9 +398,9 @@ public class OAuthMachine
         }
         catch ( PwmUnrecoverableException e )
         {
-            LOGGER.error( pwmRequest, "error while processing oauth token refresh: " + e.getMessage() );
+            LOGGER.error( sessionLabel, "error while processing oauth token refresh: " + e.getMessage() );
         }
-        LOGGER.error( pwmRequest, "unable to refresh oauth token for user, unauthenticated session" );
+        LOGGER.error( sessionLabel, "unable to refresh oauth token for user, unauthenticated session" );
         pwmRequest.getPwmSession().unauthenticateUser( pwmRequest );
         return true;
     }
@@ -428,7 +432,7 @@ public class OAuthMachine
                 throw new IllegalStateException( "unexpected oAuthUseCase: " + oAuthUseCase );
         }
 
-        LOGGER.trace( pwmRequest, () -> "issuing oauth state id="
+        LOGGER.trace( sessionLabel, () -> "issuing oauth state id="
                 + oAuthState.getStateID() + " with the next destination URL set to " + oAuthState.getNextUrl() );
 
 
@@ -455,7 +459,7 @@ public class OAuthMachine
 
         final MacroMachine macroMachine = MacroMachine.forUser( pwmRequest, userIdentity );
         final String username = macroMachine.expandMacros( macroText );
-        LOGGER.debug( pwmRequest, () -> "calculated username value for user as: " + username );
+        LOGGER.debug( sessionLabel, () -> "calculated username value for user as: " + username );
 
         final String grantUrl = settings.getLoginURL();
         final String signUrl = grantUrl.replace( "/grant", "/sign" );
@@ -472,28 +476,64 @@ public class OAuthMachine
             requestPayload.put( "data", JsonUtil.serializeCollection( listWrapper ) );
         }
 
-        LOGGER.debug( pwmRequest, () -> "preparing to send username to OAuth /sign endpoint for future injection to /grant redirect" );
+        LOGGER.debug( sessionLabel, () -> "preparing to send username to OAuth /sign endpoint for future injection to /grant redirect" );
         final PwmHttpClientResponse restResults = makeHttpRequest( pwmRequest, "OAuth pre-inject username signing service", settings, signUrl, requestPayload, null );
 
         final String resultBody = restResults.getBody();
         final Map<String, String> resultBodyMap = JsonUtil.deserializeStringMap( resultBody );
         final String data = resultBodyMap.get( "data" );
-        LOGGER.debug( pwmRequest, () -> "oauth /sign endpoint returned signed username data: " + data );
+        LOGGER.debug( sessionLabel, () -> "oauth /sign endpoint returned signed username data: " + data );
         return data;
     }
 
-    private static String readAttributeFromBodyMap( final Map<String, String> bodyMap, final String attributeNames )
+    public String readAttributeFromBodyMap(
+            final String bodyString,
+            final String attributeNames
+    )
     {
-        final List<String> attributeValues = StringUtil.splitAndTrim( attributeNames, "," );
-
-        for ( final String attribute : attributeValues )
+        try
         {
-            final String value = bodyMap.get( attribute );
-            if ( !StringUtil.isEmpty( value ) )
+            final Map<String, Object> bodyMap = JsonUtil.deserializeMap( bodyString );
+            final List<String> attributeValues = StringUtil.splitAndTrim( attributeNames, "," );
+
+            for ( final String attribute : attributeValues )
             {
-                return value;
+                final Object objValue = bodyMap.get( attribute );
+                if ( objValue != null )
+                {
+                    if ( objValue instanceof Double && JavaHelper.doubleContainsLongValue( (Double) objValue ) )
+                    {
+                        final long longValue = ( ( Double ) objValue ).longValue();
+                        return Long.toString( longValue );
+                    }
+
+                    final Object singleObjValue;
+                    if ( objValue instanceof Collection )
+                    {
+                        if ( ( ( Collection ) objValue ).isEmpty() )
+                        {
+                            return null;
+                        }
+
+                        singleObjValue = ( ( Collection ) objValue ).iterator().next();
+                    }
+                    else
+                    {
+                        singleObjValue = objValue;
+                    }
+
+                    final String strValue = singleObjValue.toString();
+                    if ( !StringUtil.isEmpty( strValue ) )
+                    {
+                        return strValue;
+                    }
+                }
             }
         }
+        catch ( Exception e )
+        {
+            LOGGER.debug( sessionLabel, () -> "unexpected error parsing json response: " + e.getMessage() );
+        }
 
         return null;
     }

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

@@ -32,6 +32,6 @@ import java.io.Serializable;
 class OAuthResolveResults implements Serializable
 {
     private String accessToken;
-    private int expiresSeconds;
+    private long expiresSeconds;
     private String refreshToken;
 }

+ 15 - 13
server/src/main/java/password/pwm/http/servlet/peoplesearch/PeopleSearchConfiguration.java

@@ -30,10 +30,9 @@ import password.pwm.config.Configuration;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.profile.LdapProfile;
 import password.pwm.config.value.data.FormConfiguration;
-import password.pwm.config.value.data.UserPermission;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.http.PwmRequest;
-import password.pwm.ldap.LdapPermissionTester;
+import password.pwm.util.java.StringUtil;
 import password.pwm.util.java.TimeDuration;
 
 import java.util.Collections;
@@ -65,7 +64,7 @@ public class PeopleSearchConfiguration
         return ldapProfile.readSettingAsString( PwmSetting.LDAP_ATTRIBUTE_PHOTO_URL_OVERRIDE );
     }
 
-    boolean isPhotosEnabled( final UserIdentity actor, final SessionLabel sessionLabel )
+    public boolean isPhotosEnabled( final UserIdentity actor, final SessionLabel sessionLabel )
             throws PwmUnrecoverableException
     {
         if ( actor == null )
@@ -73,8 +72,11 @@ public class PeopleSearchConfiguration
             return false;
         }
 
-        final List<UserPermission> permissions =  pwmApplication.getConfig().readSettingAsUserPermission( PwmSetting.PEOPLE_SEARCH_PHOTO_QUERY_FILTER );
-        return LdapPermissionTester.testUserPermissions( pwmApplication, sessionLabel, actor, permissions );
+        final boolean settingEnabled = pwmApplication.getConfig().readSettingAsBoolean( PwmSetting.PEOPLE_SEARCH_ENABLE_PHOTO );
+        final String photoAttribute = getPhotoAttribute( actor );
+        final String photoUrl = getPhotoUrlOverride( actor );
+        return settingEnabled
+                && ( !StringUtil.isEmpty( photoAttribute ) || !StringUtil.isEmpty( photoUrl ) );
     }
 
     public boolean isOrgChartEnabled()
@@ -107,32 +109,32 @@ public class PeopleSearchConfiguration
         return ldapProfile.readSettingAsString( PwmSetting.LDAP_ATTRIBUTE_ORGCHART_WORKFORCEID );
     }
 
-    boolean isOrgChartShowChildCount( )
+    public boolean isOrgChartShowChildCount()
     {
         return Boolean.parseBoolean( pwmRequest.getConfig().readAppProperty( AppProperty.PEOPLESEARCH_ORGCHART_ENABLE_CHILD_COUNT ) );
     }
 
-    int getOrgChartMaxParents( )
+    public int getOrgChartMaxParents()
     {
         return Integer.parseInt( pwmRequest.getConfig().readAppProperty( AppProperty.PEOPLESEARCH_ORGCHART_MAX_PARENTS ) );
     }
 
-    boolean isEnableExportCsv( )
+    public boolean isEnableExportCsv()
     {
         return pwmApplication.getConfig().readSettingAsBoolean( PwmSetting.PEOPLE_SEARCH_ENABLE_EXPORT );
     }
 
-    int getExportCsvMaxDepth( )
+    public int getExportCsvMaxDepth()
     {
         return Integer.parseInt( pwmRequest.getConfig().readAppProperty( AppProperty.PEOPLESEARCH_EXPORT_CSV_MAX_DEPTH ) );
     }
 
-    boolean isEnableMailtoLinks( )
+    public boolean isEnableMailtoLinks()
     {
         return pwmApplication.getConfig().readSettingAsBoolean( PwmSetting.PEOPLE_SEARCH_ENABLE_TEAM_MAILTO );
     }
 
-    int getMailtoLinksMaxDepth( )
+    public int getMailtoLinksMaxDepth( )
     {
         return Integer.parseInt( pwmRequest.getConfig().readAppProperty( AppProperty.PEOPLESEARCH_EXPORT_CSV_MAX_DEPTH ) );
     }
@@ -153,7 +155,7 @@ public class PeopleSearchConfiguration
         return Integer.parseInt( pwmRequest.getConfig().readAppProperty( AppProperty.PEOPLESEARCH_EXPORT_CSV_MAX_ITEMS ) );
     }
 
-    List<FormConfiguration> getSearchForm()
+    public List<FormConfiguration> getSearchForm()
     {
         return pwmRequest.getConfig().readSettingAsForm( PwmSetting.PEOPLE_SEARCH_SEARCH_FORM );
     }
@@ -175,7 +177,7 @@ public class PeopleSearchConfiguration
         return ( int ) pwmRequest.getConfig().readSettingAsLong( PwmSetting.PEOPLE_SEARCH_RESULT_LIMIT );
     }
 
-    boolean isEnablePrinting()
+    public boolean isEnablePrinting()
     {
         return pwmRequest.getConfig().readSettingAsBoolean( PwmSetting.PEOPLE_SEARCH_ENABLE_PRINTING );
     }

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

@@ -35,6 +35,7 @@ import password.pwm.PwmConstants;
 import password.pwm.bean.UserIdentity;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.value.data.FormConfiguration;
+import password.pwm.config.value.data.UserPermission;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmOperationalException;
@@ -42,6 +43,13 @@ import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.http.PwmRequest;
 import password.pwm.http.PwmURL;
 import password.pwm.http.servlet.helpdesk.HelpdeskServletUtil;
+import password.pwm.http.servlet.peoplesearch.bean.AttributeDetailBean;
+import password.pwm.http.servlet.peoplesearch.bean.LinkReferenceBean;
+import password.pwm.http.servlet.peoplesearch.bean.OrgChartDataBean;
+import password.pwm.http.servlet.peoplesearch.bean.OrgChartReferenceBean;
+import password.pwm.http.servlet.peoplesearch.bean.SearchResultBean;
+import password.pwm.http.servlet.peoplesearch.bean.UserDetailBean;
+import password.pwm.http.servlet.peoplesearch.bean.UserReferenceBean;
 import password.pwm.i18n.Display;
 import password.pwm.ldap.LdapOperationsHelper;
 import password.pwm.ldap.LdapPermissionTester;
@@ -476,10 +484,19 @@ class PeopleSearchDataReader
         final boolean enabled = peopleSearchConfiguration.isPhotosEnabled( pwmRequest.getUserInfoIfLoggedIn(), pwmRequest.getSessionLabel() );
         if ( !enabled )
         {
-            LOGGER.debug( pwmRequest, () -> "detailed user data lookup for " + userIdentity.toString() + ", failed photo query filter, denying photo view" );
             return null;
         }
 
+        {
+            final List<UserPermission> permissions = pwmApplication.getConfig().readSettingAsUserPermission( PwmSetting.PEOPLE_SEARCH_PHOTO_QUERY_FILTER );
+            final boolean hasPermission = LdapPermissionTester.testUserPermissions( pwmApplication, pwmRequest.getSessionLabel(), userIdentity, permissions );
+            if ( !hasPermission )
+            {
+                LOGGER.debug( pwmRequest, () -> "user " + userIdentity.toString() + " failed photo query filter, denying photo view" );
+                return null;
+            }
+        }
+
         final String overrideURL = peopleSearchConfiguration.getPhotoUrlOverride( userIdentity );
         try
         {

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

@@ -40,6 +40,10 @@ import password.pwm.http.PwmHttpRequestWrapper;
 import password.pwm.http.PwmRequest;
 import password.pwm.http.PwmRequestFlag;
 import password.pwm.http.servlet.ControlledPwmServlet;
+import password.pwm.http.servlet.peoplesearch.bean.OrgChartDataBean;
+import password.pwm.http.servlet.peoplesearch.bean.PeopleSearchClientConfigBean;
+import password.pwm.http.servlet.peoplesearch.bean.SearchResultBean;
+import password.pwm.http.servlet.peoplesearch.bean.UserDetailBean;
 import password.pwm.ldap.PhotoDataBean;
 import password.pwm.svc.stats.Statistic;
 import password.pwm.svc.stats.StatisticsManager;

+ 0 - 87
server/src/main/java/password/pwm/http/servlet/peoplesearch/UserDetailBean.java

@@ -1,87 +0,0 @@
-/*
- * Password Management Servlets (PWM)
- * http://www.pwm-project.org
- *
- * Copyright (c) 2006-2009 Novell, Inc.
- * Copyright (c) 2009-2018 The PWM Project
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
- */
-
-package password.pwm.http.servlet.peoplesearch;
-
-import java.io.Serializable;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
-
-class UserDetailBean implements Serializable
-{
-    private List<String> displayNames;
-    private String userKey;
-    private Map<String, AttributeDetailBean> detail;
-    private String photoURL;
-    private List<LinkReferenceBean> links = Collections.emptyList();
-
-    public List<String> getDisplayNames( )
-    {
-        return displayNames;
-    }
-
-    public void setDisplayNames( final List<String> displayNames )
-    {
-        this.displayNames = displayNames;
-    }
-
-    public String getUserKey( )
-    {
-        return userKey;
-    }
-
-    public void setUserKey( final String userKey )
-    {
-        this.userKey = userKey;
-    }
-
-    public Map<String, AttributeDetailBean> getDetail( )
-    {
-        return detail;
-    }
-
-    public void setDetail( final Map<String, AttributeDetailBean> detail )
-    {
-        this.detail = detail;
-    }
-
-    public String getPhotoURL( )
-    {
-        return photoURL;
-    }
-
-    public void setPhotoURL( final String photoURL )
-    {
-        this.photoURL = photoURL;
-    }
-
-    public List<LinkReferenceBean> getLinks( )
-    {
-        return links;
-    }
-
-    public void setLinks( final List<LinkReferenceBean> links )
-    {
-        this.links = links;
-    }
-}

+ 3 - 63
server/src/main/java/password/pwm/http/servlet/peoplesearch/AttributeDetailBean.java → server/src/main/java/password/pwm/http/servlet/peoplesearch/bean/AttributeDetailBean.java

@@ -20,14 +20,16 @@
  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
  */
 
-package password.pwm.http.servlet.peoplesearch;
+package password.pwm.http.servlet.peoplesearch.bean;
 
+import lombok.Data;
 import password.pwm.config.value.data.FormConfiguration;
 
 import java.io.Serializable;
 import java.util.Collection;
 import java.util.List;
 
+@Data
 public class AttributeDetailBean implements Serializable
 {
     private String name;
@@ -36,66 +38,4 @@ public class AttributeDetailBean implements Serializable
     private List<String> values;
     private Collection<UserReferenceBean> userReferences;
     private boolean searchable;
-
-    public String getName( )
-    {
-        return name;
-    }
-
-    public void setName( final String name )
-    {
-        this.name = name;
-    }
-
-    public String getLabel( )
-    {
-        return label;
-    }
-
-    public void setLabel( final String label )
-    {
-        this.label = label;
-    }
-
-    public FormConfiguration.Type getType( )
-    {
-        return type;
-    }
-
-    public void setType( final FormConfiguration.Type type )
-    {
-        this.type = type;
-    }
-
-    public List<String> getValues( )
-    {
-        return values;
-    }
-
-    public void setValues( final List<String> values )
-    {
-        this.values = values;
-    }
-
-    public Collection<UserReferenceBean> getUserReferences( )
-    {
-        return userReferences;
-    }
-
-    public void setUserReferences( final Collection<UserReferenceBean> userReferences )
-    {
-        this.userReferences = userReferences;
-    }
-
-    public boolean isSearchable( )
-    {
-        return searchable;
-    }
-
-    public void setSearchable( final boolean searchable )
-    {
-        this.searchable = searchable;
-    }
-
-
 }

+ 5 - 22
server/src/main/java/password/pwm/http/servlet/peoplesearch/LinkReferenceBean.java → server/src/main/java/password/pwm/http/servlet/peoplesearch/bean/LinkReferenceBean.java

@@ -20,32 +20,15 @@
  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
  */
 
-package password.pwm.http.servlet.peoplesearch;
+package password.pwm.http.servlet.peoplesearch.bean;
+
+import lombok.Data;
 
 import java.io.Serializable;
 
-class LinkReferenceBean implements Serializable
+@Data
+public class LinkReferenceBean implements Serializable
 {
     private String name;
     private String link;
-
-    public String getName( )
-    {
-        return name;
-    }
-
-    public void setName( final String name )
-    {
-        this.name = name;
-    }
-
-    public String getLink( )
-    {
-        return link;
-    }
-
-    public void setLink( final String link )
-    {
-        this.link = link;
-    }
 }

+ 4 - 6
server/src/main/java/password/pwm/http/servlet/peoplesearch/OrgChartDataBean.java → server/src/main/java/password/pwm/http/servlet/peoplesearch/bean/OrgChartDataBean.java

@@ -20,18 +20,16 @@
  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
  */
 
-package password.pwm.http.servlet.peoplesearch;
+package password.pwm.http.servlet.peoplesearch.bean;
 
-import lombok.Getter;
-import lombok.Setter;
+import lombok.Data;
 
 import java.io.Serializable;
 import java.util.Collections;
 import java.util.List;
 
-@Getter
-@Setter
-class OrgChartDataBean implements Serializable
+@Data
+public class OrgChartDataBean implements Serializable
 {
     private OrgChartReferenceBean parent;
     private OrgChartReferenceBean self;

+ 4 - 6
server/src/main/java/password/pwm/http/servlet/peoplesearch/OrgChartReferenceBean.java → server/src/main/java/password/pwm/http/servlet/peoplesearch/bean/OrgChartReferenceBean.java

@@ -20,18 +20,16 @@
  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
  */
 
-package password.pwm.http.servlet.peoplesearch;
+package password.pwm.http.servlet.peoplesearch.bean;
 
-import lombok.Getter;
-import lombok.Setter;
+import lombok.Data;
 
 import java.io.Serializable;
 import java.util.ArrayList;
 import java.util.List;
 
-@Getter
-@Setter
-class OrgChartReferenceBean implements Serializable
+@Data
+public class OrgChartReferenceBean implements Serializable
 {
     public String userKey;
     public List<String> displayNames = new ArrayList<>();

+ 8 - 43
server/src/main/java/password/pwm/http/servlet/peoplesearch/PeopleSearchClientConfigBean.java → server/src/main/java/password/pwm/http/servlet/peoplesearch/bean/PeopleSearchClientConfigBean.java

@@ -20,7 +20,7 @@
  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
  */
 
-package password.pwm.http.servlet.peoplesearch;
+package password.pwm.http.servlet.peoplesearch.bean;
 
 import lombok.Builder;
 import lombok.Value;
@@ -31,10 +31,9 @@ import password.pwm.config.PwmSetting;
 import password.pwm.config.value.data.FormConfiguration;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.http.PwmRequest;
+import password.pwm.http.servlet.peoplesearch.PeopleSearchConfiguration;
 
 import java.io.Serializable;
-import java.util.ArrayList;
-import java.util.Collections;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Locale;
@@ -51,7 +50,7 @@ public class PeopleSearchClientConfigBean implements Serializable
     private boolean orgChartShowChildCount;
     private int orgChartMaxParents;
     private int maxAdvancedSearchAttributes;
-    private List<SearchAttribute> advancedSearchAttributes;
+    private List<SearchAttributeBean> advancedSearchAttributes;
     private boolean enableOrgChartPrinting;
     private boolean enableExport;
     private int exportMaxDepth;
@@ -59,43 +58,7 @@ public class PeopleSearchClientConfigBean implements Serializable
     private int mailtoLinkMaxDepth;
 
 
-
-    @Value
-    @Builder
-    public static class SearchAttribute implements Serializable
-    {
-        private String attribute;
-        private String label;
-        private FormConfiguration.Type type;
-        private Map<String, String> options;
-
-        public static List<SearchAttribute> searchAttributesFromForm(
-                final Locale locale,
-                final List<FormConfiguration> formConfigurations
-        )
-        {
-            final List<SearchAttribute> returnList = new ArrayList<>( );
-            for ( final FormConfiguration formConfiguration : formConfigurations )
-            {
-                final String attribute = formConfiguration.getName();
-                final String label = formConfiguration.getLabel( locale );
-
-                final SearchAttribute searchAttribute = SearchAttribute.builder()
-                        .attribute( attribute )
-                        .type( formConfiguration.getType() )
-                        .label( label )
-                        .options( formConfiguration.getSelectOptions() )
-                        .build();
-
-                returnList.add( searchAttribute );
-            }
-
-            return Collections.unmodifiableList( returnList );
-        }
-    }
-
-
-    static PeopleSearchClientConfigBean fromConfig(
+    public static PeopleSearchClientConfigBean fromConfig(
             final PwmRequest pwmRequest,
             final PeopleSearchConfiguration peopleSearchConfiguration,
             final UserIdentity userIdentity
@@ -115,7 +78,9 @@ public class PeopleSearchClientConfigBean implements Serializable
         }
 
 
-        final List<SearchAttribute> searchAttributes = SearchAttribute.searchAttributesFromForm( locale, peopleSearchConfiguration.getSearchForm() );
+        final List<SearchAttributeBean> searchAttributeBeans = SearchAttributeBean.searchAttributesFromForm(
+                locale,
+                peopleSearchConfiguration.getSearchForm() );
 
         return PeopleSearchClientConfigBean.builder()
                 .searchColumns( searchColumns )
@@ -128,7 +93,7 @@ public class PeopleSearchClientConfigBean implements Serializable
                 .enableOrgChartPrinting( peopleSearchConfiguration.isEnablePrinting() )
 
                 .maxAdvancedSearchAttributes( 3 )
-                .advancedSearchAttributes( searchAttributes )
+                .advancedSearchAttributes( searchAttributeBeans )
 
                 .mailtoLinkMaxDepth( peopleSearchConfiguration.getMailtoLinksMaxDepth() )
                 .enableMailtoLinks( peopleSearchConfiguration.isEnableMailtoLinks() )

+ 68 - 0
server/src/main/java/password/pwm/http/servlet/peoplesearch/bean/SearchAttributeBean.java

@@ -0,0 +1,68 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2018 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+package password.pwm.http.servlet.peoplesearch.bean;
+
+import lombok.Builder;
+import lombok.Value;
+import password.pwm.config.value.data.FormConfiguration;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+@Value
+@Builder
+public class SearchAttributeBean implements Serializable
+{
+    private String attribute;
+    private String label;
+    private FormConfiguration.Type type;
+    private Map<String, String> options;
+
+    public static List<SearchAttributeBean> searchAttributesFromForm(
+            final Locale locale,
+            final List<FormConfiguration> formConfigurations
+    )
+    {
+        final List<SearchAttributeBean> returnList = new ArrayList<>( );
+        for ( final FormConfiguration formConfiguration : formConfigurations )
+        {
+            final String attribute = formConfiguration.getName();
+            final String label = formConfiguration.getLabel( locale );
+
+            final SearchAttributeBean searchAttribute = SearchAttributeBean.builder()
+                    .attribute( attribute )
+                    .type( formConfiguration.getType() )
+                    .label( label )
+                    .options( formConfiguration.getSelectOptions() )
+                    .build();
+
+            returnList.add( searchAttribute );
+        }
+
+        return Collections.unmodifiableList( returnList );
+    }
+}

+ 5 - 2
server/src/main/java/password/pwm/http/servlet/peoplesearch/SearchResultBean.java → server/src/main/java/password/pwm/http/servlet/peoplesearch/bean/SearchResultBean.java

@@ -20,7 +20,9 @@
  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
  */
 
-package password.pwm.http.servlet.peoplesearch;
+package password.pwm.http.servlet.peoplesearch.bean;
+
+import lombok.Data;
 
 import lombok.Builder;
 import lombok.Value;
@@ -31,7 +33,8 @@ import java.util.Map;
 
 @Value
 @Builder( toBuilder = true )
-class SearchResultBean implements Serializable
+@Data
+public class SearchResultBean implements Serializable
 {
     private List<Map<String, Object>> searchResults;
     private boolean sizeExceeded;

+ 39 - 0
server/src/main/java/password/pwm/http/servlet/peoplesearch/bean/UserDetailBean.java

@@ -0,0 +1,39 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2018 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+package password.pwm.http.servlet.peoplesearch.bean;
+
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.List;
+import java.util.Map;
+
+@Data
+public class UserDetailBean implements Serializable
+{
+    private List<String> displayNames;
+    private String userKey;
+    private Map<String, AttributeDetailBean> detail;
+    private String photoURL;
+    private List<LinkReferenceBean> links;
+}

+ 5 - 22
server/src/main/java/password/pwm/http/servlet/peoplesearch/UserReferenceBean.java → server/src/main/java/password/pwm/http/servlet/peoplesearch/bean/UserReferenceBean.java

@@ -20,32 +20,15 @@
  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
  */
 
-package password.pwm.http.servlet.peoplesearch;
+package password.pwm.http.servlet.peoplesearch.bean;
+
+import lombok.Data;
 
 import java.io.Serializable;
 
-class UserReferenceBean implements Serializable
+@Data
+public class UserReferenceBean implements Serializable
 {
     private String userKey;
     private String displayName;
-
-    public String getUserKey( )
-    {
-        return userKey;
-    }
-
-    public void setUserKey( final String userKey )
-    {
-        this.userKey = userKey;
-    }
-
-    public String getDisplayName( )
-    {
-        return displayName;
-    }
-
-    public void setDisplayName( final String displayName )
-    {
-        this.displayName = displayName;
-    }
 }

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

@@ -25,6 +25,7 @@ package password.pwm.http.tag.value;
 import com.novell.ldapchai.exception.ChaiUnavailableException;
 import password.pwm.AppProperty;
 import password.pwm.Permission;
+import password.pwm.PwmApplication;
 import password.pwm.PwmApplicationMode;
 import password.pwm.PwmConstants;
 import password.pwm.config.PwmSetting;
@@ -39,7 +40,6 @@ import password.pwm.util.macro.MacroMachine;
 
 import javax.servlet.jsp.JspPage;
 import javax.servlet.jsp.PageContext;
-import java.awt.ComponentOrientation;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashSet;
@@ -238,7 +238,8 @@ public enum PwmValue
     static class ClientETag implements ValueOutput
     {
         @Override
-        public String valueOutput( final PwmRequest pwmRequest, final PageContext pageContext ) throws ChaiUnavailableException, PwmUnrecoverableException
+        public String valueOutput( final PwmRequest pwmRequest, final PageContext pageContext )
+                throws PwmUnrecoverableException
         {
             return ClientApiServlet.makeClientEtag( pwmRequest );
         }
@@ -247,7 +248,7 @@ public enum PwmValue
     static class LocaleCodeOutput implements ValueOutput
     {
         @Override
-        public String valueOutput( final PwmRequest pwmRequest, final PageContext pageContext ) throws ChaiUnavailableException, PwmUnrecoverableException
+        public String valueOutput( final PwmRequest pwmRequest, final PageContext pageContext )
         {
             return pwmRequest.getLocale().toLanguageTag();
         }
@@ -256,18 +257,19 @@ public enum PwmValue
     static class LocaleDirOutput implements ValueOutput
     {
         @Override
-        public String valueOutput( final PwmRequest pwmRequest, final PageContext pageContext ) throws ChaiUnavailableException, PwmUnrecoverableException
+        public String valueOutput( final PwmRequest pwmRequest, final PageContext pageContext )
         {
             final Locale locale = pwmRequest.getLocale();
-            final ComponentOrientation orient = ComponentOrientation.getOrientation( locale );
-            return orient != null && !orient.isLeftToRight() ? "rtl" : "ltr";
+            final PwmApplication pwmApplication = pwmRequest.getPwmApplication();
+            final LocaleHelper.TextDirection textDirection = LocaleHelper.textDirectionForLocale( pwmApplication, locale );
+            return textDirection.name();
         }
     }
 
     static class LocaleNameOutput implements ValueOutput
     {
         @Override
-        public String valueOutput( final PwmRequest pwmRequest, final PageContext pageContext ) throws ChaiUnavailableException, PwmUnrecoverableException
+        public String valueOutput( final PwmRequest pwmRequest, final PageContext pageContext )
         {
             final Locale locale = pwmRequest.getLocale();
             return locale.getDisplayName( locale );
@@ -277,7 +279,7 @@ public enum PwmValue
     static class LocaleFlagFileOutput implements ValueOutput
     {
         @Override
-        public String valueOutput( final PwmRequest pwmRequest, final PageContext pageContext ) throws ChaiUnavailableException, PwmUnrecoverableException
+        public String valueOutput( final PwmRequest pwmRequest, final PageContext pageContext )
         {
             final String flagFileName = pwmRequest.getConfig().getKnownLocaleFlagMap().get( pwmRequest.getLocale() );
             return flagFileName == null ? "" : flagFileName;
@@ -287,7 +289,8 @@ public enum PwmValue
     static class InactiveTimeRemainingOutput implements ValueOutput
     {
         @Override
-        public String valueOutput( final PwmRequest pwmRequest, final PageContext pageContext ) throws ChaiUnavailableException, PwmUnrecoverableException
+        public String valueOutput( final PwmRequest pwmRequest, final PageContext pageContext )
+                throws PwmUnrecoverableException
         {
             return IdleTimeoutCalculator.idleTimeoutForRequest( pwmRequest ).asLongString();
         }

+ 38 - 0
server/src/main/java/password/pwm/svc/event/AuditField.java

@@ -0,0 +1,38 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2018 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+package password.pwm.svc.event;
+
+public enum AuditField
+{
+    type,
+    eventCode,
+    timestamp,
+    message,
+    narrative,
+    perpetratorID,
+    perpetratorDN,
+    sourceAddress,
+    sourceHost,
+    targetID,
+    targetDN,
+}

+ 36 - 0
server/src/main/java/password/pwm/svc/event/AuditFormatter.java

@@ -0,0 +1,36 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2018 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+package password.pwm.svc.event;
+
+import password.pwm.PwmApplication;
+import password.pwm.error.PwmUnrecoverableException;
+
+public interface AuditFormatter
+{
+    String convertAuditRecordToMessage(
+            PwmApplication pwmApplication,
+            AuditRecord auditRecord
+    )
+            throws PwmUnrecoverableException;
+
+}

+ 197 - 0
server/src/main/java/password/pwm/svc/event/CEFAuditFormatter.java

@@ -0,0 +1,197 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2018 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+package password.pwm.svc.event;
+
+import password.pwm.AppProperty;
+import password.pwm.PwmApplication;
+import password.pwm.PwmConstants;
+import password.pwm.bean.SessionLabel;
+import password.pwm.config.Configuration;
+import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.util.i18n.LocaleHelper;
+import password.pwm.util.java.JavaHelper;
+import password.pwm.util.java.JsonUtil;
+import password.pwm.util.java.StringUtil;
+import password.pwm.util.macro.MacroMachine;
+
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Optional;
+
+public class CEFAuditFormatter implements AuditFormatter
+{
+    private static final String CEF_EXTENSION_SEPARATOR = "|";
+    private static final Map<String, String> CEF_VALUE_ESCAPES;
+
+    static
+    {
+        final Map<String, String> map = new LinkedHashMap<>( );
+        map.put( "\\", "\\\\" );
+        map.put( "=", "\\=" );
+        map.put( "|", "\\|" );
+        map.put( "\n", "\\n" );
+        CEF_VALUE_ESCAPES = Collections.unmodifiableMap( map );
+    }
+
+    enum CEFAuditField
+    {
+        cat( AuditField.type ),
+        act( AuditField.eventCode ),
+        rt( AuditField.timestamp ),
+        msg( AuditField.message ),
+        reason( AuditField.narrative ),
+        suid( AuditField.perpetratorID ),
+        suser( AuditField.perpetratorDN ),
+        src( AuditField.sourceAddress ),
+        srchost( AuditField.sourceHost ),
+        duid( AuditField.targetID ),
+        duser( AuditField.targetDN ),
+        dvchost( null ),
+        dtz( null ),;
+
+        private final AuditField auditField;
+
+        CEFAuditField( final AuditField auditField )
+        {
+            this.auditField = auditField;
+        }
+
+        public AuditField getAuditField()
+        {
+            return auditField;
+        }
+    }
+
+    @Override
+    public String convertAuditRecordToMessage(
+            final PwmApplication pwmApplication,
+            final AuditRecord auditRecord
+    )
+            throws PwmUnrecoverableException
+    {
+        final Configuration configuration = pwmApplication.getConfig();
+        final String cefTimezone = configuration.readAppProperty( AppProperty.AUDIT_SYSLOG_CEF_TIMEZONE );
+
+        final Map<String, Object> auditRecordMap = JsonUtil.deserializeMap( JsonUtil.serialize( auditRecord ) );
+
+        final String headerSeverity = configuration.readAppProperty( AppProperty.AUDIT_SYSLOG_CEF_HEADER_SEVERITY );
+        final String headerProduct = configuration.readAppProperty( AppProperty.AUDIT_SYSLOG_CEF_HEADER_PRODUCT );
+        final String headerVendor = configuration.readAppProperty( AppProperty.AUDIT_SYSLOG_CEF_HEADER_VENDOR );
+        final Optional<String> srcHost = JavaHelper.deriveLocalServerHostname( configuration );
+
+        final MacroMachine macroMachine = MacroMachine.forNonUserSpecific( pwmApplication, SessionLabel.SYSTEM_LABEL );
+
+        final String cefFieldName = LocaleHelper.getLocalizedMessage(
+                PwmConstants.DEFAULT_LOCALE,
+                auditRecord.getEventCode().getMessage(),
+                configuration
+        );
+
+        final StringBuilder cefOutput = new StringBuilder(  );
+
+        // cef header
+        {
+            // cef declaration:version prefix
+            cefOutput.append( "CEF:0" );
+
+            // Device Vendor
+            cefOutput.append( CEFAuditFormatter.CEF_EXTENSION_SEPARATOR );
+            cefOutput.append( macroMachine.expandMacros( headerVendor ) );
+
+            // Device Product
+            cefOutput.append( CEFAuditFormatter.CEF_EXTENSION_SEPARATOR );
+            cefOutput.append( macroMachine.expandMacros( headerProduct ) );
+
+            // Device Version
+            cefOutput.append( CEFAuditFormatter.CEF_EXTENSION_SEPARATOR );
+            cefOutput.append( PwmConstants.SERVLET_VERSION );
+
+            // Device Event Class ID
+            cefOutput.append( CEFAuditFormatter.CEF_EXTENSION_SEPARATOR );
+            cefOutput.append( auditRecord.getEventCode() );
+
+            // field name
+            cefOutput.append( CEFAuditFormatter.CEF_EXTENSION_SEPARATOR );
+            cefOutput.append( cefFieldName );
+
+            // severity
+            cefOutput.append( CEFAuditFormatter.CEF_EXTENSION_SEPARATOR );
+            cefOutput.append( macroMachine.expandMacros( headerSeverity ) );
+        }
+
+        cefOutput.append( CEFAuditFormatter.CEF_EXTENSION_SEPARATOR );
+
+        srcHost.ifPresent( s -> appendCefValue( CEFAuditField.dvchost.name(), s, cefOutput ) );
+
+        if ( StringUtil.isEmpty( cefTimezone ) )
+        {
+            appendCefValue( CEFAuditField.dtz.name(), cefTimezone, cefOutput );
+        }
+
+        for ( final CEFAuditField cefAuditField : CEFAuditField.values() )
+        {
+            if ( cefAuditField.getAuditField() != null )
+            {
+                final String auditFieldName = cefAuditField.getAuditField().name();
+                final Object value = auditRecordMap.get( auditFieldName );
+                if ( value != null )
+                {
+                    final String valueString = value.toString();
+                    appendCefValue( auditFieldName, valueString, cefOutput );
+                }
+            }
+        }
+
+        final int cefLength = CEFAuditFormatter.CEF_EXTENSION_SEPARATOR.length();
+        if ( cefOutput.substring( cefOutput.length() - cefLength ).equals( CEFAuditFormatter.CEF_EXTENSION_SEPARATOR ) )
+        {
+            cefOutput.replace( cefOutput.length() - cefLength, cefOutput.length(), "" );
+        }
+
+        return cefOutput.toString();
+    }
+
+    private static void appendCefValue( final String name, final String value, final StringBuilder cefOutput )
+    {
+        if ( !StringUtil.isEmpty( value ) && !StringUtil.isEmpty( name ) )
+        {
+            cefOutput.append( " " );
+            cefOutput.append( name );
+            cefOutput.append( "=" );
+            cefOutput.append( escapeCEFValue( value ) );
+        }
+    }
+
+    private static String escapeCEFValue( final String value )
+    {
+        String replacedValue = value;
+        for ( final Map.Entry<String, String> entry : CEFAuditFormatter.CEF_VALUE_ESCAPES.entrySet() )
+        {
+            final String pattern = entry.getKey();
+            final String replacement = entry.getValue();
+            replacedValue = replacedValue.replace( pattern, replacement );
+        }
+        return replacedValue;
+    }
+}

+ 95 - 0
server/src/main/java/password/pwm/svc/event/JsonAuditFormatter.java

@@ -0,0 +1,95 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2018 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+package password.pwm.svc.event;
+
+import password.pwm.AppProperty;
+import password.pwm.PwmApplication;
+import password.pwm.PwmConstants;
+import password.pwm.config.Configuration;
+import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.util.java.JsonUtil;
+
+public class JsonAuditFormatter implements AuditFormatter
+{
+    @Override
+    public String convertAuditRecordToMessage(
+            final PwmApplication pwmApplication,
+            final AuditRecord auditRecord
+    )
+            throws PwmUnrecoverableException
+    {
+        final Configuration configuration = pwmApplication.getConfig();
+        final int maxLength = Integer.parseInt( configuration.readAppProperty( AppProperty.AUDIT_SYSLOG_MAX_MESSAGE_LENGTH ) );
+        String jsonValue = "";
+        final StringBuilder message = new StringBuilder();
+        message.append( PwmConstants.PWM_APP_NAME );
+        message.append( " " );
+
+        jsonValue = JsonUtil.serialize( auditRecord );
+
+        if ( message.length() + jsonValue.length() <= maxLength )
+        {
+            message.append( jsonValue );
+        }
+        else
+        {
+            final AuditRecord inputRecord = JsonUtil.cloneUsingJson( auditRecord, auditRecord.getClass() );
+            inputRecord.message = inputRecord.message == null ? "" : inputRecord.message;
+            inputRecord.narrative = inputRecord.narrative == null ? "" : inputRecord.narrative;
+
+            final String truncateMessage = configuration.readAppProperty( AppProperty.AUDIT_SYSLOG_TRUNCATE_MESSAGE );
+            final AuditRecord copiedRecord = JsonUtil.cloneUsingJson( auditRecord, auditRecord.getClass() );
+            copiedRecord.message = "";
+            copiedRecord.narrative = "";
+            final int shortenedMessageLength = message.length()
+                    + JsonUtil.serialize( copiedRecord ).length()
+                    + truncateMessage.length();
+            final int maxMessageAndNarrativeLength = maxLength - ( shortenedMessageLength + ( truncateMessage.length() * 2 ) );
+            int maxMessageLength = inputRecord.getMessage().length();
+            int maxNarrativeLength = inputRecord.getNarrative().length();
+
+            {
+                int top = maxMessageAndNarrativeLength;
+                while ( maxMessageLength + maxNarrativeLength > maxMessageAndNarrativeLength )
+                {
+                    top--;
+                    maxMessageLength = Math.min( maxMessageLength, top );
+                    maxNarrativeLength = Math.min( maxNarrativeLength, top );
+                }
+            }
+
+            copiedRecord.message = inputRecord.getMessage().length() > maxMessageLength
+                    ? inputRecord.message.substring( 0, maxMessageLength ) + truncateMessage
+                    : inputRecord.message;
+
+            copiedRecord.narrative = inputRecord.getNarrative().length() > maxNarrativeLength
+                    ? inputRecord.narrative.substring( 0, maxNarrativeLength ) + truncateMessage
+                    : inputRecord.narrative;
+
+            message.append( JsonUtil.serialize( copiedRecord ) );
+        }
+
+        return message.toString();
+    }
+
+}

+ 23 - 198
server/src/main/java/password/pwm/svc/event/SyslogAuditService.java

@@ -40,7 +40,6 @@ import org.graylog2.syslog4j.impl.net.udp.UDPNetSyslogConfig;
 import password.pwm.AppProperty;
 import password.pwm.PwmApplication;
 import password.pwm.PwmConstants;
-import password.pwm.bean.SessionLabel;
 import password.pwm.config.Configuration;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.option.SyslogOutputFormat;
@@ -53,17 +52,14 @@ import password.pwm.health.HealthStatus;
 import password.pwm.health.HealthTopic;
 import password.pwm.svc.stats.Statistic;
 import password.pwm.svc.stats.StatisticsManager;
-import password.pwm.util.i18n.LocaleHelper;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.JsonUtil;
-import password.pwm.util.java.StringUtil;
 import password.pwm.util.java.TimeDuration;
 import password.pwm.util.localdb.LocalDB;
 import password.pwm.util.localdb.LocalDBException;
 import password.pwm.util.localdb.LocalDBStoredQueue;
 import password.pwm.util.localdb.WorkQueueProcessor;
 import password.pwm.util.logging.PwmLogger;
-import password.pwm.util.macro.MacroMachine;
 import password.pwm.util.secure.X509Utils;
 
 import javax.net.SocketFactory;
@@ -74,11 +70,8 @@ import java.security.KeyManagementException;
 import java.security.NoSuchAlgorithmException;
 import java.security.cert.X509Certificate;
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.Collections;
-import java.util.LinkedHashMap;
 import java.util.List;
-import java.util.Map;
 
 
 public class SyslogAuditService
@@ -89,18 +82,6 @@ public class SyslogAuditService
     private static final String SYSLOG_INSTANCE_NAME = "syslog-audit";
     private static final int LENGTH_OVERSIZE = 1024;
 
-    private static final Map<String, String> CEF_VALUE_ESCAPES;
-
-
-    static
-    {
-        final Map<String, String> map = new LinkedHashMap<>( );
-        map.put( "\\", "\\\\" );
-        map.put( "=", "\\=" );
-        map.put( "|", "\"" );
-        CEF_VALUE_ESCAPES = Collections.unmodifiableMap( map );
-    }
-
     private SyslogIF syslogInstance = null;
     private ErrorInformation lastError = null;
     private List<X509Certificate> certificates = null;
@@ -110,17 +91,15 @@ public class SyslogAuditService
 
     private final Configuration configuration;
     private final PwmApplication pwmApplication;
-    private final SyslogOutputFormat syslogOutputFormat;
-    private final Map<String, String> syslogCefExtensions;
+    private final AuditFormatter auditFormatter;
+
 
-    public SyslogAuditService( final PwmApplication pwmApplication )
+    SyslogAuditService( final PwmApplication pwmApplication )
             throws LocalDBException
     {
-        syslogOutputFormat = pwmApplication.getConfig().readSettingAsEnum( PwmSetting.AUDIT_SYSLOG_OUTPUT_FORMAT, SyslogOutputFormat.class );
         this.pwmApplication = pwmApplication;
         this.configuration = pwmApplication.getConfig();
         this.certificates = configuration.readSettingAsCertificate( PwmSetting.AUDIT_SYSLOG_CERTIFICATES );
-        this.syslogCefExtensions = figureCefSyslogExtensions( configuration );
 
         final List<String> syslogConfigStringArray = configuration.readSettingAsStringArray( PwmSetting.AUDIT_SYSLOG_SERVERS );
         try
@@ -138,6 +117,24 @@ public class SyslogAuditService
             LOGGER.error( "error parsing syslog configuration for  syslogConfigStrings ERROR: " + e.getMessage() );
         }
 
+        {
+            final SyslogOutputFormat syslogOutputFormat = pwmApplication.getConfig().readSettingAsEnum( PwmSetting.AUDIT_SYSLOG_OUTPUT_FORMAT, SyslogOutputFormat.class );
+            switch ( syslogOutputFormat )
+            {
+                case JSON:
+                    auditFormatter = new JsonAuditFormatter();
+                    break;
+
+                case CEF:
+                    auditFormatter = new CEFAuditFormatter();
+                    break;
+
+                default:
+                    JavaHelper.unhandledSwitchStatement( syslogOutputFormat );
+                    throw new IllegalStateException();
+            }
+        }
+
         final WorkQueueProcessor.Settings settings = WorkQueueProcessor.Settings.builder()
                 .maxEvents( Integer.parseInt( configuration.readAppProperty( AppProperty.QUEUE_SYSLOG_MAX_COUNT ) ) )
                 .retryDiscardAge( TimeDuration.of( Long.parseLong( configuration.readAppProperty( AppProperty.QUEUE_SYSLOG_MAX_AGE_MS ) ), TimeDuration.Unit.MILLISECONDS ) )
@@ -216,23 +213,9 @@ public class SyslogAuditService
     {
 
         final String syslogMsg;
-
         try
         {
-            switch ( syslogOutputFormat )
-            {
-                case JSON:
-                    syslogMsg = convertAuditRecordToSyslogMessage( event, configuration );
-                    break;
-
-                case CEF:
-                    syslogMsg = convertAuditRecordToCEFMessage( event, configuration );
-                    break;
-
-                default:
-                    JavaHelper.unhandledSwitchStatement( syslogOutputFormat );
-                    throw new IllegalStateException();
-            }
+            syslogMsg = auditFormatter.convertAuditRecordToMessage( pwmApplication, event );
         }
         catch ( PwmUnrecoverableException e )
         {
@@ -242,6 +225,7 @@ public class SyslogAuditService
         }
 
 
+
         try
         {
             workQueueProcessor.submit( syslogMsg );
@@ -306,154 +290,6 @@ public class SyslogAuditService
         syslogInstance = null;
     }
 
-
-    private static String convertAuditRecordToSyslogMessage(
-            final AuditRecord auditRecord,
-            final Configuration configuration
-    )
-    {
-        final int maxLength = Integer.parseInt( configuration.readAppProperty( AppProperty.AUDIT_SYSLOG_MAX_MESSAGE_LENGTH ) );
-        String jsonValue = "";
-        final StringBuilder message = new StringBuilder();
-        message.append( PwmConstants.PWM_APP_NAME );
-        message.append( " " );
-
-        jsonValue = JsonUtil.serialize( auditRecord );
-
-        if ( message.length() + jsonValue.length() <= maxLength )
-        {
-            message.append( jsonValue );
-        }
-        else
-        {
-            final AuditRecord inputRecord = JsonUtil.cloneUsingJson( auditRecord, auditRecord.getClass() );
-            inputRecord.message = inputRecord.message == null ? "" : inputRecord.message;
-            inputRecord.narrative = inputRecord.narrative == null ? "" : inputRecord.narrative;
-
-            final String truncateMessage = configuration.readAppProperty( AppProperty.AUDIT_SYSLOG_TRUNCATE_MESSAGE );
-            final AuditRecord copiedRecord = JsonUtil.cloneUsingJson( auditRecord, auditRecord.getClass() );
-            copiedRecord.message = "";
-            copiedRecord.narrative = "";
-            final int shortenedMessageLength = message.length()
-                    + JsonUtil.serialize( copiedRecord ).length()
-                    + truncateMessage.length();
-            final int maxMessageAndNarrativeLength = maxLength - ( shortenedMessageLength + ( truncateMessage.length() * 2 ) );
-            int maxMessageLength = inputRecord.getMessage().length();
-            int maxNarrativeLength = inputRecord.getNarrative().length();
-
-            {
-                int top = maxMessageAndNarrativeLength;
-                while ( maxMessageLength + maxNarrativeLength > maxMessageAndNarrativeLength )
-                {
-                    top--;
-                    maxMessageLength = Math.min( maxMessageLength, top );
-                    maxNarrativeLength = Math.min( maxNarrativeLength, top );
-                }
-            }
-
-            copiedRecord.message = inputRecord.getMessage().length() > maxMessageLength
-                    ? inputRecord.message.substring( 0, maxMessageLength ) + truncateMessage
-                    : inputRecord.message;
-
-            copiedRecord.narrative = inputRecord.getNarrative().length() > maxNarrativeLength
-                    ? inputRecord.narrative.substring( 0, maxNarrativeLength ) + truncateMessage
-                    : inputRecord.narrative;
-
-            message.append( JsonUtil.serialize( copiedRecord ) );
-        }
-
-        return message.toString();
-    }
-
-    private String convertAuditRecordToCEFMessage(
-            final AuditRecord auditRecord,
-            final Configuration configuration
-    )
-            throws PwmUnrecoverableException
-    {
-        final Map<String, Object> auditRecordMap = JsonUtil.deserializeMap( JsonUtil.serialize( auditRecord ) );
-        final String separator = "|";
-
-        final String headerSeverity = configuration.readAppProperty( AppProperty.AUDIT_SYSLOG_CEF_HEADER_SEVERITY );
-        final String headerProduct = configuration.readAppProperty( AppProperty.AUDIT_SYSLOG_CEF_HEADER_PRODUCT );
-        final String headerVendor = configuration.readAppProperty( AppProperty.AUDIT_SYSLOG_CEF_HEADER_VENDOR );
-
-
-        final MacroMachine macroMachine = MacroMachine.forNonUserSpecific( pwmApplication, SessionLabel.SYSTEM_LABEL );
-
-        final String auditFieldName = LocaleHelper.getLocalizedMessage(
-                PwmConstants.DEFAULT_LOCALE,
-                auditRecord.getEventCode().getMessage(),
-                configuration
-        );
-
-        final StringBuilder cefOutput = new StringBuilder(  );
-
-        // cef declaration:version prefix
-        cefOutput.append( "CEF:0" );
-
-        // Device Vendor
-        cefOutput.append( separator );
-        cefOutput.append( macroMachine.expandMacros( headerVendor ) );
-
-        // Device Product
-        cefOutput.append( separator );
-        cefOutput.append( macroMachine.expandMacros( headerProduct ) );
-
-        // Device Version
-        cefOutput.append( separator );
-        cefOutput.append( PwmConstants.SERVLET_VERSION );
-
-        // Device Event Class ID
-        cefOutput.append( separator );
-        cefOutput.append( auditRecord.getEventCode() );
-
-        // field name
-        cefOutput.append( separator );
-        cefOutput.append( auditFieldName );
-
-        // severity
-        cefOutput.append( separator );
-        cefOutput.append( macroMachine.expandMacros( headerSeverity ) );
-
-        boolean extensionAdded = false;
-        for ( final Map.Entry<String, String> entry : syslogCefExtensions.entrySet() )
-        {
-            final Object value = auditRecordMap.get( entry.getKey() );
-            if ( value != null )
-            {
-                if ( !extensionAdded )
-                {
-                    cefOutput.append( separator );
-                    extensionAdded = true;
-                }
-                else
-                {
-                    cefOutput.append( " " );
-                }
-                cefOutput.append( entry.getValue() );
-                cefOutput.append( "=" );
-                cefOutput.append( escapeCEFValue( value.toString() ) );
-            }
-        }
-
-        return cefOutput.toString();
-    }
-
-
-
-    private static String escapeCEFValue( final String value )
-    {
-        String replacedValue = value;
-        for ( final Map.Entry<String, String> entry : CEF_VALUE_ESCAPES.entrySet() )
-        {
-            final String pattern = entry.getKey();
-            final String replacement = entry.getValue();
-            replacedValue = replacedValue.replace( pattern, replacement );
-        }
-        return replacedValue;
-    }
-
     @Getter
     @AllArgsConstructor( access = AccessLevel.PRIVATE )
     public static class SyslogConfig implements Serializable
@@ -558,15 +394,4 @@ public class SyslogAuditService
             return newClass;
         }
     }
-
-    private Map<String, String> figureCefSyslogExtensions( final Configuration config )
-    {
-        final String pairSplitter = ":";
-        final String keyValueSplitter = ",";
-        final String configuredString = config.readAppProperty( AppProperty.AUDIT_SYSLOG_CEF_EXTENSIONS );
-
-        final List<String> pairs = Arrays.asList( configuredString.split( pairSplitter ) );
-        final Map<String, String> map = StringUtil.convertStringListToNameValuePair( pairs, keyValueSplitter );
-        return Collections.unmodifiableMap( map );
-    }
 }

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

@@ -22,6 +22,8 @@
 
 package password.pwm.svc.event;
 
+import lombok.Builder;
+
 import java.io.Serializable;
 import java.time.Instant;
 
@@ -37,6 +39,7 @@ public class UserAuditRecord extends AuditRecord implements Serializable
     protected String sourceHost;
 
     @SuppressWarnings( "checkstyle:ParameterNumber" )
+    @Builder
     protected UserAuditRecord(
             final Instant timestamp,
             final AuditEvent eventCode,

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

@@ -63,7 +63,7 @@ public class HttpTelemetrySender implements TelemetrySender
             throws PwmUnrecoverableException
     {
         final PwmHttpClientConfiguration pwmHttpClientConfiguration = PwmHttpClientConfiguration.builder()
-                .trustManager( new X509Utils.PromiscuousTrustManager() )
+                .trustManager( new X509Utils.PromiscuousTrustManager( SessionLabel.TELEMETRY_SESSION_LABEL ) )
                 .build();
         final PwmHttpClient pwmHttpClient = new PwmHttpClient( pwmApplication, SessionLabel.TELEMETRY_SESSION_LABEL, pwmHttpClientConfiguration );
         final String body = JsonUtil.serialize( statsPublishBean );

+ 46 - 18
server/src/main/java/password/pwm/svc/wordlist/WordlistBucket.java

@@ -39,18 +39,15 @@ import java.util.Map;
 import java.util.Set;
 import java.util.TreeMap;
 import java.util.TreeSet;
-import java.util.concurrent.atomic.AtomicLong;
 
 class WordlistBucket
 {
     private static final PwmLogger LOGGER = PwmLogger.forClass( WordlistBucket.class );
-    private static final String KEY_LAST_ISSUED_KEY = "_______lastKey_";
 
     private final PwmApplication pwmApplication;
     private final WordlistConfiguration wordlistConfiguration;
     private final LocalDB.DB db;
     private final WordlistType type;
-    private final AtomicLong seedlistTopKey = new AtomicLong(  );
 
 
     WordlistBucket(
@@ -64,14 +61,6 @@ class WordlistBucket
         this.wordlistConfiguration = wordlistConfiguration;
         this.db = wordlistConfiguration.getDb();
         this.type = type;
-
-        final String valueOfLastKey = pwmApplication.getLocalDB().get( db, KEY_LAST_ISSUED_KEY );
-
-        seedlistTopKey.set(
-                StringUtil.isEmpty( valueOfLastKey )
-                        ? 0
-                        : Long.parseLong( valueOfLastKey )
-        );
     }
 
     boolean containsWord( final String word ) throws LocalDBException
@@ -126,7 +115,7 @@ class WordlistBucket
             if ( seedCount > 1000 )
             {
                 final long randomKey = pwmApplication.getSecureService().pwmRandom().nextLong( seedCount );
-                return pwmApplication.getLocalDB().get( db, String.valueOf( randomKey ) );
+                return pwmApplication.getLocalDB().get( db, seedlistLongToKey( randomKey ) );
             }
         }
         catch ( Exception e )
@@ -137,9 +126,17 @@ class WordlistBucket
         throw new PwmUnrecoverableException( PwmError.ERROR_INTERNAL, "seedlist word not available" );
     }
 
-    void addWords( final Collection<String> words ) throws LocalDBException
+    void addWords( final Collection<String> words, final AbstractWordlist abstractWordlist )
+            throws LocalDBException
     {
-        pwmApplication.getLocalDB().putAll( db, getWriteTxnForValue( words ) );
+        final WordlistStatus initialStatus = abstractWordlist.readWordlistStatus();
+        final MutableLongIncrementer valueIncrementer = new MutableLongIncrementer( initialStatus.getValueCount() );
+        pwmApplication.getLocalDB().putAll( db, getWriteTxnForValue( words, valueIncrementer ) );
+        if ( initialStatus.getValueCount() != valueIncrementer.get() )
+        {
+            final WordlistStatus incrementedStatus = initialStatus.toBuilder().valueCount( valueIncrementer.get() ).build();
+            abstractWordlist.writeWordlistStatus( incrementedStatus );
+        }
     }
 
     long size() throws LocalDBException
@@ -150,11 +147,10 @@ class WordlistBucket
 
     void clear() throws LocalDBException
     {
-        seedlistTopKey.set( 0 );
         pwmApplication.getLocalDB().truncate( db );
     }
 
-    private Map<String, String> getWriteTxnForValue( final Collection<String> words ) throws LocalDBException
+    private Map<String, String> getWriteTxnForValue( final Collection<String> words, final MutableLongIncrementer valueIncrementer ) throws LocalDBException
     {
         switch ( type )
         {
@@ -166,8 +162,9 @@ class WordlistBucket
                     final String normalizedWord = normalizeWord( word );
                     if ( !StringUtil.isEmpty( normalizedWord ) )
                     {
-                        returnSet.put( String.valueOf( seedlistTopKey.incrementAndGet() ), normalizedWord );
-                        returnSet.put( KEY_LAST_ISSUED_KEY, String.valueOf( seedlistTopKey.get() ) );
+                        final long nextLong = valueIncrementer.getAndIncrement();
+                        final String nextKey = seedlistLongToKey( nextLong );
+                        returnSet.put( nextKey, normalizedWord );
                     }
                 }
                 return Collections.unmodifiableMap( returnSet );
@@ -181,6 +178,7 @@ class WordlistBucket
                     final String normalizedWord = normalizeWord( word );
                     if ( !StringUtil.isEmpty( normalizedWord ) )
                     {
+                        valueIncrementer.getAndIncrement();
                         returnSet.put( normalizedWord, "" );
                     }
                 }
@@ -248,4 +246,34 @@ class WordlistBucket
         return testWords;
     }
 
+    private static long seedlistKeyToLong( final String key )
+    {
+        return Long.parseLong( key, 36 );
+    }
+
+    private static String seedlistLongToKey( final long longValue )
+    {
+        return Long.toString( longValue, 36 );
+    }
+
+    public static class MutableLongIncrementer
+    {
+        private long value;
+
+        MutableLongIncrementer( final long value )
+        {
+            this.value = value;
+        }
+
+        public long getAndIncrement()
+        {
+            value++;
+            return value;
+        }
+
+        public long get()
+        {
+            return value;
+        }
+    }
 }

+ 14 - 13
server/src/main/java/password/pwm/svc/wordlist/WordlistImporter.java

@@ -28,6 +28,7 @@ import password.pwm.error.PwmError;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.util.TransactionSizeCalculator;
 import password.pwm.util.java.ConditionalTaskExecutor;
+import password.pwm.util.java.JsonUtil;
 import password.pwm.util.java.Percent;
 import password.pwm.util.java.PwmNumberFormat;
 import password.pwm.util.java.StringUtil;
@@ -170,7 +171,7 @@ class WordlistImporter implements Runnable
         rootWordlist.setActivity( Wordlist.Activity.Importing );
 
         final ConditionalTaskExecutor metaUpdater = new ConditionalTaskExecutor(
-                () -> rootWordlist.writeWordlistStatus( WordlistStatus.builder()
+                () -> rootWordlist.writeWordlistStatus( rootWordlist.readWordlistStatus().toBuilder()
                         .sourceType( sourceType )
                         .storeDate( Instant.now() )
                         .remoteInfo( wordlistSourceInfo )
@@ -180,7 +181,7 @@ class WordlistImporter implements Runnable
         );
 
         final ConditionalTaskExecutor debugOutputter = new ConditionalTaskExecutor(
-                () -> getLogger().debug( () -> makeStatString() ),
+                () -> getLogger().debug( this::makeStatString ),
                 new ConditionalTaskExecutor.TimeDurationPredicate( AbstractWordlist.DEBUG_OUTPUT_FREQUENCY )
         );
 
@@ -246,7 +247,7 @@ class WordlistImporter implements Runnable
         final long startTime = System.currentTimeMillis();
 
         //add the elements
-        wordlistBucket.addWords( bufferedWords );
+        wordlistBucket.addWords( bufferedWords, rootWordlist );
 
         if ( cancelFlag.getAsBoolean() )
         {
@@ -272,16 +273,16 @@ class WordlistImporter implements Runnable
         getLogger().info( () -> "population complete, added " + wordlistSize
                 + " total words in " + TimeDuration.compactFromCurrent( startTime ) );
 
-        {
-            final WordlistStatus wordlistStatus = WordlistStatus.builder()
-                    .remoteInfo( wordlistSourceInfo )
-                    .storeDate( Instant.now() )
-                    .sourceType( sourceType )
-                    .completed( true )
-                    .bytes( zipFileReader.getByteCount() )
-                    .build();
-            rootWordlist.writeWordlistStatus( wordlistStatus );
-        }
+        rootWordlist.writeWordlistStatus( rootWordlist.readWordlistStatus().toBuilder()
+                .remoteInfo( wordlistSourceInfo )
+                .storeDate( Instant.now() )
+                .sourceType( sourceType )
+                .completed( true )
+                .bytes( zipFileReader.getByteCount() )
+                .build()
+        );
+
+        getLogger().debug( () -> "final post-population status: " + JsonUtil.serialize( rootWordlist.readWordlistStatus() ) );
     }
 
     private PwmLogger getLogger()

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

@@ -82,7 +82,7 @@ class WordlistSource
             {
                 final boolean promiscuous = Boolean.parseBoolean( pwmApplication.getConfig().readAppProperty( AppProperty.HTTP_CLIENT_PROMISCUOUS_WORDLIST_ENABLE ) );
                 final PwmHttpClientConfiguration pwmHttpClientConfiguration = PwmHttpClientConfiguration.builder()
-                        .trustManager( promiscuous ? new X509Utils.PromiscuousTrustManager() : null )
+                        .trustManager( promiscuous ? new X509Utils.PromiscuousTrustManager( null ) : null )
                         .build();
                 final PwmHttpClient client = new PwmHttpClient( pwmApplication, null, pwmHttpClientConfiguration );
                 return client.streamForUrl( wordlistConfiguration.getAutoImportUrl() );

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

@@ -32,13 +32,15 @@ import java.time.Instant;
 @Builder( toBuilder = true )
 public class WordlistStatus implements Serializable
 {
-    public static final int CURRENT_VERSION = 3;
+    public static final int CURRENT_VERSION = 5;
 
     @Builder.Default
     private int version = CURRENT_VERSION;
+
     private boolean completed;
     private WordlistSourceType sourceType;
     private Instant storeDate;
     private WordlistSourceInfo remoteInfo;
     private long bytes;
+    private long valueCount;
 }

+ 278 - 0
server/src/main/java/password/pwm/util/PropertyConfigurationImporter.java

@@ -0,0 +1,278 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2018 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+package password.pwm.util;
+
+import password.pwm.config.PwmSetting;
+import password.pwm.config.StoredValue;
+import password.pwm.config.stored.ConfigurationProperty;
+import password.pwm.config.stored.StoredConfigurationImpl;
+import password.pwm.config.value.BooleanValue;
+import password.pwm.config.value.PasswordValue;
+import password.pwm.config.value.StringArrayValue;
+import password.pwm.config.value.StringValue;
+import password.pwm.config.value.UserPermissionValue;
+import password.pwm.config.value.X509CertificateValue;
+import password.pwm.config.value.data.UserPermission;
+import password.pwm.error.PwmException;
+import password.pwm.util.java.JavaHelper;
+import password.pwm.util.java.StringUtil;
+import password.pwm.util.secure.X509Utils;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.security.cert.X509Certificate;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Properties;
+import java.util.regex.Pattern;
+
+public class PropertyConfigurationImporter
+{
+    private static final String LDAP_PROFILE = "default";
+
+    private Map<String, String> inputMap;
+
+    public enum PropertyKey
+    {
+        TEMPLATE_LDAP,
+        DISPLAY_THEME,
+
+        ID_VAULT_HOST,
+        ID_VAULT_LDAPS_PORT,
+        ID_VAULT_ADMIN_LDAP,
+        ID_VAULT_PASSWORD,
+        USER_CONTAINER,
+        UA_ADMIN,
+
+        SSO_SERVER_HOST,
+        SSO_SERVER_SSL_PORT,
+        SSO_SERVICE_PWD,
+
+        CONFIGURATION_PWD,
+
+        LDAP_SERVERCERTS,
+        OAUTH_IDSERVER_SERVERCERTS,
+        AUDIT_SERVERCERTS,;
+    }
+
+    public PropertyConfigurationImporter()
+    {
+    }
+
+    private void readInputFile( final InputStream propertiesInput )
+            throws IOException
+    {
+        final Properties inputProperties = new Properties( );
+        inputProperties.load( propertiesInput );
+        final Map<String, String> inputMap = JavaHelper.propertiesToStringMap( inputProperties );
+        stripValueDelimiters( inputMap );
+        this.inputMap = inputMap;
+    }
+
+    public StoredConfigurationImpl readConfiguration( final InputStream propertiesInput )
+            throws PwmException, IOException
+    {
+        readInputFile( propertiesInput );
+
+        final StoredConfigurationImpl storedConfiguration = StoredConfigurationImpl.newStoredConfiguration( );
+        storedConfiguration.initNewRandomSecurityKey( );
+        storedConfiguration.writeConfigProperty( 
+                ConfigurationProperty.CONFIG_IS_EDITABLE, Boolean.toString( true ) );
+        storedConfiguration.writeConfigProperty( 
+                ConfigurationProperty.CONFIG_EPOCH, String.valueOf( 0 ) );
+
+        // static values
+        storedConfiguration.writeSetting( PwmSetting.TEMPLATE_LDAP, new StringValue( 
+                        inputMap.getOrDefault( PropertyKey.TEMPLATE_LDAP.name( ), "NOVL_IDM" ) ),
+                null );
+
+        if ( inputMap.containsKey( PropertyKey.DISPLAY_THEME.name( ) ) )
+        {
+            storedConfiguration.writeSetting( PwmSetting.PASSWORD_POLICY_SOURCE, new StringValue( 
+                            inputMap.get( PropertyKey.DISPLAY_THEME.name( ) ) ),
+                    null );
+        }
+
+        storedConfiguration.writeSetting( PwmSetting.DISPLAY_HOME_BUTTON, new BooleanValue( false ), null );
+        storedConfiguration.writeSetting( PwmSetting.LOGOUT_AFTER_PASSWORD_CHANGE, new BooleanValue( false ), null );
+        storedConfiguration.writeSetting( PwmSetting.PASSWORD_REQUIRE_CURRENT, new BooleanValue( false ), null );
+        storedConfiguration.writeSetting( PwmSetting.PASSWORD_POLICY_SOURCE, new StringValue( "LDAP" ), null );
+        storedConfiguration.writeSetting( PwmSetting.CERTIFICATE_VALIDATION_MODE, new StringValue( "CA_ONLY" ), null );
+        {
+            final String notes = "Configuration generated via properties import at " + JavaHelper.toIsoDate( Instant.now( ) );
+            storedConfiguration.writeSetting( PwmSetting.NOTES, new StringValue( notes ), null );
+        }
+
+        // ldap server
+        storedConfiguration.writeSetting( PwmSetting.LDAP_SERVER_URLS, LDAP_PROFILE, makeLdapServerUrlValue( ), null );
+        storedConfiguration.writeSetting( PwmSetting.LDAP_PROXY_USER_DN, LDAP_PROFILE,
+                new PasswordValue( PasswordData.forStringValue( inputMap.get( PropertyKey.ID_VAULT_ADMIN_LDAP.name( ) ) ) ), null );
+        storedConfiguration.writeSetting( PwmSetting.LDAP_PROXY_USER_PASSWORD, LDAP_PROFILE,
+                new PasswordValue( PasswordData.forStringValue( inputMap.get( PropertyKey.ID_VAULT_PASSWORD.name( ) ) ) ), null );
+        storedConfiguration.writeSetting( PwmSetting.LDAP_CONTEXTLESS_ROOT, LDAP_PROFILE,
+                new StringArrayValue( Collections.singletonList( inputMap.get( PropertyKey.USER_CONTAINER.name( ) ) ) ), null );
+
+        // oauth
+        storedConfiguration.writeSetting( PwmSetting.OAUTH_ID_LOGIN_URL, new StringValue( makeOAuthBaseUrl( ) + "/grant" ), null );
+        storedConfiguration.writeSetting( PwmSetting.OAUTH_ID_CODERESOLVE_URL, new StringValue( makeOAuthBaseUrl( ) + "/authcoderesolve" ), null );
+        storedConfiguration.writeSetting( PwmSetting.OAUTH_ID_ATTRIBUTES_URL, new StringValue( makeOAuthBaseUrl( ) + "/getattributes" ), null );
+        storedConfiguration.writeSetting( PwmSetting.OAUTH_ID_CLIENTNAME, new StringValue( "sspr" ), null );
+        storedConfiguration.writeSetting( PwmSetting.OAUTH_ID_DN_ATTRIBUTE_NAME, new StringValue( "name" ), null );
+        storedConfiguration.writeSetting( PwmSetting.OAUTH_ID_SECRET,
+                new PasswordValue( PasswordData.forStringValue( inputMap.get( PropertyKey.SSO_SERVICE_PWD.name( ) ) ) ), null );
+
+        //urls
+        storedConfiguration.writeSetting( PwmSetting.URL_FORWARD, makeForwardUrl( ), null );
+        storedConfiguration.writeSetting( PwmSetting.URL_LOGOUT, makeLogoutUrl( ), null );
+        storedConfiguration.writeSetting( PwmSetting.PWM_SITE_URL, makeSelfUrl( ), null );
+        storedConfiguration.writeSetting( PwmSetting.SECURITY_REDIRECT_WHITELIST, makeWhitelistUrl( ), null );
+
+        // admin settings
+        storedConfiguration.writeSetting( PwmSetting.QUERY_MATCH_PWM_ADMIN, makeAdminPermissions( ), null );
+        storedConfiguration.setPassword( inputMap.get( PropertyKey.CONFIGURATION_PWD.name( ) ) );
+
+        // certificates
+        {
+            final Optional<Collection<X509Certificate>> optionalCert = readCertificate( PropertyKey.LDAP_SERVERCERTS );
+            if ( optionalCert.isPresent( ) )
+            {
+                storedConfiguration.writeSetting( PwmSetting.LDAP_SERVER_CERTS, LDAP_PROFILE, new X509CertificateValue( optionalCert.get( ) ), null );
+            }
+        }
+        {
+            final Optional<Collection<X509Certificate>> optionalCert = readCertificate( PropertyKey.AUDIT_SERVERCERTS );
+            if ( optionalCert.isPresent( ) )
+            {
+                storedConfiguration.writeSetting( PwmSetting.AUDIT_SYSLOG_CERTIFICATES, new X509CertificateValue( optionalCert.get( ) ), null );
+            }
+        }
+        {
+            final Optional<Collection<X509Certificate>> optionalCert = readCertificate( PropertyKey.OAUTH_IDSERVER_SERVERCERTS );
+            if ( optionalCert.isPresent( ) )
+            {
+                storedConfiguration.writeSetting( PwmSetting.OAUTH_ID_CERTIFICATE, new X509CertificateValue( optionalCert.get( ) ), null );
+            }
+        }
+
+
+        return storedConfiguration;
+    }
+
+    private String makeOAuthBaseUrl( )
+    {
+        return "https://" + inputMap.get( PropertyKey.SSO_SERVER_HOST.name( ) )
+                + ":" + inputMap.get( PropertyKey.SSO_SERVER_SSL_PORT.name( ) )
+                + "/osp/a/idm/auth/oauth2";
+    }
+
+    private StringArrayValue makeWhitelistUrl( )
+    {
+        return new StringArrayValue( Collections.singletonList( "https://" + inputMap.get( PropertyKey.SSO_SERVER_HOST.name( ) )
+                + ":" + inputMap.get( PropertyKey.SSO_SERVER_SSL_PORT.name( ) ) ) );
+    }
+
+    private StoredValue makeSelfUrl( )
+    {
+        return new StringValue( "https://" + inputMap.get( PropertyKey.SSO_SERVER_HOST.name( ) )
+                + ":" + inputMap.get( PropertyKey.SSO_SERVER_SSL_PORT.name( ) )
+                + "/sspr" );
+    }
+
+    private StoredValue makeForwardUrl( )
+    {
+        return new StringValue( "https://" + inputMap.get( PropertyKey.SSO_SERVER_HOST.name( ) )
+                + ":" + inputMap.get( PropertyKey.SSO_SERVER_SSL_PORT.name( ) )
+                + "/idmdash/#/landing" );
+    }
+
+    private StoredValue makeLogoutUrl( )
+    {
+        final String targetValue = "https://" + inputMap.get( PropertyKey.SSO_SERVER_HOST.name( ) )
+                + ":" + inputMap.get( PropertyKey.SSO_SERVER_SSL_PORT.name( ) )
+                + "/sspr";
+
+        return new StringValue( "https://" + inputMap.get( PropertyKey.SSO_SERVER_HOST.name( ) )
+                + ":" + inputMap.get( PropertyKey.SSO_SERVER_SSL_PORT.name( ) )
+                + "/osp/a/idm/auth/app/logout?target="
+                + StringUtil.urlEncode( targetValue ) );
+    }
+
+    private StoredValue makeLdapServerUrlValue( )
+    {
+        final String ldapUrl = "ldaps://" + inputMap.get( PropertyKey.ID_VAULT_HOST.name( ) )
+                + ":" + inputMap.get( PropertyKey.ID_VAULT_LDAPS_PORT.name( ) );
+        return new StringArrayValue( Collections.singletonList( ldapUrl ) );
+    }
+
+    private StoredValue makeAdminPermissions( )
+    {
+        final String filter = "( objectclass=* )";
+        final List<UserPermission> permissions = new ArrayList<>( );
+        permissions.add( new UserPermission( UserPermission.Type.ldapQuery, LDAP_PROFILE, filter,
+                inputMap.get( PropertyKey.ID_VAULT_ADMIN_LDAP.name( ) ) ) );
+        permissions.add( new UserPermission( UserPermission.Type.ldapQuery, LDAP_PROFILE, filter,
+                inputMap.get( PropertyKey.UA_ADMIN.name( ) ) ) );
+        return new UserPermissionValue( permissions );
+    }
+
+    private void stripValueDelimiters( final Map<String, String> map )
+    {
+        final Pattern pattern = Pattern.compile( "^'|'$" );
+        map.replaceAll( ( key, value ) -> pattern.matcher( value ).replaceAll( "" ) );
+    }
+
+    private Optional<Collection<X509Certificate>> readCertificate( 
+            final PropertyKey propertyKey
+    )
+            throws IOException
+    {
+        final String base64Cert = inputMap.get( propertyKey.name( ) );
+        if ( !StringUtil.isEmpty( base64Cert ) )
+        {
+            final List<X509Certificate> returnCerts = new ArrayList<>( );
+            for ( final String splitB64Cert : StringUtil.splitAndTrim( base64Cert, "," ) )
+            {
+                try
+                {
+                    final X509Certificate cert = X509Utils.certificateFromBase64( splitB64Cert );
+                    if ( cert != null )
+                    {
+                        returnCerts.add( cert );
+                    }
+
+                }
+                catch ( Exception e )
+                {
+                    throw new IOException( "error importing key " + propertyKey.name() + ", error: " + e.getMessage() );
+                }
+            }
+            return Optional.of( returnCerts );
+        }
+        return Optional.empty( );
+    }
+}

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

@@ -55,7 +55,7 @@ import password.pwm.util.cli.commands.ExportStatsCommand;
 import password.pwm.util.cli.commands.HelpCommand;
 import password.pwm.util.cli.commands.ImportHttpsKeyStoreCommand;
 import password.pwm.util.cli.commands.ImportLocalDBCommand;
-import password.pwm.util.cli.commands.ImportIDMConfigCommand;
+import password.pwm.util.cli.commands.ImportPropertyConfigCommand;
 import password.pwm.util.cli.commands.ImportResponsesCommand;
 import password.pwm.util.cli.commands.LdapSchemaExtendCommand;
 import password.pwm.util.cli.commands.LocalDBInfoCommand;
@@ -128,7 +128,7 @@ public class MainClass
         commandList.add( new ShellCommand() );
         commandList.add( new ConfigResetHttpsCommand() );
         commandList.add( new HelpCommand() );
-        commandList.add( new ImportIDMConfigCommand() );
+        commandList.add( new ImportPropertyConfigCommand() );
         commandList.add( new ResetInstanceIDCommand() );
 
         final Map<String, CliCommand> sortedMap = new TreeMap<>();

+ 0 - 248
server/src/main/java/password/pwm/util/cli/commands/ImportIDMConfigCommand.java

@@ -1,248 +0,0 @@
-/*
- * Password Management Servlets (PWM)
- * http://www.pwm-project.org
- *
- * Copyright (c) 2006-2009 Novell, Inc.
- * Copyright (c) 2009-2018 The PWM Project
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
- */
-
-package password.pwm.util.cli.commands;
-
-import password.pwm.config.PwmSetting;
-import password.pwm.config.StoredValue;
-import password.pwm.config.stored.ConfigurationProperty;
-import password.pwm.config.stored.StoredConfigurationImpl;
-import password.pwm.config.value.BooleanValue;
-import password.pwm.config.value.PasswordValue;
-import password.pwm.config.value.StringArrayValue;
-import password.pwm.config.value.StringValue;
-import password.pwm.config.value.UserPermissionValue;
-import password.pwm.config.value.data.UserPermission;
-import password.pwm.error.PwmOperationalException;
-import password.pwm.error.PwmUnrecoverableException;
-import password.pwm.util.PasswordData;
-import password.pwm.util.cli.CliParameters;
-import password.pwm.util.java.JavaHelper;
-import password.pwm.util.java.StringUtil;
-
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
-import java.util.Properties;
-import java.util.regex.Pattern;
-
-/**
- * Import IDM silent properties installer file and create new configuration.
- */
-public class ImportIDMConfigCommand extends AbstractCliCommand
-{
-    @Override
-    void doCommand( )
-            throws Exception
-    {
-        final File configFile = cliEnvironment.getConfigurationFile();
-
-        if ( configFile.exists() )
-        {
-            out( "this command can not be run with an existing configuration in place.  Exiting..." );
-            return;
-        }
-
-        final File inputFile = ( File ) cliEnvironment.getOptions().get( CliParameters.REQUIRED_EXISTING_INPUT_FILE.getName() );
-
-        try
-        {
-            final IDMImporter importer = new IDMImporter( new FileInputStream( inputFile ) );
-            final StoredConfigurationImpl storedConfiguration = importer.readConfiguration();
-
-            try ( OutputStream outputStream = new FileOutputStream( configFile ) )
-            {
-                storedConfiguration.toXml( outputStream );
-                out( "output configuration file " + configFile.getAbsolutePath() );
-            }
-
-        }
-        catch ( Exception e )
-        {
-            out( "error during import: " + e.getMessage() );
-        }
-    }
-
-    @Override
-    public CliParameters getCliParameters( )
-    {
-        final CliParameters cliParameters = new CliParameters();
-        cliParameters.commandName = "ImportIDMConfig";
-        cliParameters.description = "Import an IDM installer script and create a new configuration";
-        cliParameters.options = Collections.singletonList( CliParameters.REQUIRED_EXISTING_INPUT_FILE );
-
-        cliParameters.needsPwmApplication = true;
-        cliParameters.needsLocalDB = false;
-        cliParameters.readOnly = false;
-
-        return cliParameters;
-    }
-
-    private enum IdmProperty
-    {
-        ID_VAULT_HOST,
-        ID_VAULT_LDAPS_PORT,
-        ID_VAULT_ADMIN_LDAP,
-        ID_VAULT_PASSWORD,
-        USER_CONTAINER,
-        UA_ADMIN,
-
-        SSO_SERVER_HOST,
-        SSO_SERVER_SSL_PORT,
-        SSO_SERVICE_PWD,
-
-        CONFIGURATION_PWD,
-    }
-
-    private static class IDMImporter
-    {
-        private static final String LDAP_PROFILE = "default";
-        private final Map<String, String> inputMap;
-
-        IDMImporter( final InputStream inputStream ) throws IOException
-        {
-            final Properties inputProperties = new Properties();
-            inputProperties.load( inputStream );
-            final Map<String, String> inputMap = JavaHelper.propertiesToStringMap( inputProperties );
-            stripValueDelimiters( inputMap );
-            this.inputMap = inputMap;
-        }
-
-        StoredConfigurationImpl readConfiguration() throws PwmUnrecoverableException, PwmOperationalException
-        {
-            final StoredConfigurationImpl storedConfiguration = StoredConfigurationImpl.newStoredConfiguration();
-            storedConfiguration.initNewRandomSecurityKey();
-            storedConfiguration.writeConfigProperty(
-                    ConfigurationProperty.CONFIG_IS_EDITABLE, Boolean.toString( true ) );
-            storedConfiguration.writeConfigProperty(
-                    ConfigurationProperty.CONFIG_EPOCH, String.valueOf( 0 ) );
-
-            // static values
-            storedConfiguration.writeSetting( PwmSetting.TEMPLATE_LDAP, new StringValue( "NOVL_IDM" ), null );
-            storedConfiguration.writeSetting( PwmSetting.INTERFACE_THEME, new StringValue( "mdefault" ), null );
-            storedConfiguration.writeSetting( PwmSetting.DISPLAY_HOME_BUTTON, new BooleanValue( false ), null );
-            storedConfiguration.writeSetting( PwmSetting.LOGOUT_AFTER_PASSWORD_CHANGE, new BooleanValue( false ), null );
-            storedConfiguration.writeSetting( PwmSetting.PASSWORD_REQUIRE_CURRENT, new BooleanValue( false ), null );
-            storedConfiguration.writeSetting( PwmSetting.PASSWORD_POLICY_SOURCE, new StringValue( "LDAP" ), null );
-
-            // ldap server
-            storedConfiguration.writeSetting( PwmSetting.LDAP_SERVER_URLS, LDAP_PROFILE, makeLdapServerUrlValue(), null );
-            storedConfiguration.writeSetting( PwmSetting.LDAP_PROXY_USER_DN, LDAP_PROFILE,
-                    new PasswordValue( PasswordData.forStringValue( inputMap.get( IdmProperty.ID_VAULT_ADMIN_LDAP.name() ) ) ), null );
-            storedConfiguration.writeSetting( PwmSetting.LDAP_PROXY_USER_PASSWORD, LDAP_PROFILE,
-                    new PasswordValue( PasswordData.forStringValue( inputMap.get( IdmProperty.ID_VAULT_PASSWORD.name() ) ) ), null );
-            storedConfiguration.writeSetting( PwmSetting.LDAP_CONTEXTLESS_ROOT, LDAP_PROFILE,
-                    new StringArrayValue( Collections.singletonList( inputMap.get( IdmProperty.USER_CONTAINER.name() ) ) ), null );
-
-            // oauth
-            storedConfiguration.writeSetting( PwmSetting.OAUTH_ID_LOGIN_URL, new StringValue( makeOAuthBaseUrl() + "/grant" ), null );
-            storedConfiguration.writeSetting( PwmSetting.OAUTH_ID_CODERESOLVE_URL, new StringValue( makeOAuthBaseUrl() + "/authcoderesolve" ), null );
-            storedConfiguration.writeSetting( PwmSetting.OAUTH_ID_ATTRIBUTES_URL, new StringValue( makeOAuthBaseUrl() + "/getattributes" ), null );
-            storedConfiguration.writeSetting( PwmSetting.OAUTH_ID_CLIENTNAME, new StringValue( "sspr" ), null );
-            storedConfiguration.writeSetting( PwmSetting.OAUTH_ID_DN_ATTRIBUTE_NAME, new StringValue( "name" ), null );
-            storedConfiguration.writeSetting( PwmSetting.OAUTH_ID_SECRET,
-                    new PasswordValue( PasswordData.forStringValue( inputMap.get( IdmProperty.SSO_SERVICE_PWD.name() ) ) ), null );
-
-            //urls
-            storedConfiguration.writeSetting( PwmSetting.URL_FORWARD, makeForwardUrl(), null );
-            storedConfiguration.writeSetting( PwmSetting.URL_LOGOUT, makeLogoutUrl(), null );
-            storedConfiguration.writeSetting( PwmSetting.PWM_SITE_URL, makeSelfUrl(), null );
-            storedConfiguration.writeSetting( PwmSetting.SECURITY_REDIRECT_WHITELIST, makeWhitelistUrl(), null );
-
-            // admin settings
-            storedConfiguration.writeSetting( PwmSetting.QUERY_MATCH_PWM_ADMIN, makeAdminPermissions(), null );
-            storedConfiguration.setPassword( inputMap.get( IdmProperty.CONFIGURATION_PWD.name() ) );
-
-            return storedConfiguration;
-        }
-
-        String makeOAuthBaseUrl()
-        {
-            return "https://" + inputMap.get( IdmProperty.SSO_SERVER_HOST.name() )
-                    + ":" + inputMap.get( IdmProperty.SSO_SERVER_SSL_PORT.name() )
-                    + "/osp/a/idm/auth/oauth2";
-        }
-
-        StringArrayValue makeWhitelistUrl()
-        {
-            return new StringArrayValue( Collections.singletonList( "https://" + inputMap.get( IdmProperty.SSO_SERVER_HOST.name() )
-                    + ":" + inputMap.get( IdmProperty.SSO_SERVER_SSL_PORT.name() ) ) );
-        }
-
-        StoredValue makeSelfUrl()
-        {
-            return new StringValue( "https://" + inputMap.get( IdmProperty.SSO_SERVER_HOST.name() )
-                    + ":" + inputMap.get( IdmProperty.SSO_SERVER_SSL_PORT.name() )
-                    + "/sspr" );
-        }
-
-        StoredValue makeForwardUrl()
-        {
-            return new StringValue( "https://" + inputMap.get( IdmProperty.SSO_SERVER_HOST.name() )
-                    + ":" + inputMap.get( IdmProperty.SSO_SERVER_SSL_PORT.name() )
-                    + "/idmdash/#/landing" );
-        }
-
-        StoredValue makeLogoutUrl()
-        {
-            final String targetValue = "https://" + inputMap.get( IdmProperty.SSO_SERVER_HOST.name() )
-                    + ":" + inputMap.get( IdmProperty.SSO_SERVER_SSL_PORT.name() )
-                    + "/sspr";
-
-            return new StringValue( "https://" + inputMap.get( IdmProperty.SSO_SERVER_HOST.name() )
-                    + ":" + inputMap.get( IdmProperty.SSO_SERVER_SSL_PORT.name() )
-                    + "/osp/a/idm/auth/app/logout?target="
-                    + StringUtil.urlEncode( targetValue ) );
-        }
-
-        StoredValue makeLdapServerUrlValue( )
-        {
-            final String ldapUrl = "ldaps://" + inputMap.get( IdmProperty.ID_VAULT_HOST.name() )
-                    + ":" + inputMap.get( IdmProperty.ID_VAULT_LDAPS_PORT.name() );
-            return new StringArrayValue( Collections.singletonList( ldapUrl ) );
-        }
-
-        StoredValue makeAdminPermissions( )
-        {
-            final String filter = "(objectclass=*)";
-            final List<UserPermission> permissions = new ArrayList<>();
-            permissions.add( new UserPermission( UserPermission.Type.ldapQuery, LDAP_PROFILE, filter,
-                    inputMap.get( IdmProperty.ID_VAULT_ADMIN_LDAP.name() ) ) );
-            permissions.add( new UserPermission( UserPermission.Type.ldapQuery, LDAP_PROFILE, filter,
-                    inputMap.get( IdmProperty.UA_ADMIN.name() ) ) );
-            return new UserPermissionValue( permissions );
-        }
-
-        static void stripValueDelimiters( final Map<String, String> map )
-        {
-            final Pattern pattern = Pattern.compile( "^'|'$" );
-            map.replaceAll( ( key, value ) -> pattern.matcher( value ).replaceAll( "" ) );
-        }
-    }
-}

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

@@ -0,0 +1,87 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2018 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+package password.pwm.util.cli.commands;
+
+import password.pwm.config.stored.StoredConfigurationImpl;
+import password.pwm.util.PropertyConfigurationImporter;
+import password.pwm.util.cli.CliParameters;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.OutputStream;
+
+import java.util.Collections;
+
+/**
+ * Import properties configuration file and create new configuration.
+ */
+public class ImportPropertyConfigCommand extends AbstractCliCommand
+{
+    @Override
+    void doCommand( )
+            throws Exception
+    {
+        final File configFile = cliEnvironment.getConfigurationFile();
+
+        if ( configFile.exists() )
+        {
+            out( "this command can not be run with an existing configuration in place.  Exiting..." );
+            return;
+        }
+
+        final File inputFile = ( File ) cliEnvironment.getOptions().get( CliParameters.REQUIRED_EXISTING_INPUT_FILE.getName() );
+
+        try
+        {
+            final PropertyConfigurationImporter importer = new PropertyConfigurationImporter();
+            final StoredConfigurationImpl storedConfiguration = importer.readConfiguration( new FileInputStream( inputFile ) );
+
+            try ( OutputStream outputStream = new FileOutputStream( configFile ) )
+            {
+                storedConfiguration.toXml( outputStream );
+                out( "output configuration file " + configFile.getAbsolutePath() );
+            }
+        }
+        catch ( Exception e )
+        {
+            out( "error during import: " + e.getMessage() );
+        }
+    }
+
+    @Override
+    public CliParameters getCliParameters( )
+    {
+        final CliParameters cliParameters = new CliParameters();
+        cliParameters.commandName = "ImportPropertyConfig";
+        cliParameters.description = "Import an property based configuration and create a new configuration";
+        cliParameters.options = Collections.singletonList( CliParameters.REQUIRED_EXISTING_INPUT_FILE );
+
+        cliParameters.needsPwmApplication = false;
+        cliParameters.needsLocalDB = false;
+        cliParameters.readOnly = false;
+
+        return cliParameters;
+    }
+
+}

+ 18 - 0
server/src/main/java/password/pwm/util/i18n/LocaleHelper.java

@@ -22,6 +22,7 @@
 
 package password.pwm.util.i18n;
 
+import password.pwm.AppProperty;
 import password.pwm.PwmApplication;
 import password.pwm.PwmConstants;
 import password.pwm.bean.pub.SessionStateInfoBean;
@@ -52,12 +53,19 @@ import java.util.MissingResourceException;
 import java.util.ResourceBundle;
 import java.util.StringTokenizer;
 import java.util.TreeMap;
+import java.util.regex.Pattern;
 import java.util.stream.Collectors;
 
 public class LocaleHelper
 {
     private static final PwmLogger LOGGER = PwmLogger.forClass( LocaleHelper.class );
 
+    public enum TextDirection
+    {
+        rtl,
+        ltr,
+    }
+
     public static Class classForShortName( final String shortName )
     {
         if ( shortName == null || shortName.isEmpty() )
@@ -481,4 +489,14 @@ public class LocaleHelper
     {
         return getLocalizedMessage( locale, Display.Value_NotApplicable, null );
     }
+
+    public static TextDirection textDirectionForLocale( final PwmApplication pwmApplication, final Locale locale )
+    {
+        final String rtlRegex = pwmApplication.getConfig().readAppProperty( AppProperty.L10N_RTL_REGEX );
+        final Pattern rtlPattern = Pattern.compile( rtlRegex );
+        final String languageString = locale.getLanguage();
+        return languageString != null && rtlPattern.matcher( languageString ).find()
+                ? TextDirection.rtl
+                : TextDirection.ltr;
+    }
 }

+ 83 - 13
server/src/main/java/password/pwm/util/java/JavaHelper.java

@@ -28,6 +28,8 @@ import org.apache.commons.csv.CSVPrinter;
 import org.apache.commons.io.IOUtils;
 import password.pwm.PwmApplication;
 import password.pwm.PwmConstants;
+import password.pwm.config.Configuration;
+import password.pwm.config.PwmSetting;
 import password.pwm.http.ContextManager;
 import password.pwm.util.logging.PwmLogger;
 
@@ -45,6 +47,7 @@ import java.lang.management.LockInfo;
 import java.lang.management.MonitorInfo;
 import java.lang.management.ThreadInfo;
 import java.lang.reflect.Method;
+import java.net.URI;
 import java.text.DateFormat;
 import java.text.SimpleDateFormat;
 import java.time.Instant;
@@ -54,6 +57,7 @@ import java.util.Arrays;
 import java.util.Calendar;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.Comparator;
 import java.util.Date;
 import java.util.Enumeration;
 import java.util.GregorianCalendar;
@@ -63,8 +67,8 @@ import java.util.Map;
 import java.util.Objects;
 import java.util.Optional;
 import java.util.Properties;
+import java.util.Set;
 import java.util.TimeZone;
-import java.util.TreeSet;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 import java.util.concurrent.LinkedBlockingQueue;
@@ -73,6 +77,7 @@ import java.util.concurrent.ThreadFactory;
 import java.util.concurrent.ThreadPoolExecutor;
 import java.util.concurrent.TimeUnit;
 import java.util.function.Predicate;
+import java.util.stream.Collectors;
 
 public class JavaHelper
 {
@@ -426,17 +431,6 @@ public class JavaHelper
         return PwmConstants.PWM_APP_NAME + "-" + instanceName + "-" + theClass.getSimpleName();
     }
 
-    public static Properties newSortedProperties( )
-    {
-        return new Properties()
-        {
-            public synchronized Enumeration<Object> keys( )
-            {
-                return Collections.enumeration( new TreeSet<>( super.keySet() ) );
-            }
-        };
-    }
-
     public static ThreadFactory makePwmThreadFactory( final String namePrefix, final boolean daemon )
     {
         return new ThreadFactory()
@@ -509,7 +503,7 @@ public class JavaHelper
         executor.allowCoreThreadTimeOut( true );
         return executor;
     }
-    
+
     /**
      * Copy of {@link ThreadInfo#toString()} but with the MAX_FRAMES changed from 8 to 256.
      * @param threadInfo thread information
@@ -700,4 +694,80 @@ public class JavaHelper
         properties.forEach( ( key, value ) -> returnMap.put( ( String ) key, (String) value ) );
         return returnMap;
     }
+
+    public static Optional<String> deriveLocalServerHostname( final Configuration configuration )
+    {
+        if ( configuration != null )
+        {
+            final String siteUrl = configuration.readSettingAsString( PwmSetting.PWM_SITE_URL );
+            if ( !StringUtil.isEmpty( siteUrl ) )
+            {
+                try
+                {
+                    final URI parsedUri = URI.create( siteUrl );
+                    {
+                        final String uriHost = parsedUri.getHost();
+                        return Optional.ofNullable( uriHost );
+                    }
+                }
+                catch ( IllegalArgumentException e )
+                {
+                    LOGGER.trace( () -> " error parsing siteURL hostname: " + e.getMessage() );
+                }
+            }
+        }
+        return Optional.empty();
+    }
+
+    public static class SortedProperties extends Properties
+    {
+        @Override
+        public synchronized Enumeration<Object> keys()
+        {
+            return Collections.enumeration( super.keySet().stream()
+                    .sorted( Comparator.comparing( Object::toString ) )
+                    .collect( Collectors.toList() ) );
+        }
+
+        @Override
+        public synchronized Set<Map.Entry<Object, Object>> entrySet()
+        {
+            return super.entrySet().stream()
+                    .sorted( Comparator.comparing( o -> o.getKey().toString() ) )
+                    .collect( Collectors.toCollection( LinkedHashSet::new ) );
+        }
+    }
+
+    public static int silentParseInt( final String input, final int defaultValue )
+    {
+        try
+        {
+            return Integer.parseInt( input );
+        }
+        catch ( NumberFormatException e )
+        {
+            return defaultValue;
+        }
+    }
+
+    public static long silentParseLong( final String input, final long defaultValue )
+    {
+        try
+        {
+            return Long.parseLong( input );
+        }
+        catch ( NumberFormatException e )
+        {
+            return defaultValue;
+        }
+    }
+
+    public static boolean doubleContainsLongValue( final Double input )
+    {
+        return input.equals( Math.floor( input ) )
+                && !Double.isInfinite( input )
+                && !Double.isNaN( input )
+                && input <= Long.MAX_VALUE
+                && input >= Long.MIN_VALUE;
+    }
 }

+ 2 - 39
server/src/main/java/password/pwm/util/localdb/XodusLocalDB.java

@@ -138,20 +138,6 @@ public class XodusLocalDB implements LocalDBProvider
         LOGGER.trace( () -> "preparing to open with configuration " + JsonUtil.serializeMap( environmentConfig.getSettings() ) );
         environment = Environments.newInstance( dbDirectory.getAbsolutePath() + File.separator + FILE_SUB_PATH, environmentConfig );
 
-        try
-        {
-            if ( !getDirtyFile().exists() )
-            {
-                Files.createFile( getDirtyFile().toPath() );
-                LOGGER.trace( () -> "created openLock file" );
-            }
-        }
-        catch ( IOException e )
-        {
-            LOGGER.error( "error creating openLock file: " + e.getMessage() );
-        }
-
-
         LOGGER.trace( () -> "environment open (" + TimeDuration.fromCurrent( startTime ).asCompactString() + ")" );
 
         environment.executeInTransaction( txn ->
@@ -177,21 +163,13 @@ public class XodusLocalDB implements LocalDBProvider
     @Override
     public void close( ) throws LocalDBException
     {
+        final Instant startTime = Instant.now();
         if ( environment != null && environment.isOpen() )
         {
             environment.close();
-            try
-            {
-                Files.deleteIfExists( getDirtyFile().toPath() );
-                LOGGER.trace( () -> "deleted openLock file" );
-            }
-            catch ( IOException e )
-            {
-                LOGGER.error( "error creating openLock file: " + e.getMessage() );
-            }
         }
         status = LocalDB.Status.CLOSED;
-        LOGGER.debug( () -> "closed" );
+        LOGGER.debug( () -> "closed (" + TimeDuration.compactFromCurrent( startTime ) + ")" );
     }
 
     private EnvironmentConfig makeEnvironmentConfig( final Map<String, String> initParameters )
@@ -201,16 +179,6 @@ public class XodusLocalDB implements LocalDBProvider
         environmentConfig.setMemoryUsage( 50 * 1024 * 1024 );
         environmentConfig.setEnvGatherStatistics( true );
 
-        if ( Files.exists( getDirtyFile().toPath() ) )
-        {
-            environmentConfig.setGcUtilizationFromScratch( true );
-            LOGGER.warn( "environment not closed cleanly, will re-calculate GC" );
-        }
-        else
-        {
-            LOGGER.debug( () -> "environment was closed cleanly" );
-        }
-
         for ( final Map.Entry<String, String> entry : initParameters.entrySet() )
         {
             final String key = entry.getKey();
@@ -661,9 +629,4 @@ public class XodusLocalDB implements LocalDBProvider
             LOGGER.error( "error writing LocalDB readme file: " + e.getMessage() );
         }
     }
-
-    private File getDirtyFile()
-    {
-        return new File( this.getFileLocation().getAbsolutePath() + File.separator + FILE_SUB_PATH + File.separator + "xodus.open" );
-    }
 }

+ 15 - 14
server/src/main/java/password/pwm/util/logging/LocalDBLogger.java

@@ -43,6 +43,7 @@ import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
+import java.util.Objects;
 import java.util.Queue;
 import java.util.concurrent.ArrayBlockingQueue;
 import java.util.concurrent.Executors;
@@ -75,17 +76,22 @@ public class LocalDBLogger implements PwmService
 
     private static final String STORAGE_FORMAT_VERSION = "3";
 
-    public LocalDBLogger( final PwmApplication pwmApplication, final LocalDB localDB, final LocalDBLoggerSettings settings )
+    public LocalDBLogger(
+            final PwmApplication pwmApplication,
+            final LocalDB localDB,
+            final LocalDBLoggerSettings settings
+    )
             throws LocalDBException
     {
-        if ( localDB == null )
-        {
-            throw new NullPointerException( "localDB can not be null" );
-        }
+        Objects.requireNonNull( localDB, "localDB can not be null" );
 
         final Instant startTime = Instant.now();
         status = STATUS.OPENING;
-        this.settings = settings;
+
+        this.settings = settings == null
+                ? LocalDBLoggerSettings.builder().build().applyValueChecks()
+                : settings.applyValueChecks();
+
         this.localDB = localDB;
         this.localDBListQueue = LocalDBStoredQueue.createLocalDBStoredQueue(
                 pwmApplication,
@@ -93,19 +99,14 @@ public class LocalDBLogger implements PwmService
                 LocalDB.DB.EVENTLOG_EVENTS
         );
 
-        if ( settings.getMaxEvents() == 0 )
+        if ( this.settings.getMaxEvents() == 0 )
         {
             LOGGER.info( () -> "maxEvents set to zero, clearing LocalDBLogger history and LocalDBLogger will remain closed" );
             localDBListQueue.clear();
             throw new IllegalArgumentException( "maxEvents=0, will remain closed" );
         }
 
-        if ( localDB == null )
-        {
-            throw new IllegalArgumentException( "LocalDB is not available" );
-        }
-
-        eventQueue = new ArrayBlockingQueue<>( settings.getMaxBufferSize(), true );
+        eventQueue = new ArrayBlockingQueue<>( this.settings.getMaxBufferSize(), true );
 
         if ( pwmApplication != null )
         {
@@ -137,7 +138,7 @@ public class LocalDBLogger implements PwmService
         cleanerService.scheduleAtFixedRate( new CleanupTask(), 0, 1, TimeUnit.MINUTES );
         writerService.scheduleWithFixedDelay( new FlushTask(), 0, 103, TimeUnit.MILLISECONDS );
 
-        cleanOnWriteFlag.set( eventQueue.size() >= settings.getMaxEvents() );
+        cleanOnWriteFlag.set( eventQueue.size() >= this.settings.getMaxEvents() );
 
         final TimeDuration timeDuration = TimeDuration.fromCurrent( startTime );
         LOGGER.info( () -> "open in " + timeDuration.asCompactString() + ", " + debugStats() );

+ 34 - 105
server/src/main/java/password/pwm/util/logging/LocalDBLoggerSettings.java

@@ -22,6 +22,8 @@
 
 package password.pwm.util.logging;
 
+import lombok.Builder;
+import lombok.Data;
 import password.pwm.AppProperty;
 import password.pwm.config.Configuration;
 import password.pwm.config.PwmSetting;
@@ -32,68 +34,46 @@ import java.util.Collections;
 import java.util.HashSet;
 import java.util.Set;
 
+@Data
+@Builder( toBuilder = true )
 public class LocalDBLoggerSettings implements Serializable
 {
+    @Builder.Default
     static final int MINIMUM_MAXIMUM_EVENTS = 100;
+
+    @Builder.Default
     static final TimeDuration MINIMUM_MAX_AGE = TimeDuration.HOUR;
 
-    private final int maxEvents;
-    private final TimeDuration maxAge;
-    private final Set<Flag> flags;
-    private final int maxBufferSize;
-    private final TimeDuration maxBufferWaitTime;
-    private final int maxTrimSize;
+    @Builder.Default
+    private int maxEvents = 1000 * 1000;
 
-    public enum Flag
-    {
-        DevDebug,
-    }
+    @Builder.Default
+    private TimeDuration maxAge = TimeDuration.of( 7, TimeDuration.Unit.DAYS );
 
-    private LocalDBLoggerSettings(
-            final int maxEvents,
-            final TimeDuration maxAge,
-            final Set<Flag> flags,
-            final int maxBufferSize,
-            final TimeDuration maxBufferWaitTime,
-            final int maxTrimSize
-    )
-    {
-        this.maxEvents = maxEvents < 1 ? 0 : Math.max( MINIMUM_MAXIMUM_EVENTS, maxEvents );
-        this.maxAge = maxAge == null || maxAge.isShorterThan( MINIMUM_MAX_AGE ) ? MINIMUM_MAX_AGE : maxAge;
-        this.flags = flags == null ? Collections.<Flag>emptySet() : Collections.unmodifiableSet( flags );
-        this.maxBufferSize = maxBufferSize;
-        this.maxBufferWaitTime = maxBufferWaitTime;
-        this.maxTrimSize = maxTrimSize;
-    }
+    @Builder.Default
+    private Set<Flag> flags = Collections.emptySet();
 
-    public int getMaxEvents( )
-    {
-        return maxEvents;
-    }
+    @Builder.Default
+    private int maxBufferSize = 1000;
 
-    public TimeDuration getMaxBufferWaitTime( )
-    {
-        return maxBufferWaitTime;
-    }
+    @Builder.Default
+    private TimeDuration maxBufferWaitTime = TimeDuration.of( 1, TimeDuration.Unit.MINUTES );
 
-    public TimeDuration getMaxAge( )
-    {
-        return maxAge;
-    }
+    @Builder.Default
+    private int maxTrimSize = 501;
 
-    public Set<Flag> getFlags( )
-    {
-        return flags;
-    }
 
-    public int getMaxBufferSize( )
+    public enum Flag
     {
-        return maxBufferSize;
+        DevDebug,
     }
 
-    public int getMaxTrimSize( )
+    LocalDBLoggerSettings applyValueChecks()
     {
-        return maxTrimSize;
+        return toBuilder()
+                .maxEvents( maxEvents < 1 ? 0 : Math.max( MINIMUM_MAXIMUM_EVENTS, maxEvents ) )
+                .maxAge( maxAge == null || maxAge.isShorterThan( MINIMUM_MAX_AGE ) ? MINIMUM_MAX_AGE : maxAge )
+                .build();
     }
 
     public static LocalDBLoggerSettings fromConfiguration( final Configuration configuration )
@@ -113,64 +93,13 @@ public class LocalDBLoggerSettings implements Serializable
         );
         final int maxTrimSize = Integer.parseInt( configuration.readAppProperty( AppProperty.LOCALDB_LOGWRITER_MAX_TRIM_SIZE ) );
 
-        return new Builder()
-                .setMaxEvents( maxEvents )
-                .setMaxAge( maxAge )
-                .setFlags( flags )
-                .setMaxBufferSize( maxBufferSize )
-                .setMaxBufferWaitTime( maxBufferWaitTime )
-                .setMaxTrimSize( maxTrimSize )
-                .createLocalDBLoggerSettings();
-    }
-
-    public static class Builder
-    {
-        private int maxEvents = 1 * 1000 * 1000;
-        private TimeDuration maxAge = TimeDuration.of( 7, TimeDuration.Unit.DAYS );
-        private Set<Flag> flags = Collections.emptySet();
-        private int maxBufferSize = 1000;
-        private TimeDuration maxBufferWaitTime = TimeDuration.of( 1, TimeDuration.Unit.MINUTES );
-        private int maxTrimSize = 501;
-
-        public Builder setMaxEvents( final int maxEvents )
-        {
-            this.maxEvents = maxEvents;
-            return this;
-        }
-
-        public Builder setMaxAge( final TimeDuration maxAge )
-        {
-            this.maxAge = maxAge;
-            return this;
-        }
-
-        public Builder setFlags( final Set<Flag> flags )
-        {
-            this.flags = flags;
-            return this;
-        }
-
-        public Builder setMaxTrimSize( final int maxTrimSize )
-        {
-            this.maxTrimSize = maxTrimSize;
-            return this;
-        }
-
-        public Builder setMaxBufferSize( final int maxBufferSize )
-        {
-            this.maxBufferSize = maxBufferSize;
-            return this;
-        }
-
-        public Builder setMaxBufferWaitTime( final TimeDuration maxBufferWaitTime )
-        {
-            this.maxBufferWaitTime = maxBufferWaitTime;
-            return this;
-        }
-
-        public LocalDBLoggerSettings createLocalDBLoggerSettings( )
-        {
-            return new LocalDBLoggerSettings( maxEvents, maxAge, flags, maxBufferSize, maxBufferWaitTime, maxTrimSize );
-        }
+        return LocalDBLoggerSettings.builder()
+                .maxEvents( maxEvents )
+                .maxAge( maxAge )
+                .flags( flags )
+                .maxBufferSize( maxBufferSize )
+                .maxBufferWaitTime( maxBufferWaitTime )
+                .maxTrimSize( maxTrimSize )
+                .build().applyValueChecks();
     }
 }

+ 17 - 0
server/src/main/java/password/pwm/util/macro/InternalMacros.java

@@ -50,6 +50,7 @@ public abstract class InternalMacros
         final Map<Class<? extends MacroImplementation>, MacroImplementation.Scope> defaultMacros = new HashMap<>();
         defaultMacros.put( PwmSettingReference.class, MacroImplementation.Scope.Static );
         defaultMacros.put( PwmAppName.class, MacroImplementation.Scope.Static );
+        defaultMacros.put( PwmVendorName.class, MacroImplementation.Scope.Static );
         defaultMacros.put( PwmContextPath.class, MacroImplementation.Scope.System );
         defaultMacros.put( EncodingMacro.class, MacroImplementation.Scope.Static );
         defaultMacros.put( CasingMacro.class, MacroImplementation.Scope.Static );
@@ -429,4 +430,20 @@ public abstract class InternalMacros
             return PwmConstants.PWM_APP_NAME;
         }
     }
+
+    public static class PwmVendorName extends InternalAbstractMacro
+    {
+        private static final Pattern PATTERN = Pattern.compile( "@PwmVendorName@" );
+
+        public Pattern getRegExPattern( )
+        {
+            return PATTERN;
+        }
+
+        public String replaceValue( final String matchValue, final MacroRequestInfo macroRequestInfo )
+                throws MacroParseException
+        {
+            return PwmConstants.PWM_VENDOR_NAME;
+        }
+    }
 }

+ 15 - 4
server/src/main/java/password/pwm/util/queue/SmsQueueManager.java

@@ -47,6 +47,7 @@ import password.pwm.svc.stats.Statistic;
 import password.pwm.svc.stats.StatisticsManager;
 import password.pwm.util.BasicAuthInfo;
 import password.pwm.util.PasswordData;
+import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.JsonUtil;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.java.TimeDuration;
@@ -498,11 +499,21 @@ public class SmsQueueManager implements PwmService
 
             final PwmHttpClientRequest pwmHttpClientRequest = makeRequest( requestData );
 
-            final PwmHttpClientConfiguration pwmHttpClientConfiguration = PwmHttpClientConfiguration.builder()
-                    .certificates( config.readSettingAsCertificate( PwmSetting.SMS_GATEWAY_CERTIFICATES ) )
-                    .build();
+            final PwmHttpClient pwmHttpClient;
+            {
+                if ( JavaHelper.isEmpty( config.readSettingAsCertificate( PwmSetting.SMS_GATEWAY_CERTIFICATES ) ) )
+                {
+                    pwmHttpClient = new PwmHttpClient( pwmApplication, sessionLabel );
+                }
+                else
+                {
+                    final PwmHttpClientConfiguration clientConfiguration = PwmHttpClientConfiguration.builder()
+                            .certificates( config.readSettingAsCertificate( PwmSetting.SMS_GATEWAY_CERTIFICATES ) )
+                            .build();
 
-            final PwmHttpClient pwmHttpClient = new PwmHttpClient( pwmApplication, sessionLabel, pwmHttpClientConfiguration );
+                    pwmHttpClient = new PwmHttpClient( pwmApplication, sessionLabel, clientConfiguration );
+                }
+            }
 
             try
             {

+ 49 - 12
server/src/main/java/password/pwm/util/secure/X509Utils.java

@@ -47,11 +47,14 @@ import javax.net.ssl.SSLContext;
 import javax.net.ssl.SSLSocket;
 import javax.net.ssl.SSLSocketFactory;
 import javax.net.ssl.TrustManager;
+import javax.net.ssl.TrustManagerFactory;
 import javax.net.ssl.X509TrustManager;
 import java.io.ByteArrayInputStream;
 import java.io.IOException;
 import java.io.OutputStream;
 import java.net.URI;
+import java.security.GeneralSecurityException;
+import java.security.KeyStore;
 import java.security.PrivateKey;
 import java.security.SecureRandom;
 import java.security.cert.CertificateEncodingException;
@@ -269,6 +272,13 @@ public abstract class X509Utils
 
     public static class PromiscuousTrustManager implements X509TrustManager
     {
+        private final SessionLabel sessionLabel;
+
+        public PromiscuousTrustManager( final SessionLabel sessionLabel )
+        {
+            this.sessionLabel = sessionLabel;
+        }
+
         public X509Certificate[] getAcceptedIssuers( )
         {
             return new X509Certificate[ 0 ];
@@ -284,7 +294,7 @@ public abstract class X509Utils
             logMsg( certs, authType );
         }
 
-        private static void logMsg( final X509Certificate[] certs, final String authType )
+        private void logMsg( final X509Certificate[] certs, final String authType )
         {
             if ( certs != null )
             {
@@ -292,7 +302,7 @@ public abstract class X509Utils
                 {
                     try
                     {
-                        LOGGER.warn( "blind trusting certificate during authType=" + authType + ", subject=" + cert.getSubjectDN().toString() );
+                        LOGGER.warn( sessionLabel, "blind trusting certificate during authType=" + authType + ", subject=" + cert.getSubjectDN().toString() );
                     }
                     catch ( Exception e )
                     {
@@ -414,12 +424,10 @@ public abstract class X509Utils
             throws CertificateEncodingException, PwmUnrecoverableException
     {
         return x509Certificate.toString()
-                + "\n:MD5 checksum: " + SecureEngine.hash( new ByteArrayInputStream( x509Certificate.getEncoded() ), PwmHashAlgorithm.MD5 )
-                + "\n:SHA1 checksum: " + SecureEngine.hash( new ByteArrayInputStream( x509Certificate.getEncoded() ), PwmHashAlgorithm.SHA1 )
-                + "\n:SHA2-256 checksum: " + SecureEngine.hash( new ByteArrayInputStream( x509Certificate.getEncoded() ), PwmHashAlgorithm.SHA256 )
-                + "\n:SHA2-512 checksum: " + SecureEngine.hash( new ByteArrayInputStream( x509Certificate.getEncoded() ), PwmHashAlgorithm.SHA512 );
-
-
+                + "\n:MD5 checksum: " + hash( x509Certificate, PwmHashAlgorithm.MD5 )
+                + "\n:SHA1 checksum: " + hash( x509Certificate, PwmHashAlgorithm.SHA1 )
+                + "\n:SHA2-256 checksum: " + hash( x509Certificate, PwmHashAlgorithm.SHA256 )
+                + "\n:SHA2-512 checksum: " + hash( x509Certificate, PwmHashAlgorithm.SHA512 );
     }
 
     public static String makeDebugText( final X509Certificate x509Certificate )
@@ -473,10 +481,9 @@ public abstract class X509Utils
         returnMap.put( CertDebugInfoKey.expireDate.toString(), JavaHelper.toIsoDate( cert.getNotAfter() ) );
         try
         {
-            returnMap.put( CertDebugInfoKey.md5Hash.toString(), SecureEngine.hash( new ByteArrayInputStream( cert.getEncoded() ), PwmHashAlgorithm.MD5 ) );
-            returnMap.put( CertDebugInfoKey.sha1Hash.toString(), SecureEngine.hash( new ByteArrayInputStream( cert.getEncoded() ), PwmHashAlgorithm.SHA1 ) );
-            returnMap.put( CertDebugInfoKey.sha512Hash.toString(), SecureEngine.hash( new ByteArrayInputStream( cert.getEncoded() ),
-                    PwmHashAlgorithm.SHA512 ) );
+            returnMap.put( CertDebugInfoKey.md5Hash.toString(), hash( cert, PwmHashAlgorithm.MD5 ) );
+            returnMap.put( CertDebugInfoKey.sha1Hash.toString(), hash( cert, PwmHashAlgorithm.SHA1 ) );
+            returnMap.put( CertDebugInfoKey.sha512Hash.toString(), hash( cert, PwmHashAlgorithm.SHA512 ) );
             if ( JavaHelper.enumArrayContainsValue( flags, DebugInfoFlag.IncludeCertificateDetail ) )
             {
                 returnMap.put( CertDebugInfoKey.detail.toString(), X509Utils.makeDetailText( cert ) );
@@ -533,4 +540,34 @@ public abstract class X509Utils
         }
         return Collections.emptyList();
     }
+
+    public static TrustManager[] getDefaultJavaTrustManager( final Configuration configuration )
+            throws PwmUnrecoverableException
+    {
+        try
+        {
+            final TrustManagerFactory tmf = TrustManagerFactory.getInstance( TrustManagerFactory.getDefaultAlgorithm() );
+            tmf.init( (KeyStore) null );
+            return tmf.getTrustManagers();
+        }
+        catch ( GeneralSecurityException e )
+        {
+            final String errorMsg = "unexpected error loading default java TrustManager: " + e.getMessage();
+            final ErrorInformation errorInformation = new ErrorInformation( PwmError.ERROR_INTERNAL, errorMsg );
+            throw new PwmUnrecoverableException( errorInformation );
+        }
+    }
+
+    public static String hash( final X509Certificate certificate, final PwmHashAlgorithm pwmHashAlgorithm )
+            throws PwmUnrecoverableException
+    {
+        try
+        {
+            return SecureEngine.hash( new ByteArrayInputStream( certificate.getEncoded() ), pwmHashAlgorithm );
+        }
+        catch ( CertificateEncodingException e )
+        {
+            throw PwmUnrecoverableException.newException( PwmError.ERROR_INTERNAL, "unexpected error encoding certificate: " + e.getMessage() );
+        }
+    }
 }

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

@@ -32,10 +32,10 @@ application.wordlistRetryImportSeconds=600
 audit.events.emailFrom=Audit Event Notification <@DefaultEmailFromAddress@>
 audit.events.emailSubject=@PwmAppName@ - Audit Event - %EVENT%
 audit.events.localdb.maxBulkRemovals=301
-audit.syslog.cef.extensions=type,cat:eventCode,act:timestamp,rt:message,msg:narrative,reason:perpetratorID,suid:perpetratorDN,suser:sourceAddress,dvc:sourceHost,dvchost:targetID,duid:targetDN,duser
+audit.syslog.cef.timezone=Zulu
 audit.syslog.cef.header.product=@PwmAppName@
-audit.syslog.cef.header.severity=Unknown
-audit.syslog.cef.header.vendor=@PwmAppName@
+audit.syslog.cef.header.severity=Medium
+audit.syslog.cef.header.vendor=@PwmVendorName@
 audit.syslog.message.length=900
 audit.syslog.message.truncateMsg=[truncated]
 auth.allowSSOwithUnknownPassword=true
@@ -123,6 +123,7 @@ http.client.alwaysLogEntities=false
 http.client.socketTimeoutMs=60000
 http.client.connectTimeoutMs=60000
 http.client.requestTimeoutMs=60000
+http.client.enableHostnameVerification=true
 http.client.promiscuous.wordlist.enable=true
 http.header.server=@PwmAppName@
 http.header.sendContentLanguage=true
@@ -158,6 +159,7 @@ http.parameter.ssoOverride=sso
 http.parameter.oauth.accessToken=access_token
 http.parameter.oauth.attributes=attributes
 http.parameter.oauth.clientID=client_id
+http.parameter.oauth.clientSecret=client_secret
 http.parameter.oauth.code=code
 http.parameter.oauth.expires=expires_in
 http.parameter.oauth.responseType=response_type
@@ -176,6 +178,7 @@ intruder.minimumDelayPenaltyMS=300
 intruder.maximumDelayPenaltyMS=3000
 intruder.delayPerCountMS=200
 intruder.delayMaxJitterMS=2000
+l10n.rtl.regex=^(ar|dv|he|iw|fa|nqo|ps|sd|ug|ur|yi|.*[-_](Arab|Hebr|Thaa|Nkoo|Tfng))(?!.*[-_](Latn|Cyrl)($|-|_))($|-|_)
 ldap.resolveCanonicalDN=true
 ldap.cache.canonical.enable=true
 ldap.cache.canonical.seconds=600

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

@@ -32,10 +32,12 @@ httpHeaderAuthorizationBasic=Basic
 defaultBadPasswordAttempt=BADPASSWORDATTEMPT
 log.removedValue=*hidden*
 pwm.appName=PWM
+pwm.vendorName=PWM
 trial=false
 url.pwm-home=https://github.com/pwm-project/pwm
 paramName.token=token
 defaultConfigFilename=PwmConfiguration.xml
+defaultPropertiesConfigFilename=silent.properties
 enableEulaDisplay=false
 missingVersionString=[Version Missing]
 applicationPathInfoFile=applicationPath.properties

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

@@ -3153,6 +3153,11 @@
             <value>false</value>
         </default>
     </setting>
+    <setting hidden="false" key="peopleSearch.enablePhoto" level="1">
+        <default>
+            <value>true</value>
+        </default>
+    </setting>
     <setting hidden="false" key="peopleSearch.enableOrgChart" level="1">
         <default>
             <value>true</value>
@@ -3277,7 +3282,7 @@
             <property key="Maximum">7</property>
         </properties>
     </setting>
-    <setting hidden="false" key="peopleSearch.displayName.user" level="1" required="true">
+    <setting hidden="true" key="peopleSearch.displayName.user" level="1" required="true">
         <flag>MacroSupport</flag>
         <default>
             <value>@LDAP:givenName@ @LDAP:sn@ - @LDAP:title@</value>
@@ -3668,7 +3673,7 @@
             <value>false</value>
         </default>
     </setting>
-    <setting hidden="false" key="helpdesk.displayName" level="2">
+    <setting hidden="true" key="helpdesk.displayName" level="2">
         <default>
             <value>@LDAP:givenName@ @LDAP:sn@ - @User:ID@ - @User:Email@</value>
         </default>

+ 8 - 1
server/src/main/resources/password/pwm/i18n/Display.properties

@@ -79,6 +79,8 @@ Button_ExportOrgChart=Export Organizational Chart
 Button_EmailTeam=Email Team Members
 Button_TokenVerification=Token Verification
 Button_SendToken=Send Token
+Button_Remove=Remove
+Button_AddSearchAttribute=Add Search Attribute
 Display_ActivateUser=To confirm your identity, please enter the following information. Your information will be used to locate and activate your user account.<p/>Be sure to complete the process, or your account will not be activated properly.
 Display_AutoGeneratedPassword=Auto-generate a new password
 Display_CapsLockIsOn=CAPS LOCK is on
@@ -215,10 +217,12 @@ Display_UpdateProfile=Please update the following information\:
 Display_UpdateProfileConfirm=Please review the following information you have entered and confirm.
 Display_UpdateProfileEnterCode=To verify your email address, a code has been sent to you at <b>%1%</b>.  Please enter the code here to continue.
 Display_UpdateProfileEnterCodeSMS=To verify your phone number, a code has been sent to you at <b>%1%</b>.  Please enter the code here to continue.
+Display_Uploading=Uploading...
 Display_UserEventHistory=This page shows your password event history. Only actions performed with this application are shown here. All times listed are in the %1% timezone.
 Display_TypingWait=Waiting for typing to complete....
 Display_EmailPrefix=Email -
 Display_SmsPrefix=SMS -
+Display_SearchAttrsUnique=Search attributes must be unique
 Field_AccountEnabled=Account Enabled
 Field_AccountExpired=Account Expired
 Field_AccountExpirationTime=Account Expiration Time
@@ -296,7 +300,7 @@ Long_Title_Main_Menu=Password self-service main menu. From here you can change y
 Long_Title_NewUser=Create a new user account.
 Long_Title_PeopleSearch=Lookup contact information for your colleagues.
 Long_Title_SetupResponses=Security questions and answers allow you to recover a forgotten password.
-Long_Title_SetupOtpSecret=Setup mobile app device.  If you forget your password, you can use your mobile device to authenticate to this site.
+Long_Title_SetupOtpSecret=Setup your mobile device to allow you to recover a forgotten password.
 Long_Title_Shortcuts=Personalized shortcuts.
 Long_Title_UpdateProfile=Update your user profile data.
 Long_Title_UserEventHistory=Password event history.  See when you have changed your password in the past.
@@ -351,6 +355,7 @@ Title_TitleBarAuthenticated=@User:ID@  Password Self Service
 Title_TitleBar=Password Self Service
 Title_UpdateProfile=Update Profile
 Title_UpdateProfileConfirm=Confirm Profile Data
+Title_UploadPhoto=Upload Photo
 Title_UserData=My Data
 Title_UserEventHistory=Password History
 Title_UserInformation=My Account
@@ -363,6 +368,7 @@ Title_Organization=Organization
 Title_Print=Print
 Title_ExportOrgChart=Export Organizational Chart
 Title_EmailOrgChart=Email Team Members
+Title_AdvancedSearch=Advanced Search
 Tooltip_PasswordStrength=The password strength meter shows how easy it is to guess your password. Try the following to make your password stronger\:<ul><li>Make the password longer</li><li>Do not repeat letters or numbers</li><li>Use mixed (upper and lower) case letters</li><li>Add more numbers</li><li>Add more symbol characters</li></ul>
 Confirm_DeleteUser=Are you sure you wish to proceed?  If you continue, the selected user will be deleted permanently.  This can not be undone.
 Confirm=Are you sure you wish to proceed?
@@ -381,3 +387,4 @@ Label_ExportLevelDepth=Export Level Depth
 Label_EmailLevelDepth=Team Level Depth
 Label_EmailTeamMembersFound=Email Addresses Found
 Label_StartingFrom=starting from {{personName}}
+Notice_UploadFailure=The upload has failed.  Please try again or ask an administrator for assistance.

+ 3 - 2
server/src/main/resources/password/pwm/i18n/Display_ca.properties

@@ -66,6 +66,7 @@ Button_OK=D'acord
 Button_SendEmail=Envia un correu electr\u00f2nic
 Button_Export=Exporta
 Button_ExportOrgChart=Exporta l'organigrama
+Button_EmailTeam=Envia un correu electr\u00f2nic als membres de l'equip
 Button_TokenVerification=Verificaci\u00f3 de testimonis
 Button_SendToken=Envia el testimoni
 Display_ActivateUser=Per tal de confirmar la seva identitat, introdueixi la informaci\u00f3 seg\u00fcent. La informaci\u00f3 s'utilitzar\u00e0 per localitzar i activar el seu compte d'usuari.<p/>Asseguri's de finalitzar el proc\u00e9s o el compte no s'activar\u00e0 correctament.
@@ -149,7 +150,7 @@ Display_RecoverTokenSendChoices=Per verificar la seva identitat, se li enviar\u0
 Display_RecoverTokenSendOneChoice=Per verificar la seva identitat, enviarem un codi de seguretat a %1%.
 Display_RecoverTokenSendChoiceEmail=Enviar el codi al seu correu electr\u00f2nic registrat.
 Display_RecoverTokenSendChoiceSMS=Enviar el codi al seu tel\u00e8fon m\u00f2bil amb un missatge de text (SMS).
-Display_RecoverTokenSuccess=Gr\u00e0cies!  El codi de seguretat enviat a <b>%1%</b> s'ha verificat.
+Display_RecoverTokenSuccess=Gr\u00e0cies\!  El codi de seguretat enviat a <b>%1%</b> s'ha verificat.
 Display_RecoverChoiceReset=Establir una contrasenya nova. Si ha oblidat la contrasenya i vol establir-ne una de nova, cliqui aqu\u00ed. El seu compte tamb\u00e9 es desbloquejar\u00e0 quan estableixi una contrasenya nova.
 Display_RecoverChoiceResetInhibit=Ara mateix no pot canviar la seva contrasenya perqu\u00e8 \u00e9s dins del l\u00edmit de durada de contrasenya m\u00ednima.
 Display_RecoverChoiceUnlock=Desbloquejar el compte. Si recorda la contrasenya, pot desbloquejar el compte seleccionant aquesta opci\u00f3. No es canviar\u00e0 la contrasenya.
@@ -360,7 +361,7 @@ Value_Default=Per defecte
 Value_ProgressComplete=Completa
 Value_ProgressInProgress=En curs
 Placeholder_Search=Cerca
-Instructions_ExportOrgChart1=Cliqui el bot\u00f3 Exporta per iniciar la desc\u00e0rrega de les dades de l'organigrama. Quan la desc\u00e0rrega hagi finalitzat, podr\u00e0 importar les dades a programes com el Visio per aplicar-hi el format que prefereixi.
+Instructions_ExportOrgChart1=Faci clic al bot\u00f3 Exporta per iniciar la desc\u00e0rrega de les dades de l'organigrama. Despr\u00e9s de la desc\u00e0rrega, podr\u00e0 fer servir les dades per importar les dades d'aquest organigrama al programa que tri\u00ef. Les dades s'exportaran en format CSV.
 Instructions_ExportOrgChart2=Tri\u00ef la profunditat d'exportaci\u00f3 i premi el bot\u00f3 Exporta a continuaci\u00f3.
 Instructions_EmailTeam1=La llista de correu electr\u00f2nic es genera a partir de les dades de l'organitzaci\u00f3 des d'aquest punt. En clicar el bot\u00f3 Envia per correu electr\u00f2nic, el seu programa de correu electr\u00f2nic per defecte s'hauria d'obrir autom\u00e0ticament i mostrar la llista d'adreces de correu electr\u00f2nic ja emplenada.
 Instructions_EmailTeam2=Tri\u00ef la profunditat de l'equip i premi el bot\u00f3 Envia per correu electr\u00f2nic a continuaci\u00f3.

+ 3 - 2
server/src/main/resources/password/pwm/i18n/Display_da.properties

@@ -66,6 +66,7 @@ Button_OK=OK
 Button_SendEmail=Send e-mail
 Button_Export=Eksport\u00e9r
 Button_ExportOrgChart=Eksport\u00e9r organisationsdiagram
+Button_EmailTeam=Send en e-mail til teammedlemmer
 Button_TokenVerification=Tokenbekr\u00e6ftelse
 Button_SendToken=Send token
 Display_ActivateUser=Angiv f\u00f8lgende oplysninger for at bekr\u00e6fte din identitet. Dine oplysninger bruges til at lokalisere og aktivere din brugerkonto.<p/>S\u00f8rg for at gennemf\u00f8re processen, ellers aktiveres din konto ikke korrekt.
@@ -149,7 +150,7 @@ Display_RecoverTokenSendChoices=Der sendes en sikkerhedskode til dig for at bekr
 Display_RecoverTokenSendOneChoice=En sikkerhedskode sendes til dig p\u00e5 %1% for at f\u00e5 bekr\u00e6ftet din identitet.
 Display_RecoverTokenSendChoiceEmail=Send koden til din registrerede e-mailadresse.
 Display_RecoverTokenSendChoiceSMS=Send koden til din mobiltelefon via sms.
-Display_RecoverTokenSuccess=Tak! Din sikkerhedskode, der er sendt til <b>%1%</b>, er blevet bekr\u00e6ftet.
+Display_RecoverTokenSuccess=Tak\! Din sikkerhedskode, der blev sendt til <b>%1%</b>, er blevet bekr\u00e6ftet.
 Display_RecoverChoiceReset=Angiv en ny adgangskode. Hvis du har glemt adgangskoden og gerne vil angive en ny, skal du klikke her. Din konto l\u00e5ses op, n\u00e5r du angiver en ny adgangskode.
 Display_RecoverChoiceResetInhibit=Din adgangskode kan ikke \u00e6ndres p\u00e5 nuv\u00e6rende tidspunkt, da du er inden for minimumgr\u00e6nsen for adgangskodens levetid.
 Display_RecoverChoiceUnlock=L\u00e5s din konto op. Hvis du kan huske din adgangskode, kan du l\u00e5se din konto op ved at v\u00e6lge denne indstilling. Adgangskoden \u00e6ndres ikke.
@@ -358,7 +359,7 @@ Value_Default=Standard
 Value_ProgressComplete=Udf\u00f8rt
 Value_ProgressInProgress=I gang
 Placeholder_Search=S\u00f8g
-Instructions_ExportOrgChart1=Klik p\u00e5 knappen Eksport\u00e9r for at begynde download af data fra organisationsdiagrammet. N\u00e5r du er f\u00e6rdig med at downloade, kan du importere disse data i et program som f.eks. Visio, s\u00e5 de kan formateres til det \u00f8nskede output.
+Instructions_ExportOrgChart1=Klik p\u00e5 knappen Export\u00e9r for at begynde at downloade organisationsdiagramdata. N\u00e5r download er fuldf\u00f8rt, kan du bruge dataene til at importere disse organisationsdiagramdata til et valgfrit program. Dataene eksporteres i CSV-format.
 Instructions_ExportOrgChart2=V\u00e6lg dybden af eksportniveauet, og tryk p\u00e5 knappen Eksport\u00e9r nedenfor.
 Instructions_EmailTeam1=E-maillisten oprettes p\u00e5 baggrund af organisationsdata fra og med dette punkt. N\u00e5r du klikker p\u00e5 knappen Send e-mail, \u00e5bnes dit standard-e-mailprogram automatisk, og listen over e-mailadresser er udfyldt p\u00e5 forh\u00e5nd.
 Instructions_EmailTeam2=V\u00e6lg dybden af teamniveauet, og tryk p\u00e5 knappen E-mail nedenfor.

+ 3 - 2
server/src/main/resources/password/pwm/i18n/Display_de.properties

@@ -66,6 +66,7 @@ Button_OK=OK
 Button_SendEmail=Email senden
 Button_Export=Exportieren
 Button_ExportOrgChart=Organigramm exportieren
+Button_EmailTeam=Email an Teammitglieder
 Button_TokenVerification=Token-\u00dcberpr\u00fcfung
 Button_SendToken=Token senden
 Display_ActivateUser=Geben Sie die folgenden Informationen ein, um Ihre Identit\u00e4t zu best\u00e4tigen. Anhand dieser Informationen wird Ihr Benutzerkonto ermittelt und aktiviert.<p/>Schlie\u00dfen Sie den Vorgang vollst\u00e4ndig ab, damit Ihr Konto ordnungsgem\u00e4\u00df aktiviert werden kann.
@@ -149,7 +150,7 @@ Display_RecoverTokenSendChoices=Zur \u00dcberpr\u00fcfung Ihrer Identit\u00e4t w
 Display_RecoverTokenSendOneChoice=Zur \u00dcberpr\u00fcfung Ihrer Identit\u00e4t wird Ihnen ein Sicherheitscode an %1% gesendet.
 Display_RecoverTokenSendChoiceEmail=Code an die registrierte Email-Adresse senden.
 Display_RecoverTokenSendChoiceSMS=Code in einer Textnachricht (SMS) an das Mobiltelefon senden.
-Display_RecoverTokenSuccess=Vielen Dank! Ihr an <b>%1%</b> gesendeter Sicherheitscode wurde \u00fcberpr\u00fcft.
+Display_RecoverTokenSuccess=Vielen Dank\! Ihr an <b>%1%</b> gesendeter Sicherheitscode wurde \u00fcberpr\u00fcft.
 Display_RecoverChoiceReset=Legen Sie ein neues Passwort fest. Falls Sie Ihr Passwort vergessen haben und ein neues Passwort festlegen m\u00f6chten, klicken Sie hier. Ihr Konto wird auch entsperrt, wenn Sie ein neues Passwort festlegen.
 Display_RecoverChoiceResetInhibit=Das Passwort kann zurzeit nicht ge\u00e4ndert werden, weil die minimale G\u00fcltigkeitsdauer des Passworts noch nicht erreicht wurde.
 Display_RecoverChoiceUnlock=Entsperren Sie Ihr Konto. Wenn Sie sich an Ihr Passwort erinnern, k\u00f6nnen Sie das Konto durch Auswahl dieser Option entsperren. Das Passwort wird nicht ge\u00e4ndert.
@@ -358,7 +359,7 @@ Value_Default=Standard
 Value_ProgressComplete=Abgeschlossen
 Value_ProgressInProgress=In Bearbeitung
 Placeholder_Search=Suchen
-Instructions_ExportOrgChart1=Klicken Sie auf die Schaltfl\u00e4che 'Exportieren', um mit dem Herunterladen der Organigrammdaten zu beginnen. Nach Abschluss des Herunterladens k\u00f6nnen Sie die Daten in ein Programm wie Visio importieren und wie gew\u00fcnscht f\u00fcr die Ausgabe formatieren.
+Instructions_ExportOrgChart1=Klicken Sie auf die Schaltfl\u00e4che 'Exportieren', um mit dem Herunterladen der Organigrammdaten zu beginnen. Nachdem das Herunterladen abgeschlossen ist, k\u00f6nnen Sie die Organigrammdaten in ein Programm Ihrer Wahl importieren. Die Daten werden im CSV-Format exportiert.
 Instructions_ExportOrgChart2=W\u00e4hlen Sie die Exporttiefe und klicken Sie unten auf die Schaltfl\u00e4che 'Exportieren'.
 Instructions_EmailTeam1=Die Email-Liste wird basierend auf Organigrammdaten ab diesem Punkt generiert. Wenn Sie auf die Schaltfl\u00e4che 'Email senden' klicken, wird das standardm\u00e4\u00dfige Email-Programm ge\u00f6ffnet und das Email-Adressfeld automatisch mit der Liste ausgef\u00fcllt.
 Instructions_EmailTeam2=W\u00e4hlen Sie die Teamebenentiefe und klicken Sie unten auf die Schaltfl\u00e4che 'Email'.

+ 3 - 2
server/src/main/resources/password/pwm/i18n/Display_en_CA.properties

@@ -66,6 +66,7 @@ Button_OK=OK
 Button_SendEmail=Send Email
 Button_Export=Export
 Button_ExportOrgChart=Export Organizational Chart
+Button_EmailTeam=Email Team Members
 Button_TokenVerification=Token Verification
 Button_SendToken=Send Token
 Display_ActivateUser=To confirm your identity, please enter the following information.Your information will be used to locate and activate your user account.<p/>Be sure to complete the process or your account will not be activated properly.
@@ -149,7 +150,7 @@ Display_RecoverTokenSendChoices=To verify your identity, a security code will be
 Display_RecoverTokenSendOneChoice=To verify your identity, a security code will be sent to you at %1%.
 Display_RecoverTokenSendChoiceEmail=Send code to your registered email address.
 Display_RecoverTokenSendChoiceSMS=Send code to your mobile phone using text messaging (SMS).
-Display_RecoverTokenSuccess=Thank you! Your security code sent to <b>%1%</b> has been verified.
+Display_RecoverTokenSuccess=Thank you\! Your security code sent to <b>%1%</b> has been verified.
 Display_RecoverChoiceReset=Set a new password. If you have forgotten your password and would like to set a new one, click here. Your account will also be unlocked when you set a new password.
 Display_RecoverChoiceResetInhibit=Your password cannot be changed at this time because it is within the minimum password lifetime limit.
 Display_RecoverChoiceUnlock=Unlock your account. If you remember your password, you can unlock your account by selecting this option. Your password will not be changed.
@@ -358,7 +359,7 @@ Value_Default=Default
 Value_ProgressComplete=Complete
 Value_ProgressInProgress=In Progress
 Placeholder_Search=Search
-Instructions_ExportOrgChart1=Click the Export button to begin download of the organizational chart data. After download is complete, you can import the data into a program like Visio so that it can be formatted into the desired output.
+Instructions_ExportOrgChart1=Click the Export button to begin download of the organizational chart data. After download is complete, you can use the data to import this org chart data into a program of your choice. The data is exported in CSV format.
 Instructions_ExportOrgChart2=Choose the export level depth and press the Export button below.
 Instructions_EmailTeam1=The email list is generated based on organizational data starting at this point. When you click the Send Email button, your default email program should automatically open, with the list of email addresses pre-filled.
 Instructions_EmailTeam2=Choose the team level depth and press the Email button below.

+ 3 - 2
server/src/main/resources/password/pwm/i18n/Display_es.properties

@@ -66,6 +66,7 @@ Button_OK=Aceptar
 Button_SendEmail=Enviar correo electr\u00f3nico
 Button_Export=Exportar
 Button_ExportOrgChart=Exportar organigrama corporativo
+Button_EmailTeam=Miembros del equipo de correo electr\u00f3nico
 Button_TokenVerification=Verificaci\u00f3n de testigo
 Button_SendToken=Enviar testigo
 Display_ActivateUser=Para confirmar su identidad, introduzca la siguiente informaci\u00f3n. Su informaci\u00f3n se utilizar\u00e1 para localizar y activar su cuenta de usuario.<p/>Aseg\u00farese de completar el proceso o no se activar\u00e1 correctamente su cuenta.
@@ -149,7 +150,7 @@ Display_RecoverTokenSendChoices=Se le enviar\u00e1 un c\u00f3digo de seguridad p
 Display_RecoverTokenSendOneChoice=A fin de verificar su identidad, se le enviar\u00e1 un c\u00f3digo de seguridad a %1%.
 Display_RecoverTokenSendChoiceEmail=Enviar el c\u00f3digo a su direcci\u00f3n de correo electr\u00f3nico registrada.
 Display_RecoverTokenSendChoiceSMS=Enviar el c\u00f3digo a su tel\u00e9fono m\u00f3vil mediante mensaje de texto (SMS).
-Display_RecoverTokenSuccess=\u00a1Gracias! Su c\u00f3digo de seguridad enviado a <b>%1%</b> ha sido verificado.
+Display_RecoverTokenSuccess=\u00a1Gracias\!  Su c\u00f3digo de seguridad enviado a <b>%1%</b> ha sido verificado.
 Display_RecoverChoiceReset=Establezca una nueva contrase\u00f1a. Si ha olvidado su contrase\u00f1a y desea establecer una nueva, haga clic aqu\u00ed. Su cuenta se desbloquear\u00e1 tambi\u00e9n al configurar una nueva contrase\u00f1a.
 Display_RecoverChoiceResetInhibit=No se puede cambiar su contrase\u00f1a en este momento porque se encuentra dentro del l\u00edmite m\u00ednimo de duraci\u00f3n de la contrase\u00f1a.
 Display_RecoverChoiceUnlock=Desbloquee su cuenta. Si recuerda su contrase\u00f1a, puede desbloquear su cuenta seleccionando esta opci\u00f3n. No se modificar\u00e1 su contrase\u00f1a.
@@ -358,7 +359,7 @@ Value_Default=Por defecto
 Value_ProgressComplete=Finalizado
 Value_ProgressInProgress=En curso
 Placeholder_Search=Buscar
-Instructions_ExportOrgChart1=Haga clic en el bot\u00f3n Exportar para comenzar a descargar los datos del organigrama corporativo. Una vez finalizada la descarga, podr\u00e1 importar los datos a un programa como Visio para darles el formato de salida deseado.
+Instructions_ExportOrgChart1=Haga clic en el bot\u00f3n Exportar para descargar los datos del organigrama corporativo. Cuando finalice la descarga, podr\u00e1 usar estos datos para importar el organigrama corporativo al programa de su elecci\u00f3n. Los datos se exportan en formato CSV.
 Instructions_ExportOrgChart2=Elija la profundidad del nivel de exportaci\u00f3n y pulse el bot\u00f3n Exportar a continuaci\u00f3n.
 Instructions_EmailTeam1=La lista de correo electr\u00f3nico se genera en funci\u00f3n de los datos corporativos a partir de este punto. Al hacer clic en el bot\u00f3n Enviar correo electr\u00f3nico, deber\u00eda abrirse autom\u00e1ticamente el programa de correo electr\u00f3nico por defecto con la lista de direcciones de correo previamente completada.
 Instructions_EmailTeam2=Elija la profundidad del nivel de equipo y pulse el bot\u00f3n Correo electr\u00f3nico a continuaci\u00f3n.

+ 3 - 2
server/src/main/resources/password/pwm/i18n/Display_fr.properties

@@ -66,6 +66,7 @@ Button_OK=OK
 Button_SendEmail=Envoyer un message \u00e9lectronique
 Button_Export=Exporter
 Button_ExportOrgChart=Exporter l'organigramme
+Button_EmailTeam=Envoyer un message \u00e9lectronique aux membres de l'\u00e9quipe
 Button_TokenVerification=V\u00e9rification du jeton
 Button_SendToken=Envoyer un jeton
 Display_ActivateUser=Pour confirmer votre identit\u00e9, entrez les informations suivantes. Elles serviront \u00e0 localiser votre compte utilisateur et \u00e0 l'activer.<p/>Veillez \u00e0 finaliser le processus pour que votre compte soit activ\u00e9 correctement.
@@ -149,7 +150,7 @@ Display_RecoverTokenSendChoices=Un code de s\u00e9curit\u00e9 vous sera envoy\u0
 Display_RecoverTokenSendOneChoice=Pour v\u00e9rifier votre identit\u00e9, un code de s\u00e9curit\u00e9 vous sera envoy\u00e9 \u00e0 l'adresse/au num\u00e9ro %1%.
 Display_RecoverTokenSendChoiceEmail=Envoyer le code \u00e0 votre adresse \u00e9lectronique enregistr\u00e9e.
 Display_RecoverTokenSendChoiceSMS=Envoyer le code par message texte (SMS) sur votre t\u00e9l\u00e9phone mobile.
-Display_RecoverTokenSuccess=Merci ! Votre code de s\u00e9curit\u00e9 envoy\u00e9 \u00e0 l'adresse/au num\u00e9ro <b>%1%</b> a \u00e9t\u00e9 v\u00e9rifi\u00e9.
+Display_RecoverTokenSuccess=Merci \! Votre code de s\u00e9curit\u00e9 envoy\u00e9 \u00e0 l'adresse/au num\u00e9ro <b>%1%</b> a \u00e9t\u00e9 v\u00e9rifi\u00e9.
 Display_RecoverChoiceReset=D\u00e9finissez un nouveau mot de passe. Si vous avez oubli\u00e9 votre mot de passe et souhaitez en d\u00e9finir un nouveau, cliquez ici. Votre compte sera \u00e9galement d\u00e9verrouill\u00e9 lorsque vous d\u00e9finirez un nouveau mot de passe.
 Display_RecoverChoiceResetInhibit=Votre mot de passe ne peut pas \u00eatre modifi\u00e9 pour l'instant, car il est compris dans la limite minimale de dur\u00e9e de vie du mot de passe.
 Display_RecoverChoiceUnlock=D\u00e9verrouillez votre compte. Si vous vous souvenez de votre mot de passe, vous pouvez d\u00e9verrouiller votre compte en s\u00e9lectionnant cette option. Votre mot de passe ne sera pas chang\u00e9.
@@ -358,7 +359,7 @@ Value_Default=Valeur par d\u00e9faut
 Value_ProgressComplete=Effectu\u00e9
 Value_ProgressInProgress=En cours
 Placeholder_Search=Rechercher
-Instructions_ExportOrgChart1=Cliquez sur le bouton Exporter pour d\u00e9marrer le t\u00e9l\u00e9chargement des donn\u00e9es de l'organigramme. Une fois le t\u00e9l\u00e9chargement effectu\u00e9, vous pouvez importer les donn\u00e9es dans un programme tel que Visio pour qu'elles puissent \u00eatre format\u00e9es selon le r\u00e9sultat souhait\u00e9.
+Instructions_ExportOrgChart1=Cliquez sur le bouton Exporter pour commencer le t\u00e9l\u00e9chargement des donn\u00e9es de l'organigramme. \u00c0 l'issue du t\u00e9l\u00e9chargement, vous pouvez importer ces donn\u00e9es dans le programme de votre choix. Elles sont export\u00e9es au format CSV.
 Instructions_ExportOrgChart2=S\u00e9lectionnez la profondeur du niveau d'exportation et appuyez sur le bouton Exporter ci-dessous.
 Instructions_EmailTeam1=La liste des adresses \u00e9lectroniques est g\u00e9n\u00e9r\u00e9e sur la base des donn\u00e9es organisationnelles \u00e0 partir de ce point. Lorsque vous cliquez sur le bouton Envoyer un message \u00e9lectronique, votre programme de messagerie par d\u00e9faut doit s'ouvrir automatiquement, avec la liste pr\u00e9compl\u00e9t\u00e9e avec les adresses \u00e9lectroniques.
 Instructions_EmailTeam2=S\u00e9lectionnez la profondeur du niveau de l'\u00e9quipe et appuyez sur le bouton Courrier \u00e9lectronique ci-dessous.

+ 3 - 2
server/src/main/resources/password/pwm/i18n/Display_fr_CA.properties

@@ -66,6 +66,7 @@ Button_OK=OK
 Button_SendEmail=Envoyer un courriel
 Button_Export=Exporter
 Button_ExportOrgChart=Exporter l'organigramme
+Button_EmailTeam=Envoyer un courriel aux membres de l'\u00e9quipe
 Button_TokenVerification=V\u00e9rification du jeton
 Button_SendToken=Envoyer un jeton
 Display_ActivateUser=Pour confirmer votre identit\u00e9, entrez les informations suivantes. Elles serviront \u00e0 localiser votre compte utilisateur et \u00e0 l'activer.<p/>Veillez \u00e0 finaliser le processus pour que votre compte soit activ\u00e9 correctement.
@@ -149,7 +150,7 @@ Display_RecoverTokenSendChoices=Un code de s\u00e9curit\u00e9 vous sera envoy\u0
 Display_RecoverTokenSendOneChoice=Pour v\u00e9rifier votre identit\u00e9, un code de s\u00e9curit\u00e9 vous sera envoy\u00e9 \u00e0 %1%.
 Display_RecoverTokenSendChoiceEmail=Envoyer le code \u00e0 votre adresse \u00e9lectronique enregistr\u00e9e.
 Display_RecoverTokenSendChoiceSMS=Envoyer le code par messagerie texte (SMS) sur votre t\u00e9l\u00e9phone mobile.
-Display_RecoverTokenSuccess=Merci!  Votre code de s\u00e9curit\u00e9 envoy\u00e9 \u00e0 <b>%1%</b> a \u00e9t\u00e9 v\u00e9rifi\u00e9.
+Display_RecoverTokenSuccess=Merci\!  Votre code de s\u00e9curit\u00e9 envoy\u00e9 \u00e0 <b>%1%</b> a \u00e9t\u00e9 v\u00e9rifi\u00e9.
 Display_RecoverChoiceReset=D\u00e9finissez un nouveau mot de passe. Si vous avez oubli\u00e9 votre mot de passe et souhaitez en d\u00e9finir un nouveau, cliquez ici. Votre compte sera \u00e9galement d\u00e9verrouill\u00e9 lorsque vous d\u00e9finirez un nouveau mot de passe.
 Display_RecoverChoiceResetInhibit=Votre mot de passe ne peut pas \u00eatre modifi\u00e9 pour le moment parce qu'il n'a pas encore atteint la dur\u00e9e de vie minimale du mot de passe.
 Display_RecoverChoiceUnlock=D\u00e9verrouillez votre compte. Si vous vous souvenez de votre mot de passe, vous pouvez d\u00e9verrouiller votre compte en s\u00e9lectionnant cette option. Votre mot de passe ne sera pas chang\u00e9.
@@ -358,7 +359,7 @@ Value_Default=Valeur par d\u00e9faut
 Value_ProgressComplete=Effectu\u00e9
 Value_ProgressInProgress=En cours
 Placeholder_Search=Rechercher
-Instructions_ExportOrgChart1=Cliquez sur le bouton Exporter pour commencer le t\u00e9l\u00e9chargement des donn\u00e9es de l'organigramme. Une fois le t\u00e9l\u00e9chargement termin\u00e9, vous pouvez importer les donn\u00e9es dans un programme tel que Visio afin de les formater pour obtenir le r\u00e9sultat souhait\u00e9.
+Instructions_ExportOrgChart1=Cliquez sur le bouton Exporter pour commencer le t\u00e9l\u00e9chargement des donn\u00e9es de l'organigramme. Une fois le t\u00e9l\u00e9chargement termin\u00e9, vous pouvez utiliser les donn\u00e9es pour importer ces donn\u00e9es d\u2019organigramme dans un programme de votre choix. Les donn\u00e9es sont export\u00e9es au format CSV.
 Instructions_ExportOrgChart2=Choisissez la profondeur du niveau d'exportation et appuyez sur le bouton Exporter ci-dessous.
 Instructions_EmailTeam1=La liste de courriels est g\u00e9n\u00e9r\u00e9e sur la base des donn\u00e9es organisationnelles \u00e0 partir de ce point. Lorsque vous cliquez sur le bouton Envoyer un courriel, votre programme de courriel par d\u00e9faut devrait s'ouvrir automatiquement, la liste des adresses de courriel \u00e9tant pr\u00e9remplie.
 Instructions_EmailTeam2=Choisissez la profondeur du niveau d'\u00e9quipe, et appuyez sur le bouton Envoyer un courriel ci-dessous.

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