Kaynağa Gözat

Merge branch 'master' into patch-2

Jason Rivard 6 yıl önce
ebeveyn
işleme
63ec02bd1e
100 değiştirilmiş dosya ile 3067 ekleme ve 705 silme
  1. 55 0
      build/checkstyle-header.xml
  2. 2 0
      build/checkstyle-import.xml
  3. 2 19
      build/checkstyle.xml
  4. 22 0
      build/license-header-js.txt
  5. 2 1
      build/license-header-xml.txt
  6. 4 0
      client/pom.xml
  7. 1 2
      client/src/modules/helpdesk/helpdesk-detail.component.ts
  8. 1 1
      client/src/services/helpdesk-config.service.test-data.ts
  9. 1 0
      client/src/services/helpdesk.service.test-data.ts
  10. 1 2
      client/src/services/object.service.ts
  11. 1 2
      client/src/services/people.service.dev.ts
  12. 1 2
      client/src/services/promise.service.ts
  13. 1 90
      data-service/pom.xml
  14. 14 7
      data-service/src/main/java/password/pwm/receiver/Settings.java
  15. 11 3
      data-service/src/main/java/password/pwm/receiver/Storage.java
  16. 2 2
      data-service/src/main/webapp/META-INF/context.xml
  17. 2 1
      data-service/src/main/webapp/WEB-INF/web.xml
  18. 3 1
      docker/pom.xml
  19. 2 25
      onejar/pom.xml
  20. 21 10
      onejar/src/main/java/password/pwm/onejar/ArgumentParser.java
  21. 30 15
      onejar/src/main/java/password/pwm/onejar/TomcatOneJarMain.java
  22. 22 0
      onejar/src/main/resources/ROOT-redirect-webapp/WEB-INF/index.jsp
  23. 20 0
      onejar/src/main/resources/ROOT-redirect-webapp/WEB-INF/web.xml
  24. 180 0
      pom.xml
  25. 122 0
      pwm-cr/pom.xml
  26. 415 0
      pwm-cr/src/main/java/password/pwm/cr/ChaiXmlResponseSetSerializer.java
  27. 48 0
      pwm-cr/src/main/java/password/pwm/cr/CrUtils.java
  28. 27 0
      pwm-cr/src/main/java/password/pwm/cr/JsonStoredResponseSerializer.java
  29. 56 0
      pwm-cr/src/main/java/password/pwm/cr/StoredItemUtils.java
  30. 91 0
      pwm-cr/src/main/java/password/pwm/cr/api/ChallengeItemPolicy.java
  31. 46 0
      pwm-cr/src/main/java/password/pwm/cr/api/ChallengeSetPolicy.java
  32. 29 0
      pwm-cr/src/main/java/password/pwm/cr/api/QuestionSource.java
  33. 29 0
      pwm-cr/src/main/java/password/pwm/cr/api/ResponseLevel.java
  34. 38 0
      pwm-cr/src/main/java/password/pwm/cr/api/StoredChallengeItem.java
  35. 40 0
      pwm-cr/src/main/java/password/pwm/cr/api/StoredResponseItem.java
  36. 42 0
      pwm-cr/src/main/java/password/pwm/cr/api/StoredResponseSet.java
  37. 53 0
      pwm-cr/src/main/java/password/pwm/cr/hash/AbstractHashMachine.java
  38. 76 0
      pwm-cr/src/main/java/password/pwm/cr/hash/HashFactory.java
  39. 46 0
      pwm-cr/src/main/java/password/pwm/cr/hash/HashParameter.java
  40. 126 0
      pwm-cr/src/main/java/password/pwm/cr/hash/PBKDF2HashMachine.java
  41. 50 0
      pwm-cr/src/main/java/password/pwm/cr/hash/ResponseHashAlgorithm.java
  42. 5 9
      pwm-cr/src/main/java/password/pwm/cr/hash/ResponseHashMachine.java
  43. 28 0
      pwm-cr/src/main/java/password/pwm/cr/hash/ResponseHashMachineSpi.java
  44. 67 0
      pwm-cr/src/main/java/password/pwm/cr/hash/TextHashMachine.java
  45. 144 0
      pwm-cr/src/main/java/password/pwm/cr/hash/TypicalHashMachine.java
  46. 139 0
      pwm-cr/src/test/java/password/pwm/cr/ChaiXmlResponseSet1Test.java
  47. 53 0
      pwm-cr/src/test/java/password/pwm/cr/ChaiXmlResponseSetReaderTest.java
  48. 47 0
      pwm-cr/src/test/resources/password/pwm/cr/ChaiXmlResponseSet1.xml
  49. 5 131
      rest-test-service/pom.xml
  50. 2 2
      rest-test-service/src/main/webapp/META-INF/context.xml
  51. 4 3
      rest-test-service/src/main/webapp/WEB-INF/web.xml
  52. 5 145
      server/pom.xml
  53. 3 0
      server/src/main/java/password/pwm/AppProperty.java
  54. 1 1
      server/src/main/java/password/pwm/PwmApplication.java
  55. 2 1
      server/src/main/java/password/pwm/PwmEnvironment.java
  56. 5 5
      server/src/main/java/password/pwm/bean/LocalSessionStateBean.java
  57. 1 1
      server/src/main/java/password/pwm/bean/LoginInfoBean.java
  58. 2 3
      server/src/main/java/password/pwm/config/PwmSetting.java
  59. 1 2
      server/src/main/java/password/pwm/config/profile/LdapProfile.java
  60. 2 1
      server/src/main/java/password/pwm/config/value/ActionValue.java
  61. 5 0
      server/src/main/java/password/pwm/config/value/data/FormConfiguration.java
  62. 2 0
      server/src/main/java/password/pwm/error/PwmError.java
  63. 1 0
      server/src/main/java/password/pwm/health/HealthMessage.java
  64. 2 0
      server/src/main/java/password/pwm/http/PwmSession.java
  65. 4 0
      server/src/main/java/password/pwm/http/PwmURL.java
  66. 0 1
      server/src/main/java/password/pwm/http/SessionManager.java
  67. 9 3
      server/src/main/java/password/pwm/http/client/PwmHttpClient.java
  68. 10 3
      server/src/main/java/password/pwm/http/filter/RequestInitializationFilter.java
  69. 5 5
      server/src/main/java/password/pwm/http/filter/SessionFilter.java
  70. 12 8
      server/src/main/java/password/pwm/ldap/LdapOperationsHelper.java
  71. 7 3
      server/src/main/java/password/pwm/svc/cache/CacheDebugItem.java
  72. 19 15
      server/src/main/java/password/pwm/svc/cache/CacheService.java
  73. 27 1
      server/src/main/java/password/pwm/svc/cache/MemoryCacheStore.java
  74. 38 0
      server/src/main/java/password/pwm/svc/cluster/ClusterDataServiceProvider.java
  75. 49 83
      server/src/main/java/password/pwm/svc/cluster/ClusterMachine.java
  76. 111 17
      server/src/main/java/password/pwm/svc/cluster/ClusterService.java
  77. 14 7
      server/src/main/java/password/pwm/svc/cluster/ClusterSettings.java
  78. 36 0
      server/src/main/java/password/pwm/svc/cluster/ClusterStatistics.java
  79. 147 0
      server/src/main/java/password/pwm/svc/cluster/DatabaseClusterDataService.java
  80. 178 0
      server/src/main/java/password/pwm/svc/cluster/LDAPClusterDataService.java
  81. 3 3
      server/src/main/java/password/pwm/svc/cluster/StoredNodeData.java
  82. 4 1
      server/src/main/java/password/pwm/svc/shorturl/AbstractUrlShortener.java
  83. 1 0
      server/src/main/java/password/pwm/util/BasicAuthInfo.java
  84. 5 1
      server/src/main/java/password/pwm/util/CaptchaUtility.java
  85. 8 7
      server/src/main/java/password/pwm/util/RandomPasswordGenerator.java
  86. 3 0
      server/src/main/java/password/pwm/util/form/FormUtility.java
  87. 2 0
      server/src/main/java/password/pwm/util/java/JavaHelper.java
  88. 5 5
      server/src/main/java/password/pwm/util/java/TimeDuration.java
  89. 5 5
      server/src/main/java/password/pwm/util/localdb/LocalDB.java
  90. 25 1
      server/src/main/java/password/pwm/util/localdb/XodusLocalDB.java
  91. 11 10
      server/src/main/java/password/pwm/util/operations/PasswordUtility.java
  92. 12 0
      server/src/main/java/password/pwm/util/operations/cr/CrOperator.java
  93. 14 3
      server/src/main/java/password/pwm/util/operations/otp/AbstractOtpOperator.java
  94. 3 11
      server/src/main/java/password/pwm/util/operations/otp/DbOtpOperator.java
  95. 0 14
      server/src/main/java/password/pwm/util/operations/otp/LdapOtpOperator.java
  96. 4 0
      server/src/main/java/password/pwm/util/operations/otp/OTPPamUtil.java
  97. 6 0
      server/src/main/java/password/pwm/util/operations/otp/OTPUrlUtil.java
  98. 11 2
      server/src/main/java/password/pwm/util/operations/otp/PasscodeGenerator.java
  99. 3 0
      server/src/main/resources/password/pwm/AppProperty.properties
  100. 15 9
      server/src/main/resources/password/pwm/config/PwmSetting.xml

+ 55 - 0
build/checkstyle-header.xml

@@ -0,0 +1,55 @@
+<?xml version="1.0"?>
+<!--
+  ~ Password Management Servlets (PWM)
+  ~ http://www.pwm-project.org
+  ~
+  ~ Copyright (c) 2006-2009 Novell, Inc.
+  ~ Copyright (c) 2009-2018 The PWM Project
+  ~
+  ~ This program is free software; you can redistribute it and/or modify
+  ~ it under the terms of the GNU General Public License as published by
+  ~ the Free Software Foundation; either version 2 of the License, or
+  ~ (at your option) any later version.
+  ~
+  ~ This program is distributed in the hope that it will be useful,
+  ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+  ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  ~ GNU General Public License for more details.
+  ~
+  ~ You should have received a copy of the GNU General Public License
+  ~ along with this program; if not, write to the Free Software
+  ~ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+  -->
+
+<!DOCTYPE module PUBLIC
+        "-//Puppy Crawl//DTD Check Configuration 1.2//EN"
+        "http://www.puppycrawl.com/dtds/configuration_1_2.dtd">
+
+<!--
+  PWM Checkstyle definition
+-->
+
+<module name="Checker">
+
+    <module name="Header">
+        <property name="headerFile" value="${basedir}/build/license-header-jsp.txt"/>
+        <property name="fileExtensions" value="jsp"/>
+    </module>
+    <module name="Header">
+        <property name="headerFile" value="${basedir}/build/license-header-java.txt"/>
+        <property name="fileExtensions" value="java,css"/>
+    </module>
+    <module name="Header">
+        <property name="headerFile" value="${basedir}/build/license-header-xml.txt"/>
+        <property name="fileExtensions" value="xml,svg"/>
+    </module>
+    <module name="Header">
+        <property name="headerFile" value="${basedir}/build/license-header-properties.txt"/>
+        <property name="fileExtensions" value="properties"/>
+    </module>
+    <module name="Header">
+        <property name="headerFile" value="${basedir}/build/license-header-js.txt"/>
+        <property name="fileExtensions" value="js,ts"/>
+    </module>
+
+</module>

+ 2 - 0
build/checkstyle-import.xml

@@ -37,6 +37,8 @@
     <allow pkg="java.lang"/>
     <allow pkg="java.lang"/>
     <allow pkg="java.security"/>
     <allow pkg="java.security"/>
 
 
+    <allow pkg="org.apache.catalina"/>
+
     <!-- chai -->
     <!-- chai -->
     <allow pkg="com.novell.ldapchai"/>
     <allow pkg="com.novell.ldapchai"/>
 
 

+ 2 - 19
build/checkstyle.xml

@@ -31,23 +31,6 @@
 
 
 <module name="Checker">
 <module name="Checker">
 
 
-    <module name="Header">
-        <property name="headerFile" value="${basedir}/../build/license-header-jsp.txt"/>
-        <property name="fileExtensions" value="jsp"/>
-    </module>
-    <module name="Header">
-        <property name="headerFile" value="${basedir}/../build/license-header-java.txt"/>
-        <property name="fileExtensions" value="java,css"/>
-    </module>
-    <module name="Header">
-        <property name="headerFile" value="${basedir}/../build/license-header-xml.txt"/>
-        <property name="fileExtensions" value="xml,svg"/>
-    </module>
-    <module name="Header">
-        <property name="headerFile" value="${basedir}/../build/license-header-properties.txt"/>
-        <property name="fileExtensions" value="properties"/>
-    </module>
-
 
 
     <!-- Checks that each Java package has a Javadoc file used for commenting. -->
     <!-- Checks that each Java package has a Javadoc file used for commenting. -->
     <!-- See http://checkstyle.sf.net/config_javadoc.html#JavadocPackage       -->
     <!-- See http://checkstyle.sf.net/config_javadoc.html#JavadocPackage       -->
@@ -76,7 +59,7 @@
         <property name="lineSeparator" value="lf_cr_crlf" />
         <property name="lineSeparator" value="lf_cr_crlf" />
     </module>
     </module>
 
 
-    <module name="TreeWalker">
+    <module name="TreeWalker" >
         <property name="cacheFile" value="target/checkstyle.cache"/>
         <property name="cacheFile" value="target/checkstyle.cache"/>
 
 
         <!-- required for SuppressWarningsFilter (and other Suppress* rules not used here) -->
         <!-- required for SuppressWarningsFilter (and other Suppress* rules not used here) -->
@@ -179,7 +162,7 @@
         <module name="RedundantImport"/>
         <module name="RedundantImport"/>
         <module name="UnusedImports"/>
         <module name="UnusedImports"/>
         <module name="ImportControl">
         <module name="ImportControl">
-            <property name="file" value="${basedir}/../build/checkstyle-import.xml"/>
+            <property name="file" value="${basedir}/build/checkstyle-import.xml"/>
         </module>
         </module>
 
 
 
 

+ 22 - 0
build/license-header-js.txt

@@ -0,0 +1,22 @@
+/*
+ * 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
+ */
+

+ 2 - 1
build/license-header-xml.txt

@@ -1,7 +1,8 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <?xml version="1.0" encoding="UTF-8"?>
 <!--
 <!--
   ~ Password Management Servlets (PWM)
   ~ Password Management Servlets (PWM)
-  ~ Copyright (c) 2006-2018 Novell, Inc.
+  ~ 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
   ~ 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
   ~ it under the terms of the GNU General Public License as published by

+ 4 - 0
client/pom.xml

@@ -14,6 +14,10 @@
 
 
     <name>PWM Password Self Service: Client</name>
     <name>PWM Password Self Service: Client</name>
 
 
+    <properties>
+        <project.root.basedir>${project.basedir}/..</project.root.basedir>
+    </properties>
+
     <build>
     <build>
         <plugins>
         <plugins>
             <plugin>
             <plugin>

+ 1 - 2
client/src/modules/helpdesk/helpdesk-detail.component.ts

@@ -1,6 +1,6 @@
 /*
 /*
  * Password Management Servlets (PWM)
  * Password Management Servlets (PWM)
-  htt://www.pwm-project.org
+ * http://www.pwm-project.org
  *
  *
  * Copyright (c) 2006-2009 Novell, Inc.
  * Copyright (c) 2006-2009 Novell, Inc.
  * Copyright (c) 2009-2018 The PWM Project
  * Copyright (c) 2009-2018 The PWM Project
@@ -20,7 +20,6 @@
  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
  */
  */
 
 
-
 import {Component} from '../../component';
 import {Component} from '../../component';
 import {IHelpDeskService, ISuccessResponse} from '../../services/helpdesk.service';
 import {IHelpDeskService, ISuccessResponse} from '../../services/helpdesk.service';
 import {IScope, ui} from 'angular';
 import {IScope, ui} from 'angular';

+ 1 - 1
client/src/services/helpdesk-config.service.test-data.ts

@@ -1,4 +1,3 @@
-
 /*
 /*
  * Password Management Servlets (PWM)
  * Password Management Servlets (PWM)
  * http://www.pwm-project.org
  * http://www.pwm-project.org
@@ -20,6 +19,7 @@
  * along with this program; if not, write to the Free Software
  * along with this program; if not, write to the Free Software
  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
  */
  */
+
 /* tslint:disable */
 /* tslint:disable */
 
 
 export const helpdeskProcessAction_clientData = {
 export const helpdeskProcessAction_clientData = {

+ 1 - 0
client/src/services/helpdesk.service.test-data.ts

@@ -19,6 +19,7 @@
  * along with this program; if not, write to the Free Software
  * along with this program; if not, write to the Free Software
  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
  */
  */
+
 /* tslint:disable */
 /* tslint:disable */
 
 
 export const getRecentVerifications_response = {
 export const getRecentVerifications_response = {

+ 1 - 2
client/src/services/object.service.ts

@@ -3,7 +3,7 @@
  * http://www.pwm-project.org
  * http://www.pwm-project.org
  *
  *
  * Copyright (c) 2006-2009 Novell, Inc.
  * Copyright (c) 2006-2009 Novell, Inc.
- * Copyright (c) 2009-2016 The PWM Project
+ * Copyright (c) 2009-2018 The PWM Project
  *
  *
  * This program is free software; you can redistribute it and/or modify
  * 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
  * it under the terms of the GNU General Public License as published by
@@ -20,7 +20,6 @@
  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
  */
  */
 
 
-
 export default class ObjectService {
 export default class ObjectService {
     // ES5 implementation of Object.assign
     // ES5 implementation of Object.assign
     // Source from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign
     // Source from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign

+ 1 - 2
client/src/services/people.service.dev.ts

@@ -8,7 +8,7 @@
  * This program is free software; you can redistribute it and/or modify
  * 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
  * it under the terms of the GNU General Public License as published by
  * the Free Software Foundation; either version 2 of the License, or
  * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later versionI.
+ * (at your option) any later version.
  *
  *
  * This program is distributed in the hope that it will be useful,
  * This program is distributed in the hope that it will be useful,
  * but WITHOUT ANY WARRANTY; without even the implied warranty of
  * but WITHOUT ANY WARRANTY; without even the implied warranty of
@@ -20,7 +20,6 @@
  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
  */
  */
 
 
-
 import { IPromise, IQService, ITimeoutService } from 'angular';
 import { IPromise, IQService, ITimeoutService } from 'angular';
 import { IPerson } from '../models/person.model';
 import { IPerson } from '../models/person.model';
 import { IPeopleService } from './people.service';
 import { IPeopleService } from './people.service';

+ 1 - 2
client/src/services/promise.service.ts

@@ -3,7 +3,7 @@
  * http://www.pwm-project.org
  * http://www.pwm-project.org
  *
  *
  * Copyright (c) 2006-2009 Novell, Inc.
  * Copyright (c) 2006-2009 Novell, Inc.
- * Copyright (c) 2009-2016 The PWM Project
+ * Copyright (c) 2009-2018 The PWM Project
  *
  *
  * This program is free software; you can redistribute it and/or modify
  * 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
  * it under the terms of the GNU General Public License as published by
@@ -20,7 +20,6 @@
  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
  */
  */
 
 
-
 import { IPromise, IQService } from 'angular';
 import { IPromise, IQService } from 'angular';
 
 
 // Pattern explained at https://www.bennadel.com/blog/2731-canceling-a-promise-in-angularjs.htm
 // Pattern explained at https://www.bennadel.com/blog/2731-canceling-a-promise-in-angularjs.htm

+ 1 - 90
data-service/pom.xml

@@ -33,21 +33,10 @@
         <timestamp.iso>${maven.build.timestamp}</timestamp.iso>
         <timestamp.iso>${maven.build.timestamp}</timestamp.iso>
         <maven.build.timestamp.format>yyyy-MM-dd'T'HH:mm:ss'Z'</maven.build.timestamp.format>
         <maven.build.timestamp.format>yyyy-MM-dd'T'HH:mm:ss'Z'</maven.build.timestamp.format>
         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+        <project.root.basedir>${project.basedir}/..</project.root.basedir>
     </properties>
     </properties>
 
 
     <profiles>
     <profiles>
-        <profile>
-            <id>skip-tests</id>
-            <properties>
-                <skipTests>true</skipTests>
-            </properties>
-        </profile>
-        <profile>
-            <id>skip-checkstyle</id>
-            <properties>
-                <checkstyle.skip>true</checkstyle.skip>
-            </properties>
-        </profile>
     </profiles>
     </profiles>
 
 
     <build>
     <build>
@@ -64,51 +53,6 @@
                     </execution>
                     </execution>
                 </executions>
                 </executions>
             </plugin>
             </plugin>
-            <plugin>
-                <groupId>org.apache.maven.plugins</groupId>
-                <artifactId>maven-surefire-plugin</artifactId>
-                <version>2.19.1</version>
-                <configuration>
-                    <excludes>
-                        <exclude>password.pwm.manual.*</exclude>
-                    </excludes>
-                </configuration>
-            </plugin>
-            <plugin>
-                <groupId>org.apache.maven.plugins</groupId>
-                <artifactId>maven-source-plugin</artifactId>
-                <version>2.4</version>
-                <executions>
-                    <execution>
-                        <id>attach-sources</id>
-                        <goals>
-                            <goal>jar-no-fork</goal>
-                        </goals>
-                    </execution>
-                </executions>
-            </plugin>
-            <plugin>
-                <groupId>org.apache.maven.plugins</groupId>
-                <artifactId>maven-javadoc-plugin</artifactId>
-                <version>3.0.1</version>
-                <configuration>
-                    <archive>
-                        <manifestEntries>
-                            <Implementation-Archive-Name>pwm.javadoc</Implementation-Archive-Name>
-                            <Implementation-Title>${project.name}</Implementation-Title>
-                            <Implementation-Version>${project.version}</Implementation-Version>
-                            <Implementation-Vendor>${project.organization.name}</Implementation-Vendor>
-                            <Implementation-URL>${project.organization.url}</Implementation-URL>
-                            <Implementation-Build-Java-Vendor>${java.vendor}</Implementation-Build-Java-Vendor>
-                            <Implementation-Build-Java-Version>${java.version}</Implementation-Build-Java-Version>
-                            <Implementation-Build>${build.number}</Implementation-Build>
-                            <Implementation-Build-Timestamp>${timestamp.iso}</Implementation-Build-Timestamp>
-                            <Implementation-Revision>${build.revision}</Implementation-Revision>
-                            <Implementation-Version-Display>v${project.version} b${build.number} r${build.revision}</Implementation-Version-Display>
-                        </manifestEntries>
-                    </archive>
-                </configuration>
-            </plugin>
             <plugin>
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-war-plugin</artifactId>
                 <artifactId>maven-war-plugin</artifactId>
@@ -129,39 +73,6 @@
                     </archive>
                     </archive>
                 </configuration>
                 </configuration>
             </plugin>
             </plugin>
-            <plugin>
-                <groupId>org.apache.maven.plugins</groupId>
-                <artifactId>maven-checkstyle-plugin</artifactId>
-                <version>3.0.0</version>
-                <dependencies>
-                    <dependency>
-                        <groupId>com.puppycrawl.tools</groupId>
-                        <artifactId>checkstyle</artifactId>
-                        <version>8.11</version>
-                    </dependency>
-                </dependencies>
-                <executions>
-                    <execution>
-                        <id>validate</id>
-                        <phase>validate</phase>
-                        <configuration>
-                            <propertyExpansion>basedir=${basedir}</propertyExpansion>
-                            <configLocation>../build/checkstyle.xml</configLocation>
-                            <encoding>UTF-8</encoding>
-                            <consoleOutput>true</consoleOutput>
-                            <includeTestResources>false</includeTestResources>
-                            <failsOnError>true</failsOnError>
-                            <includes>**/*.java,**/*.jsp,**/*.properties,**/*.xml,**/*.css,**/*.svg</includes>
-                            <sourceDirectories>
-                                <directory>src/main</directory>
-                            </sourceDirectories>
-                        </configuration>
-                        <goals>
-                            <goal>check</goal>
-                        </goals>
-                    </execution>
-                </executions>
-            </plugin>
             <plugin>
             <plugin>
                 <artifactId>maven-resources-plugin</artifactId>
                 <artifactId>maven-resources-plugin</artifactId>
                 <version>2.7</version>
                 <version>2.7</version>

+ 14 - 7
data-service/src/main/java/password/pwm/receiver/Settings.java

@@ -25,8 +25,12 @@ package password.pwm.receiver;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.java.TimeDuration;
 import password.pwm.util.java.TimeDuration;
 
 
-import java.io.FileReader;
+import java.io.File;
+import java.io.FileInputStream;
 import java.io.IOException;
 import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.nio.charset.StandardCharsets;
 import java.util.Collections;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.Map;
@@ -74,14 +78,17 @@ public class Settings
     static Settings readFromFile( final String filename ) throws IOException
     static Settings readFromFile( final String filename ) throws IOException
     {
     {
         final Properties properties = new Properties();
         final Properties properties = new Properties();
-        properties.load( new FileReader( filename ) );
-        final Map<Setting, String> returnMap = new HashMap<>();
-        for ( final Setting setting : Setting.values() )
+        try ( Reader reader = new InputStreamReader( new FileInputStream( new File( filename ) ), StandardCharsets.UTF_8 ) )
         {
         {
-            final String value = properties.getProperty( setting.name(), setting.getDefaultValue() );
-            returnMap.put( setting, value );
+            properties.load( reader );
+            final Map<Setting, String> returnMap = new HashMap<>();
+            for ( final Setting setting : Setting.values() )
+            {
+                final String value = properties.getProperty( setting.name(), setting.getDefaultValue() );
+                returnMap.put( setting, value );
+            }
+            return new Settings( Collections.unmodifiableMap( returnMap ) );
         }
         }
-        return new Settings( Collections.unmodifiableMap( returnMap ) );
     }
     }
 
 
     public String getSetting( final Setting setting )
     public String getSetting( final Setting setting )

+ 11 - 3
data-service/src/main/java/password/pwm/receiver/Storage.java

@@ -60,11 +60,11 @@ public class Storage
             throw new IOException( "data path '" + dataPath + "' does not exist" );
             throw new IOException( "data path '" + dataPath + "' does not exist" );
         }
         }
 
 
-        final File stoagePath = new File( dataPath.getAbsolutePath() + File.separator + "storage" );
-        stoagePath.mkdir();
+        final File storagePath = new File( dataPath.getAbsolutePath() + File.separator + "storage" );
+        mkdirs( storagePath );
 
 
         final EnvironmentConfig environmentConfig = new EnvironmentConfig();
         final EnvironmentConfig environmentConfig = new EnvironmentConfig();
-        environment = Environments.newInstance( stoagePath.getAbsolutePath(), environmentConfig );
+        environment = Environments.newInstance( storagePath.getAbsolutePath(), environmentConfig );
 
 
         environment.executeInTransaction( txn -> store
         environment.executeInTransaction( txn -> store
                 = environment.openStore( "store1", StoreConfig.WITHOUT_DUPLICATES, txn ) );
                 = environment.openStore( "store1", StoreConfig.WITHOUT_DUPLICATES, txn ) );
@@ -216,4 +216,12 @@ public class Storage
         }
         }
     }
     }
 
 
+    static void mkdirs( final File file ) throws IOException
+    {
+        if ( !file.mkdirs() )
+        {
+            throw new IOException( "unable to create path " + file.getAbsolutePath() );
+        }
+    }
+
 }
 }

+ 2 - 2
data-service/src/main/webapp/META-INF/context.xml

@@ -1,7 +1,8 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <?xml version="1.0" encoding="UTF-8"?>
 <!--
 <!--
   ~ Password Management Servlets (PWM)
   ~ Password Management Servlets (PWM)
-  ~ Copyright (c) 2006-2018 Novell, Inc.
+  ~ 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
   ~ 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
   ~ it under the terms of the GNU General Public License as published by
@@ -18,7 +19,6 @@
   ~ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
   ~ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
   -->
   -->
 
 
-
 <Context tldValidation="false" unloadDelay="30000" useHttpOnly="true">
 <Context tldValidation="false" unloadDelay="30000" useHttpOnly="true">
 
 
 </Context>
 </Context>

+ 2 - 1
data-service/src/main/webapp/WEB-INF/web.xml

@@ -1,7 +1,8 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <?xml version="1.0" encoding="UTF-8"?>
 <!--
 <!--
   ~ Password Management Servlets (PWM)
   ~ Password Management Servlets (PWM)
-  ~ Copyright (c) 2006-2018 Novell, Inc.
+  ~ 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
   ~ 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
   ~ it under the terms of the GNU General Public License as published by

+ 3 - 1
docker/pom.xml

@@ -17,6 +17,7 @@
 
 
     <properties>
     <properties>
         <skipDocker>false</skipDocker>
         <skipDocker>false</skipDocker>
+        <project.root.basedir>${project.basedir}/..</project.root.basedir>
     </properties>
     </properties>
 
 
     <profiles>
     <profiles>
@@ -36,7 +37,7 @@
             <plugin>
             <plugin>
                 <groupId>com.google.cloud.tools</groupId>
                 <groupId>com.google.cloud.tools</groupId>
                 <artifactId>jib-maven-plugin</artifactId>
                 <artifactId>jib-maven-plugin</artifactId>
-                <version>0.9.8</version>
+                <version>0.9.9</version>
                 <executions>
                 <executions>
                     <execution>
                     <execution>
                         <id>make-docker-image</id>
                         <id>make-docker-image</id>
@@ -62,6 +63,7 @@
                                 <ports>8443</ports>
                                 <ports>8443</ports>
                             </container>
                             </container>
                             <useCurrentTimestamp>true</useCurrentTimestamp>
                             <useCurrentTimestamp>true</useCurrentTimestamp>
+                            <allowInsecureRegistries>true</allowInsecureRegistries>
                         </configuration>
                         </configuration>
                     </execution>
                     </execution>
                 </executions>
                 </executions>

+ 2 - 25
onejar/pom.xml

@@ -16,10 +16,9 @@
     <name>PWM Password Self Service: Executable JAR</name>
     <name>PWM Password Self Service: Executable JAR</name>
 
 
     <properties>
     <properties>
+        <project.root.basedir>${project.basedir}/..</project.root.basedir>
         <tomcat.version>9.0.10</tomcat.version>
         <tomcat.version>9.0.10</tomcat.version>
         <jetty-version>9.4.11.v20180605</jetty-version>
         <jetty-version>9.4.11.v20180605</jetty-version>
-
-
     </properties>
     </properties>
 
 
     <build>
     <build>
@@ -89,29 +88,7 @@
             <type>war</type>
             <type>war</type>
             <scope>provided</scope>
             <scope>provided</scope>
         </dependency>
         </dependency>
-        <!--
-        <dependency>
-            <groupId>org.eclipse.jetty</groupId>
-            <artifactId>jetty-server</artifactId>
-            <version>${jetty-version}</version>
-        </dependency>
-        <dependency>
-            <groupId>org.eclipse.jetty</groupId>
-            <artifactId>jetty-servlet</artifactId>
-            <version>${jetty-version}</version>
-        </dependency>
-        <dependency>
-            <groupId>org.eclipse.jetty</groupId>
-            <artifactId>jetty-webapp</artifactId>
-            <version>${jetty-version}</version>
-        </dependency>
-        <dependency>
-            <groupId>org.eclipse.jetty</groupId>
-            <artifactId>apache-jsp</artifactId>
-            <version>${jetty-version}</version>
-            <type>jar</type>
-        </dependency>
-        -->
+
         <!-- embedded tomcat -->
         <!-- embedded tomcat -->
         <dependency>
         <dependency>
             <groupId>org.apache.tomcat.embed</groupId>
             <groupId>org.apache.tomcat.embed</groupId>

+ 21 - 10
onejar/src/main/java/password/pwm/onejar/ArgumentParser.java

@@ -110,9 +110,9 @@ public class ArgumentParser
     private Map<Argument, String> mapFromProperties( final String filename ) throws ArgumentParserException
     private Map<Argument, String> mapFromProperties( final String filename ) throws ArgumentParserException
     {
     {
         final Properties props = new Properties();
         final Properties props = new Properties();
-        try
+        try ( InputStream is = new FileInputStream( new File( filename ) ) )
         {
         {
-            props.load( new FileInputStream( new File( filename ) ) );
+            props.load( is );
         }
         }
         catch ( IOException e )
         catch ( IOException e )
         {
         {
@@ -167,9 +167,9 @@ public class ArgumentParser
             final File inputWarFile = new File( argumentMap.get( Argument.war ) );
             final File inputWarFile = new File( argumentMap.get( Argument.war ) );
             if ( !inputWarFile.exists() )
             if ( !inputWarFile.exists() )
             {
             {
-                System.out.println( "output war file " + inputWarFile.getAbsolutePath() + "does not exist" );
-                System.exit( -1 );
-                return null;
+                final String msg = "output war file " + inputWarFile.getAbsolutePath() + "does not exist";
+                System.out.println( msg );
+                throw new IllegalStateException( msg );
             }
             }
             onejarConfig.setWar( new FileInputStream( inputWarFile ) );
             onejarConfig.setWar( new FileInputStream( inputWarFile ) );
         }
         }
@@ -187,8 +187,9 @@ public class ArgumentParser
             }
             }
             catch ( NumberFormatException e )
             catch ( NumberFormatException e )
             {
             {
-                System.out.println( Argument.port.name() + " argument must be numeric" );
-                System.exit( -1 );
+                final String msg = Argument.port.name() + " argument must be numeric";
+                System.out.println( msg );
+                throw new IllegalStateException( msg );
             }
             }
         }
         }
 
 
@@ -249,14 +250,16 @@ public class ArgumentParser
         return file;
         return file;
     }
     }
 
 
-    private static File figureDefaultWorkPath( final OnejarConfig onejarConfig ) throws ArgumentParserException
+    private static File figureDefaultWorkPath( final OnejarConfig onejarConfig )
+            throws ArgumentParserException, IOException
     {
     {
         final String userHomePath = System.getProperty( "user.home" );
         final String userHomePath = System.getProperty( "user.home" );
         if ( userHomePath != null && !userHomePath.isEmpty() )
         if ( userHomePath != null && !userHomePath.isEmpty() )
         {
         {
             final File basePath = new File( userHomePath + File.separator
             final File basePath = new File( userHomePath + File.separator
                     + Resource.defaultWorkPathName.getValue() );
                     + Resource.defaultWorkPathName.getValue() );
-            basePath.mkdir();
+
+            mkdirs( basePath );
 
 
             final String workPath;
             final String workPath;
             {
             {
@@ -274,7 +277,7 @@ public class ArgumentParser
                 workPath = workPathStr;
                 workPath = workPathStr;
             }
             }
             final File workFile = new File( workPath );
             final File workFile = new File( workPath );
-            workFile.mkdirs();
+            mkdirs( workFile );
             TomcatOneJarMain.out( "using work directory: " + workPath );
             TomcatOneJarMain.out( "using work directory: " + workPath );
             return workFile;
             return workFile;
         }
         }
@@ -311,4 +314,12 @@ public class ArgumentParser
         }
         }
         return stringBuilder.toString();
         return stringBuilder.toString();
     }
     }
+
+    static void mkdirs( final File file ) throws IOException
+    {
+        if ( !file.mkdirs() && !file.exists() )
+        {
+            throw new IOException( "unable to create path " + file.getAbsolutePath() );
+        }
+    }
 }
 }

+ 30 - 15
onejar/src/main/java/password/pwm/onejar/TomcatOneJarMain.java

@@ -30,14 +30,17 @@ import org.apache.catalina.util.ServerInfo;
 import javax.servlet.ServletException;
 import javax.servlet.ServletException;
 import java.io.BufferedReader;
 import java.io.BufferedReader;
 import java.io.File;
 import java.io.File;
-import java.io.FileWriter;
+import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStream;
 import java.io.InputStreamReader;
 import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
 import java.lang.reflect.Method;
 import java.net.URL;
 import java.net.URL;
 import java.net.URLClassLoader;
 import java.net.URLClassLoader;
+import java.nio.charset.StandardCharsets;
 import java.nio.file.FileVisitOption;
 import java.nio.file.FileVisitOption;
 import java.nio.file.Files;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Path;
@@ -105,7 +108,8 @@ public class TomcatOneJarMain
         final InputStream warSource = onejarConfig.getWar();
         final InputStream warSource = onejarConfig.getWar();
         final ZipInputStream zipInputStream = new ZipInputStream( warSource );
         final ZipInputStream zipInputStream = new ZipInputStream( warSource );
         final File outputFolder = getWarFolder( onejarConfig );
         final File outputFolder = getWarFolder( onejarConfig );
-        outputFolder.mkdir();
+
+        ArgumentParser.mkdirs( outputFolder );
 
 
         ZipEntry zipEntry = zipInputStream.getNextEntry();
         ZipEntry zipEntry = zipInputStream.getNextEntry();
 
 
@@ -116,7 +120,7 @@ public class TomcatOneJarMain
 
 
             if ( !zipEntry.isDirectory() )
             if ( !zipEntry.isDirectory() )
             {
             {
-                newFile.getParentFile().mkdirs();
+                ArgumentParser.mkdirs( newFile.getParentFile() );
                 Files.copy( zipInputStream, newFile.toPath() );
                 Files.copy( zipInputStream, newFile.toPath() );
             }
             }
             zipEntry = zipInputStream.getNextEntry();
             zipEntry = zipInputStream.getNextEntry();
@@ -151,18 +155,18 @@ public class TomcatOneJarMain
 
 
         {
         {
             final File basePath = new File( onejarConfig.getWorkingPath().getPath() + File.separator + "b" );
             final File basePath = new File( onejarConfig.getWorkingPath().getPath() + File.separator + "b" );
-            basePath.mkdir();
+            ArgumentParser.mkdirs( basePath );
             tomcat.setBaseDir( basePath.getAbsolutePath() );
             tomcat.setBaseDir( basePath.getAbsolutePath() );
         }
         }
         {
         {
             final File basePath = new File( onejarConfig.getWorkingPath().getPath() + File.separator + "a" );
             final File basePath = new File( onejarConfig.getWorkingPath().getPath() + File.separator + "a" );
-            basePath.mkdir();
+            ArgumentParser.mkdirs( basePath );
             tomcat.getServer().setCatalinaBase( basePath );
             tomcat.getServer().setCatalinaBase( basePath );
             tomcat.getServer().setCatalinaHome( basePath );
             tomcat.getServer().setCatalinaHome( basePath );
         }
         }
         {
         {
             final File workPath = new File( onejarConfig.getWorkingPath().getPath() + File.separator + "w" );
             final File workPath = new File( onejarConfig.getWorkingPath().getPath() + File.separator + "w" );
-            workPath.mkdir();
+            ArgumentParser.mkdirs( workPath );
             tomcat.getHost().setAppBase( workPath.getAbsolutePath() );
             tomcat.getHost().setAppBase( workPath.getAbsolutePath() );
         }
         }
 
 
@@ -199,9 +203,9 @@ public class TomcatOneJarMain
         final String srcRootIndex = "ROOT-redirect-webapp/WEB-INF/index.jsp";
         final String srcRootIndex = "ROOT-redirect-webapp/WEB-INF/index.jsp";
 
 
         final File redirBase = new File( onejarConfig.getWorkingPath().getAbsoluteFile() + File.separator + "redirectBase" );
         final File redirBase = new File( onejarConfig.getWorkingPath().getAbsoluteFile() + File.separator + "redirectBase" );
-        redirBase.mkdirs();
+        ArgumentParser.mkdirs( redirBase );
         {
         {
-            new File (redirBase.getAbsolutePath() + File.separator + "WEB-INF" ).mkdirs();
+            ArgumentParser.mkdirs( new File ( redirBase.getAbsolutePath() + File.separator + "WEB-INF" ) );
             copyFileAndReplace(
             copyFileAndReplace(
                     srcRootWebXml,
                     srcRootWebXml,
                     redirBase.getPath() + File.separator + "WEB-INF" + File.separator + "web.xml",
                     redirBase.getPath() + File.separator + "WEB-INF" + File.separator + "web.xml",
@@ -287,9 +291,12 @@ public class TomcatOneJarMain
         final File webInfPath = new File( warPath.getAbsolutePath() + File.separator + "WEB-INF" + File.separator + "lib" );
         final File webInfPath = new File( warPath.getAbsolutePath() + File.separator + "WEB-INF" + File.separator + "lib" );
         final File[] jarFiles = webInfPath.listFiles();
         final File[] jarFiles = webInfPath.listFiles();
         final List<URL> jarURLList = new ArrayList<>();
         final List<URL> jarURLList = new ArrayList<>();
-        for ( final File jarFile : jarFiles )
+        if ( jarFiles != null )
         {
         {
-            jarURLList.add( jarFile.toURI().toURL() );
+            for ( final File jarFile : jarFiles )
+            {
+                jarURLList.add( jarFile.toURI().toURL() );
+            }
         }
         }
         final URLClassLoader classLoader = URLClassLoader.newInstance( jarURLList.toArray( new URL[ jarURLList.size() ] ) );
         final URLClassLoader classLoader = URLClassLoader.newInstance( jarURLList.toArray( new URL[ jarURLList.size() ] ) );
         final Class pwmMainClass = classLoader.loadClass( "password.pwm.util.cli.MainClass" );
         final Class pwmMainClass = classLoader.loadClass( "password.pwm.util.cli.MainClass" );
@@ -321,7 +328,10 @@ public class TomcatOneJarMain
         properties.setProperty( "AutoExportHttpsKeyStorePassword", onejarConfig.getKeystorePass() );
         properties.setProperty( "AutoExportHttpsKeyStorePassword", onejarConfig.getKeystorePass() );
         properties.setProperty( "AutoExportHttpsKeyStoreAlias", KEYSTORE_ALIAS );
         properties.setProperty( "AutoExportHttpsKeyStoreAlias", KEYSTORE_ALIAS );
         final File propFile = getPwmAppPropertiesFile( onejarConfig );
         final File propFile = getPwmAppPropertiesFile( onejarConfig );
-        properties.store( new FileWriter( propFile ), "auto-generated file" );
+        try ( Writer writer = new OutputStreamWriter( new FileOutputStream( propFile ), StandardCharsets.UTF_8 ) )
+        {
+            properties.store( writer, "auto-generated file" );
+        }
     }
     }
 
 
     static void copyFileAndReplace(
     static void copyFileAndReplace(
@@ -331,9 +341,14 @@ public class TomcatOneJarMain
     )
     )
             throws IOException
             throws IOException
     {
     {
-        final InputStream inputStream = TomcatOneJarMain.class.getClassLoader().getResourceAsStream( srcPath );
-        String contents = new BufferedReader(new InputStreamReader(inputStream)).lines().collect( Collectors.joining("\n"));
-        contents = contents.replace( "[[[ROOT_CONTEXT]]]", rootcontext );
-        Files.write( Paths.get( destPath ), contents.getBytes());
+        try ( InputStream inputStream = TomcatOneJarMain.class.getClassLoader().getResourceAsStream( srcPath ) )
+        {
+            try ( BufferedReader reader = new BufferedReader( new InputStreamReader( inputStream, "UTF8" ) ) )
+            {
+                String contents = reader.lines().collect( Collectors.joining( "\n" ) );
+                contents = contents.replace( "[[[ROOT_CONTEXT]]]", rootcontext );
+                Files.write( Paths.get( destPath ), contents.getBytes( "UTF8" ) );
+            }
+        }
     }
     }
 }
 }

+ 22 - 0
onejar/src/main/resources/ROOT-redirect-webapp/WEB-INF/index.jsp

@@ -1,2 +1,24 @@
+<%--
+ ~ 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
+--%>
+
 <%@ page session="false" contentType="text/html" %>
 <%@ page session="false" contentType="text/html" %>
 <html><head><meta http-equiv="refresh" content="0;URL='/[[[ROOT_CONTEXT]]]'"/></head></html>
 <html><head><meta http-equiv="refresh" content="0;URL='/[[[ROOT_CONTEXT]]]'"/></head></html>

+ 20 - 0
onejar/src/main/resources/ROOT-redirect-webapp/WEB-INF/web.xml

@@ -1,4 +1,24 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Password Management Servlets (PWM)
+  ~ 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
+  -->
+
 <web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 <web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xmlns="http://java.sun.com/xml/ns/javaee"
          xmlns="http://java.sun.com/xml/ns/javaee"
          xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
          xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"

+ 180 - 0
pom.xml

@@ -31,10 +31,14 @@
         <timestamp.iso>${maven.build.timestamp}</timestamp.iso>
         <timestamp.iso>${maven.build.timestamp}</timestamp.iso>
         <maven.compiler.source>1.8</maven.compiler.source>
         <maven.compiler.source>1.8</maven.compiler.source>
         <maven.compiler.target>1.8</maven.compiler.target>
         <maven.compiler.target>1.8</maven.compiler.target>
+        <project.root.basedir>${project.basedir}</project.root.basedir>
+        <skipTests>false</skipTests>
+        <skipSpotbugs>false</skipSpotbugs>
     </properties>
     </properties>
 
 
     <modules>
     <modules>
         <module>client</module>
         <module>client</module>
+        <module>pwm-cr</module>
         <module>server</module>
         <module>server</module>
         <module>webapp</module>
         <module>webapp</module>
         <module>onejar</module>
         <module>onejar</module>
@@ -43,8 +47,106 @@
         <module>docker</module>
         <module>docker</module>
     </modules>
     </modules>
 
 
+    <profiles>
+        <profile>
+            <id>skip-checkstyle</id>
+            <properties>
+                <checkstyle.skip>true</checkstyle.skip>
+            </properties>
+        </profile>
+        <profile>
+            <id>skip-spotbugs</id>
+            <properties>
+                <skipSpotbugs>true</skipSpotbugs>
+            </properties>
+        </profile>
+        <profile>
+            <id>skip-tests</id>
+            <properties>
+                <skipTests>true</skipTests>
+            </properties>
+        </profile>
+        <profile>
+            <id>skip-javadoc</id>
+            <properties>
+                <maven.javadoc.skip>true</maven.javadoc.skip>
+            </properties>
+        </profile>
+    </profiles>
+
     <build>
     <build>
         <plugins>
         <plugins>
+            <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>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-javadoc-plugin</artifactId>
+                <version>3.0.1</version>
+                <executions>
+                    <execution>
+                        <goals>
+                            <goal>jar</goal>
+                        </goals>
+                        <configuration>
+                            <archive>
+                                <manifestEntries>
+                                    <Implementation-Archive-Name>pwm.javadoc</Implementation-Archive-Name>
+                                    <Implementation-Title>${project.name}</Implementation-Title>
+                                    <Implementation-Version>${project.version}</Implementation-Version>
+                                    <Implementation-Vendor>${project.organization.name}</Implementation-Vendor>
+                                    <Implementation-URL>${project.organization.url}</Implementation-URL>
+                                    <Implementation-Build-Java-Vendor>${java.vendor}</Implementation-Build-Java-Vendor>
+                                    <Implementation-Build-Java-Version>${java.version}</Implementation-Build-Java-Version>
+                                    <Implementation-Build>${build.number}</Implementation-Build>
+                                    <Implementation-Build-Timestamp>${timestamp.iso}</Implementation-Build-Timestamp>
+                                    <Implementation-Revision>${build.revision}</Implementation-Revision>
+                                    <Implementation-Version-Display>v${project.version} b${build.number} r${build.revision}</Implementation-Version-Display>
+                                </manifestEntries>
+                            </archive>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-source-plugin</artifactId>
+                <version>3.0.1</version>
+                <executions>
+                    <execution>
+                        <goals>
+                            <goal>jar</goal>
+                        </goals>
+                        <configuration>
+                            <includePom>true</includePom>
+                            <attach>true</attach>
+                            <archive>
+                                <manifestEntries>
+                                    <Implementation-Archive-Name>pwm.source</Implementation-Archive-Name>
+                                    <Implementation-Title>${project.name}</Implementation-Title>
+                                    <Implementation-Version>${project.version}</Implementation-Version>
+                                    <Implementation-Vendor>${project.organization.name}</Implementation-Vendor>
+                                    <Implementation-URL>${project.organization.url}</Implementation-URL>
+                                    <Implementation-Build-Java-Vendor>${java.vendor}</Implementation-Build-Java-Vendor>
+                                    <Implementation-Build-Java-Version>${java.version}</Implementation-Build-Java-Version>
+                                    <Implementation-Build>${build.number}</Implementation-Build>
+                                    <Implementation-Build-Timestamp>${timestamp.iso}</Implementation-Build-Timestamp>
+                                    <Implementation-Revision>${build.revision}</Implementation-Revision>
+                                    <Implementation-Version-Display>v${project.version} b${build.number} r${build.revision}</Implementation-Version-Display>
+                                </manifestEntries>
+                            </archive>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
             <plugin>
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-enforcer-plugin</artifactId>
                 <artifactId>maven-enforcer-plugin</artifactId>
@@ -74,6 +176,84 @@
                     <target>${maven.compiler.target}</target>
                     <target>${maven.compiler.target}</target>
                 </configuration>
                 </configuration>
             </plugin>
             </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-checkstyle-plugin</artifactId>
+                <version>3.0.0</version>
+                <dependencies>
+                    <dependency>
+                        <groupId>com.puppycrawl.tools</groupId>
+                        <artifactId>checkstyle</artifactId>
+                        <version>8.11</version>
+                    </dependency>
+                </dependencies>
+                <executions>
+                    <execution>
+                        <id>checkstyle</id>
+                        <phase>validate</phase>
+                        <configuration>
+                            <propertyExpansion>basedir=${project.root.basedir}</propertyExpansion>
+                            <configLocation>${project.root.basedir}/build/checkstyle.xml</configLocation>
+                            <encoding>UTF-8</encoding>
+                            <consoleOutput>true</consoleOutput>
+                            <includeTestResources>false</includeTestResources>
+                            <failsOnError>true</failsOnError>
+                            <includes>**/*.java,**/*.jsp,**/*.properties,**/*.xml,**/*.css,**/*.svg</includes>
+                            <sourceDirectories>
+                                <directory>src/main</directory>
+                            </sourceDirectories>
+                        </configuration>
+                        <goals>
+                            <goal>check</goal>
+                        </goals>
+                    </execution>
+                    <execution>
+                        <id>checkstyle-header</id>
+                        <phase>validate</phase>
+                        <configuration>
+                            <propertyExpansion>basedir=${project.root.basedir}</propertyExpansion>
+                            <configLocation>${project.root.basedir}/build/checkstyle-header.xml</configLocation>
+                            <encoding>UTF-8</encoding>
+                            <consoleOutput>true</consoleOutput>
+                            <includeTestResources>false</includeTestResources>
+                            <failsOnError>true</failsOnError>
+                            <includes>**/**</includes>
+                            <sourceDirectories>
+                                <directory>src</directory>
+                            </sourceDirectories>
+                        </configuration>
+                        <goals>
+                            <goal>check</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+            <plugin>
+                <groupId>com.github.spotbugs</groupId>
+                <artifactId>spotbugs-maven-plugin</artifactId>
+                <version>3.1.3.1</version>
+                <dependencies>
+                    <dependency>
+                        <groupId>com.github.spotbugs</groupId>
+                        <artifactId>spotbugs</artifactId>
+                        <version>3.1.6</version>
+                    </dependency>
+                </dependencies>
+                <configuration>
+                    <fork>false</fork>
+                    <excludeFilterFile>${project.root.basedir}/build/spotbugs-exclude.xml</excludeFilterFile>
+                    <includeTests>false</includeTests>
+                    <skip>${skipSpotbugs}</skip>
+                </configuration>
+                <executions>
+                    <execution>
+                        <phase>test</phase>
+                        <goals>
+                            <goal>check</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
         </plugins>
         </plugins>
     </build>
     </build>
 
 

+ 122 - 0
pwm-cr/pom.xml

@@ -0,0 +1,122 @@
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+
+    <parent>
+        <groupId>org.pwm-project</groupId>
+        <artifactId>pwm-parent</artifactId>
+        <version>1.8.0-SNAPSHOT</version>
+        <relativePath>../pom.xml</relativePath>
+    </parent>
+
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>pwm-cr</artifactId>
+    <packaging>jar</packaging>
+
+    <name>PWM Password Self Service: Challenge/Response JAR</name>
+
+    <description>Library for managing challenge/response security policies, stored data, and validation</description>
+
+    <properties>
+        <project.root.basedir>${project.basedir}/..</project.root.basedir>
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>com.google.code.gson</groupId>
+            <artifactId>gson</artifactId>
+            <version>2.8.5</version>
+        </dependency>
+        <dependency>
+            <groupId>org.jdom</groupId>
+            <artifactId>jdom2</artifactId>
+            <version>2.0.6</version>
+        </dependency>
+        <dependency>
+            <groupId>log4j</groupId>
+            <artifactId>log4j</artifactId>
+            <version>1.2.17</version>
+        </dependency>
+        <dependency>
+            <groupId>net.iharder</groupId>
+            <artifactId>base64</artifactId>
+            <version>2.3.9</version>
+        </dependency>
+        <dependency>
+            <groupId>org.bouncycastle</groupId>
+            <artifactId>bcpkix-jdk15on</artifactId>
+            <version>1.60</version>
+        </dependency>
+
+        <!-- Test dependencies -->
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+            <version>4.12</version>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <resources>
+            <resource>
+                <directory>src/main/resources</directory>
+                <filtering>true</filtering>
+            </resource>
+            <resource>
+                <directory>src/main/java</directory> <!-- include the src in the main output jar -->
+                <targetPath>src</targetPath>
+            </resource>
+        </resources>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-jar-plugin</artifactId>
+                <version>3.1.0</version>
+                <configuration>
+                    <archive>
+                        <manifestEntries>
+                            <Implementation-Archive-Name>pwm.jar</Implementation-Archive-Name>
+                            <Implementation-Title>${project.name}</Implementation-Title>
+                            <Implementation-Version>${project.version}</Implementation-Version>
+                            <Implementation-Vendor>${project.organization.name}</Implementation-Vendor>
+                            <Implementation-URL>${project.organization.url}</Implementation-URL>
+                            <Implementation-Build-Java-Vendor>${java.vendor}</Implementation-Build-Java-Vendor>
+                            <Implementation-Build-Java-Version>${java.version}</Implementation-Build-Java-Version>
+                            <Implementation-Build>${build.number}</Implementation-Build>
+                            <Implementation-Build-Timestamp>${timestamp.iso}</Implementation-Build-Timestamp>
+                            <Implementation-Revision>${build.revision}</Implementation-Revision>
+                            <Implementation-Version-Display>v${project.version} b${build.number} r${build.revision}</Implementation-Version-Display>
+                        </manifestEntries>
+                    </archive>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+
+    <profiles>
+    </profiles>
+
+    <distributionManagement>
+        <snapshotRepository>
+            <id>ossrh</id>
+            <url>https://oss.sonatype.org/content/repositories/snapshots</url>
+        </snapshotRepository>
+    </distributionManagement>
+
+    <developers>
+        <developer>
+            <name>Jason Rivard</name>
+            <email>https://github.com/jrivard</email>
+            <organization>LDAP Chai</organization>
+            <organizationUrl>https://github.com/ldapchai/</organizationUrl>
+        </developer>
+    </developers>
+
+    <scm>
+        <connection>scm:git:git@github.com:ldapchai/chaiCR.git</connection>
+        <developerConnection>scm:git:git@github.com:ldapchai/chaiCR.git</developerConnection>
+        <url>git@github.com:ldapchai/chaiCR.git</url>
+    </scm>
+
+</project>

+ 415 - 0
pwm-cr/src/main/java/password/pwm/cr/ChaiXmlResponseSetSerializer.java

@@ -0,0 +1,415 @@
+/*
+ * 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.cr;
+
+import net.iharder.Base64;
+import org.jdom2.Attribute;
+import org.jdom2.DataConversionException;
+import org.jdom2.Document;
+import org.jdom2.Element;
+import org.jdom2.JDOMException;
+import org.jdom2.Text;
+import org.jdom2.input.SAXBuilder;
+import org.jdom2.output.Format;
+import org.jdom2.output.XMLOutputter;
+import password.pwm.cr.api.StoredChallengeItem;
+import password.pwm.cr.api.StoredResponseItem;
+import password.pwm.cr.api.StoredResponseSet;
+import password.pwm.cr.api.ResponseLevel;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.io.Writer;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.text.ParseException;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+
+public class ChaiXmlResponseSetSerializer
+{
+
+    public enum Type
+    {
+        USER,
+        HELPDESK,
+    }
+
+    static final String SALT_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
+
+    static final String XML_NODE_ROOT = "ResponseSet";
+    static final String XML_ATTRIBUTE_MIN_RANDOM_REQUIRED = "minRandomRequired";
+    static final String XML_ATTRIBUTE_LOCALE = "locale";
+
+
+    static final String XML_NODE_RESPONSE = "response";
+    static final String XML_NODE_HELPDESK_RESPONSE = "helpdesk-response";
+    static final String XML_NODE_CHALLENGE = "challenge";
+    static final String XML_NODE_ANSWER_VALUE = "answer";
+
+    static final String XML_ATTRIBUTE_VERSION = "version";
+    static final String XML_ATTRIBUTE_CHAI_VERSION = "chaiVersion";
+    static final String XML_ATTRIBUTE_ADMIN_DEFINED = "adminDefined";
+    static final String XML_ATTRIBUTE_REQUIRED = "required";
+    static final String XML_ATTRIBUTE_HASH_COUNT = "hashcount";
+    static final String XML_ATTRIBUTE_CONTENT_FORMAT = "format";
+    static final String XML_ATTRIBUTE_SALT = "salt";
+    static final String XNL_ATTRIBUTE_MIN_LENGTH = "minLength";
+    static final String XNL_ATTRIBUTE_MAX_LENGTH = "maxLength";
+    static final String XML_ATTRIBUTE_CASE_INSENSITIVE = "caseInsensitive";
+
+    // identifier from challenge set.
+    static final String XML_ATTRIBUTE_CHALLENGE_SET_IDENTIFER = "challengeSetID";
+    static final String XML_ATTRIBUTE_TIMESTAMP = "time";
+
+    static final String VALUE_VERSION = "pwmCR-1";
+
+
+    public StoredResponseSet read( final Reader input, final Type type )
+    {
+        final Map<Type, StoredResponseSet> values = read( input );
+        return values.get( type );
+    }
+
+    public Map<Type, StoredResponseSet> read( final Reader input )
+    {
+        if ( input == null )
+        {
+            throw new NullPointerException( "input can not be null" );
+        }
+        final List<StoredChallengeItem> crMap = new ArrayList<>();
+        final List<StoredChallengeItem> helpdeskCrMap = new ArrayList<>();
+        final int minRandRequired;
+        final Attribute localeAttr;
+        boolean caseInsensitive = false;
+        String csIdentifier = null;
+        Instant timestamp = null;
+
+        try
+        {
+            final SAXBuilder builder = new SAXBuilder();
+            final Document doc = builder.build( input );
+            final Element rootElement = doc.getRootElement();
+            minRandRequired = rootElement.getAttribute( XML_ATTRIBUTE_MIN_RANDOM_REQUIRED ).getIntValue();
+            localeAttr = rootElement.getAttribute( XML_ATTRIBUTE_LOCALE );
+
+            {
+                final Attribute caseAttr = rootElement.getAttribute( XML_ATTRIBUTE_CASE_INSENSITIVE );
+                if ( caseAttr != null && caseAttr.getBooleanValue() )
+                {
+                    caseInsensitive = true;
+                }
+            }
+
+            {
+                final Attribute csIdentiferAttr = rootElement.getAttribute( XML_ATTRIBUTE_CHALLENGE_SET_IDENTIFER );
+                if ( csIdentiferAttr != null )
+                {
+                    csIdentifier = csIdentiferAttr.getValue();
+                }
+            }
+
+            {
+                final Attribute timeAttr = rootElement.getAttribute( XML_ATTRIBUTE_TIMESTAMP );
+                if ( timeAttr != null )
+                {
+                    final String timeStr = timeAttr.getValue();
+                    try
+                    {
+                        timestamp = CrUtils.parseDateString( timeStr );
+                    }
+                    catch ( ParseException e )
+                    {
+                        throw new IllegalArgumentException( "unexpected error attempting to parse timestamp: " + e.getMessage() );
+                    }
+                }
+            }
+
+            for ( final Element loopResponseElement : rootElement.getChildren() )
+            {
+                final Type type = XML_NODE_HELPDESK_RESPONSE.equals( loopResponseElement.getName() )
+                        ? Type.HELPDESK
+                        : XML_NODE_RESPONSE.equals( loopResponseElement.getName() )
+                        ? Type.USER
+                        : null;
+                if ( type != null )
+                {
+                    final StoredResponseItem storedResponseItem = parseAnswerElement( loopResponseElement.getChild( XML_NODE_ANSWER_VALUE ) );
+                    if ( storedResponseItem != null )
+                    {
+                        final StoredChallengeItem storedChallengeItem = parseResponseElement( loopResponseElement, storedResponseItem );
+                        switch ( type )
+                        {
+                            case USER:
+                                crMap.add( storedChallengeItem );
+                                break;
+
+                            case HELPDESK:
+                                helpdeskCrMap.add( storedChallengeItem );
+                                break;
+
+                            default:
+                                throw new IllegalStateException( "unknown response type '" + type + "'" );
+
+                        }
+                    }
+                }
+            }
+        }
+        catch ( JDOMException | IOException | NullPointerException e )
+        {
+            throw new IllegalArgumentException( "error parsing stored response record: " + e.getMessage() );
+        }
+
+        final String strLocale = localeAttr != null ? localeAttr.getValue() : null;
+
+
+        final Map<Type, StoredResponseSet> returnMap = new HashMap<>();
+        {
+            final StoredResponseSet userResponseSet = StoredResponseSet.builder()
+                    .id( csIdentifier )
+                    .caseSensitive( !caseInsensitive )
+                    .minRandomsDuringResponse( minRandRequired )
+                    .storedChallengeItems( Collections.unmodifiableList( crMap ) )
+                    .locale( strLocale )
+                    .timestamp( timestamp )
+                    .build();
+            returnMap.put( Type.USER, userResponseSet );
+        }
+
+        {
+            final StoredResponseSet helpdeskStoredResponseSet = StoredResponseSet.builder()
+                    .id( csIdentifier )
+                    .caseSensitive( !caseInsensitive )
+                    .minRandomsDuringResponse( minRandRequired )
+                    .storedChallengeItems( Collections.unmodifiableList( helpdeskCrMap ) )
+                    .locale( strLocale )
+                    .timestamp( timestamp )
+                    .build();
+            returnMap.put( Type.HELPDESK, helpdeskStoredResponseSet );
+        }
+
+
+        return Collections.unmodifiableMap( returnMap );
+    }
+
+    private static String elementNameForType( final Type type )
+    {
+        switch ( type )
+        {
+            case USER:
+                return XML_NODE_RESPONSE;
+
+            case HELPDESK:
+                return XML_NODE_HELPDESK_RESPONSE;
+
+            default:
+                throw new IllegalArgumentException( "unknown type '" + type + "'" );
+        }
+    }
+
+    private static StoredChallengeItem parseResponseElement(
+            final Element responseElement,
+            final StoredResponseItem storedResponseItem
+    )
+
+            throws DataConversionException
+    {
+        /*
+        final boolean adminDefined = responseElement.getAttribute( XML_ATTRIBUTE_ADMIN_DEFINED ) != null
+                && responseElement.getAttribute( XML_ATTRIBUTE_ADMIN_DEFINED ).getBooleanValue();
+
+        final int minLength = responseElement.getAttribute( XNL_ATTRIBUTE_MIN_LENGTH ) == null
+                ? 0
+                : responseElement.getAttribute( XNL_ATTRIBUTE_MIN_LENGTH ).getIntValue();
+
+        final int maxLength = responseElement.getAttribute( XNL_ATTRIBUTE_MAX_LENGTH ) == null
+                ? 0
+                : responseElement.getAttribute( XNL_ATTRIBUTE_MAX_LENGTH ).getIntValue();
+
+                */
+
+        final boolean required = responseElement.getAttribute( XML_ATTRIBUTE_REQUIRED ) != null
+                && responseElement.getAttribute( XML_ATTRIBUTE_REQUIRED ).getBooleanValue();
+
+        final String challengeText = responseElement.getChild( XML_NODE_CHALLENGE ) == null
+                ? ""
+                : responseElement.getChild( XML_NODE_CHALLENGE ).getText();
+
+        return StoredChallengeItem.builder()
+                .responseLevel( required ? ResponseLevel.REQUIRED : ResponseLevel.RANDOM )
+                .questionText( challengeText )
+                .id( makeId( challengeText ) )
+                .answer( storedResponseItem )
+                .build();
+    }
+
+    private static StoredResponseItem parseAnswerElement( final Element element )
+    {
+        final String answerValue = element.getText();
+        final String salt = element.getAttribute( XML_ATTRIBUTE_SALT ) == null ? "" : element.getAttribute( XML_ATTRIBUTE_SALT ).getValue();
+        final String hashCount = element.getAttribute( XML_ATTRIBUTE_HASH_COUNT ) == null ? "1" : element.getAttribute( XML_ATTRIBUTE_HASH_COUNT ).getValue();
+        int saltCount = 1;
+        try
+        {
+            saltCount = Integer.parseInt( hashCount );
+        }
+        catch ( NumberFormatException e )
+        { /* noop */ }
+        final String formatStr = element.getAttributeValue( XML_ATTRIBUTE_CONTENT_FORMAT ) == null ? "" : element.getAttributeValue( XML_ATTRIBUTE_CONTENT_FORMAT );
+
+        return StoredResponseItem.builder()
+                .format( formatStr )
+                .salt( salt )
+                .hash( answerValue )
+                .iterations( saltCount )
+                .build();
+    }
+
+    private static String makeId(
+            final String questionText
+    )
+            throws IllegalStateException
+    {
+        final MessageDigest md;
+        try
+        {
+            md = MessageDigest.getInstance( "SHA1" );
+            final byte[] hashedBytes = md.digest( questionText.getBytes( StandardCharsets.UTF_8 ) );
+            return net.iharder.Base64.encodeBytes( hashedBytes, Base64.URL_SAFE );
+        }
+        catch ( NoSuchAlgorithmException | IOException e )
+        {
+            throw new IllegalStateException( "unable to load SHA1 message digest algorithm: " + e.getMessage() );
+        }
+    }
+
+
+    public void write( final Writer writer, final Map<Type, StoredResponseSet> responseSets ) throws IOException
+    {
+        final StoredResponseSet rs = responseSets.get( Type.USER );
+        if ( rs == null )
+        {
+            throw new IllegalArgumentException( "responseSet must contain user type responses" );
+        }
+
+        final Element rootElement = new Element( XML_NODE_ROOT );
+        rootElement.setAttribute( XML_ATTRIBUTE_MIN_RANDOM_REQUIRED, String.valueOf( rs.getMinRandomsDuringResponse() ) );
+        rootElement.setAttribute( XML_ATTRIBUTE_LOCALE, rs.getLocale().toString() );
+        rootElement.setAttribute( XML_ATTRIBUTE_VERSION, VALUE_VERSION );
+        rootElement.setAttribute( XML_ATTRIBUTE_CHAI_VERSION, VALUE_VERSION );
+
+        if ( !rs.isCaseSensitive() )
+        {
+            rootElement.setAttribute( XML_ATTRIBUTE_CASE_INSENSITIVE, "true" );
+        }
+
+        if ( rs.getId() != null )
+        {
+            rootElement.setAttribute( XML_ATTRIBUTE_CHALLENGE_SET_IDENTIFER, rs.getId() );
+        }
+
+        if ( rs.getTimestamp() != null )
+        {
+            rootElement.setAttribute( XML_ATTRIBUTE_TIMESTAMP, CrUtils.formatDateString( rs.getTimestamp() ) );
+        }
+
+        attachChallenges( rootElement, rs.getStoredChallengeItems(), Type.USER );
+        if ( responseSets.containsKey( Type.HELPDESK ) )
+        {
+            final List<StoredChallengeItem> helpdeskChallengeItems = responseSets.get( Type.HELPDESK ).getStoredChallengeItems();
+            attachChallenges( rootElement, helpdeskChallengeItems, Type.HELPDESK );
+        }
+
+
+        final Document doc = new Document( rootElement );
+        final XMLOutputter outputter = new XMLOutputter();
+        final Format format = Format.getRawFormat();
+        format.setTextMode( Format.TextMode.PRESERVE );
+        format.setLineSeparator( "" );
+        outputter.setFormat( format );
+        outputter.output( doc, writer );
+    }
+
+    private static void attachChallenges(
+            final Element parentElement,
+            final List<StoredChallengeItem> storedChallengeItems,
+            final Type type
+    )
+    {
+        if ( storedChallengeItems == null )
+        {
+            return;
+        }
+
+        if ( storedChallengeItems != null )
+        {
+            for ( final StoredChallengeItem storedChallengeItem : storedChallengeItems )
+            {
+                final StoredResponseItem storedResponseItem = storedChallengeItem.getAnswer();
+                final String responseElementName = elementNameForType( type );
+                final Element responseElement = challengeToXml( storedChallengeItem, storedResponseItem, responseElementName );
+                parentElement.addContent( responseElement );
+            }
+        }
+
+    }
+
+    private static Element challengeToXml(
+            final StoredChallengeItem loopChallenge,
+            final StoredResponseItem answer,
+            final String elementName
+    )
+    {
+        final Element responseElement = new Element( elementName );
+        responseElement.addContent( new Element( XML_NODE_CHALLENGE ).addContent( new Text( loopChallenge.getQuestionText() ) ) );
+        final Element answerElement = answerToXml( loopChallenge.getAnswer() );
+        responseElement.addContent( answerElement );
+        responseElement.setAttribute( XML_ATTRIBUTE_REQUIRED, Boolean.toString( loopChallenge.getResponseLevel() == ResponseLevel.REQUIRED ) );
+        return responseElement;
+    }
+
+    private static Element answerToXml( final StoredResponseItem storedResponseItem )
+    {
+        final Element answerElement = new Element( XML_NODE_ANSWER_VALUE );
+        answerElement.setText( storedResponseItem.getHash() );
+        if ( storedResponseItem.getSalt() != null && !storedResponseItem.getSalt().isEmpty() )
+        {
+            answerElement.setAttribute( XML_ATTRIBUTE_SALT, storedResponseItem.getSalt() );
+        }
+        answerElement.setAttribute( XML_ATTRIBUTE_CONTENT_FORMAT, storedResponseItem.getFormat() );
+        if ( storedResponseItem.getIterations() > 1 )
+        {
+            answerElement.setAttribute( XML_ATTRIBUTE_HASH_COUNT, String.valueOf( storedResponseItem.getIterations() ) );
+        }
+        return answerElement;
+    }
+
+
+}

+ 48 - 0
pwm-cr/src/main/java/password/pwm/cr/CrUtils.java

@@ -0,0 +1,48 @@
+/*
+ * 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.cr;
+
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.time.Instant;
+import java.util.TimeZone;
+
+public class CrUtils
+{
+    static final String DATE_FORMAT = "yyyy-MM-dd HH:mm:ss Z";
+
+    static Instant parseDateString( final String input ) throws ParseException
+    {
+        final SimpleDateFormat dateFormatter = new SimpleDateFormat( DATE_FORMAT );
+        dateFormatter.setTimeZone( TimeZone.getTimeZone( "Zulu" ) );
+        return dateFormatter.parse( input ).toInstant();
+    }
+
+    static String formatDateString( final Instant input )
+    {
+        final SimpleDateFormat dateFormatter = new SimpleDateFormat( DATE_FORMAT );
+        dateFormatter.setTimeZone( TimeZone.getTimeZone( "Zulu" ) );
+        return dateFormatter.format( input );
+    }
+}

+ 27 - 0
pwm-cr/src/main/java/password/pwm/cr/JsonStoredResponseSerializer.java

@@ -0,0 +1,27 @@
+/*
+ * 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.cr;
+
+public class JsonStoredResponseSerializer
+{
+}

+ 56 - 0
pwm-cr/src/main/java/password/pwm/cr/StoredItemUtils.java

@@ -0,0 +1,56 @@
+/*
+ * 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.cr;
+
+import password.pwm.cr.api.ResponseLevel;
+import password.pwm.cr.api.StoredChallengeItem;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+public final class StoredItemUtils
+{
+
+    private StoredItemUtils( )
+    {
+    }
+
+    public static List<StoredChallengeItem> filterStoredChallenges(
+            final List<StoredChallengeItem> input,
+            final ResponseLevel responseLevel )
+    {
+        final List<StoredChallengeItem> returnList = new ArrayList<>();
+        if ( input != null )
+        {
+            for ( final StoredChallengeItem storedChallengeItem : input )
+            {
+                if ( storedChallengeItem.getResponseLevel() == responseLevel )
+                {
+                    returnList.add( storedChallengeItem );
+                }
+            }
+        }
+        return Collections.unmodifiableList( returnList );
+    }
+}

+ 91 - 0
pwm-cr/src/main/java/password/pwm/cr/api/ChallengeItemPolicy.java

@@ -0,0 +1,91 @@
+/*
+ * 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.cr.api;
+
+import lombok.Builder;
+import lombok.Value;
+
+import java.io.Serializable;
+
+@Builder
+@Value
+public class ChallengeItemPolicy implements Serializable
+{
+    @Builder.Default
+    private final String questionText = "";
+
+    @Builder.Default
+    private int minLength = 1;
+
+    @Builder.Default
+    private int maxLength = 255;
+
+    @Builder.Default
+    private int maxQuestionCharsInAnswer = 0;
+
+    @Builder.Default
+    private boolean enforceWordList = false;
+
+    @Builder.Default
+    private QuestionSource questionSource = QuestionSource.ADMIN_DEFINED;
+
+    @Builder.Default
+    private ResponseLevel responseLevel = ResponseLevel.REQUIRED;
+
+    public void validate( ) throws IllegalArgumentException
+    {
+        if ( questionSource == null )
+        {
+            throw new IllegalArgumentException( "questionSource can not be null" );
+        }
+
+        if ( responseLevel == null )
+        {
+            throw new IllegalArgumentException( "responseLevel can not be null" );
+        }
+
+        if ( questionText == null || questionText.isEmpty() )
+        {
+            if ( questionSource == QuestionSource.ADMIN_DEFINED )
+            {
+                throw new IllegalArgumentException( "questionText is required when questionSource is "
+                        + QuestionSource.ADMIN_DEFINED.toString() );
+            }
+        }
+
+        if ( minLength < 1 )
+        {
+            throw new IllegalArgumentException( "minLength must be greater than zero" );
+        }
+
+        if ( maxLength < 1 )
+        {
+            throw new IllegalArgumentException( "maxLength must be greater than zero" );
+        }
+
+        if ( maxQuestionCharsInAnswer < 0 )
+        {
+            throw new IllegalArgumentException( "maxQuestionCharsInAnswer must be zero or greater" );
+        }
+    }
+}

+ 46 - 0
pwm-cr/src/main/java/password/pwm/cr/api/ChallengeSetPolicy.java

@@ -0,0 +1,46 @@
+/*
+ * 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.cr.api;
+
+import lombok.Builder;
+import lombok.Value;
+
+import java.io.Serializable;
+import java.util.List;
+
+@Value
+@Builder
+public class ChallengeSetPolicy implements Serializable
+{
+    private String id;
+
+    private String locale;
+
+    private List<ChallengeItemPolicy> challengeItemPolicies;
+
+    private int minRandomsDuringResponse;
+
+    private int minRandomsDuringSetup;
+
+    private boolean caseSensitive;
+}

+ 29 - 0
pwm-cr/src/main/java/password/pwm/cr/api/QuestionSource.java

@@ -0,0 +1,29 @@
+/*
+ * 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.cr.api;
+
+public enum QuestionSource
+{
+    ADMIN_DEFINED,
+    USER_DEFINED,
+}

+ 29 - 0
pwm-cr/src/main/java/password/pwm/cr/api/ResponseLevel.java

@@ -0,0 +1,29 @@
+/*
+ * 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.cr.api;
+
+public enum ResponseLevel
+{
+    REQUIRED,
+    RANDOM,
+}

+ 38 - 0
pwm-cr/src/main/java/password/pwm/cr/api/StoredChallengeItem.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.cr.api;
+
+import lombok.Builder;
+import lombok.Value;
+
+import java.io.Serializable;
+
+@Value
+@Builder
+public class StoredChallengeItem implements Serializable
+{
+    private String id;
+    private String questionText;
+    private ResponseLevel responseLevel;
+    private StoredResponseItem answer;
+}

+ 40 - 0
pwm-cr/src/main/java/password/pwm/cr/api/StoredResponseItem.java

@@ -0,0 +1,40 @@
+/*
+ * 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.cr.api;
+
+import lombok.Builder;
+import lombok.Value;
+
+import java.io.Serializable;
+
+@Value
+@Builder
+public class StoredResponseItem implements Serializable
+{
+    private String format;
+    private String hash;
+    private String salt;
+
+    /** Number of hash iterations. */
+    private final int iterations;
+}

+ 42 - 0
pwm-cr/src/main/java/password/pwm/cr/api/StoredResponseSet.java

@@ -0,0 +1,42 @@
+/*
+ * 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.cr.api;
+
+import lombok.Builder;
+import lombok.Value;
+
+import java.io.Serializable;
+import java.time.Instant;
+import java.util.List;
+
+@Value
+@Builder
+public class StoredResponseSet implements Serializable
+{
+    private String id;
+    private String locale;
+    private List<StoredChallengeItem> storedChallengeItems;
+    private int minRandomsDuringResponse;
+    private boolean caseSensitive;
+    private Instant timestamp;
+}

+ 53 - 0
pwm-cr/src/main/java/password/pwm/cr/hash/AbstractHashMachine.java

@@ -0,0 +1,53 @@
+/*
+ * 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.cr.hash;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+public abstract class AbstractHashMachine implements ResponseHashMachineSpi
+{
+    private final Map<String, String> parameters = new HashMap<>();
+
+    public void init( final Map<String, String> parameters )
+    {
+
+    }
+
+    public Map<String, String> defaultParameters( )
+    {
+        return null;
+    }
+
+    Map<String, String> effectiveParameters( )
+    {
+        return Collections.unmodifiableMap( parameters );
+    }
+
+    protected boolean isCaseSensative( )
+    {
+        return effectiveParameters().containsKey( HashParameter.caseSensitive.toString() )
+                && Boolean.parseBoolean( effectiveParameters().get( HashParameter.caseSensitive.toString() ) );
+    }
+}

+ 76 - 0
pwm-cr/src/main/java/password/pwm/cr/hash/HashFactory.java

@@ -0,0 +1,76 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2018 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+package password.pwm.cr.hash;
+
+import password.pwm.cr.api.StoredResponseItem;
+
+import java.util.Map;
+
+public class HashFactory
+{
+    StoredResponseItem responseItemForRawValue(
+            final String response,
+            final ResponseHashAlgorithm responseHashAlgorithm,
+            final Map<HashParameter, String> parameters
+
+    )
+    {
+        return null;
+    }
+
+    public static boolean testResponseItem(
+            final StoredResponseItem storedResponseItem,
+            final String answer
+    )
+    {
+        final ResponseHashMachine responseHashMachine = machineForStoredResponse( storedResponseItem );
+        return responseHashMachine.test( storedResponseItem, answer );
+    }
+
+
+    private static ResponseHashMachine machineForStoredResponse( final StoredResponseItem storedResponseItem )
+    {
+        final String algName = storedResponseItem.getFormat();
+        final ResponseHashAlgorithm alg;
+        try
+        {
+            alg = ResponseHashAlgorithm.valueOf( algName );
+        }
+        catch ( IllegalArgumentException e )
+        {
+            throw new IllegalArgumentException( "unknown format type '" + algName + "'" );
+        }
+        final Class algClass = alg.getImplementingClass();
+        final ResponseHashMachineSpi responseHashMachine;
+        try
+        {
+            responseHashMachine = ( ResponseHashMachineSpi ) algClass.newInstance();
+        }
+        catch ( Exception e )
+        {
+            throw new IllegalStateException( "unexpected error instantiating response hash machine spi class: " + e.getMessage() );
+        }
+        responseHashMachine.init( alg );
+        return responseHashMachine;
+    }
+}

+ 46 - 0
pwm-cr/src/main/java/password/pwm/cr/hash/HashParameter.java

@@ -0,0 +1,46 @@
+/*
+ * 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.cr.hash;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public enum HashParameter
+{
+    iterations,
+    outputLength,
+    saltLength,
+    caseSensitive,;
+
+    static Map<String, String> untypedParamMap( final Map<HashParameter, String> parameters )
+    {
+        final Map<String, String> returnMap = new HashMap<>();
+        for ( final Map.Entry<HashParameter, String> entry : parameters.entrySet() )
+        {
+            final HashParameter key = entry.getKey();
+            final String value = entry.getValue();
+            returnMap.put( key.toString(), value );
+        }
+        return returnMap;
+    }
+}

+ 126 - 0
pwm-cr/src/main/java/password/pwm/cr/hash/PBKDF2HashMachine.java

@@ -0,0 +1,126 @@
+/*
+ * 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.cr.hash;
+
+import net.iharder.Base64;
+import password.pwm.cr.api.StoredResponseItem;
+
+import javax.crypto.SecretKeyFactory;
+import javax.crypto.spec.PBEKeySpec;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+class PBKDF2HashMachine extends AbstractHashMachine implements ResponseHashMachineSpi
+{
+
+    private ResponseHashAlgorithm responseHashAlgorithm;
+
+    PBKDF2HashMachine( )
+    {
+    }
+
+    public void init( final ResponseHashAlgorithm responseHashAlgorithm )
+    {
+        this.responseHashAlgorithm = responseHashAlgorithm;
+        switch ( responseHashAlgorithm )
+        {
+            case PBKDF2:
+            case PBKDF2_SHA256:
+            case PBKDF2_SHA512:
+                break;
+
+            default:
+                throw new IllegalArgumentException( "implementation does not support hash algorithm " + responseHashAlgorithm );
+        }
+    }
+
+    @Override
+    public Map<String, String> defaultParameters( )
+    {
+        final Map<String, String> map = new HashMap<>();
+        map.put( HashParameter.caseSensitive.toString(), String.valueOf( false ) );
+        return Collections.unmodifiableMap( map );
+    }
+
+    @Override
+    public StoredResponseItem generate( final String input )
+    {
+        //@todo
+        return null;
+    }
+
+    @Override
+    public boolean test( final StoredResponseItem hashedResponse, final String input )
+    {
+        final String newHash = hashValue( input, hashedResponse.getIterations(), hashedResponse.getSalt() );
+        return newHash.equals( hashedResponse.getHash() );
+    }
+
+    private String hashValue( final String input, final int iterations, final String salt )
+    {
+        try
+        {
+            final PBEKeySpec spec;
+            final SecretKeyFactory skf;
+            {
+                final String methodName;
+                final int keyLength;
+                switch ( responseHashAlgorithm )
+                {
+                    case PBKDF2:
+                        methodName = "PBKDF2WithHmacSHA1";
+                        keyLength = 64 * 8;
+                        break;
+
+                    case PBKDF2_SHA256:
+                        methodName = "PBKDF2WithHmacSHA256";
+                        keyLength = 128 * 8;
+                        break;
+
+                    case PBKDF2_SHA512:
+                        methodName = "PBKDF2WithHmacSHA512";
+                        keyLength = 192 * 8;
+                        break;
+
+                    default:
+                        throw new IllegalStateException( "formatType not supported: " + responseHashAlgorithm.toString() );
+
+                }
+
+                final char[] chars = input.toCharArray();
+                final byte[] saltBytes = salt.getBytes( "UTF-8" );
+
+                spec = new PBEKeySpec( chars, saltBytes, iterations, keyLength );
+                skf = SecretKeyFactory.getInstance( methodName );
+            }
+            final byte[] hash = skf.generateSecret( spec ).getEncoded();
+            return Base64.encodeBytes( hash );
+        }
+        catch ( Exception e )
+        {
+            throw new IllegalStateException( "unable to perform PBKDF2 hashing operation: " + e.getMessage() );
+        }
+    }
+
+}

+ 50 - 0
pwm-cr/src/main/java/password/pwm/cr/hash/ResponseHashAlgorithm.java

@@ -0,0 +1,50 @@
+/*
+ * 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.cr.hash;
+
+public enum ResponseHashAlgorithm
+{
+    TEXT( TextHashMachine.class ),
+    MD5( TypicalHashMachine.class ),
+    SHA1( TypicalHashMachine.class ),
+    SHA1_SALT( TypicalHashMachine.class ),
+    SHA256_SALT( TypicalHashMachine.class ),
+    SHA512_SALT( TypicalHashMachine.class ),
+    //    BCRYPT(),
+//    SCRYPT(),
+    PBKDF2( PBKDF2HashMachine.class ),
+    PBKDF2_SHA256( PBKDF2HashMachine.class ),
+    PBKDF2_SHA512( PBKDF2HashMachine.class ),;
+
+    private final Class<? extends ResponseHashMachineSpi> implementingClass;
+
+    ResponseHashAlgorithm( final Class<? extends ResponseHashMachineSpi> responseHashMachineSpi )
+    {
+        this.implementingClass = responseHashMachineSpi;
+    }
+
+    public Class<? extends ResponseHashMachineSpi> getImplementingClass( )
+    {
+        return implementingClass;
+    }
+}

+ 5 - 9
server/src/main/java/password/pwm/svc/cluster/ClusterProvider.java → pwm-cr/src/main/java/password/pwm/cr/hash/ResponseHashMachine.java

@@ -20,17 +20,13 @@
  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
  */
  */
 
 
-package password.pwm.svc.cluster;
+package password.pwm.cr.hash;
 
 
-import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.cr.api.StoredResponseItem;
 
 
-import java.util.List;
-
-public interface ClusterProvider
+public interface ResponseHashMachine
 {
 {
-    void close( );
-
-    boolean isMaster( );
+    StoredResponseItem generate( String input );
 
 
-    List<NodeInfo> nodes( ) throws PwmUnrecoverableException;
+    boolean test( StoredResponseItem storedResponseItem, String input );
 }
 }

+ 28 - 0
pwm-cr/src/main/java/password/pwm/cr/hash/ResponseHashMachineSpi.java

@@ -0,0 +1,28 @@
+/*
+ * 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.cr.hash;
+
+public interface ResponseHashMachineSpi extends ResponseHashMachine
+{
+    void init( ResponseHashAlgorithm algorithm );
+}

+ 67 - 0
pwm-cr/src/main/java/password/pwm/cr/hash/TextHashMachine.java

@@ -0,0 +1,67 @@
+/*
+ * 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.cr.hash;
+
+import password.pwm.cr.api.StoredResponseItem;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+class TextHashMachine extends AbstractHashMachine implements ResponseHashMachineSpi
+{
+
+    TextHashMachine( )
+    {
+    }
+
+    @Override
+    public void init( final ResponseHashAlgorithm algorithm )
+    {
+
+    }
+
+    @Override
+    public Map<String, String> defaultParameters( )
+    {
+        final Map<String, String> defaultParamMap = new HashMap<>();
+        defaultParamMap.put( HashParameter.caseSensitive.toString(), Boolean.toString( false ) );
+        return Collections.unmodifiableMap( defaultParamMap );
+    }
+
+    @Override
+    public StoredResponseItem generate( final String input )
+    {
+        return null;
+    }
+
+    @Override
+    public boolean test( final StoredResponseItem hash, final String input )
+    {
+        if ( input == null || hash == null )
+        {
+            return false;
+        }
+        return false;
+    }
+}

+ 144 - 0
pwm-cr/src/main/java/password/pwm/cr/hash/TypicalHashMachine.java

@@ -0,0 +1,144 @@
+/*
+ * 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.cr.hash;
+
+import net.iharder.Base64;
+import password.pwm.cr.api.StoredResponseItem;
+
+import java.io.UnsupportedEncodingException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+public class TypicalHashMachine extends AbstractHashMachine implements ResponseHashMachineSpi
+{
+
+    private static final Map<ResponseHashAlgorithm, String> SUPPORTED_FORMATS;
+
+    enum VERSION
+    {
+        // original version had bug where only one iteration was ever actually performed regardless of hashCount value
+        A,
+
+        // nominal working version
+        B,
+    }
+
+    static
+    {
+        final Map<ResponseHashAlgorithm, String> map = new HashMap<>();
+        map.put( ResponseHashAlgorithm.MD5, "MD5" );
+        map.put( ResponseHashAlgorithm.SHA1, "SHA1" );
+        map.put( ResponseHashAlgorithm.SHA1_SALT, "SHA1" );
+        map.put( ResponseHashAlgorithm.SHA256_SALT, "SHA-256" );
+        map.put( ResponseHashAlgorithm.SHA512_SALT, "SHA-512" );
+        SUPPORTED_FORMATS = Collections.unmodifiableMap( map );
+    }
+
+    private ResponseHashAlgorithm responseHashAlgorithm;
+
+    public TypicalHashMachine( )
+    {
+    }
+
+    public void init( final ResponseHashAlgorithm responseHashAlgorithm )
+    {
+        this.responseHashAlgorithm = responseHashAlgorithm;
+        if ( !SUPPORTED_FORMATS.containsKey( responseHashAlgorithm ) )
+        {
+            throw new IllegalArgumentException( "implementation does not support hash algorithm " + responseHashAlgorithm );
+        }
+    }
+
+    @Override
+    public Map<String, String> defaultParameters( )
+    {
+        final Map<String, String> map = new HashMap<>();
+        map.put( HashParameter.caseSensitive.toString(), String.valueOf( false ) );
+        return Collections.unmodifiableMap( map );
+    }
+
+    @Override
+    public StoredResponseItem generate( final String input )
+    {
+        //@todo
+        return null;
+    }
+
+    @Override
+    public boolean test( final StoredResponseItem hashedResponse, final String input )
+    {
+        final String newHash = doHash( input, hashedResponse.getIterations(), ResponseHashAlgorithm.SHA1_SALT, VERSION.B );
+        return newHash.equals( hashedResponse.getHash() );
+    }
+
+    static String doHash(
+            final String input,
+            final int hashCount,
+            final ResponseHashAlgorithm formatType,
+            final VERSION version
+    )
+            throws IllegalStateException
+    {
+        final String algorithm = SUPPORTED_FORMATS.get( formatType );
+        final MessageDigest md;
+        try
+        {
+            md = MessageDigest.getInstance( algorithm );
+        }
+        catch ( NoSuchAlgorithmException e )
+        {
+            throw new IllegalStateException( "unable to load " + algorithm + " message digest algorithm: " + e.getMessage() );
+        }
+
+
+        byte[] hashedBytes;
+        try
+        {
+            hashedBytes = input.getBytes( "UTF-8" );
+        }
+        catch ( UnsupportedEncodingException e )
+        {
+            throw new IllegalStateException( "unsupported UTF8 byte encoding: " + e.getMessage() );
+        }
+
+        switch ( version )
+        {
+            case A:
+                hashedBytes = md.digest( hashedBytes );
+                return Base64.encodeBytes( hashedBytes );
+
+            case B:
+                for ( int i = 0; i < hashCount; i++ )
+                {
+                    hashedBytes = md.digest( hashedBytes );
+                }
+                return Base64.encodeBytes( hashedBytes );
+
+            default:
+                throw new IllegalStateException( "unexpected version enum in hash method" );
+        }
+    }
+}

+ 139 - 0
pwm-cr/src/test/java/password/pwm/cr/ChaiXmlResponseSet1Test.java

@@ -0,0 +1,139 @@
+/*
+ * 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.cr;
+
+import org.junit.Assert;
+import org.junit.Test;
+import password.pwm.cr.api.ResponseLevel;
+import password.pwm.cr.api.StoredChallengeItem;
+import password.pwm.cr.api.StoredResponseItem;
+import password.pwm.cr.api.StoredResponseSet;
+import password.pwm.cr.hash.HashFactory;
+
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.nio.charset.Charset;
+
+public class ChaiXmlResponseSet1Test {
+
+    @Test
+    public void testReadingStoredChaiXmlChallengeSet() throws IOException {
+        /*
+        final Reader reader = readInputXmlFile();
+        StoredResponseSet storedResponseSet = new ChaiXmlResponseSetSerializer().read(reader, ChaiXmlResponseSetSerializer.Type.USER);
+
+        testUserResponseSetValidity(storedResponseSet);
+        */
+    }
+
+
+    @Test
+    public void testReadingStoredChaiHelpdeskXmlChallengeSet() throws IOException {
+        final Reader reader = readInputXmlFile();
+        StoredResponseSet storedResponseSet = new ChaiXmlResponseSetSerializer().read(reader, ChaiXmlResponseSetSerializer.Type.HELPDESK);
+
+        testHelpdeskResponseSetValidity(storedResponseSet);
+    }
+
+    @Test
+    public void testReadWriteRead() throws IOException {
+        /*
+        final ChaiXmlResponseSetSerializer chaiXmlResponseSetSerializer = new ChaiXmlResponseSetSerializer();
+
+
+        final Map<ChaiXmlResponseSetSerializer.Type,StoredResponseSet> firstResponsesRead;
+        {
+            final Reader reader = readInputXmlFile();
+            firstResponsesRead = chaiXmlResponseSetSerializer.read(reader);
+        }
+
+        final String firstResponsesWritten;
+        {
+            final StringWriter writer = new StringWriter();
+            new ChaiXmlResponseSetSerializer().write(writer, firstResponsesRead);
+            firstResponsesWritten = writer.toString();
+        }
+
+        final Map<ChaiXmlResponseSetSerializer.Type,StoredResponseSet> secondResponsesRead;
+        {
+            final Reader reader = new StringReader(firstResponsesWritten);
+            secondResponsesRead = chaiXmlResponseSetSerializer.read(reader);
+        }
+
+        testUserResponseSetValidity(secondResponsesRead.get(ChaiXmlResponseSetSerializer.Type.USER));
+        testHelpdeskResponseSetValidity(secondResponsesRead.get(ChaiXmlResponseSetSerializer.Type.HELPDESK));
+        */
+    }
+
+    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());
+
+        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)) {
+                final StoredResponseItem storedResponseItem = storedChallengeItem.getAnswer();
+                Assert.assertTrue(HashFactory.testResponseItem(storedResponseItem, "book"));
+                Assert.assertFalse(HashFactory.testResponseItem(storedResponseItem, "wrong answer"));
+            }
+
+            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"));
+            }
+
+            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"));
+            }
+
+            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"));
+            }
+        }
+
+    }
+
+    private void testHelpdeskResponseSetValidity(final StoredResponseSet storedResponseSet) {
+        Assert.assertEquals(2, storedResponseSet.getStoredChallengeItems().size());
+
+        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)) {
+                final StoredResponseItem storedResponseItem = storedChallengeItem.getAnswer();
+
+            }
+        }
+    }
+
+}

+ 53 - 0
pwm-cr/src/test/java/password/pwm/cr/ChaiXmlResponseSetReaderTest.java

@@ -0,0 +1,53 @@
+/*
+ * 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.cr;
+
+import org.junit.Test;
+import password.pwm.cr.api.ChallengeItemPolicy;
+import password.pwm.cr.api.QuestionSource;
+import password.pwm.cr.api.ResponseLevel;
+
+
+public class ChaiXmlResponseSetReaderTest {
+
+    @Test(expected=IllegalArgumentException.class)
+    public void testBogusMaxLength() throws Exception {
+
+        ChallengeItemPolicy.builder()
+                .questionText("question 1!")
+                .maxLength(-3)
+                .build().validate();
+    }
+
+    @Test
+    public void testValidChallengeItemCreations() {
+        ChallengeItemPolicy.builder()
+                .questionText("question 1!")
+                .minLength(1)
+                .maxLength(10)
+                .questionSource(QuestionSource.ADMIN_DEFINED)
+                .responseLevel(ResponseLevel.REQUIRED)
+                .build();
+
+    }
+}

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

@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Password Management Servlets (PWM)
+  ~ 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
+  -->
+
+<ResponseSet minRandomRequired="2" locale="en" version="2" chaiVersion="0.6.9-SNAPSHOT" caseInsensitive="true" challengeSetID="SSPR-defined [Version Missing]" time="2016-08-23 08:48:15 +0000">
+    <response adminDefined="true" required="false" minLength="4" maxLength="200">
+        <challenge>What is the name of the main character in your favorite book?</challenge>
+        <answer salt="WHc5dJydH8xBHoqpS1fsnEhHtETdjblt" format="PBKDF2" hashcount="100000">OYfp1MdBrysBfaYHu+KSOhieagPilStxSMMVSuIz8DgtygXI2yHWdHEh42FMhdRUjHRUS0PbdPpGhuptgXCBXQ==</answer>
+    </response>
+    <response adminDefined="true" required="false" minLength="4" maxLength="200">
+        <challenge>What is the name of your favorite teacher?</challenge>
+        <answer salt="vA4aGz6KhNKRcnMj2nSLzWgHgXw0LcRr" format="SHA1_SALT" hashcount="100000">B:Hm9U8bh2oXzqFnPif8wChoVosss=</answer>
+    </response>
+    <response adminDefined="true" required="false" minLength="4" maxLength="200">
+        <challenge>What was the name of your childhood best friend?</challenge>
+        <answer salt="hUOJI16WDk1bCrVtAhuURmQl5NhIn7XV" format="PBKDF2" hashcount="100000">FhZELpheB9JSAju8vpxwmEik7dvlV38d/iXPpalSw1g3i2lqZAgGt2ntv24K7OklzcR3HfoKHNMqIhKlwljovg==</answer>
+    </response>
+    <response adminDefined="true" required="false" minLength="4" maxLength="200">
+        <challenge>What was your favorite show as a child?</challenge>
+        <answer salt="hd0vlgkhBOJZizCpAm4Ip1gNO5JvZTcO" format="PBKDF2" hashcount="100000">OYs6l6CH8E0fhyNlp8cfzO1YATgFygsimw37ah+LJevNdCRpDe9eKrDlCXQEFDgqumrTOwHGTa56/PTEwptXpQ==</answer>
+    </response>
+    <helpdesk-response adminDefined="true" required="false" minLength="4" maxLength="200">
+        <challenge>Question 1</challenge>
+        <answer format="HELPDESK">H4sIAAAAAAAAAIvx-82rNP_i_ouVZwNn50a-BwBCsGs2EAAAAA==</answer>
+    </helpdesk-response>
+    <helpdesk-response adminDefined="true" required="false" minLength="4" maxLength="200">
+        <challenge>Question 2</challenge>
+        <answer format="HELPDESK">H4sIAAAAAAAAAIuQYQ59M3HZ5VvFk6_dNZjsAAAQvpfpEAAAAA==</answer>
+    </helpdesk-response>
+</ResponseSet>

+ 5 - 131
rest-test-service/pom.xml

@@ -9,119 +9,26 @@
 
 
     <modelVersion>4.0.0</modelVersion>
     <modelVersion>4.0.0</modelVersion>
 
 
-    <artifactId>rest-test-server</artifactId>
+    <artifactId>rest-test-service</artifactId>
     <packaging>war</packaging>
     <packaging>war</packaging>
 
 
     <name>PWM Password Self Service: Web Service Test Server</name>
     <name>PWM Password Self Service: Web Service Test Server</name>
 
 
+    <properties>
+        <project.root.basedir>${project.basedir}/..</project.root.basedir>
+    </properties>
+
     <profiles>
     <profiles>
-        <profile>
-            <id>skip-tests</id>
-            <properties>
-                <skipTests>true</skipTests>
-            </properties>
-        </profile>
         <profile>
         <profile>
             <id>skip-javadoc</id>
             <id>skip-javadoc</id>
             <properties>
             <properties>
                 <maven.javadoc.skip>true</maven.javadoc.skip>
                 <maven.javadoc.skip>true</maven.javadoc.skip>
             </properties>
             </properties>
         </profile>
         </profile>
-        <profile>
-            <id>skip-checkstyle</id>
-            <properties>
-                <checkstyle.skip>true</checkstyle.skip>
-            </properties>
-        </profile>
-        <profile>
-            <id>skip-spotbugs</id>
-            <properties>
-                <skipSpotbugs>true</skipSpotbugs>
-            </properties>
-        </profile>
     </profiles>
     </profiles>
 
 
     <build>
     <build>
         <plugins>
         <plugins>
-            <plugin>
-                <groupId>com.github.spotbugs</groupId>
-                <artifactId>spotbugs-maven-plugin</artifactId>
-                <version>3.1.3.1</version>
-                <dependencies>
-                    <dependency>
-                        <groupId>com.github.spotbugs</groupId>
-                        <artifactId>spotbugs</artifactId>
-                        <version>3.1.6</version>
-                    </dependency>
-                </dependencies>
-                <configuration>
-                    <fork>false</fork>
-                    <excludeFilterFile>../build/spotbugs-exclude.xml</excludeFilterFile>
-                    <includeTests>false</includeTests>
-                    <skip>${skipSpotbugs}</skip>
-                </configuration>
-                <executions>
-                    <execution>
-                        <phase>test</phase>
-                        <goals>
-                            <goal>check</goal>
-                        </goals>
-                    </execution>
-                </executions>
-            </plugin>
-            <plugin>
-                <groupId>org.apache.maven.plugins</groupId>
-                <artifactId>maven-surefire-plugin</artifactId>
-                <version>2.19.1</version>
-                <configuration>
-                    <skipTests>${skipTests}</skipTests>
-                </configuration>
-            </plugin>
-            <plugin>
-                <groupId>org.apache.maven.plugins</groupId>
-                <artifactId>maven-source-plugin</artifactId>
-                <version>3.0.1</version>
-                <configuration>
-                    <includePom>true</includePom>
-                    <archive>
-                        <manifestEntries>
-                            <Implementation-Archive-Name>pwm.source</Implementation-Archive-Name>
-                            <Implementation-Title>${project.name}</Implementation-Title>
-                            <Implementation-Version>${project.version}</Implementation-Version>
-                            <Implementation-Vendor>${project.organization.name}</Implementation-Vendor>
-                            <Implementation-URL>${project.organization.url}</Implementation-URL>
-                            <Implementation-Build-Java-Vendor>${java.vendor}</Implementation-Build-Java-Vendor>
-                            <Implementation-Build-Java-Version>${java.version}</Implementation-Build-Java-Version>
-                            <Implementation-Build>${build.number}</Implementation-Build>
-                            <Implementation-Build-Timestamp>${timestamp.iso}</Implementation-Build-Timestamp>
-                            <Implementation-Revision>${build.revision}</Implementation-Revision>
-                            <Implementation-Version-Display>v${project.version} b${build.number} r${build.revision}</Implementation-Version-Display>
-                        </manifestEntries>
-                    </archive>
-                </configuration>
-            </plugin>
-            <plugin>
-                <groupId>org.apache.maven.plugins</groupId>
-                <artifactId>maven-javadoc-plugin</artifactId>
-                <version>3.0.0</version>
-                <configuration>
-                    <archive>
-                        <manifestEntries>
-                            <Implementation-Archive-Name>pwm.javadoc</Implementation-Archive-Name>
-                            <Implementation-Title>${project.name}</Implementation-Title>
-                            <Implementation-Version>${project.version}</Implementation-Version>
-                            <Implementation-Vendor>${project.organization.name}</Implementation-Vendor>
-                            <Implementation-URL>${project.organization.url}</Implementation-URL>
-                            <Implementation-Build-Java-Vendor>${java.vendor}</Implementation-Build-Java-Vendor>
-                            <Implementation-Build-Java-Version>${java.version}</Implementation-Build-Java-Version>
-                            <Implementation-Build>${build.number}</Implementation-Build>
-                            <Implementation-Build-Timestamp>${timestamp.iso}</Implementation-Build-Timestamp>
-                            <Implementation-Revision>${build.revision}</Implementation-Revision>
-                            <Implementation-Version-Display>v${project.version} b${build.number} r${build.revision}</Implementation-Version-Display>
-                        </manifestEntries>
-                    </archive>
-                </configuration>
-            </plugin>
             <plugin>
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-war-plugin</artifactId>
                 <artifactId>maven-war-plugin</artifactId>
@@ -146,39 +53,6 @@
                     </archive>
                     </archive>
                 </configuration>
                 </configuration>
             </plugin>
             </plugin>
-            <plugin>
-                <groupId>org.apache.maven.plugins</groupId>
-                <artifactId>maven-checkstyle-plugin</artifactId>
-                <version>3.0.0</version>
-                <dependencies>
-                    <dependency>
-                        <groupId>com.puppycrawl.tools</groupId>
-                        <artifactId>checkstyle</artifactId>
-                        <version>8.11</version>
-                    </dependency>
-                </dependencies>
-                <executions>
-                    <execution>
-                        <id>validate</id>
-                        <phase>validate</phase>
-                        <configuration>
-                            <propertyExpansion>basedir=${basedir}</propertyExpansion>
-                            <configLocation>../build/checkstyle.xml</configLocation>
-                            <encoding>UTF-8</encoding>
-                            <consoleOutput>true</consoleOutput>
-                            <includeTestResources>false</includeTestResources>
-                            <failsOnError>true</failsOnError>
-                            <includes>**/*.java,**/*.jsp,**/*.properties,**/*.xml,**/*.css,**/*.svg</includes>
-                            <sourceDirectories>
-                                <directory>src/main</directory>
-                            </sourceDirectories>
-                        </configuration>
-                        <goals>
-                            <goal>check</goal>
-                        </goals>
-                    </execution>
-                </executions>
-            </plugin>
         </plugins>
         </plugins>
     </build>
     </build>
 
 

+ 2 - 2
rest-test-service/src/main/webapp/META-INF/context.xml

@@ -1,7 +1,8 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <?xml version="1.0" encoding="UTF-8"?>
 <!--
 <!--
   ~ Password Management Servlets (PWM)
   ~ Password Management Servlets (PWM)
-  ~ Copyright (c) 2006-2018 Novell, Inc.
+  ~ 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
   ~ 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
   ~ it under the terms of the GNU General Public License as published by
@@ -18,7 +19,6 @@
   ~ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
   ~ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
   -->
   -->
 
 
-
 <Context tldValidation="false" unloadDelay="30000" useHttpOnly="true">
 <Context tldValidation="false" unloadDelay="30000" useHttpOnly="true">
 
 
 </Context>
 </Context>

+ 4 - 3
rest-test-service/src/main/webapp/WEB-INF/web.xml

@@ -1,7 +1,8 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <?xml version="1.0" encoding="UTF-8"?>
 <!--
 <!--
   ~ Password Management Servlets (PWM)
   ~ Password Management Servlets (PWM)
-  ~ Copyright (c) 2006-2018 Novell, Inc.
+  ~ 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
   ~ 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
   ~ it under the terms of the GNU General Public License as published by
@@ -22,9 +23,9 @@
          xmlns="http://java.sun.com/xml/ns/javaee"
          xmlns="http://java.sun.com/xml/ns/javaee"
          xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
          xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
          id="PWM-rest-test-servlet" version="3.0">
          id="PWM-rest-test-servlet" version="3.0">
-    <display-name>PWM REST Test Server</display-name>
+    <display-name>PWM Web Service Test Server</display-name>
     <!-- <distributable/> Clustering/Session replication is not supported -->
     <!-- <distributable/> Clustering/Session replication is not supported -->
-    <description>PWM REST Test Server</description>
+    <description>PWM Web Service Test Server</description>
     <welcome-file-list>
     <welcome-file-list>
         <welcome-file>index.jsp</welcome-file>
         <welcome-file>index.jsp</welcome-file>
     </welcome-file-list>
     </welcome-file-list>

+ 5 - 145
server/pom.xml

@@ -15,35 +15,11 @@
     <name>PWM Password Self Service: Server JAR</name>
     <name>PWM Password Self Service: Server JAR</name>
 
 
     <properties>
     <properties>
+        <project.root.basedir>${project.basedir}/..</project.root.basedir>
         <skipTests>false</skipTests>
         <skipTests>false</skipTests>
-        <skipSpotbugs>false</skipSpotbugs>
     </properties>
     </properties>
 
 
     <profiles>
     <profiles>
-        <profile>
-            <id>skip-tests</id>
-            <properties>
-                <skipTests>true</skipTests>
-            </properties>
-        </profile>
-        <profile>
-            <id>skip-javadoc</id>
-            <properties>
-                <maven.javadoc.skip>true</maven.javadoc.skip>
-            </properties>
-        </profile>
-        <profile>
-            <id>skip-checkstyle</id>
-            <properties>
-                <checkstyle.skip>true</checkstyle.skip>
-            </properties>
-        </profile>
-        <profile>
-            <id>skip-spotbugs</id>
-            <properties>
-                <skipSpotbugs>true</skipSpotbugs>
-            </properties>
-        </profile>
         <profile>
         <profile>
             <!-- Builds a zip file containing the built war file, along with the supplemental directory -->
             <!-- Builds a zip file containing the built war file, along with the supplemental directory -->
             <id>release</id>
             <id>release</id>
@@ -110,121 +86,6 @@
             </plugin>
             </plugin>
             -->
             -->
 
 
-            <plugin>
-                <groupId>com.github.spotbugs</groupId>
-                <artifactId>spotbugs-maven-plugin</artifactId>
-                <version>3.1.3.1</version>
-                <dependencies>
-                    <dependency>
-                        <groupId>com.github.spotbugs</groupId>
-                        <artifactId>spotbugs</artifactId>
-                        <version>3.1.6</version>
-                    </dependency>
-                </dependencies>
-                <configuration>
-                    <fork>false</fork>
-                    <excludeFilterFile>../build/spotbugs-exclude.xml</excludeFilterFile>
-                    <includeTests>false</includeTests>
-                    <skip>${skipSpotbugs}</skip>
-                </configuration>
-                <executions>
-                    <execution>
-                        <phase>test</phase>
-                        <goals>
-                            <goal>check</goal>
-                        </goals>
-                    </execution>
-                </executions>
-            </plugin>
-            <plugin>
-                <groupId>org.apache.maven.plugins</groupId>
-                <artifactId>maven-surefire-plugin</artifactId>
-                <version>2.19.1</version>
-                <configuration>
-                    <skipTests>${skipTests}</skipTests>
-                    <excludes>
-                        <exclude>password.pwm.manual.*</exclude>
-                    </excludes>
-                </configuration>
-            </plugin>
-            <plugin>
-                <groupId>org.apache.maven.plugins</groupId>
-                <artifactId>maven-source-plugin</artifactId>
-                <version>3.0.1</version>
-                <configuration>
-                    <includePom>true</includePom>
-                    <archive>
-                        <manifestEntries>
-                            <Implementation-Archive-Name>pwm.source</Implementation-Archive-Name>
-                            <Implementation-Title>${project.name}</Implementation-Title>
-                            <Implementation-Version>${project.version}</Implementation-Version>
-                            <Implementation-Vendor>${project.organization.name}</Implementation-Vendor>
-                            <Implementation-URL>${project.organization.url}</Implementation-URL>
-                            <Implementation-Build-Java-Vendor>${java.vendor}</Implementation-Build-Java-Vendor>
-                            <Implementation-Build-Java-Version>${java.version}</Implementation-Build-Java-Version>
-                            <Implementation-Build>${build.number}</Implementation-Build>
-                            <Implementation-Build-Timestamp>${timestamp.iso}</Implementation-Build-Timestamp>
-                            <Implementation-Revision>${build.revision}</Implementation-Revision>
-                            <Implementation-Version-Display>v${project.version} b${build.number} r${build.revision}</Implementation-Version-Display>
-                        </manifestEntries>
-                    </archive>
-                </configuration>
-            </plugin>
-            <plugin>
-                <groupId>org.apache.maven.plugins</groupId>
-                <artifactId>maven-javadoc-plugin</artifactId>
-                <version>3.0.1</version>
-                <configuration>
-                    <archive>
-                        <manifestEntries>
-                            <Implementation-Archive-Name>pwm.javadoc</Implementation-Archive-Name>
-                            <Implementation-Title>${project.name}</Implementation-Title>
-                            <Implementation-Version>${project.version}</Implementation-Version>
-                            <Implementation-Vendor>${project.organization.name}</Implementation-Vendor>
-                            <Implementation-URL>${project.organization.url}</Implementation-URL>
-                            <Implementation-Build-Java-Vendor>${java.vendor}</Implementation-Build-Java-Vendor>
-                            <Implementation-Build-Java-Version>${java.version}</Implementation-Build-Java-Version>
-                            <Implementation-Build>${build.number}</Implementation-Build>
-                            <Implementation-Build-Timestamp>${timestamp.iso}</Implementation-Build-Timestamp>
-                            <Implementation-Revision>${build.revision}</Implementation-Revision>
-                            <Implementation-Version-Display>v${project.version} b${build.number} r${build.revision}</Implementation-Version-Display>
-                        </manifestEntries>
-                    </archive>
-                </configuration>
-            </plugin>
-            <plugin>
-                <groupId>org.apache.maven.plugins</groupId>
-                <artifactId>maven-checkstyle-plugin</artifactId>
-                <version>3.0.0</version>
-                <dependencies>
-                    <dependency>
-                        <groupId>com.puppycrawl.tools</groupId>
-                        <artifactId>checkstyle</artifactId>
-                        <version>8.11</version>
-                    </dependency>
-                </dependencies>
-                <executions>
-                    <execution>
-                        <id>validate</id>
-                        <phase>validate</phase>
-                        <configuration>
-                            <propertyExpansion>basedir=${basedir}</propertyExpansion>
-                            <configLocation>../build/checkstyle.xml</configLocation>
-                            <encoding>UTF-8</encoding>
-                            <consoleOutput>true</consoleOutput>
-                            <includeTestResources>false</includeTestResources>
-                            <failsOnError>true</failsOnError>
-                            <includes>**/*.java,**/*.jsp,**/*.properties,**/*.xml,**/*.css,**/*.svg</includes>
-                            <sourceDirectories>
-                                <directory>src/main</directory>
-                            </sourceDirectories>
-                        </configuration>
-                        <goals>
-                            <goal>check</goal>
-                        </goals>
-                    </execution>
-                </executions>
-            </plugin>
             <plugin>
             <plugin>
                 <artifactId>maven-resources-plugin</artifactId>
                 <artifactId>maven-resources-plugin</artifactId>
                 <version>2.7</version>
                 <version>2.7</version>
@@ -412,7 +273,6 @@
                     </execution>
                     </execution>
                 </executions>
                 </executions>
             </plugin>
             </plugin>
-
         </plugins>
         </plugins>
     </build>
     </build>
 
 
@@ -421,7 +281,7 @@
             <plugin> <!-- checks owsp vulnerability database -->
             <plugin> <!-- checks owsp vulnerability database -->
                 <groupId>org.owasp</groupId>
                 <groupId>org.owasp</groupId>
                 <artifactId>dependency-check-maven</artifactId>
                 <artifactId>dependency-check-maven</artifactId>
-                <version>3.1.2</version>
+                <version>3.3.1</version>
                 <reportSets>
                 <reportSets>
                     <reportSet>
                     <reportSet>
                         <reports>
                         <reports>
@@ -445,13 +305,13 @@
         <dependency>
         <dependency>
             <groupId>org.mockito</groupId>
             <groupId>org.mockito</groupId>
             <artifactId>mockito-core</artifactId>
             <artifactId>mockito-core</artifactId>
-            <version>2.13.0</version>
+            <version>2.21.0</version>
             <scope>test</scope>
             <scope>test</scope>
         </dependency>
         </dependency>
         <dependency>
         <dependency>
             <groupId>org.assertj</groupId>
             <groupId>org.assertj</groupId>
             <artifactId>assertj-core</artifactId>
             <artifactId>assertj-core</artifactId>
-            <version>3.9.1</version>
+            <version>3.10.0</version>
             <scope>test</scope>
             <scope>test</scope>
         </dependency>
         </dependency>
         <dependency>
         <dependency>
@@ -618,7 +478,7 @@
         <dependency>
         <dependency>
             <groupId>com.blueconic</groupId>
             <groupId>com.blueconic</groupId>
             <artifactId>browscap-java</artifactId>
             <artifactId>browscap-java</artifactId>
-            <version>1.2.3</version>
+            <version>1.2.4</version>
         </dependency>
         </dependency>
         <dependency>
         <dependency>
             <groupId>org.jetbrains.xodus</groupId>
             <groupId>org.jetbrains.xodus</groupId>

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

@@ -81,6 +81,9 @@ public enum AppProperty
     CLUSTER_DB_HEARTBEAT_SECONDS                    ( "cluster.db.heartbeatSeconds" ),
     CLUSTER_DB_HEARTBEAT_SECONDS                    ( "cluster.db.heartbeatSeconds" ),
     CLUSTER_DB_NODE_TIMEOUT_SECONDS                 ( "cluster.db.nodeTimeoutSeconds" ),
     CLUSTER_DB_NODE_TIMEOUT_SECONDS                 ( "cluster.db.nodeTimeoutSeconds" ),
     CLUSTER_DB_NODE_PURGE_SECONDS                   ( "cluster.db.nodePurgeSeconds" ),
     CLUSTER_DB_NODE_PURGE_SECONDS                   ( "cluster.db.nodePurgeSeconds" ),
+    CLUSTER_LDAP_HEARTBEAT_SECONDS                  ( "cluster.ldap.heartbeatSeconds" ),
+    CLUSTER_LDAP_NODE_TIMEOUT_SECONDS               ( "cluster.ldap.nodeTimeoutSeconds" ),
+    CLUSTER_LDAP_NODE_PURGE_SECONDS                 ( "cluster.ldap.nodePurgeSeconds" ),
     DB_JDBC_LOAD_STRATEGY                           ( "db.jdbcLoadStrategy" ),
     DB_JDBC_LOAD_STRATEGY                           ( "db.jdbcLoadStrategy" ),
     DB_CONNECTIONS_MAX                              ( "db.connections.max" ),
     DB_CONNECTIONS_MAX                              ( "db.connections.max" ),
     DB_CONNECTIONS_TIMEOUT_MS                       ( "db.connections.timeoutMs" ),
     DB_CONNECTIONS_TIMEOUT_MS                       ( "db.connections.timeoutMs" ),

+ 1 - 1
server/src/main/java/password/pwm/PwmApplication.java

@@ -660,7 +660,7 @@ public class PwmApplication
 
 
     private String fetchInstanceID( final LocalDB localDB, final PwmApplication pwmApplication )
     private String fetchInstanceID( final LocalDB localDB, final PwmApplication pwmApplication )
     {
     {
-        String newInstanceID = pwmApplication.getConfig().readSettingAsString( PwmSetting.PWM_INSTANCE_NAME );
+        String newInstanceID = pwmApplication.getPwmEnvironment().getParameters().get( PwmEnvironment.ApplicationParameter.InstanceID );
 
 
         if ( newInstanceID != null && newInstanceID.trim().length() > 0 )
         if ( newInstanceID != null && newInstanceID.trim().length() > 0 )
         {
         {

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

@@ -76,7 +76,8 @@ public class PwmEnvironment
         AutoWriteTomcatConfOutputFile,
         AutoWriteTomcatConfOutputFile,
         AppliancePort,
         AppliancePort,
         ApplianceHostnameFile,
         ApplianceHostnameFile,
-        ApplianceTokenFile,;
+        ApplianceTokenFile,
+        InstanceID,;
 
 
         public static ApplicationParameter forString( final String input )
         public static ApplicationParameter forString( final String input )
         {
         {

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

@@ -31,11 +31,11 @@ import java.time.Instant;
 import java.util.Locale;
 import java.util.Locale;
 
 
 /**
 /**
- * Only information that is particular to the http session is stored in the
- * session bean.  Information more topical to the user is stored in {@link UserInfoBean}.
- * <p/>
- * For any given HTTP session using PWM, one and only one {@link LocalSessionStateBean} will be
- * created.
+ * <p>Only information that is particular to the http session is stored in the
+ * session bean.  Information more topical to the user is stored in {@link UserInfoBean}.</p>
+ *
+ * <p>For any given HTTP session using PWM, one and only one {@link LocalSessionStateBean} will be
+ * created.</p>
  *
  *
  * @author Jason D. Rivard
  * @author Jason D. Rivard
  */
  */

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

@@ -42,7 +42,7 @@ import java.util.Set;
 
 
 
 
 /**
 /**
- * <p>This bean is synchronized across application sessions by {@link password.pwm.http.state.SessionLoginProvider}.</p>
+ * <p>This bean is synchronized across application sessions by {@code SessionLoginProvider}.</p>
  *
  *
  * <p>Short serialized names are used to shrink the effective size of the login cookie.</p>
  * <p>Short serialized names are used to shrink the effective size of the login cookie.</p>
  */
  */

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

@@ -81,8 +81,6 @@ public enum PwmSetting
             "pwm.homeURL", PwmSettingSyntax.STRING, PwmSettingCategory.GENERAL ),
             "pwm.homeURL", PwmSettingSyntax.STRING, PwmSettingCategory.GENERAL ),
     URL_INTRO(
     URL_INTRO(
             "pwm.introURL", PwmSettingSyntax.SELECT, PwmSettingCategory.GENERAL ),
             "pwm.introURL", PwmSettingSyntax.SELECT, PwmSettingCategory.GENERAL ),
-    PWM_INSTANCE_NAME(
-            "pwmInstanceName", PwmSettingSyntax.STRING, PwmSettingCategory.GENERAL ),
     IDLE_TIMEOUT_SECONDS(
     IDLE_TIMEOUT_SECONDS(
             "idleTimeoutSeconds", PwmSettingSyntax.DURATION, PwmSettingCategory.GENERAL ),
             "idleTimeoutSeconds", PwmSettingSyntax.DURATION, PwmSettingCategory.GENERAL ),
     HIDE_CONFIGURATION_HEALTH_WARNINGS(
     HIDE_CONFIGURATION_HEALTH_WARNINGS(
@@ -95,11 +93,12 @@ public enum PwmSetting
             "http.proxy.url", PwmSettingSyntax.STRING, PwmSettingCategory.GENERAL ),
             "http.proxy.url", PwmSettingSyntax.STRING, PwmSettingCategory.GENERAL ),
     HTTP_PROXY_EXCEPTIONS(
     HTTP_PROXY_EXCEPTIONS(
             "http.proxy.exceptions", PwmSettingSyntax.STRING_ARRAY, PwmSettingCategory.GENERAL ),
             "http.proxy.exceptions", PwmSettingSyntax.STRING_ARRAY, PwmSettingCategory.GENERAL ),
-
     APP_PROPERTY_OVERRIDES(
     APP_PROPERTY_OVERRIDES(
             "pwm.appProperty.overrides", PwmSettingSyntax.STRING_ARRAY, PwmSettingCategory.GENERAL ),
             "pwm.appProperty.overrides", PwmSettingSyntax.STRING_ARRAY, PwmSettingCategory.GENERAL ),
 
 
     // clustering
     // clustering
+    CLUSTER_STORAGE_MODE(
+            "cluster.storageMode", PwmSettingSyntax.SELECT, PwmSettingCategory.CLUSTERING ),
     SECURITY_LOGIN_SESSION_MODE(
     SECURITY_LOGIN_SESSION_MODE(
             "security.loginSession.mode", PwmSettingSyntax.SELECT, PwmSettingCategory.CLUSTERING ),
             "security.loginSession.mode", PwmSettingSyntax.SELECT, PwmSettingCategory.CLUSTERING ),
     SECURITY_MODULE_SESSION_MODE(
     SECURITY_MODULE_SESSION_MODE(

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

@@ -35,7 +35,6 @@ import password.pwm.config.StoredValue;
 import password.pwm.config.stored.StoredConfigurationImpl;
 import password.pwm.config.stored.StoredConfigurationImpl;
 import password.pwm.config.value.data.UserPermission;
 import password.pwm.config.value.data.UserPermission;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.error.PwmUnrecoverableException;
-import password.pwm.ldap.LdapPermissionTester;
 import password.pwm.svc.cache.CacheKey;
 import password.pwm.svc.cache.CacheKey;
 import password.pwm.svc.cache.CachePolicy;
 import password.pwm.svc.cache.CachePolicy;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.java.StringUtil;
@@ -146,7 +145,7 @@ public class LdapProfile extends AbstractProfile implements Profile
         final boolean enableCanonicalCache = Boolean.parseBoolean( pwmApplication.getConfig().readAppProperty( AppProperty.LDAP_CACHE_CANONICAL_ENABLE ) );
         final boolean enableCanonicalCache = Boolean.parseBoolean( pwmApplication.getConfig().readAppProperty( AppProperty.LDAP_CACHE_CANONICAL_ENABLE ) );
 
 
         String canonicalValue = null;
         String canonicalValue = null;
-        final CacheKey cacheKey = CacheKey.newKey( LdapPermissionTester.class, null, "canonicalDN-" + this.getIdentifier() + "-" + dnValue );
+        final CacheKey cacheKey = CacheKey.newKey( LdapProfile.class, null, "canonicalDN-" + this.getIdentifier() + "-" + dnValue );
         if ( enableCanonicalCache )
         if ( enableCanonicalCache )
         {
         {
             final String cachedDN = pwmApplication.getCacheService().get( cacheKey, String.class );
             final String cachedDN = pwmApplication.getCacheService().get( cacheKey, String.class );

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

@@ -310,7 +310,8 @@ public class ActionValue extends AbstractValue implements StoredValue
 
 
     /**
     /**
      * Convert to json map where the certificate values are replaced with debug info for display in the config editor.
      * Convert to json map where the certificate values are replaced with debug info for display in the config editor.
-     * @return
+     *
+     * @return a map suitable for json serialization for debug purposes
      */
      */
     public List<Map<String, Object>> toInfoMap( )
     public List<Map<String, Object>> toInfoMap( )
     {
     {

+ 5 - 0
server/src/main/java/password/pwm/config/value/data/FormConfiguration.java

@@ -437,6 +437,11 @@ public class FormConfiguration implements Serializable
 
 
     /**
     /**
      * Return false if an invalid email address is issued.
      * Return false if an invalid email address is issued.
+     *
+     * @param config application configuration
+     * @param address the email address value to test.
+     *
+     * @return true if the email address is valid.
      */
      */
     public static boolean testEmailAddress( final Configuration config, final String address )
     public static boolean testEmailAddress( final Configuration config, final String address )
     {
     {

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

@@ -303,6 +303,8 @@ public enum PwmError
             5091, "Error_FileTypeIncorrect", null ),
             5091, "Error_FileTypeIncorrect", null ),
     ERROR_FILE_TOO_LARGE(
     ERROR_FILE_TOO_LARGE(
             5092, "Error_FileTooLarge", null ),
             5092, "Error_FileTooLarge", null ),
+    ERROR_CLUSTER_SERVICE_ERROR(
+            5093, "Error_ClusterServiceError", null ),
 
 
     ERROR_REMOTE_ERROR_VALUE(
     ERROR_REMOTE_ERROR_VALUE(
             6000, "Error_RemoteErrorValue", null, ErrorFlag.Permanent ),
             6000, "Error_RemoteErrorValue", null, ErrorFlag.Permanent ),

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

@@ -50,6 +50,7 @@ public enum HealthMessage
     Appliance_PendingUpdates( HealthStatus.CAUTION, HealthTopic.Appliance ),
     Appliance_PendingUpdates( HealthStatus.CAUTION, HealthTopic.Appliance ),
     Appliance_UpdatesNotEnabled( HealthStatus.CAUTION, HealthTopic.Appliance ),
     Appliance_UpdatesNotEnabled( HealthStatus.CAUTION, HealthTopic.Appliance ),
     Appliance_UpdateServiceNotConfigured( HealthStatus.WARN, HealthTopic.Appliance ),
     Appliance_UpdateServiceNotConfigured( HealthStatus.WARN, HealthTopic.Appliance ),
+    Cluster_Error( HealthStatus.CAUTION, HealthTopic.Application ),
     Config_MissingProxyDN( HealthStatus.CONFIG, HealthTopic.Configuration ),
     Config_MissingProxyDN( HealthStatus.CONFIG, HealthTopic.Configuration ),
     Config_MissingProxyPassword( HealthStatus.CONFIG, HealthTopic.Configuration ),
     Config_MissingProxyPassword( HealthStatus.CONFIG, HealthTopic.Configuration ),
     Config_NoSiteURL( HealthStatus.WARN, HealthTopic.Configuration ),
     Config_NoSiteURL( HealthStatus.WARN, HealthTopic.Configuration ),

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

@@ -235,6 +235,8 @@ public class PwmSession implements Serializable
 
 
     /**
     /**
      * Unauthenticate the pwmSession.
      * Unauthenticate the pwmSession.
+     *
+     * @param pwmRequest current request of the user
      */
      */
     public void unauthenticateUser( final PwmRequest pwmRequest )
     public void unauthenticateUser( final PwmRequest pwmRequest )
     {
     {

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

@@ -64,6 +64,10 @@ public class PwmURL
     /**
     /**
      * Compare two uri strings for equality of 'base'.  Specifically, the schema, host and port
      * Compare two uri strings for equality of 'base'.  Specifically, the schema, host and port
      * are compared for equality.
      * are compared for equality.
+     *
+     * @param uri1 uri to compare
+     * @param uri2 uri to compare
+     * @return true if scheama, host and port of uri1 and uri2 are equal.
      */
      */
     public static boolean compareUriBase( final String uri1, final String uri2 )
     public static boolean compareUriBase( final String uri1, final String uri2 )
     {
     {

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

@@ -51,7 +51,6 @@ import java.util.List;
 /**
 /**
  * Wraps an <i>HttpSession</i> to provide additional PWM-related session
  * Wraps an <i>HttpSession</i> to provide additional PWM-related session
  * management activities.
  * management activities.
- * <p/>
  *
  *
  * @author Jason D. Rivard
  * @author Jason D. Rivard
  */
  */

+ 9 - 3
server/src/main/java/password/pwm/http/client/PwmHttpClient.java

@@ -44,6 +44,7 @@ import org.apache.http.conn.HttpClientConnectionManager;
 import org.apache.http.conn.routing.HttpRoute;
 import org.apache.http.conn.routing.HttpRoute;
 import org.apache.http.conn.routing.HttpRoutePlanner;
 import org.apache.http.conn.routing.HttpRoutePlanner;
 import org.apache.http.conn.socket.ConnectionSocketFactory;
 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.NoopHostnameVerifier;
 import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
 import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
 import org.apache.http.entity.StringEntity;
 import org.apache.http.entity.StringEntity;
@@ -89,12 +90,13 @@ import java.time.Instant;
 import java.util.LinkedHashMap;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.List;
 import java.util.Map;
 import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
 
 
 public class PwmHttpClient
 public class PwmHttpClient
 {
 {
     private static final PwmLogger LOGGER = PwmLogger.forClass( PwmHttpClient.class );
     private static final PwmLogger LOGGER = PwmLogger.forClass( PwmHttpClient.class );
 
 
-    private static int classCounter = 0;
+    private static final AtomicInteger REQUEST_COUNTER = new AtomicInteger( 0 );
 
 
     private final PwmApplication pwmApplication;
     private final PwmApplication pwmApplication;
     private final SessionLabel sessionLabel;
     private final SessionLabel sessionLabel;
@@ -128,6 +130,7 @@ public class PwmHttpClient
             throws PwmUnrecoverableException
             throws PwmUnrecoverableException
     {
     {
         final HttpClientBuilder clientBuilder = HttpClientBuilder.create();
         final HttpClientBuilder clientBuilder = HttpClientBuilder.create();
+        clientBuilder.useSystemProperties();
         clientBuilder.setUserAgent( PwmConstants.PWM_APP_NAME + " " + PwmConstants.SERVLET_VERSION );
         clientBuilder.setUserAgent( PwmConstants.PWM_APP_NAME + " " + PwmConstants.SERVLET_VERSION );
         final boolean httpClientPromiscuousEnable = Boolean.parseBoolean( configuration.readAppProperty( AppProperty.SECURITY_HTTP_PROMISCUOUS_ENABLE ) );
         final boolean httpClientPromiscuousEnable = Boolean.parseBoolean( configuration.readAppProperty( AppProperty.SECURITY_HTTP_PROMISCUOUS_ENABLE ) );
 
 
@@ -149,7 +152,10 @@ public class PwmHttpClient
                         new SecureRandom() );
                         new SecureRandom() );
 
 
                 final SSLConnectionSocketFactory sslConnectionFactory = new SSLConnectionSocketFactory( sslContext, NoopHostnameVerifier.INSTANCE );
                 final SSLConnectionSocketFactory sslConnectionFactory = new SSLConnectionSocketFactory( sslContext, NoopHostnameVerifier.INSTANCE );
-                final Registry<ConnectionSocketFactory> registry = RegistryBuilder.<ConnectionSocketFactory>create().register( "https", sslConnectionFactory ).build();
+                final Registry<ConnectionSocketFactory> registry = RegistryBuilder.<ConnectionSocketFactory>create()
+                        .register( "https", sslConnectionFactory )
+                        .register( "http", PlainConnectionSocketFactory.INSTANCE )
+                        .build();
                 final HttpClientConnectionManager ccm = new BasicHttpClientConnectionManager( registry );
                 final HttpClientConnectionManager ccm = new BasicHttpClientConnectionManager( registry );
 
 
                 clientBuilder.setSSLSocketFactory( sslConnectionFactory );
                 clientBuilder.setSSLSocketFactory( sslConnectionFactory );
@@ -271,7 +277,7 @@ public class PwmHttpClient
             throws IOException, URISyntaxException, PwmUnrecoverableException
             throws IOException, URISyntaxException, PwmUnrecoverableException
     {
     {
         final Instant startTime = Instant.now();
         final Instant startTime = Instant.now();
-        final int counter = classCounter++;
+        final int counter = REQUEST_COUNTER.getAndIncrement();
 
 
         LOGGER.trace( sessionLabel, "preparing to send (id=" + counter + ") "
         LOGGER.trace( sessionLabel, "preparing to send (id=" + counter + ") "
                 + clientRequest.toDebugString( this ) );
                 + clientRequest.toDebugString( this ) );

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

@@ -481,9 +481,16 @@ public class RequestInitializationFilter implements Filter
      * Returns the IP address of the user.  If there is an X-Forwarded-For header in the request, that address will
      * Returns the IP address of the user.  If there is an X-Forwarded-For header in the request, that address will
      * be used.  Otherwise, the source address of the request is used.
      * be used.  Otherwise, the source address of the request is used.
      *
      *
+     * @param request the http request object
+     * @param config the application configuration
      * @return String containing the textual representation of the source IP address, or null if the request is invalid.
      * @return String containing the textual representation of the source IP address, or null if the request is invalid.
+     * @throws PwmUnrecoverableException if unable to read the network address
      */
      */
-    public static String readUserIPAddress( final HttpServletRequest request, final Configuration config ) throws PwmUnrecoverableException
+    public static String readUserIPAddress(
+            final HttpServletRequest request,
+            final Configuration config
+    )
+            throws PwmUnrecoverableException
     {
     {
         final boolean useXForwardedFor = config != null && config.readSettingAsBoolean( PwmSetting.USE_X_FORWARDED_FOR_HEADER );
         final boolean useXForwardedFor = config != null && config.readSettingAsBoolean( PwmSetting.USE_X_FORWARDED_FOR_HEADER );
 
 
@@ -713,7 +720,7 @@ public class RequestInitializationFilter implements Filter
                 performCsrfHeaderChecks
                 performCsrfHeaderChecks
                         && !pwmRequest.getMethod().isIdempotent()
                         && !pwmRequest.getMethod().isIdempotent()
                         && !pwmRequest.getURL().isRestService()
                         && !pwmRequest.getURL().isRestService()
-                )
+        )
         {
         {
             final String originValue = pwmRequest.readHeaderValueAsString( HttpHeader.Origin );
             final String originValue = pwmRequest.readHeaderValueAsString( HttpHeader.Origin );
             final String referrerValue = pwmRequest.readHeaderValueAsString( HttpHeader.Referer );
             final String referrerValue = pwmRequest.readHeaderValueAsString( HttpHeader.Referer );
@@ -814,7 +821,7 @@ public class RequestInitializationFilter implements Filter
                         HttpHeader.Referer,
                         HttpHeader.Referer,
                         HttpHeader.Origin,
                         HttpHeader.Origin,
                 }
                 }
-                )
+        )
         {
         {
             values.put( header.getHttpName(), pwmRequest.readHeaderValueAsString( header ) );
             values.put( header.getHttpName(), pwmRequest.readHeaderValueAsString( header ) );
         }
         }

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

@@ -64,11 +64,11 @@ import java.util.List;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.concurrent.atomic.AtomicInteger;
 
 
 /**
 /**
- * This session filter (invoked by the container through the web.xml descriptor) wraps all calls to the
- * servlets in the container.
- * <p/>
- * It is responsible for managing some aspects of the user session and also for enforcing security
- * functionality such as intruder lockout.
+ * <p>This session filter (invoked by the container through the web.xml descriptor) wraps all calls to the
+ * servlets in the container.</p>
+ *
+ * <p>It is responsible for managing some aspects of the user session and also for enforcing security
+ * functionality such as intruder lockout.</p>
  *
  *
  * @author Jason D. Rivard
  * @author Jason D. Rivard
  */
  */

+ 12 - 8
server/src/main/java/password/pwm/ldap/LdapOperationsHelper.java

@@ -253,15 +253,17 @@ public class LdapOperationsHelper
     }
     }
 
 
     /**
     /**
-     * Writes a Map of values to ldap onto the supplied user object.
-     * The map key must be a string of attribute names.
-     * <p/>
-     * Any ldap operation exceptions are not reported (but logged).
+     * <p>Writes a Map of values to ldap onto the supplied user object.
+     * The map key must be a string of attribute names.</p>
      *
      *
-     * @param theUser  User to write to
+     * <p>Any ldap operation exceptions are not reported (but logged).</p>
+     *
+     * @param theUser User to write to.
      * @param valueMap A map with String keys and String values.
      * @param valueMap A map with String keys and String values.
+     * @param macroMachine used to resolve macros before values are written.
+     * @param expandMacros a boolean to indicate if value macros should be expanded.
      * @throws ChaiUnavailableException if the directory is unavailable
      * @throws ChaiUnavailableException if the directory is unavailable
-     * @throws PwmOperationalException  if their is an unexpected ldap problem
+     * @throws PwmUnrecoverableException if their is an unexpected ldap problem
      */
      */
     public static void writeFormValuesToLdap(
     public static void writeFormValuesToLdap(
             final ChaiUser theUser,
             final ChaiUser theUser,
@@ -772,10 +774,12 @@ public class LdapOperationsHelper
      * Update the user's "lastUpdated" attribute. By default this is
      * Update the user's "lastUpdated" attribute. By default this is
      * "pwmLastUpdate" attribute
      * "pwmLastUpdate" attribute
      *
      *
+     * @param pwmApplication a reference to the application
+     * @param sessionLabel for debugging
      * @param userIdentity ldap user to operate on
      * @param userIdentity ldap user to operate on
      * @return true if successful;
      * @return true if successful;
-     * @throws com.novell.ldapchai.exception.ChaiUnavailableException if the
-     *                                                                directory is unavailable
+     * @throws ChaiUnavailableException if the directory is unavailable
+     * @throws PwmUnrecoverableException if the operation fails
      */
      */
     public static boolean updateLastPasswordUpdateAttribute(
     public static boolean updateLastPasswordUpdateAttribute(
             final PwmApplication pwmApplication,
             final PwmApplication pwmApplication,

+ 7 - 3
server/src/main/java/password/pwm/svc/cache/CacheDebugItem.java

@@ -22,14 +22,18 @@
 
 
 package password.pwm.svc.cache;
 package password.pwm.svc.cache;
 
 
+import lombok.Builder;
 import lombok.Value;
 import lombok.Value;
 
 
 import java.io.Serializable;
 import java.io.Serializable;
 
 
 @Value
 @Value
+@Builder
 class CacheDebugItem implements Serializable
 class CacheDebugItem implements Serializable
 {
 {
-    private CacheKey cacheKey;
-    private String age;
-    private int chars;
+    private final String srcClass;
+    private final String userIdentity;
+    private final String valueID;
+    private final String age;
+    private final int chars;
 }
 }

+ 19 - 15
server/src/main/java/password/pwm/svc/cache/CacheService.java

@@ -29,17 +29,20 @@ import password.pwm.error.PwmException;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.health.HealthRecord;
 import password.pwm.health.HealthRecord;
 import password.pwm.svc.PwmService;
 import password.pwm.svc.PwmService;
+import password.pwm.util.java.ConditionalTaskExecutor;
 import password.pwm.util.java.JsonUtil;
 import password.pwm.util.java.JsonUtil;
-import password.pwm.util.java.TimeDuration;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.logging.PwmLogger;
 
 
 import java.io.Serializable;
 import java.io.Serializable;
 import java.time.Instant;
 import java.time.Instant;
 import java.util.ArrayList;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.LinkedHashMap;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.List;
 import java.util.Map;
 import java.util.Map;
+import java.util.TreeMap;
+import java.util.concurrent.TimeUnit;
 
 
 public class CacheService implements PwmService
 public class CacheService implements PwmService
 {
 {
@@ -49,7 +52,7 @@ public class CacheService implements PwmService
 
 
     private STATUS status = STATUS.NEW;
     private STATUS status = STATUS.NEW;
 
 
-    private Instant lastTraceOutput;
+    private ConditionalTaskExecutor traceDebugOutputter;
 
 
     @Override
     @Override
     public STATUS status( )
     public STATUS status( )
@@ -86,6 +89,10 @@ public class CacheService implements PwmService
         status = STATUS.OPENING;
         status = STATUS.OPENING;
         final int maxMemItems = Integer.parseInt( pwmApplication.getConfig().readAppProperty( AppProperty.CACHE_MEMORY_MAX_ITEMS ) );
         final int maxMemItems = Integer.parseInt( pwmApplication.getConfig().readAppProperty( AppProperty.CACHE_MEMORY_MAX_ITEMS ) );
         memoryCacheStore = new MemoryCacheStore( maxMemItems );
         memoryCacheStore = new MemoryCacheStore( maxMemItems );
+        this.traceDebugOutputter = new ConditionalTaskExecutor(
+                ( ) -> outputTraceInfo(),
+                new ConditionalTaskExecutor.TimeDurationPredicate( 1, TimeUnit.MINUTES )
+        );
         status = STATUS.OPEN;
         status = STATUS.OPEN;
     }
     }
 
 
@@ -104,7 +111,10 @@ public class CacheService implements PwmService
     @Override
     @Override
     public ServiceInfoBean serviceInfo( )
     public ServiceInfoBean serviceInfo( )
     {
     {
-        return new ServiceInfoBean( Collections.emptyList() );
+        final Map<String, String> debugInfo = new TreeMap<>( );
+        debugInfo.putAll( JsonUtil.deserializeStringMap( JsonUtil.serialize( memoryCacheStore.getCacheStoreInfo() ) ) );
+        debugInfo.putAll( JsonUtil.deserializeStringMap( JsonUtil.serializeMap( memoryCacheStore.storedClassHistogram( "histogram." ) ) ) );
+        return new ServiceInfoBean( Collections.emptyList(), debugInfo );
     }
     }
 
 
     public Map<String, Serializable> debugInfo( )
     public Map<String, Serializable> debugInfo( )
@@ -112,6 +122,7 @@ public class CacheService implements PwmService
         final Map<String, Serializable> debugInfo = new LinkedHashMap<>( );
         final Map<String, Serializable> debugInfo = new LinkedHashMap<>( );
         debugInfo.put( "memory-statistics", memoryCacheStore.getCacheStoreInfo() );
         debugInfo.put( "memory-statistics", memoryCacheStore.getCacheStoreInfo() );
         debugInfo.put( "memory-items", new ArrayList<Serializable>( memoryCacheStore.getCacheDebugItems() ) );
         debugInfo.put( "memory-items", new ArrayList<Serializable>( memoryCacheStore.getCacheDebugItems() ) );
+        debugInfo.put( "memory-histogram", new HashMap<>( memoryCacheStore.storedClassHistogram( "" ) ) );
         return Collections.unmodifiableMap( debugInfo );
         return Collections.unmodifiableMap( debugInfo );
     }
     }
 
 
@@ -136,11 +147,11 @@ public class CacheService implements PwmService
         }
         }
         final Instant expirationDate = cachePolicy.getExpiration();
         final Instant expirationDate = cachePolicy.getExpiration();
         memoryCacheStore.store( cacheKey, expirationDate, payload );
         memoryCacheStore.store( cacheKey, expirationDate, payload );
-        outputTraceInfo();
+
+        traceDebugOutputter.conditionallyExecuteTask();
     }
     }
 
 
     public <T> T get( final CacheKey cacheKey, final Class<T> classOfT  )
     public <T> T get( final CacheKey cacheKey, final Class<T> classOfT  )
-            throws PwmUnrecoverableException
     {
     {
         if ( cacheKey == null )
         if ( cacheKey == null )
         {
         {
@@ -158,28 +169,21 @@ public class CacheService implements PwmService
             payload = memoryCacheStore.read( cacheKey, classOfT );
             payload = memoryCacheStore.read( cacheKey, classOfT );
         }
         }
 
 
-        outputTraceInfo();
+        traceDebugOutputter.conditionallyExecuteTask();
 
 
         return (T) payload;
         return (T) payload;
     }
     }
 
 
     private void outputTraceInfo( )
     private void outputTraceInfo( )
     {
     {
-        if ( lastTraceOutput == null || TimeDuration.fromCurrent( lastTraceOutput ).isLongerThan( 30 * 1000 ) )
-        {
-            lastTraceOutput = Instant.now();
-        }
-        else
-        {
-            return;
-        }
-
         final StringBuilder traceOutput = new StringBuilder();
         final StringBuilder traceOutput = new StringBuilder();
         if ( memoryCacheStore != null )
         if ( memoryCacheStore != null )
         {
         {
             final CacheStoreInfo info = memoryCacheStore.getCacheStoreInfo();
             final CacheStoreInfo info = memoryCacheStore.getCacheStoreInfo();
             traceOutput.append( ", memCache=" );
             traceOutput.append( ", memCache=" );
             traceOutput.append( JsonUtil.serialize( info ) );
             traceOutput.append( JsonUtil.serialize( info ) );
+            traceOutput.append( ", histogram=" );
+            traceOutput.append( JsonUtil.serializeMap( memoryCacheStore.storedClassHistogram( "" ) ) );
         }
         }
         LOGGER.trace( traceOutput );
         LOGGER.trace( traceOutput );
     }
     }

+ 27 - 1
server/src/main/java/password/pwm/svc/cache/MemoryCacheStore.java

@@ -37,6 +37,7 @@ import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Collections;
 import java.util.List;
 import java.util.List;
 import java.util.Map;
 import java.util.Map;
+import java.util.TreeMap;
 
 
 class MemoryCacheStore implements CacheStore
 class MemoryCacheStore implements CacheStore
 {
 {
@@ -103,7 +104,18 @@ class MemoryCacheStore implements CacheStore
             final Instant storeDate = cacheValueWrapper.getExpirationDate();
             final Instant storeDate = cacheValueWrapper.getExpirationDate();
             final String age = Duration.between( storeDate, Instant.now() ).toString();
             final String age = Duration.between( storeDate, Instant.now() ).toString();
             final int chars = JsonUtil.serialize( cacheValueWrapper.getPayload() ).length();
             final int chars = JsonUtil.serialize( cacheValueWrapper.getPayload() ).length();
-            final CacheDebugItem cacheDebugItem = new CacheDebugItem( cacheKey, age, chars );
+            final String keyClass = cacheKey.getSrcClass() == null ? "null" : cacheKey.getSrcClass().getName();
+            final String keyUserID = cacheKey.getUserIdentity() == null ? "null" : cacheKey.getUserIdentity().toDisplayString();
+            final String keyValue = cacheKey.getValueID() == null ? "null" : cacheKey.getValueID();
+
+            final CacheDebugItem cacheDebugItem = CacheDebugItem.builder()
+                    .srcClass( keyClass )
+                    .userIdentity( keyUserID )
+                    .valueID( keyValue )
+                    .age( age )
+                    .chars( chars )
+                    .build();
+
             items.add( cacheDebugItem );
             items.add( cacheDebugItem );
         }
         }
         return Collections.unmodifiableList( items );
         return Collections.unmodifiableList( items );
@@ -117,4 +129,18 @@ class MemoryCacheStore implements CacheStore
         private final Instant expirationDate;
         private final Instant expirationDate;
         private final Serializable payload;
         private final Serializable payload;
     }
     }
+
+    Map<String, Integer> storedClassHistogram( final String prefix )
+    {
+        final Map<String, Integer> output = new TreeMap<>(  );
+        for ( final CacheKey cacheKey : memoryStore.asMap().keySet() )
+        {
+            final String className = cacheKey.getSrcClass() == null ? "n/a" : cacheKey.getSrcClass().getSimpleName();
+            final String key = prefix + className;
+            final Integer currentValue = output.getOrDefault( key, 0 );
+            final Integer newValue = currentValue + 1;
+            output.put( key, newValue );
+        }
+        return output;
+    }
 }
 }

+ 38 - 0
server/src/main/java/password/pwm/svc/cluster/ClusterDataServiceProvider.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.cluster;
+
+import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.util.java.TimeDuration;
+
+import java.util.Map;
+
+public interface ClusterDataServiceProvider
+{
+    Map<String, StoredNodeData> readStoredData( ) throws PwmUnrecoverableException;
+
+    void writeNodeStatus( StoredNodeData storedNodeData ) throws PwmUnrecoverableException;
+
+    int purgeOutdatedNodes( TimeDuration maxNodeAge )
+            throws PwmUnrecoverableException;
+}

+ 49 - 83
server/src/main/java/password/pwm/svc/cluster/DatabaseClusterProvider.java → server/src/main/java/password/pwm/svc/cluster/ClusterMachine.java

@@ -27,11 +27,7 @@ import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmException;
 import password.pwm.error.PwmException;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.error.PwmUnrecoverableException;
-import password.pwm.util.db.DatabaseService;
-import password.pwm.util.db.DatabaseTable;
-import password.pwm.util.java.ClosableIterator;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.JavaHelper;
-import password.pwm.util.java.JsonUtil;
 import password.pwm.util.java.TimeDuration;
 import password.pwm.util.java.TimeDuration;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.logging.PwmLogger;
 
 
@@ -45,38 +41,32 @@ import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeUnit;
 
 
-class DatabaseClusterProvider implements ClusterProvider
+class ClusterMachine
 {
 {
-
-    private static final PwmLogger LOGGER = PwmLogger.forClass( DatabaseClusterProvider.class );
-
-    private static final DatabaseTable TABLE = DatabaseTable.CLUSTER_STATE;
-
-
-    private static final String KEY_PREFIX_NODE = "node-";
+    private static final PwmLogger LOGGER = PwmLogger.forClass( ClusterMachine.class );
 
 
     private final PwmApplication pwmApplication;
     private final PwmApplication pwmApplication;
-    private final DatabaseService databaseService;
     private final ScheduledExecutorService executorService;
     private final ScheduledExecutorService executorService;
+    private final ClusterDataServiceProvider clusterDataServiceProvider;
 
 
     private ErrorInformation lastError;
     private ErrorInformation lastError;
 
 
-    private final Map<String, DatabaseStoredNodeData> nodeDatas = new ConcurrentHashMap<>();
+    private final Map<String, StoredNodeData> knownNodes = new ConcurrentHashMap<>();
 
 
-    private final DatabaseClusterSettings settings;
+    private final ClusterSettings settings;
+    private final ClusterStatistics clusterStatistics = new ClusterStatistics();
 
 
-    DatabaseClusterProvider( final PwmApplication pwmApplication ) throws PwmUnrecoverableException
+    ClusterMachine(
+            final PwmApplication pwmApplication,
+            final ClusterDataServiceProvider clusterDataServiceProvider,
+            final ClusterSettings clusterSettings
+    )
     {
     {
         this.pwmApplication = pwmApplication;
         this.pwmApplication = pwmApplication;
-        this.settings = DatabaseClusterSettings.fromConfig( pwmApplication.getConfig() );
-
-        if ( !settings.isEnable() )
-        {
-            throw new PwmUnrecoverableException( new ErrorInformation( PwmError.CONFIG_FORMAT_ERROR, "database clustering is not enabled via app property" ) );
-        }
+        this.clusterDataServiceProvider = clusterDataServiceProvider;
+        this.settings = clusterSettings;
 
 
-        this.databaseService = pwmApplication.getDatabaseService();
-        this.executorService = JavaHelper.makeSingleThreadExecutorService( pwmApplication, DatabaseClusterProvider.class );
+        this.executorService = JavaHelper.makeSingleThreadExecutorService( pwmApplication, ClusterMachine.class );
 
 
         final long intervalSeconds = settings.getHeartbeatInterval().getTotalSeconds();
         final long intervalSeconds = settings.getHeartbeatInterval().getTotalSeconds();
 
 
@@ -88,19 +78,17 @@ class DatabaseClusterProvider implements ClusterProvider
         );
         );
     }
     }
 
 
-    @Override
     public void close( )
     public void close( )
     {
     {
         JavaHelper.closeAndWaitExecutor( executorService, new TimeDuration( 1, TimeUnit.SECONDS ) );
         JavaHelper.closeAndWaitExecutor( executorService, new TimeDuration( 1, TimeUnit.SECONDS ) );
     }
     }
 
 
 
 
-    @Override
     public List<NodeInfo> nodes( ) throws PwmUnrecoverableException
     public List<NodeInfo> nodes( ) throws PwmUnrecoverableException
     {
     {
         final Map<String, NodeInfo> returnObj = new TreeMap<>();
         final Map<String, NodeInfo> returnObj = new TreeMap<>();
         final String configHash = pwmApplication.getConfig().configurationHash();
         final String configHash = pwmApplication.getConfig().configurationHash();
-        for ( final DatabaseStoredNodeData storedNodeData : nodeDatas.values() )
+        for ( final StoredNodeData storedNodeData : knownNodes.values() )
         {
         {
             final boolean configMatch = configHash.equals( storedNodeData.getConfigHash() );
             final boolean configMatch = configHash.equals( storedNodeData.getConfigHash() );
             final boolean timedOut = isTimedOut( storedNodeData );
             final boolean timedOut = isTimedOut( storedNodeData );
@@ -132,7 +120,7 @@ class DatabaseClusterProvider implements ClusterProvider
 
 
     private String masterInstanceId( )
     private String masterInstanceId( )
     {
     {
-        final List<DatabaseStoredNodeData> copiedDatas = new ArrayList<>( nodeDatas.values() );
+        final List<StoredNodeData> copiedDatas = new ArrayList<>( knownNodes.values() );
         if ( copiedDatas.isEmpty() )
         if ( copiedDatas.isEmpty() )
         {
         {
             return null;
             return null;
@@ -141,7 +129,7 @@ class DatabaseClusterProvider implements ClusterProvider
         String masterID = null;
         String masterID = null;
         Instant eldestRecord = Instant.now();
         Instant eldestRecord = Instant.now();
 
 
-        for ( final DatabaseStoredNodeData nodeData : copiedDatas )
+        for ( final StoredNodeData nodeData : copiedDatas )
         {
         {
             if ( !isTimedOut( nodeData ) )
             if ( !isTimedOut( nodeData ) )
             {
             {
@@ -155,7 +143,6 @@ class DatabaseClusterProvider implements ClusterProvider
         return masterID;
         return masterID;
     }
     }
 
 
-    @Override
     public boolean isMaster( )
     public boolean isMaster( )
     {
     {
         final String myID = pwmApplication.getInstanceID();
         final String myID = pwmApplication.getInstanceID();
@@ -163,27 +150,21 @@ class DatabaseClusterProvider implements ClusterProvider
         return myID.equals( masterID );
         return myID.equals( masterID );
     }
     }
 
 
-    private boolean isMaster( final DatabaseStoredNodeData databaseStoredNodeData )
+    private boolean isMaster( final StoredNodeData storedNodeData )
     {
     {
         final String masterID = masterInstanceId();
         final String masterID = masterInstanceId();
-        return databaseStoredNodeData.getInstanceID().equals( masterID );
+        return storedNodeData.getInstanceID().equals( masterID );
     }
     }
 
 
-    private String dbKeyForStoredNode( final DatabaseStoredNodeData storedNodeData ) throws PwmUnrecoverableException
+    private boolean isTimedOut( final StoredNodeData storedNodeData )
     {
     {
-        final String instanceID = storedNodeData.getInstanceID();
-        final String hash = pwmApplication.getSecureService().hash( instanceID );
-        final String truncatedHash = hash.length() > 64
-                ? hash.substring( 0, 64 )
-                : hash;
-
-        return KEY_PREFIX_NODE + truncatedHash;
+        final TimeDuration age = TimeDuration.fromCurrent( storedNodeData.getTimestamp() );
+        return age.isLongerThan( settings.getNodeTimeout() );
     }
     }
 
 
-    private boolean isTimedOut( final DatabaseStoredNodeData storedNodeData )
+    public ErrorInformation getLastError( )
     {
     {
-        final TimeDuration age = TimeDuration.fromCurrent( storedNodeData.getTimestamp() );
-        return age.isLongerThan( settings.getNodeTimeout() );
+        return lastError;
     }
     }
 
 
     private class HeartbeatProcess implements Runnable
     private class HeartbeatProcess implements Runnable
@@ -199,15 +180,14 @@ class DatabaseClusterProvider implements ClusterProvider
         {
         {
             try
             try
             {
             {
-                final DatabaseStoredNodeData storedNodeData = DatabaseStoredNodeData.makeNew( pwmApplication );
-                final String key = dbKeyForStoredNode( storedNodeData );
-                final String value = JsonUtil.serialize( storedNodeData );
-                databaseService.getAccessor().put( TABLE, key, value );
+                final StoredNodeData storedNodeData = StoredNodeData.makeNew( pwmApplication );
+                clusterDataServiceProvider.writeNodeStatus( storedNodeData );
+                clusterStatistics.getClusterWrites().incrementAndGet();
             }
             }
             catch ( PwmException e )
             catch ( PwmException e )
             {
             {
                 final String errorMsg = "error writing database cluster heartbeat: " + e.getMessage();
                 final String errorMsg = "error writing database cluster heartbeat: " + e.getMessage();
-                final ErrorInformation errorInformation = new ErrorInformation( PwmError.ERROR_DB_UNAVAILABLE, errorMsg );
+                final ErrorInformation errorInformation = new ErrorInformation( PwmError.ERROR_CLUSTER_SERVICE_ERROR, errorMsg );
                 lastError = errorInformation;
                 lastError = errorInformation;
                 LOGGER.error( lastError );
                 LOGGER.error( lastError );
             }
             }
@@ -215,23 +195,16 @@ class DatabaseClusterProvider implements ClusterProvider
 
 
         void readNodeStatuses( )
         void readNodeStatuses( )
         {
         {
-            try ( ClosableIterator<String> tableIterator = databaseService.getAccessor().iterator( TABLE ) )
+            try
             {
             {
-                while ( tableIterator.hasNext() )
-                {
-                    final String dbKey = tableIterator.next();
-                    if ( dbKey.startsWith( KEY_PREFIX_NODE ) )
-                    {
-                        final String rawValueInDb = databaseService.getAccessor().get( TABLE, dbKey );
-                        final DatabaseStoredNodeData nodeDataInDb = JsonUtil.deserialize( rawValueInDb, DatabaseStoredNodeData.class );
-                        nodeDatas.put( nodeDataInDb.getInstanceID(), nodeDataInDb );
-                    }
-                }
+                final Map<String, StoredNodeData> readNodeData = clusterDataServiceProvider.readStoredData();
+                knownNodes.putAll( readNodeData );
+                clusterStatistics.getClusterReads().incrementAndGet();
             }
             }
             catch ( PwmException e )
             catch ( PwmException e )
             {
             {
-                final String errorMsg = "error reading database node statuses: " + e.getMessage();
-                final ErrorInformation errorInformation = new ErrorInformation( PwmError.ERROR_DB_UNAVAILABLE, errorMsg );
+                final String errorMsg = "error reading node statuses: " + e.getMessage();
+                final ErrorInformation errorInformation = new ErrorInformation( PwmError.ERROR_CLUSTER_SERVICE_ERROR, errorMsg );
                 lastError = errorInformation;
                 lastError = errorInformation;
                 LOGGER.error( lastError );
                 LOGGER.error( lastError );
             }
             }
@@ -239,30 +212,23 @@ class DatabaseClusterProvider implements ClusterProvider
 
 
         void purgeOutdatedNodes( )
         void purgeOutdatedNodes( )
         {
         {
-            for ( final DatabaseStoredNodeData storedNodeData : nodeDatas.values() )
+            try
             {
             {
-                final TimeDuration recordAge = TimeDuration.fromCurrent( storedNodeData.getTimestamp() );
-                final String instanceID = storedNodeData.getInstanceID();
-
-                if ( recordAge.isLongerThan( settings.getNodePurgeInterval() ) )
-                {
-                    // purge outdated records
-                    LOGGER.debug( "purging outdated node reference to instanceID '" + instanceID + "'" );
-
-                    try
-                    {
-                        databaseService.getAccessor().remove( TABLE, dbKeyForStoredNode( storedNodeData ) );
-                    }
-                    catch ( PwmException e )
-                    {
-                        final String errorMsg = "error purging outdated node reference: " + e.getMessage();
-                        final ErrorInformation errorInformation = new ErrorInformation( PwmError.ERROR_DB_UNAVAILABLE, errorMsg );
-                        lastError = errorInformation;
-                        LOGGER.error( lastError );
-                    }
-                    nodeDatas.remove( instanceID );
-                }
+                final int purges = clusterDataServiceProvider.purgeOutdatedNodes( settings.getNodePurgeInterval() );
+                clusterStatistics.getNodePurges().addAndGet( purges );
+            }
+            catch ( PwmException e )
+            {
+                final String errorMsg = "error purging outdated node reference: " + e.getMessage();
+                final ErrorInformation errorInformation = new ErrorInformation( PwmError.ERROR_CLUSTER_SERVICE_ERROR, errorMsg );
+                lastError = errorInformation;
+                LOGGER.error( lastError );
             }
             }
         }
         }
     }
     }
+
+    public ClusterStatistics getClusterStatistics( )
+    {
+        return clusterStatistics;
+    }
 }
 }

+ 111 - 17
server/src/main/java/password/pwm/svc/cluster/ClusterService.java

@@ -23,14 +23,24 @@
 package password.pwm.svc.cluster;
 package password.pwm.svc.cluster;
 
 
 import password.pwm.PwmApplication;
 import password.pwm.PwmApplication;
+import password.pwm.bean.UserIdentity;
+import password.pwm.config.PwmSetting;
+import password.pwm.config.option.DataStorageMethod;
+import password.pwm.error.ErrorInformation;
+import password.pwm.error.PwmError;
 import password.pwm.error.PwmException;
 import password.pwm.error.PwmException;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.health.HealthMessage;
 import password.pwm.health.HealthRecord;
 import password.pwm.health.HealthRecord;
 import password.pwm.svc.PwmService;
 import password.pwm.svc.PwmService;
+import password.pwm.util.java.JavaHelper;
+import password.pwm.util.java.JsonUtil;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.logging.PwmLogger;
 
 
 import java.util.Collections;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.List;
 import java.util.List;
+import java.util.Map;
 
 
 public class ClusterService implements PwmService
 public class ClusterService implements PwmService
 {
 {
@@ -39,7 +49,10 @@ public class ClusterService implements PwmService
 
 
     private PwmApplication pwmApplication;
     private PwmApplication pwmApplication;
     private STATUS status = STATUS.NEW;
     private STATUS status = STATUS.NEW;
-    private ClusterProvider clusterProvider;
+    private ClusterMachine clusterMachine;
+    private DataStorageMethod dataStore;
+    private ErrorInformation startupError;
+
 
 
     @Override
     @Override
     public STATUS status( )
     public STATUS status( )
@@ -53,52 +66,103 @@ public class ClusterService implements PwmService
         status = STATUS.OPENING;
         status = STATUS.OPENING;
         this.pwmApplication = pwmApplication;
         this.pwmApplication = pwmApplication;
 
 
+
         try
         try
         {
         {
-            if ( this.pwmApplication.getConfig().hasDbConfigured() )
+            final ClusterSettings clusterSettings;
+            final ClusterDataServiceProvider clusterDataServiceProvider;
+            dataStore = figureDataStorageMethod( pwmApplication );
+
+            if ( dataStore != null )
             {
             {
-                clusterProvider = new DatabaseClusterProvider( pwmApplication );
+                switch ( dataStore )
+                {
+                    case DB:
+                    {
+                        LOGGER.trace( "starting database-backed cluster provider" );
+                        clusterSettings = ClusterSettings.fromConfigForDB( pwmApplication.getConfig() );
+                        clusterDataServiceProvider = new DatabaseClusterDataService( pwmApplication );
+                    }
+                    break;
+
+                    case LDAP:
+                    {
+                        LOGGER.trace( "starting ldap-backed cluster provider" );
+                        clusterSettings = ClusterSettings.fromConfigForLDAP( pwmApplication.getConfig() );
+                        clusterDataServiceProvider = new LDAPClusterDataService( pwmApplication );
+                    }
+                    break;
+
+                    default:
+                        LOGGER.debug( "no suitable storage method configured " );
+                        JavaHelper.unhandledSwitchStatement( dataStore );
+                        return;
+
+                }
+
+                clusterMachine = new ClusterMachine( pwmApplication, clusterDataServiceProvider, clusterSettings );
+                status = STATUS.OPEN;
+                return;
             }
             }
         }
         }
-        catch ( PwmException e )
+        catch ( Exception e )
         {
         {
-            LOGGER.error( "error starting up cluster provider service: " + e.getMessage() );
-            status = STATUS.CLOSED;
-            return;
+            LOGGER.error( "error starting up cluster service: " + e.getMessage() );
         }
         }
 
 
-        status = STATUS.OPEN;
+        status = STATUS.CLOSED;
     }
     }
 
 
     @Override
     @Override
     public void close( )
     public void close( )
     {
     {
-        if ( clusterProvider != null )
+        if ( clusterMachine != null )
         {
         {
-            clusterProvider.close();
-            clusterProvider = null;
+            clusterMachine.close();
+            clusterMachine = null;
         }
         }
-        clusterProvider = null;
         status = STATUS.CLOSED;
         status = STATUS.CLOSED;
     }
     }
 
 
     @Override
     @Override
     public List<HealthRecord> healthCheck( )
     public List<HealthRecord> healthCheck( )
     {
     {
+        if ( clusterMachine != null )
+        {
+            final ErrorInformation errorInformation = clusterMachine.getLastError();
+            if ( errorInformation != null )
+            {
+                final HealthRecord healthRecord = HealthRecord.forMessage( HealthMessage.Cluster_Error, errorInformation.getDetailedErrorMsg() );
+                return Collections.singletonList( healthRecord );
+            }
+        }
+
+        if ( startupError != null )
+        {
+            final HealthRecord healthRecord = HealthRecord.forMessage( HealthMessage.Cluster_Error, startupError.getDetailedErrorMsg() );
+            return Collections.singletonList( healthRecord );
+        }
+
         return null;
         return null;
     }
     }
 
 
     @Override
     @Override
     public ServiceInfoBean serviceInfo( )
     public ServiceInfoBean serviceInfo( )
     {
     {
-        return null;
+        final Map<String, String> props = new HashMap<>();
+
+        if ( clusterMachine != null )
+        {
+            props.putAll( JsonUtil.deserializeStringMap( JsonUtil.serialize( clusterMachine.getClusterStatistics() ) ) );
+        }
+        return new ServiceInfoBean( Collections.singleton( dataStore ), props );
     }
     }
 
 
     public boolean isMaster( )
     public boolean isMaster( )
     {
     {
-        if ( clusterProvider != null )
+        if ( status == STATUS.OPEN && clusterMachine != null )
         {
         {
-            return clusterProvider.isMaster();
+            return clusterMachine.isMaster();
         }
         }
 
 
         return false;
         return false;
@@ -106,10 +170,40 @@ public class ClusterService implements PwmService
 
 
     public List<NodeInfo> nodes( ) throws PwmUnrecoverableException
     public List<NodeInfo> nodes( ) throws PwmUnrecoverableException
     {
     {
-        if ( status == STATUS.OPEN && clusterProvider != null )
+        if ( status == STATUS.OPEN && clusterMachine != null )
         {
         {
-            return clusterProvider.nodes();
+            return clusterMachine.nodes();
         }
         }
         return Collections.emptyList();
         return Collections.emptyList();
     }
     }
+
+    private DataStorageMethod figureDataStorageMethod( final PwmApplication pwmApplication )
+            throws PwmUnrecoverableException
+    {
+        final DataStorageMethod method = pwmApplication.getConfig().readSettingAsEnum( PwmSetting.CLUSTER_STORAGE_MODE, DataStorageMethod.class );
+        if ( method == DataStorageMethod.LDAP )
+        {
+            final UserIdentity userIdentity = pwmApplication.getConfig().getDefaultLdapProfile().getTestUser( pwmApplication );
+            if ( userIdentity == null )
+            {
+                final String msg = "LDAP storage type selected, but LDAP test user not defined.";
+                LOGGER.debug( msg );
+                startupError = new ErrorInformation( PwmError.ERROR_CLUSTER_SERVICE_ERROR, msg );
+                return null;
+            }
+        }
+
+        if ( method == DataStorageMethod.DB )
+        {
+            if ( !pwmApplication.getConfig().hasDbConfigured() )
+            {
+                final String msg = "DB storage type selected, but remote DB is not configured.";
+                LOGGER.debug( msg );
+                startupError = new ErrorInformation( PwmError.ERROR_CLUSTER_SERVICE_ERROR, msg );
+                return null;
+            }
+        }
+
+        return method;
+    }
 }
 }

+ 14 - 7
server/src/main/java/password/pwm/svc/cluster/DatabaseClusterSettings.java → server/src/main/java/password/pwm/svc/cluster/ClusterSettings.java

@@ -24,29 +24,36 @@ package password.pwm.svc.cluster;
 
 
 import lombok.AccessLevel;
 import lombok.AccessLevel;
 import lombok.AllArgsConstructor;
 import lombok.AllArgsConstructor;
-import lombok.Getter;
+import lombok.Value;
 import password.pwm.AppProperty;
 import password.pwm.AppProperty;
 import password.pwm.config.Configuration;
 import password.pwm.config.Configuration;
 import password.pwm.util.java.TimeDuration;
 import password.pwm.util.java.TimeDuration;
 
 
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeUnit;
 
 
-@Getter
+@Value
 @AllArgsConstructor( access = AccessLevel.PRIVATE )
 @AllArgsConstructor( access = AccessLevel.PRIVATE )
-class DatabaseClusterSettings
+class ClusterSettings
 {
 {
-    private final boolean enable;
     private final TimeDuration heartbeatInterval;
     private final TimeDuration heartbeatInterval;
     private final TimeDuration nodeTimeout;
     private final TimeDuration nodeTimeout;
     private final TimeDuration nodePurgeInterval;
     private final TimeDuration nodePurgeInterval;
 
 
-    static DatabaseClusterSettings fromConfig( final Configuration configuration )
+    static ClusterSettings fromConfigForDB( final Configuration configuration )
     {
     {
-        return new DatabaseClusterSettings(
-                Boolean.parseBoolean( configuration.readAppProperty( AppProperty.CLUSTER_DB_ENABLE ) ),
+        return new ClusterSettings(
                 new TimeDuration( Integer.parseInt( configuration.readAppProperty( AppProperty.CLUSTER_DB_HEARTBEAT_SECONDS ) ), TimeUnit.SECONDS ),
                 new TimeDuration( Integer.parseInt( configuration.readAppProperty( AppProperty.CLUSTER_DB_HEARTBEAT_SECONDS ) ), TimeUnit.SECONDS ),
                 new TimeDuration( Integer.parseInt( configuration.readAppProperty( AppProperty.CLUSTER_DB_NODE_TIMEOUT_SECONDS ) ), TimeUnit.SECONDS ),
                 new TimeDuration( Integer.parseInt( configuration.readAppProperty( AppProperty.CLUSTER_DB_NODE_TIMEOUT_SECONDS ) ), TimeUnit.SECONDS ),
                 new TimeDuration( Integer.parseInt( configuration.readAppProperty( AppProperty.CLUSTER_DB_NODE_PURGE_SECONDS ) ), TimeUnit.SECONDS )
                 new TimeDuration( Integer.parseInt( configuration.readAppProperty( AppProperty.CLUSTER_DB_NODE_PURGE_SECONDS ) ), TimeUnit.SECONDS )
         );
         );
     }
     }
+
+    static ClusterSettings fromConfigForLDAP( final Configuration configuration )
+    {
+        return new ClusterSettings(
+                new TimeDuration( Integer.parseInt( configuration.readAppProperty( AppProperty.CLUSTER_LDAP_HEARTBEAT_SECONDS ) ), TimeUnit.SECONDS ),
+                new TimeDuration( Integer.parseInt( configuration.readAppProperty( AppProperty.CLUSTER_LDAP_NODE_TIMEOUT_SECONDS ) ), TimeUnit.SECONDS ),
+                new TimeDuration( Integer.parseInt( configuration.readAppProperty( AppProperty.CLUSTER_LDAP_NODE_PURGE_SECONDS ) ), TimeUnit.SECONDS )
+        );
+    }
 }
 }

+ 36 - 0
server/src/main/java/password/pwm/svc/cluster/ClusterStatistics.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.cluster;
+
+import lombok.Value;
+
+import java.io.Serializable;
+import java.util.concurrent.atomic.AtomicInteger;
+
+@Value
+public class ClusterStatistics implements Serializable
+{
+    private final AtomicInteger clusterWrites = new AtomicInteger( 0 );
+    private final AtomicInteger clusterReads = new AtomicInteger( 0 );
+    private final AtomicInteger nodePurges = new AtomicInteger( 0 );
+}

+ 147 - 0
server/src/main/java/password/pwm/svc/cluster/DatabaseClusterDataService.java

@@ -0,0 +1,147 @@
+/*
+ * 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.cluster;
+
+import password.pwm.PwmApplication;
+import password.pwm.error.PwmError;
+import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.util.db.DatabaseAccessor;
+import password.pwm.util.db.DatabaseException;
+import password.pwm.util.db.DatabaseTable;
+import password.pwm.util.java.ClosableIterator;
+import password.pwm.util.java.JsonUtil;
+import password.pwm.util.java.TimeDuration;
+import password.pwm.util.logging.PwmLogger;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+public class DatabaseClusterDataService implements ClusterDataServiceProvider
+{
+    private static final PwmLogger LOGGER = PwmLogger.forClass( DatabaseClusterDataService.class );
+
+    private static final DatabaseTable TABLE = DatabaseTable.CLUSTER_STATE;
+    private static final String KEY_PREFIX_NODE = "node-";
+
+    private final PwmApplication pwmApplication;
+
+    public DatabaseClusterDataService( final PwmApplication pwmApplication )
+    {
+        this.pwmApplication = pwmApplication;
+    }
+
+    private DatabaseAccessor getDatabaseAccessor()
+            throws PwmUnrecoverableException
+    {
+        return pwmApplication.getDatabaseService().getAccessor();
+    }
+
+    private String localKeyForStoredNode( final StoredNodeData storedNodeData )
+            throws PwmUnrecoverableException
+    {
+        final String instanceID = storedNodeData.getInstanceID();
+        final String hash = pwmApplication.getSecureService().hash( instanceID );
+        final String truncatedHash = hash.length() > 64
+                ? hash.substring( 0, 64 )
+                : hash;
+
+        return KEY_PREFIX_NODE + truncatedHash;
+    }
+
+
+    @Override
+    public Map<String, StoredNodeData> readStoredData( )
+            throws PwmUnrecoverableException
+    {
+        final Map<String, StoredNodeData> returnList = new LinkedHashMap<>();
+        final DatabaseAccessor databaseAccessor = getDatabaseAccessor();
+        try ( ClosableIterator<String> tableIterator = databaseAccessor.iterator( TABLE ) )
+        {
+            while ( tableIterator.hasNext() )
+            {
+                final String dbKey = tableIterator.next();
+                if ( dbKey.startsWith( KEY_PREFIX_NODE ) )
+                {
+                    final String rawValueInDb = databaseAccessor.get( TABLE, dbKey );
+                    final StoredNodeData nodeDataInDb = JsonUtil.deserialize( rawValueInDb, StoredNodeData.class );
+                    returnList.put( nodeDataInDb.getInstanceID(), nodeDataInDb );
+                }
+            }
+        }
+        catch ( DatabaseException e )
+        {
+            throw new PwmUnrecoverableException( PwmError.ERROR_DB_UNAVAILABLE, "unexpected database error reading cluster node status: " + e.getMessage() );
+        }
+        return returnList;
+    }
+
+    @Override
+    public void writeNodeStatus( final StoredNodeData storedNodeData ) throws PwmUnrecoverableException
+    {
+        try
+        {
+            final DatabaseAccessor databaseAccessor = getDatabaseAccessor();
+            final String key = localKeyForStoredNode( storedNodeData );
+            final String value = JsonUtil.serialize( storedNodeData );
+            databaseAccessor.put( TABLE, key, value );
+        }
+        catch ( DatabaseException e )
+        {
+            throw new PwmUnrecoverableException( PwmError.ERROR_DB_UNAVAILABLE, "unexpected database error writing cluster node status: " + e.getMessage() );
+        }
+    }
+
+    @Override
+    public int purgeOutdatedNodes( final TimeDuration maxNodeAge )
+            throws PwmUnrecoverableException
+    {
+        int nodesPurged = 0;
+
+        try
+        {
+            final Map<String, StoredNodeData> nodeDatas = readStoredData();
+            final DatabaseAccessor databaseAccessor = getDatabaseAccessor();
+            for ( final StoredNodeData storedNodeData : nodeDatas.values() )
+            {
+                final TimeDuration recordAge = TimeDuration.fromCurrent( storedNodeData.getTimestamp() );
+                final String instanceID = storedNodeData.getInstanceID();
+
+
+                if ( recordAge.isLongerThan( maxNodeAge ) )
+                {
+                    // purge outdated records
+                    LOGGER.debug( "purging outdated node reference to instanceID '" + instanceID + "'" );
+
+                    databaseAccessor.remove( TABLE, localKeyForStoredNode( storedNodeData ) );
+                    nodesPurged++;
+                }
+            }
+        }
+        catch ( DatabaseException e )
+        {
+            throw new PwmUnrecoverableException( PwmError.ERROR_DB_UNAVAILABLE, "unexpected database error writing cluster node status: " + e.getMessage() );
+        }
+
+        return nodesPurged;
+    }
+}

+ 178 - 0
server/src/main/java/password/pwm/svc/cluster/LDAPClusterDataService.java

@@ -0,0 +1,178 @@
+/*
+ * 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.cluster;
+
+import com.novell.ldapchai.ChaiUser;
+import com.novell.ldapchai.exception.ChaiException;
+import lombok.Value;
+import password.pwm.PwmApplication;
+import password.pwm.bean.UserIdentity;
+import password.pwm.config.PwmSetting;
+import password.pwm.error.ErrorInformation;
+import password.pwm.error.PwmError;
+import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.util.java.JsonUtil;
+import password.pwm.util.java.TimeDuration;
+import password.pwm.util.logging.PwmLogger;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Set;
+
+public class LDAPClusterDataService implements ClusterDataServiceProvider
+{
+    private static final PwmLogger LOGGER = PwmLogger.forClass( LDAPClusterDataService.class );
+
+    private final PwmApplication pwmApplication;
+    private static final String VALUE_PREFIX = "0006#.#.#";
+
+    public LDAPClusterDataService( final PwmApplication pwmApplication )
+    {
+        this.pwmApplication = pwmApplication;
+    }
+
+    @Override
+    public Map<String, StoredNodeData> readStoredData( ) throws PwmUnrecoverableException
+    {
+        final Map<String, StoredNodeData> returnData = new LinkedHashMap<>(  );
+
+        final LDAPHelper ldapHelper = LDAPHelper.createLDAPHelper( pwmApplication );
+
+        try
+        {
+            final Set<String> values = ldapHelper.getChaiUser().readMultiStringAttribute( ldapHelper.getAttr() );
+            for ( final String value : values )
+            {
+                if ( value.startsWith( VALUE_PREFIX ) )
+                {
+                    final String rawValue = value.substring( VALUE_PREFIX.length() );
+                    final StoredNodeData storedNodeData = JsonUtil.deserialize( rawValue, StoredNodeData.class );
+                    returnData.put( storedNodeData.getInstanceID(),  storedNodeData );
+                }
+            }
+        }
+        catch ( ChaiException e )
+        {
+            throw new PwmUnrecoverableException( PwmError.ERROR_LDAP_DATA_ERROR, "error reading cluster data: " + e.getMessage() );
+        }
+
+        return returnData;
+    }
+
+    @Override
+    public void writeNodeStatus( final StoredNodeData storedNodeData ) throws PwmUnrecoverableException
+    {
+        final Map<String, StoredNodeData> currentServerData = readStoredData();
+        final StoredNodeData removeNode = currentServerData.get( storedNodeData.getInstanceID() );
+
+        final LDAPHelper ldapHelper = LDAPHelper.createLDAPHelper( pwmApplication );
+
+        final String newRawValue = VALUE_PREFIX + JsonUtil.serialize( storedNodeData );
+
+        try
+        {
+            if ( removeNode != null )
+            {
+                final String oldRawValue = VALUE_PREFIX + JsonUtil.serialize( removeNode );
+                ldapHelper.getChaiUser().replaceAttribute( ldapHelper.getAttr(), oldRawValue, newRawValue );
+            }
+            else
+            {
+                ldapHelper.getChaiUser().addAttribute( ldapHelper.getAttr(), newRawValue );
+            }
+        }
+        catch ( ChaiException e )
+        {
+            throw new PwmUnrecoverableException( PwmError.ERROR_LDAP_DATA_ERROR, "error writing cluster data: " + e.getMessage() );
+        }
+
+    }
+
+    @Override
+    public int purgeOutdatedNodes( final TimeDuration maxNodeAge ) throws PwmUnrecoverableException
+    {
+        final LDAPHelper ldapHelper = LDAPHelper.createLDAPHelper( pwmApplication );
+
+        int nodesPurged = 0;
+
+        final Map<String, StoredNodeData> nodeDatas = readStoredData();
+
+        for ( final StoredNodeData storedNodeData : nodeDatas.values() )
+        {
+            final TimeDuration recordAge = TimeDuration.fromCurrent( storedNodeData.getTimestamp() );
+            final String instanceID = storedNodeData.getInstanceID();
+
+            if ( recordAge.isLongerThan( maxNodeAge ) )
+            {
+                // purge outdated records
+                LOGGER.debug( "purging outdated node reference to instanceID '" + instanceID + "'" );
+
+                try
+                {
+                    final String oldRawValue = VALUE_PREFIX + JsonUtil.serialize( storedNodeData );
+                    ldapHelper.getChaiUser().deleteAttribute( ldapHelper.getAttr(), oldRawValue );
+                    nodesPurged++;
+                }
+                catch ( ChaiException e )
+                {
+                    throw new PwmUnrecoverableException( PwmError.ERROR_LDAP_DATA_ERROR, "error purging cluster data: " + e.getMessage() );
+                }
+            }
+        }
+
+        return nodesPurged;
+
+    }
+
+    @Value
+    private static class LDAPHelper
+    {
+        private final PwmApplication pwmApplication;
+        private final UserIdentity userIdentity;
+        private final ChaiUser chaiUser;
+        private final String attr;
+
+        private LDAPHelper( final PwmApplication pwmApplication )
+                throws PwmUnrecoverableException
+        {
+            this.pwmApplication = pwmApplication;
+
+            userIdentity = pwmApplication.getConfig().getDefaultLdapProfile().getTestUser( pwmApplication );
+            if ( userIdentity == null )
+            {
+                final String ldapProfileID = pwmApplication.getConfig().getDefaultLdapProfile().getIdentifier();
+                final String errorMsg = "a test user is not configured for ldap profile '" + ldapProfileID + "'";
+                final ErrorInformation errorInformation = new ErrorInformation( PwmError.CONFIG_FORMAT_ERROR, errorMsg );
+                throw new PwmUnrecoverableException( errorInformation );
+            }
+            chaiUser = pwmApplication.getProxiedChaiUser( userIdentity );
+            attr = userIdentity.getLdapProfile( pwmApplication.getConfig() ).readSettingAsString( PwmSetting.CHALLENGE_USER_ATTRIBUTE );
+
+        }
+
+        static LDAPHelper createLDAPHelper( final PwmApplication pwmApplication ) throws PwmUnrecoverableException
+        {
+            return new LDAPHelper( pwmApplication );
+        }
+    }
+}

+ 3 - 3
server/src/main/java/password/pwm/svc/cluster/DatabaseStoredNodeData.java → server/src/main/java/password/pwm/svc/cluster/StoredNodeData.java

@@ -33,7 +33,7 @@ import java.time.Instant;
 
 
 @Getter
 @Getter
 @AllArgsConstructor( access = AccessLevel.PRIVATE )
 @AllArgsConstructor( access = AccessLevel.PRIVATE )
-class DatabaseStoredNodeData implements Serializable
+class StoredNodeData implements Serializable
 {
 {
     private Instant timestamp;
     private Instant timestamp;
     private Instant startupTimestamp;
     private Instant startupTimestamp;
@@ -41,10 +41,10 @@ class DatabaseStoredNodeData implements Serializable
     private String guid;
     private String guid;
     private String configHash;
     private String configHash;
 
 
-    static DatabaseStoredNodeData makeNew( final PwmApplication pwmApplication )
+    static StoredNodeData makeNew( final PwmApplication pwmApplication )
             throws PwmUnrecoverableException
             throws PwmUnrecoverableException
     {
     {
-        return new DatabaseStoredNodeData(
+        return new StoredNodeData(
                 Instant.now(),
                 Instant.now(),
                 pwmApplication.getStartupTime(),
                 pwmApplication.getStartupTime(),
                 pwmApplication.getInstanceID(),
                 pwmApplication.getInstanceID(),

+ 4 - 1
server/src/main/java/password/pwm/svc/shorturl/AbstractUrlShortener.java

@@ -34,7 +34,10 @@ public interface AbstractUrlShortener
      *
      *
      * @param input   the URL to be shortened
      * @param input   the URL to be shortened
      * @param context the PwmApplication, used to retrieve configuration
      * @param context the PwmApplication, used to retrieve configuration
+     * @return the shortened uri
+     * @throws PwmUnrecoverableException if the operation fails
      */
      */
 
 
-    String shorten( String input, PwmApplication context ) throws PwmUnrecoverableException;
+    String shorten( String input, PwmApplication context )
+            throws PwmUnrecoverableException;
 }
 }

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

@@ -51,6 +51,7 @@ public class BasicAuthInfo implements Serializable
     /**
     /**
      * Extracts the basic auth info from the header.
      * Extracts the basic auth info from the header.
      *
      *
+     * @param pwmApplication a reference to the application
      * @param pwmRequest http servlet request
      * @param pwmRequest http servlet request
      * @return a BasicAuthInfo object containing username/password, or null if the "Authorization" header doesn't exist or is malformed
      * @return a BasicAuthInfo object containing username/password, or null if the "Authorization" header doesn't exist or is malformed
      */
      */

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

@@ -69,7 +69,11 @@ public class CaptchaUtility
 
 
     /**
     /**
      * Verify a reCaptcha request.  The reCaptcha request API is documented at
      * Verify a reCaptcha request.  The reCaptcha request API is documented at
-     * <a href="http://recaptcha.net/apidocs/captcha/"/>reCaptcha API.
+     * <a href="http://recaptcha.net/apidocs/captcha/">reCaptcha API</a>.
+     *
+     * @param pwmRequest request object
+     * @return true if captcha passes
+     * @throws PwmUnrecoverableException if the operation fails.
      */
      */
     public static boolean verifyReCaptcha(
     public static boolean verifyReCaptcha(
             final PwmRequest pwmRequest
             final PwmRequest pwmRequest

+ 8 - 7
server/src/main/java/password/pwm/util/RandomPasswordGenerator.java

@@ -114,18 +114,19 @@ public class RandomPasswordGenerator
 
 
 
 
     /**
     /**
-     * Creates a new password that satisfies the password rules.  All rules are checked for.  If for some
-     * reason the RANDOM algorithm can not generate a valid password, null will be returned.
-     * <p/>
-     * If there is an identifiable reason the password can not be created (such as mis-configured rules) then
-     * an {@link com.novell.ldapchai.exception.ImpossiblePasswordPolicyException} will be thrown.
+     * <p>Creates a new password that satisfies the password rules.  All rules are checked for.  If for some
+     * reason the RANDOM algorithm can not generate a valid password, null will be returned.</p>
+     *
+     * <p>If there is an identifiable reason the password can not be created (such as mis-configured rules) then
+     * an {@link com.novell.ldapchai.exception.ImpossiblePasswordPolicyException} will be thrown.</p>
      *
      *
      * @param sessionLabel          A valid pwmSession
      * @param sessionLabel          A valid pwmSession
      * @param randomGeneratorConfig Policy to be used during generation
      * @param randomGeneratorConfig Policy to be used during generation
      * @param pwmApplication        Used to read configuration, seedmanager and other services.
      * @param pwmApplication        Used to read configuration, seedmanager and other services.
      * @return A randomly generated password value that meets the requirements of this {@code PasswordPolicy}
      * @return A randomly generated password value that meets the requirements of this {@code PasswordPolicy}
-     * @throws com.novell.ldapchai.exception.ImpossiblePasswordPolicyException If there is no way to create a password using the configured rules and
-     *                                                                         default seed phrase
+     * @throws ImpossiblePasswordPolicyException If there is no way to create a password using the configured rules and
+     *                                        default seed phrase
+     * @throws PwmUnrecoverableException if the operation can not be completed
      */
      */
     public static PasswordData createRandomPassword(
     public static PasswordData createRandomPassword(
             final SessionLabel sessionLabel,
             final SessionLabel sessionLabel,

+ 3 - 0
server/src/main/java/password/pwm/util/form/FormUtility.java

@@ -414,6 +414,9 @@ public class FormUtility
      * and checks to make sure the ParamConfig value meets the requirements of the ParamConfig itself.
      * and checks to make sure the ParamConfig value meets the requirements of the ParamConfig itself.
      *
      *
      * @param formValues - a Map containing String keys of parameter names and ParamConfigs as values
      * @param formValues - a Map containing String keys of parameter names and ParamConfigs as values
+     * @param locale used for error messages
+     * @param configuration current application configuration
+     *
      * @throws password.pwm.error.PwmDataValidationException - If there is a problem with any of the fields
      * @throws password.pwm.error.PwmDataValidationException - If there is a problem with any of the fields
      * @throws password.pwm.error.PwmUnrecoverableException  if an unexpected error occurs
      * @throws password.pwm.error.PwmUnrecoverableException  if an unexpected error occurs
      */
      */

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

@@ -472,6 +472,8 @@ public class JavaHelper
 
 
     /**
     /**
      * Copy of {@link ThreadInfo#toString()} but with the MAX_FRAMES changed from 8 to 256.
      * Copy of {@link ThreadInfo#toString()} but with the MAX_FRAMES changed from 8 to 256.
+     * @param threadInfo thread information
+     * @return a stacktrace string with newline formatting
      */
      */
     public static String threadInfoToString( final ThreadInfo threadInfo )
     public static String threadInfoToString( final ThreadInfo threadInfo )
     {
     {

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

@@ -42,11 +42,11 @@ import java.util.Locale;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeUnit;
 
 
 /**
 /**
- * An immutable class representing a time period.  The internal value of the time period is
- * stored as milliseconds.
- * <p/>
- * Negative time durations are not permitted.  Operations that would result in a negative value
- * are negated and will instead result in positive values.
+ * <p>An immutable class representing a time period.  The internal value of the time period is
+ * stored as milliseconds.</p>
+ *
+ * <p>Negative time durations are not permitted.  Operations that would result in a negative value
+ * are negated and will instead result in positive values.</p>
  *
  *
  * @author Jason D. Rivard
  * @author Jason D. Rivard
  */
  */

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

@@ -32,12 +32,12 @@ import java.util.Collection;
 import java.util.Map;
 import java.util.Map;
 
 
 /**
 /**
- * A lightweight interface for DB interaction.  Implementations may be backed by an embedded database, an RDBMS or
- * even a simple hashmap in memory.
- * <p/>
- * Implementations are required to implement a simplistic locking policy, where any method marked with {@link LocalDB.WriteOperation}
+ * <p>A lightweight interface for DB interaction.  Implementations may be backed by an embedded database, an RDBMS or
+ * even a simple hashmap in memory.</p>
+ *
+ * <p>Implementations are required to implement a simplistic locking policy, where any method marked with {@link LocalDB.WriteOperation}
  * must block until any outstanding write or read methods are completed.  That is, concurrency is allowed for reads, but
  * must block until any outstanding write or read methods are completed.  That is, concurrency is allowed for reads, but
- * writes are gaurenteed to be single threaded.
+ * writes are guaranteed to be single threaded.</p>
  *
  *
  * @author Jason D. Rivard
  * @author Jason D. Rivard
  */
  */

+ 25 - 1
server/src/main/java/password/pwm/util/localdb/XodusLocalDB.java

@@ -36,6 +36,7 @@ import jetbrains.exodus.env.StoreConfig;
 import jetbrains.exodus.env.Transaction;
 import jetbrains.exodus.env.Transaction;
 import jetbrains.exodus.management.Statistics;
 import jetbrains.exodus.management.Statistics;
 import jetbrains.exodus.management.StatisticsItem;
 import jetbrains.exodus.management.StatisticsItem;
+import password.pwm.PwmConstants;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmError;
 import password.pwm.util.java.ConditionalTaskExecutor;
 import password.pwm.util.java.ConditionalTaskExecutor;
@@ -48,6 +49,8 @@ import java.io.ByteArrayOutputStream;
 import java.io.File;
 import java.io.File;
 import java.io.IOException;
 import java.io.IOException;
 import java.io.Serializable;
 import java.io.Serializable;
+import java.nio.file.Files;
+import java.nio.file.StandardOpenOption;
 import java.time.Instant;
 import java.time.Instant;
 import java.util.Collection;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Collections;
@@ -55,6 +58,7 @@ import java.util.Date;
 import java.util.HashMap;
 import java.util.HashMap;
 import java.util.LinkedHashMap;
 import java.util.LinkedHashMap;
 import java.util.Map;
 import java.util.Map;
+import java.util.ResourceBundle;
 import java.util.Set;
 import java.util.Set;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeUnit;
 import java.util.zip.Deflater;
 import java.util.zip.Deflater;
@@ -68,6 +72,9 @@ public class XodusLocalDB implements LocalDBProvider
     private static final PwmLogger LOGGER = PwmLogger.forClass( XodusLocalDB.class );
     private static final PwmLogger LOGGER = PwmLogger.forClass( XodusLocalDB.class );
     private static final TimeDuration STATS_OUTPUT_INTERVAL = new TimeDuration( 24, TimeUnit.HOURS );
     private static final TimeDuration STATS_OUTPUT_INTERVAL = new TimeDuration( 24, TimeUnit.HOURS );
 
 
+    private static final String FILE_SUB_PATH = "xodus";
+    private static final String README_FILENAME = "README.TXT";
+
     private Environment environment;
     private Environment environment;
     private File fileLocation;
     private File fileLocation;
     private boolean readOnly;
     private boolean readOnly;
@@ -131,7 +138,7 @@ public class XodusLocalDB implements LocalDBProvider
         readOnly = parameters.containsKey( Parameter.readOnly ) && Boolean.parseBoolean( parameters.get( Parameter.readOnly ) );
         readOnly = parameters.containsKey( Parameter.readOnly ) && Boolean.parseBoolean( parameters.get( Parameter.readOnly ) );
 
 
         LOGGER.trace( "preparing to open with configuration " + JsonUtil.serializeMap( environmentConfig.getSettings() ) );
         LOGGER.trace( "preparing to open with configuration " + JsonUtil.serializeMap( environmentConfig.getSettings() ) );
-        environment = Environments.newInstance( dbDirectory.getAbsolutePath() + File.separator + "xodus", environmentConfig );
+        environment = Environments.newInstance( dbDirectory.getAbsolutePath() + File.separator + FILE_SUB_PATH, environmentConfig );
         LOGGER.trace( "environment open (" + TimeDuration.fromCurrent( startTime ).asCompactString() + ")" );
         LOGGER.trace( "environment open (" + TimeDuration.fromCurrent( startTime ).asCompactString() + ")" );
 
 
         environment.executeInTransaction( txn ->
         environment.executeInTransaction( txn ->
@@ -149,6 +156,8 @@ public class XodusLocalDB implements LocalDBProvider
         {
         {
             LOGGER.trace( "opened " + db + " with " + this.size( db ) + " records" );
             LOGGER.trace( "opened " + db + " with " + this.size( db ) + " records" );
         }
         }
+
+        outputReadme( new File( dbDirectory.getPath() + File.separator + FILE_SUB_PATH + File.separator + README_FILENAME ) );
     }
     }
 
 
     @Override
     @Override
@@ -598,4 +607,19 @@ public class XodusLocalDB implements LocalDBProvider
     {
     {
         return Collections.emptySet();
         return Collections.emptySet();
     }
     }
+
+    private static void outputReadme( final File xodusPath )
+    {
+        try
+        {
+            final ResourceBundle resourceBundle = ResourceBundle.getBundle( XodusLocalDB.class.getName() );
+            final String contents = resourceBundle.getString( "ReadmeContents" );
+            final byte[] byteContents = contents.getBytes( PwmConstants.DEFAULT_CHARSET );
+            Files.write( xodusPath.toPath(), byteContents, StandardOpenOption.TRUNCATE_EXISTING );
+        }
+        catch ( IOException e )
+        {
+            LOGGER.error( "error writing LocalDB readme file: " + e.getMessage() );
+        }
+    }
 }
 }

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

@@ -227,12 +227,13 @@ public class PasswordUtility
     }
     }
 
 
     /**
     /**
-     * This is the entry point under which all password changes are managed.
-     * The following is the general procedure when this method is invoked.
+     * <p>This is the entry point under which all password changes are managed.
+     * The following is the general procedure when this method is invoked.</p>
      * <ul>
      * <ul>
-     * <li> password is checked against PWM password requirement </li>
-     * <li> ldap password set is attempted<br/>
-     * <br/>if successful:
+     * <li> password is checked against application password policy</li>
+     * <li> ldap password set is attempted</li>
+     * </ul>
+     * if successful:
      * <ul>
      * <ul>
      * <li> uiBean is updated with old and new passwords </li>
      * <li> uiBean is updated with old and new passwords </li>
      * <li> uiBean's password expire flag is set to false </li>
      * <li> uiBean's password expire flag is set to false </li>
@@ -240,18 +241,18 @@ public class PasswordUtility
      * <li> user email notification is sent </li>
      * <li> user email notification is sent </li>
      * <li> return true </li>
      * <li> return true </li>
      * </ul>
      * </ul>
-     * <br/>if unsuccessful
+     * if unsuccessful
      * <ul>
      * <ul>
      * <li> ssBean is updated with appropriate error </li>
      * <li> ssBean is updated with appropriate error </li>
      * <li> return false </li>
      * <li> return false </li>
      * </ul>
      * </ul>
-     * </li>
-     * </ul>
      *
      *
      * @param newPassword the new password that is being set.
      * @param newPassword the new password that is being set.
      * @param pwmSession  beanmanager for config and user info lookup
      * @param pwmSession  beanmanager for config and user info lookup
-     * @throws com.novell.ldapchai.exception.ChaiUnavailableException if the ldap directory is not unavailable
-     * @throws password.pwm.error.PwmUnrecoverableException           if user is not authenticated
+     * @param pwmApplication the application reference
+     * @throws ChaiUnavailableException if the ldap directory is not unavailable
+     * @throws PwmUnrecoverableException  if user is not authenticated
+     * @throws PwmOperationalException if operation fails
      */
      */
     public static void setActorPassword(
     public static void setActorPassword(
             final PwmSession pwmSession,
             final PwmSession pwmSession,

+ 12 - 0
server/src/main/java/password/pwm/util/operations/cr/CrOperator.java

@@ -46,12 +46,24 @@ public interface CrOperator
 {
 {
     /**
     /**
      * Read a response set suitable for use in forgotten password scenarios.
      * Read a response set suitable for use in forgotten password scenarios.
+     *
+     * @param theUser chaiUser to examine
+     * @param userIdentity identify of the user
+     * @param userGUID user's guid
+     * @return a responseSet instance suitable for use with forgotten password.
+     * @throws PwmUnrecoverableException if the operation fails
      */
      */
     ResponseSet readResponseSet( ChaiUser theUser, UserIdentity userIdentity, String userGUID )
     ResponseSet readResponseSet( ChaiUser theUser, UserIdentity userIdentity, String userGUID )
             throws PwmUnrecoverableException;
             throws PwmUnrecoverableException;
 
 
     /**
     /**
      * Read a response info bean suitable for examining the user's stored response data, but not for use during forgotten password.
      * Read a response info bean suitable for examining the user's stored response data, but not for use during forgotten password.
+     *
+     * @param theUser chaiUser to examine
+     * @param userIdentity identify of the user
+     * @param userGUID user's guid
+     * @return a bean with the users stored response data.
+     * @throws PwmUnrecoverableException if the operation fails
      */
      */
     ResponseInfoBean readResponseInfo( ChaiUser theUser, UserIdentity userIdentity, String userGUID )
     ResponseInfoBean readResponseInfo( ChaiUser theUser, UserIdentity userIdentity, String userGUID )
             throws PwmUnrecoverableException;
             throws PwmUnrecoverableException;

+ 14 - 3
server/src/main/java/password/pwm/util/operations/otp/AbstractOtpOperator.java

@@ -30,7 +30,6 @@ import password.pwm.config.PwmSetting;
 import password.pwm.config.option.OTPStorageFormat;
 import password.pwm.config.option.OTPStorageFormat;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmError;
-import password.pwm.error.PwmOperationalException;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.JsonUtil;
 import password.pwm.util.java.JsonUtil;
@@ -53,6 +52,7 @@ public abstract class AbstractOtpOperator implements OtpOperator
      *
      *
      * @param otpUserRecord input user record
      * @param otpUserRecord input user record
      * @return A string formatted record
      * @return A string formatted record
+     * @throws PwmUnrecoverableException if the operation fails
      */
      */
     public String composeOtpAttribute( final OTPUserRecord otpUserRecord )
     public String composeOtpAttribute( final OTPUserRecord otpUserRecord )
             throws PwmUnrecoverableException
             throws PwmUnrecoverableException
@@ -87,8 +87,14 @@ public abstract class AbstractOtpOperator implements OtpOperator
 
 
     /**
     /**
      * Encrypt the given string using the PWM encryption key.
      * Encrypt the given string using the PWM encryption key.
+     *
+     * @param unencrypted raw value to be encrypted
+     *
+     * @return the encrypted value
+     * @throws PwmUnrecoverableException if the operation can't be completed
      */
      */
-    public String encryptAttributeValue( final String unencrypted ) throws PwmUnrecoverableException, PwmOperationalException
+    public String encryptAttributeValue( final String unencrypted )
+            throws PwmUnrecoverableException
     {
     {
         final PwmBlockAlgorithm pwmBlockAlgorithm = figureBlockAlg();
         final PwmBlockAlgorithm pwmBlockAlgorithm = figureBlockAlg();
         final PwmSecurityKey pwmSecurityKey = pwmApplication.getConfig().getSecurityKey();
         final PwmSecurityKey pwmSecurityKey = pwmApplication.getConfig().getSecurityKey();
@@ -103,9 +109,14 @@ public abstract class AbstractOtpOperator implements OtpOperator
 
 
     /**
     /**
      * Decrypt the given string using the PWM encryption key.
      * Decrypt the given string using the PWM encryption key.
+     *
+     * @param encrypted value to be encrypted
+     *
+     * @return the decrypted value
+     * @throws PwmUnrecoverableException if the operation can't be completed
      */
      */
     public String decryptAttributeValue( final String encrypted )
     public String decryptAttributeValue( final String encrypted )
-            throws PwmUnrecoverableException, PwmOperationalException
+            throws PwmUnrecoverableException
     {
     {
         final PwmBlockAlgorithm pwmBlockAlgorithm = figureBlockAlg();
         final PwmBlockAlgorithm pwmBlockAlgorithm = figureBlockAlg();
         final PwmSecurityKey pwmSecurityKey = pwmApplication.getConfig().getSecurityKey();
         final PwmSecurityKey pwmSecurityKey = pwmApplication.getConfig().getSecurityKey();

+ 3 - 11
server/src/main/java/password/pwm/util/operations/otp/DbOtpOperator.java

@@ -33,13 +33,13 @@ import password.pwm.bean.UserIdentity;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.PwmSetting;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmError;
+import password.pwm.error.PwmException;
 import password.pwm.error.PwmOperationalException;
 import password.pwm.error.PwmOperationalException;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.http.PwmSession;
 import password.pwm.http.PwmSession;
 import password.pwm.util.db.DatabaseAccessor;
 import password.pwm.util.db.DatabaseAccessor;
 import password.pwm.util.db.DatabaseException;
 import password.pwm.util.db.DatabaseException;
 import password.pwm.util.db.DatabaseTable;
 import password.pwm.util.db.DatabaseTable;
-import password.pwm.util.localdb.LocalDBException;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.logging.PwmLogger;
 
 
 /**
 /**
@@ -85,17 +85,9 @@ public class DbOtpOperator extends AbstractOtpOperator
                 }
                 }
             }
             }
         }
         }
-        catch ( LocalDBException e )
+        catch ( PwmException e )
         {
         {
-            final String errorMsg = "unexpected LocalDB error reading responses: " + e.getMessage();
-            final ErrorInformation errorInformation = new ErrorInformation( PwmError.ERROR_UNKNOWN, errorMsg );
-            throw new PwmUnrecoverableException( errorInformation );
-        }
-        catch ( PwmOperationalException e )
-        {
-            final String errorMsg = "unexpected error reading responses: " + e.getMessage();
-            final ErrorInformation errorInformation = new ErrorInformation( PwmError.ERROR_UNKNOWN, errorMsg );
-            throw new PwmUnrecoverableException( errorInformation );
+            throw new PwmUnrecoverableException( e.getErrorInformation() );
         }
         }
         return otpConfig;
         return otpConfig;
     }
     }

+ 0 - 14
server/src/main/java/password/pwm/util/operations/otp/LdapOtpOperator.java

@@ -34,7 +34,6 @@ import password.pwm.config.PwmSetting;
 import password.pwm.config.profile.LdapProfile;
 import password.pwm.config.profile.LdapProfile;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmError;
-import password.pwm.error.PwmOperationalException;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.http.PwmSession;
 import password.pwm.http.PwmSession;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.logging.PwmLogger;
@@ -97,12 +96,6 @@ public class LdapOtpOperator extends AbstractOtpOperator
             final ErrorInformation errorInformation = new ErrorInformation( PwmError.ERROR_UNKNOWN, errorMsg );
             final ErrorInformation errorInformation = new ErrorInformation( PwmError.ERROR_UNKNOWN, errorMsg );
             throw new PwmUnrecoverableException( errorInformation );
             throw new PwmUnrecoverableException( errorInformation );
         }
         }
-        catch ( PwmOperationalException e )
-        {
-            final String errorMsg = "unexpected error reading responses: " + e.getMessage();
-            final ErrorInformation errorInformation = new ErrorInformation( PwmError.ERROR_UNKNOWN, errorMsg );
-            throw new PwmUnrecoverableException( errorInformation );
-        }
         return otp;
         return otp;
     }
     }
 
 
@@ -160,13 +153,6 @@ public class LdapOtpOperator extends AbstractOtpOperator
             pwmOE.initCause( ex );
             pwmOE.initCause( ex );
             throw pwmOE;
             throw pwmOE;
         }
         }
-        catch ( PwmOperationalException ex )
-        {
-            final ErrorInformation errorInfo = new ErrorInformation( PwmError.ERROR_WRITING_OTP_SECRET, ex.getMessage() );
-            final PwmUnrecoverableException pwmOE = new PwmUnrecoverableException( errorInfo );
-            pwmOE.initCause( ex );
-            throw pwmOE;
-        }
     }
     }
 
 
     @Override
     @Override

+ 4 - 0
server/src/main/java/password/pwm/util/operations/otp/OTPPamUtil.java

@@ -40,6 +40,10 @@ public class OTPPamUtil
 
 
     /**
     /**
      * Split the string in lines; separate by CR, LF or CRLF.
      * Split the string in lines; separate by CR, LF or CRLF.
+     *
+     * @param text string containing line separators.
+     *
+     * @return a list of split lines
      */
      */
     public static List<String> splitLines( final String text )
     public static List<String> splitLines( final String text )
     {
     {

+ 6 - 0
server/src/main/java/password/pwm/util/operations/otp/OTPUrlUtil.java

@@ -39,6 +39,9 @@ public class OTPUrlUtil
 
 
     /**
     /**
      * Convert a OTPUserRecord object into an otpauth:// url.
      * Convert a OTPUserRecord object into an otpauth:// url.
+     * @param otp a valid otp user record
+     *
+     * @return a valid otp url string.
      */
      */
     public static String composeOtpUrl( final OTPUserRecord otp )
     public static String composeOtpUrl( final OTPUserRecord otp )
     {
     {
@@ -51,6 +54,9 @@ public class OTPUrlUtil
 
 
     /**
     /**
      * Read a string with an otpauth:// url and convert to an OTPUserRecord object.
      * Read a string with an otpauth:// url and convert to an OTPUserRecord object.
+     *
+     * @param otpInfo otp input url string.
+     * @return a user recorded generated from the input string.
      */
      */
     public static OTPUserRecord decomposeOtpUrl( final String otpInfo )
     public static OTPUserRecord decomposeOtpUrl( final String otpInfo )
     {
     {

+ 11 - 2
server/src/main/java/password/pwm/util/operations/otp/PasscodeGenerator.java

@@ -117,6 +117,8 @@ public class PasscodeGenerator
 
 
     /**
     /**
      * @return A decimal timeout code
      * @return A decimal timeout code
+     *
+     * @throws GeneralSecurityException if a security exception is generated
      */
      */
     public String generateTimeoutCode( ) throws GeneralSecurityException
     public String generateTimeoutCode( ) throws GeneralSecurityException
     {
     {
@@ -182,6 +184,7 @@ public class PasscodeGenerator
      * @param challenge A challenge to check a response against
      * @param challenge A challenge to check a response against
      * @param response  A response to verify
      * @param response  A response to verify
      * @return True if the response is valid
      * @return True if the response is valid
+     * @throws GeneralSecurityException if a security exception is generated
      */
      */
     public boolean verifyResponseCode( final long challenge, final String response )
     public boolean verifyResponseCode( final long challenge, final String response )
             throws GeneralSecurityException
             throws GeneralSecurityException
@@ -197,6 +200,7 @@ public class PasscodeGenerator
      *
      *
      * @param timeoutCode The timeout code
      * @param timeoutCode The timeout code
      * @return True if the timeout code is valid
      * @return True if the timeout code is valid
+     * @throws GeneralSecurityException if a security exception is generated
      */
      */
     public boolean verifyTimeoutCode( final String timeoutCode )
     public boolean verifyTimeoutCode( final String timeoutCode )
             throws GeneralSecurityException
             throws GeneralSecurityException
@@ -214,9 +218,14 @@ public class PasscodeGenerator
      * @param pastIntervals   The number of past intervals to check
      * @param pastIntervals   The number of past intervals to check
      * @param futureIntervals The number of future intervals to check
      * @param futureIntervals The number of future intervals to check
      * @return True if the timeout code is valid
      * @return True if the timeout code is valid
+     * @throws GeneralSecurityException if a security exception is generated
      */
      */
-    public boolean verifyTimeoutCode( final String timeoutCode, final int pastIntervals,
-                                      final int futureIntervals ) throws GeneralSecurityException
+    public boolean verifyTimeoutCode(
+            final String timeoutCode,
+            final int pastIntervals,
+            final int futureIntervals
+    )
+            throws GeneralSecurityException
     {
     {
         final long currentInterval = clock.getCurrentInterval();
         final long currentInterval = clock.getCurrentInterval();
         final String expectedResponse = generateResponseCode( currentInterval );
         final String expectedResponse = generateResponseCode( currentInterval );

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

@@ -58,6 +58,9 @@ cluster.db.enable=true
 cluster.db.heartbeatSeconds=60
 cluster.db.heartbeatSeconds=60
 cluster.db.nodeTimeoutSeconds=600
 cluster.db.nodeTimeoutSeconds=600
 cluster.db.nodePurgeSeconds=86400
 cluster.db.nodePurgeSeconds=86400
+cluster.ldap.heartbeatSeconds=60
+cluster.ldap.nodeTimeoutSeconds=600
+cluster.ldap.nodePurgeSeconds=86400
 config.reloadOnChange=true
 config.reloadOnChange=true
 config.maxJdbcJarSize=10240000
 config.maxJdbcJarSize=10240000
 config.maxPersistentLoginSeconds=3600
 config.maxPersistentLoginSeconds=3600

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

@@ -1,7 +1,8 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <?xml version="1.0" encoding="UTF-8"?>
 <!--
 <!--
   ~ Password Management Servlets (PWM)
   ~ Password Management Servlets (PWM)
-  ~ Copyright (c) 2006-2018 Novell, Inc.
+  ~ 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
   ~ 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
   ~ it under the terms of the GNU General Public License as published by
@@ -100,11 +101,6 @@
             <option value="/public">/public</option>
             <option value="/public">/public</option>
         </options>
         </options>
     </setting>
     </setting>
-    <setting hidden="false" key="pwmInstanceName" level="2">
-        <default>
-            <value />
-        </default>
-    </setting>
     <setting hidden="false" key="idleTimeoutSeconds" level="1" required="true">
     <setting hidden="false" key="idleTimeoutSeconds" level="1" required="true">
         <properties>
         <properties>
             <property key="Minimum">60</property>
             <property key="Minimum">60</property>
@@ -984,9 +980,7 @@
     </setting>
     </setting>
     <setting hidden="false" key="sms.httpRequestHeaders" level="2">
     <setting hidden="false" key="sms.httpRequestHeaders" level="2">
         <regex>^[A-Za-z0-9_\.-]+:.*</regex>
         <regex>^[A-Za-z0-9_\.-]+:.*</regex>
-        <default>
-            <value />
-        </default>
+        <default/>
     </setting>
     </setting>
     <setting hidden="false" key="sms.maxTextLength" level="2" required="true">
     <setting hidden="false" key="sms.maxTextLength" level="2" required="true">
         <default>
         <default>
@@ -1566,6 +1560,18 @@
             <value>28800</value>
             <value>28800</value>
         </default>
         </default>
     </setting>
     </setting>
+    <setting hidden="false" key="cluster.storageMode" level="2">
+        <default>
+            <value>LDAP</value>
+        </default>
+        <default template="DB">
+            <value>DB</value>
+        </default>
+        <options>
+            <option value="LDAP">LDAP Directory</option>
+            <option value="DB">Remote Database</option>
+        </options>
+    </setting>
     <setting hidden="false" key="security.loginSession.mode" level="2">
     <setting hidden="false" key="security.loginSession.mode" level="2">
         <default>
         <default>
             <value>CRYPTCOOKIE</value>
             <value>CRYPTCOOKIE</value>

Bu fark içinde çok fazla dosya değişikliği olduğu için bazı dosyalar gösterilmiyor