Jelajahi Sumber

Merge branch 'master' into patch-12

Jason Rivard 6 tahun lalu
induk
melakukan
86fb60ee64
78 mengubah file dengan 2550 tambahan dan 1734 penghapusan
  1. 1 0
      build/checkstyle-import.xml
  2. 1 2
      client/package.json
  3. 1 1
      client/pom.xml
  4. 4 3
      client/src/modules/helpdesk/helpdesk-search-table.component.html
  5. 0 12
      client/src/modules/helpdesk/helpdesk-search-table.component.ts
  6. 7 0
      client/src/modules/helpdesk/helpdesk-search.component.scss
  7. 7 0
      client/src/modules/helpdesk/verifications-dialog.controller.ts
  8. 4 3
      client/src/modules/peoplesearch/peoplesearch-table.component.html
  9. 7 0
      client/src/modules/peoplesearch/peoplesearch-table.component.scss
  10. 0 12
      client/src/modules/peoplesearch/peoplesearch-table.component.ts
  11. 18 0
      client/src/modules/peoplesearch/person-details-dialog.component.scss
  12. 2 2
      client/webpack.config.js
  13. 3 2
      docker/pom.xml
  14. 45 1
      pom.xml
  15. 0 26
      rest-test-service/pom.xml
  16. 2 34
      server/pom.xml
  17. 1 0
      server/src/main/java/password/pwm/AppProperty.java
  18. 0 2
      server/src/main/java/password/pwm/PwmApplication.java
  19. 1 1
      server/src/main/java/password/pwm/PwmConstants.java
  20. 6 26
      server/src/main/java/password/pwm/config/PwmSettingXml.java
  21. 4 169
      server/src/main/java/password/pwm/config/profile/PwmPasswordPolicy.java
  22. 4 4
      server/src/main/java/password/pwm/config/stored/StoredConfigurationImpl.java
  23. 22 0
      server/src/main/java/password/pwm/error/ErrorInformation.java
  24. 1 1
      server/src/main/java/password/pwm/health/LDAPHealthChecker.java
  25. 1 1
      server/src/main/java/password/pwm/http/PwmHttpResponseWrapper.java
  26. 117 134
      server/src/main/java/password/pwm/http/filter/ConfigAccessFilter.java
  27. 34 0
      server/src/main/java/password/pwm/http/filter/ObsoleteUrlFilter.java
  28. 1 1
      server/src/main/java/password/pwm/http/servlet/GuestRegistrationServlet.java
  29. 1 1
      server/src/main/java/password/pwm/http/servlet/LoginServlet.java
  30. 7 3
      server/src/main/java/password/pwm/http/servlet/changepw/ChangePasswordServlet.java
  31. 1 1
      server/src/main/java/password/pwm/http/servlet/configeditor/ConfigEditorServlet.java
  32. 1 0
      server/src/main/java/password/pwm/http/servlet/configguide/ConfigGuideServlet.java
  33. 1 1
      server/src/main/java/password/pwm/http/servlet/forgottenpw/ForgottenPasswordUtil.java
  34. 1 1
      server/src/main/java/password/pwm/http/servlet/helpdesk/HelpdeskServlet.java
  35. 1 1
      server/src/main/java/password/pwm/http/servlet/newuser/NewUserUtils.java
  36. 2 7
      server/src/main/java/password/pwm/http/servlet/resource/ResourceServletService.java
  37. 2 1
      server/src/main/java/password/pwm/http/tag/PasswordRequirementsTag.java
  38. 1 0
      server/src/main/java/password/pwm/i18n/Display.java
  39. 1 1
      server/src/main/java/password/pwm/ldap/UserInfoReader.java
  40. 1 1
      server/src/main/java/password/pwm/ldap/auth/LDAPAuthenticationRequest.java
  41. 1 4
      server/src/main/java/password/pwm/svc/sessiontrack/SessionTrackService.java
  42. 14 16
      server/src/main/java/password/pwm/svc/sessiontrack/UserAgentUtils.java
  43. 1 1
      server/src/main/java/password/pwm/svc/wordlist/WordlistSource.java
  44. 1 2
      server/src/main/java/password/pwm/svc/wordlist/WordlistZipReader.java
  45. 8 8
      server/src/main/java/password/pwm/util/CaptchaUtility.java
  46. 0 1044
      server/src/main/java/password/pwm/util/PwmPasswordRuleValidator.java
  47. 17 5
      server/src/main/java/password/pwm/util/PwmScheduler.java
  48. 2 8
      server/src/main/java/password/pwm/util/java/JavaHelper.java
  49. 48 0
      server/src/main/java/password/pwm/util/java/LazySoftReference.java
  50. 1 1
      server/src/main/java/password/pwm/util/java/XmlElement.java
  51. 32 4
      server/src/main/java/password/pwm/util/java/XmlFactory.java
  52. 2 2
      server/src/main/java/password/pwm/util/operations/PasswordUtility.java
  53. 1 1
      server/src/main/java/password/pwm/util/password/PasswordCharCounter.java
  54. 781 0
      server/src/main/java/password/pwm/util/password/PasswordRuleChecks.java
  55. 202 0
      server/src/main/java/password/pwm/util/password/PasswordRuleReaderHelper.java
  56. 292 0
      server/src/main/java/password/pwm/util/password/PwmPasswordRuleUtil.java
  57. 291 0
      server/src/main/java/password/pwm/util/password/PwmPasswordRuleValidator.java
  58. 2 12
      server/src/main/java/password/pwm/util/password/RandomPasswordGenerator.java
  59. 3 6
      server/src/main/java/password/pwm/util/secure/ChecksumInputStream.java
  60. 12 11
      server/src/main/java/password/pwm/util/secure/ChecksumOutputStream.java
  61. 1 1
      server/src/main/java/password/pwm/ws/server/rest/RestRandomPasswordServer.java
  62. 1 1
      server/src/main/java/password/pwm/ws/server/rest/RestSetPasswordServer.java
  63. 1 0
      server/src/main/resources/password/pwm/AppProperty.properties
  64. 6 11
      server/src/main/resources/password/pwm/config/PwmSetting.xml
  65. 1 0
      server/src/main/resources/password/pwm/i18n/Display.properties
  66. 10 1
      server/src/test/java/password/pwm/config/PwmSettingTest.java
  67. 3 3
      server/src/test/java/password/pwm/config/profile/PasswordRuleReaderHelperTest.java
  68. 0 122
      server/src/test/java/password/pwm/util/PwmPasswordRuleValidatorTest.java
  69. 82 0
      server/src/test/java/password/pwm/util/java/XmlFactoryBenchmarkExtendedTest.java
  70. 21 2
      server/src/test/java/password/pwm/util/localdb/TestHelper.java
  71. 187 0
      server/src/test/java/password/pwm/util/password/PasswordRuleChecksTest.java
  72. 122 0
      server/src/test/java/password/pwm/util/password/PwmPasswordRuleValidatorTest.java
  73. 75 0
      server/src/test/java/password/pwm/util/password/RandomPasswordGeneratorTest.java
  74. 3 1
      webapp/pom.xml
  75. 1 1
      webapp/src/main/webapp/WEB-INF/jsp/fragment/footer.jsp
  76. 1 1
      webapp/src/main/webapp/WEB-INF/jsp/fragment/ldap-selector.jsp
  77. 2 2
      webapp/src/main/webapp/WEB-INF/jsp/fragment/message.jsp
  78. 8 8
      webapp/src/main/webapp/public/resources/js/main.js

+ 1 - 0
build/checkstyle-import.xml

@@ -64,6 +64,7 @@
     <allow pkg="com.github.tomakehurst.wiremock"/>
     <allow pkg="org.reflections"/>
     <allow pkg="org.bouncycastle.jce.provider"/>
+    <allow pkg="org.openjdk.jmh"/>
 
     <!-- gson -->
     <allow pkg="com.google.gson"/>

+ 1 - 2
client/package.json

@@ -12,8 +12,7 @@
         "clean": "rimraf dist/",
         "test": "karma start test/karma.conf.js",
         "test-single-run": "karma start test/karma.conf.js --singleRun --no-auto-watch",
-        "start": "webpack-dev-server --mode=development --port 4000 --history-api-fallback --colors",
-        "sync": "webpack --env.disableMinimize=true --mode=production --output-path=../webapp/target/pwm-1.8.0-SNAPSHOT/public/resources/webjars/pwm-client --watch --progress --colors"
+        "start": "webpack-dev-server --mode=development --port 4000 --history-api-fallback --colors"
     },
     "author": "",
     "license": "ISC",

+ 1 - 1
client/pom.xml

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

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

@@ -76,14 +76,15 @@
             <ias-icon icon="view_list_thin"></ias-icon>
         </ias-button>
         <div class="icon-divider vertical"></div>
-        <ias-button class="ias-icon-button table-configuration-menu-toggle" ias-toggle="menu1">
+        <ias-button class="ias-icon-button table-configuration-menu-toggle" ias-toggle="menu1"
+                    ng-attr-title="{{ 'Title_Settings' | translate }}">
             <ias-icon icon="configure_thick"></ias-icon>
         </ias-button>
         <ias-menu name="menu1" ias-align="end end" class="ias-styles-root">
             <div class="ias-input-container">
                 <div class="checkbox-button" ng-repeat="(key, value) in $ctrl.columnConfiguration">
-                    <input type="checkbox" ng-checked="value.visible" aria-label="Toggle column visibility" disabled/>
-                    <ias-button ng-click="$ctrl.toggleColumnVisible($event, key)">{{value.label}}</ias-button>
+                    <input type="checkbox" ng-model="value.visible" aria-label="Toggle column visibility" />
+                    <ias-button class="toggle-column-btn" ng-click="value.visible = !value.visible; $event.stopImmediatePropagation();">{{value.label}}</ias-button>
                 </div>
             </div>
         </ias-menu>

+ 0 - 12
client/src/modules/helpdesk/helpdesk-search-table.component.ts

@@ -106,18 +106,6 @@ export default class HelpDeskSearchTableComponent extends HelpDeskSearchBaseComp
         this.toggleView('search.cards');
     }
 
-    toggleColumnVisible(event, columnId): void {
-        const visibleColumns = Object.keys(this.columnConfiguration).filter((columnId) => {
-            return this.columnConfiguration[columnId].visible;
-        });
-
-        if (!(visibleColumns.length === 1 && this.columnConfiguration[columnId].visible)) {
-            this.columnConfiguration[columnId].visible = !this.columnConfiguration[columnId].visible;
-        }
-
-        event.stopImmediatePropagation();
-    }
-
     private onSearchResult(searchResult: SearchResult): void {
         this.searchResult = searchResult;
     }

+ 7 - 0
client/src/modules/helpdesk/helpdesk-search.component.scss

@@ -110,4 +110,11 @@
       }
     }
   }
+
+  .ias-input-container > .checkbox-button > .ias-button.toggle-column-btn {
+    &:focus, &:hover {
+      background-color: transparent;
+      box-shadow: none;
+    }
+  }
 }

+ 7 - 0
client/src/modules/helpdesk/verifications-dialog.controller.ts

@@ -96,6 +96,13 @@ export default class VerificationsDialogController {
                     this.status = STATUS_SELECT;
                     this.determineAvailableVerificationMethods();
                 }
+            })
+            .catch((reason: any) => {
+                alert(reason);
+
+                this.status = STATUS_NONE;
+                this.verificationStatus = STATUS_NONE;
+                this.IasDialogService.close();
             });
     }
 

+ 4 - 3
client/src/modules/peoplesearch/peoplesearch-table.component.html

@@ -80,14 +80,15 @@
                     ng-attr-title="{{ 'Title_OrgChart' | translate }}">
             <ias-icon class="ias-selected" icon="orgchart_thin"></ias-icon>
         </ias-button>
-        <ias-button class="ias-icon-button table-configuration-menu-toggle" ias-toggle="menu1">
+        <ias-button class="ias-icon-button table-configuration-menu-toggle" ias-toggle="menu1"
+                    ng-attr-title="{{ 'Title_Settings' | translate }}">
             <ias-icon icon="configure_thick"></ias-icon>
         </ias-button>
         <ias-menu name="menu1" ias-align="end end" class="ias-styles-root">
             <div class="ias-input-container">
                 <div class="checkbox-button" ng-repeat="(key, value) in $ctrl.columnConfiguration">
-                    <input type="checkbox" ng-checked="value.visible" aria-label="Toggle column visibility" disabled/>
-                    <ias-button ng-click="$ctrl.toggleColumnVisible($event, key)">{{value.label}}</ias-button>
+                    <input type="checkbox" ng-model="value.visible" aria-label="Toggle column visibility" />
+                    <ias-button class="toggle-column-btn" ng-click="value.visible = !value.visible; $event.stopImmediatePropagation();">{{value.label}}</ias-button>
                 </div>
             </div>
         </ias-menu>

+ 7 - 0
client/src/modules/peoplesearch/peoplesearch-table.component.scss

@@ -43,4 +43,11 @@
             }
         }
     }
+
+    .ias-input-container > .checkbox-button > .ias-button.toggle-column-btn {
+        &:focus, &:hover {
+            background-color: transparent;
+            box-shadow: none;
+        }
+    }
 }

+ 0 - 12
client/src/modules/peoplesearch/peoplesearch-table.component.ts

@@ -116,18 +116,6 @@ export default class PeopleSearchTableComponent extends PeopleSearchBaseComponen
         }
     }
 
-    toggleColumnVisible(event, columnId): void {
-        const visibleColumns = Object.keys(this.columnConfiguration).filter((columnId) => {
-            return this.columnConfiguration[columnId].visible;
-        });
-
-        if (!(visibleColumns.length === 1 && this.columnConfiguration[columnId].visible)) {
-            this.columnConfiguration[columnId].visible = !this.columnConfiguration[columnId].visible;
-        }
-
-        event.stopImmediatePropagation();
-    }
-
     private onSearchResult(searchResult: SearchResult): void {
         this.searchResult = searchResult;
     }

+ 18 - 0
client/src/modules/peoplesearch/person-details-dialog.component.scss

@@ -36,11 +36,29 @@
   }
 
   .person-details-dialog {
+    display: flex;
+    align-items: center;
+    justify-content: center;
     text-align: left;
     overflow: hidden;
 
     .ias-dialog-container {
+      display: flex;
+      margin: 0;
       padding: 0;
+      position: relative;
+      top: auto;
+      transform: none;
+
+      > .ias-dialog-content {
+        display: grid;
+        grid-template-rows: max-content 1fr;
+        flex-grow: 1;
+
+        > .person-details-content {
+          overflow: auto;
+        }
+      }
     }
 
     .ias-avatar {

+ 2 - 2
client/webpack.config.js

@@ -119,8 +119,8 @@ module.exports = function (env, argv) {
         // Development-specific configuration
         return webpackMerge(commonConfig, {
             entry: {
-                'peoplesearch.ng': './src/modules/peoplesearch/main.dev',
-                'helpdesk.ng': './src/modules/helpdesk/main.dev'
+                'peoplesearch.ng': './src/modules/peoplesearch/main',
+                'helpdesk.ng': './src/modules/helpdesk/main'
             },
             plugins: [
                 new HtmlWebpackPlugin({

+ 3 - 2
docker/pom.xml

@@ -34,7 +34,7 @@
             <plugin>
                 <groupId>com.google.cloud.tools</groupId>
                 <artifactId>jib-maven-plugin</artifactId>
-                <version>1.1.0</version>
+                <version>1.1.2</version>
                 <executions>
                     <execution>
                         <id>make-docker-image</id>
@@ -44,8 +44,9 @@
                         </goals>
                         <configuration>
                             <skip>${skipDocker}</skip>
+                            <jib.console>plain</jib.console>
                             <from>
-                                <image>adoptopenjdk/openjdk11:slim</image>
+                                <image>adoptopenjdk/openjdk11:jre</image>
                             </from>
                             <to>
                                 <image>${dockerImageTag}</image>

+ 45 - 1
pom.xml

@@ -294,6 +294,50 @@
             <version>4.0.0-beta1</version>
             <scope>provided</scope>
         </dependency>
-    </dependencies>
 
+        <!-- Test dependencies -->
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+            <version>4.12</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.mockito</groupId>
+            <artifactId>mockito-core</artifactId>
+            <version>2.27.0</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.assertj</groupId>
+            <artifactId>assertj-core</artifactId>
+            <version>3.12.2</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>com.github.tomakehurst</groupId>
+            <artifactId>wiremock</artifactId>
+            <version>2.23.2</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.reflections</groupId>
+            <artifactId>reflections</artifactId>
+            <version>0.9.11</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.openjdk.jmh</groupId>
+            <artifactId>jmh-core</artifactId>
+            <version>1.21</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.openjdk.jmh</groupId>
+            <artifactId>jmh-generator-annprocess</artifactId>
+            <version>1.21</version>
+            <scope>test</scope>
+        </dependency>
+
+    </dependencies>
 </project>

+ 0 - 26
rest-test-service/pom.xml

@@ -59,32 +59,6 @@
     <dependencies>
         <!-- dev tool -->
 
-        <!-- Test dependencies -->
-        <dependency>
-            <groupId>junit</groupId>
-            <artifactId>junit</artifactId>
-            <version>4.12</version>
-            <scope>test</scope>
-        </dependency>
-        <dependency>
-            <groupId>org.assertj</groupId>
-            <artifactId>assertj-core</artifactId>
-            <version>3.11.1</version>
-            <scope>test</scope>
-        </dependency>
-        <dependency>
-            <groupId>com.github.tomakehurst</groupId>
-            <artifactId>wiremock</artifactId>
-            <version>2.20.0</version>
-            <scope>test</scope>
-        </dependency>
-        <dependency>
-            <groupId>org.reflections</groupId>
-            <artifactId>reflections</artifactId>
-            <version>0.9.11</version>
-            <scope>test</scope>
-        </dependency>
-
         <!-- container dependencies -->
         <dependency>
             <groupId>javax.servlet</groupId>

+ 2 - 34
server/pom.xml

@@ -151,38 +151,6 @@
 
     <dependencies>
 
-        <!-- Test dependencies -->
-        <dependency>
-            <groupId>junit</groupId>
-            <artifactId>junit</artifactId>
-            <version>4.12</version>
-            <scope>test</scope>
-        </dependency>
-        <dependency>
-            <groupId>org.mockito</groupId>
-            <artifactId>mockito-core</artifactId>
-            <version>2.23.4</version>
-            <scope>test</scope>
-        </dependency>
-        <dependency>
-            <groupId>org.assertj</groupId>
-            <artifactId>assertj-core</artifactId>
-            <version>3.11.1</version>
-            <scope>test</scope>
-        </dependency>
-        <dependency>
-            <groupId>com.github.tomakehurst</groupId>
-            <artifactId>wiremock</artifactId>
-            <version>2.20.0</version>
-            <scope>test</scope>
-        </dependency>
-        <dependency>
-            <groupId>org.reflections</groupId>
-            <artifactId>reflections</artifactId>
-            <version>0.9.11</version>
-            <scope>test</scope>
-        </dependency>
-
         <!-- container dependencies -->
         <dependency>
             <groupId>javax.servlet</groupId>
@@ -299,7 +267,7 @@
         <dependency>
             <groupId>jaxen</groupId>
             <artifactId>jaxen</artifactId>
-            <version>1.1.6</version>
+            <version>1.2.0</version>
         </dependency>
         <dependency>
             <groupId>org.jdom</groupId>
@@ -354,7 +322,7 @@
         <dependency>
             <groupId>com.github.ziplet</groupId>
             <artifactId>ziplet</artifactId>
-            <version>2.3.0</version>
+            <version>2.4.1</version>
             <exclusions>
                 <exclusion>
                     <groupId>com.google.googlejavaformat</groupId>

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

@@ -276,6 +276,7 @@ public enum AppProperty
     PASSWORD_STRENGTH_THRESHOLD_GOOD                ( "password.strength.threshold.good" ),
     PASSWORD_STRENGTH_THRESHOLD_WEAK                ( "password.strength.threshold.weak" ),
     PASSWORD_STRENGTH_THRESHOLD_VERY_WEAK           ( "password.strength.threshold.veryWeak" ),
+    PASSWORD_RULE_WORDLIST_FAIL_WHEN_CLOSED         ( "password.rule.wordlist.failWhenClosed" ),
     PWNOTIFY_BATCH_COUNT                            ( "pwNotify.batch.count" ),
     PWNOTIFY_BATCH_DELAY_TIME_MULTIPLIER            ( "pwNotify.batch.delayTimeMultiplier" ),
     PWNOTIFY_MAX_LDAP_SEARCH_SIZE                   ( "pwNotify.maxLdapSearchSize" ),

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

@@ -300,8 +300,6 @@ public class PwmApplication
 
             pwmScheduler.immediateExecuteInNewThread( this::postInitTasks );
         }
-
-
     }
 
     private void postInitTasks( )

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

@@ -168,7 +168,7 @@ public abstract class PwmConstants
     public static final String PARAM_USERKEY = "userKey";
 
 
-    public static final String COOKIE_PERSISTENT_CONFIG_LOGIN = "persistentConfigLogin";
+    public static final String COOKIE_PERSISTENT_CONFIG_LOGIN = "CONFIG-AUTH";
 
     public static final String VALUE_REPLACEMENT_USERNAME = "%USERNAME%";
 

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

@@ -35,6 +35,7 @@ import javax.xml.validation.Schema;
 import javax.xml.validation.SchemaFactory;
 import javax.xml.validation.Validator;
 import java.io.InputStream;
+import java.lang.ref.WeakReference;
 import java.time.Instant;
 import java.util.Collections;
 import java.util.LinkedHashSet;
@@ -56,12 +57,12 @@ public class PwmSettingXml
 
     private static final PwmLogger LOGGER = PwmLogger.forClass( PwmSettingXml.class );
 
-    private static XmlDocument xmlDocCache;
+    private static WeakReference<XmlDocument> xmlDocCache = new WeakReference<>( null );
     private static final AtomicInteger LOAD_COUNTER = new AtomicInteger( 0 );
 
     private static XmlDocument readXml( )
     {
-        final XmlDocument docRefCopy = xmlDocCache;
+        final XmlDocument docRefCopy = xmlDocCache.get();
         if ( docRefCopy == null )
         {
             final InputStream inputStream = PwmSetting.class.getClassLoader().getResourceAsStream( SETTING_XML_FILENAME );
@@ -70,30 +71,9 @@ public class PwmSettingXml
                 final Instant startTime = Instant.now();
                 final XmlDocument newDoc = XmlFactory.getFactory().parseXml( inputStream );
                 final TimeDuration parseDuration = TimeDuration.fromCurrent( startTime );
-                LOGGER.trace( () -> "parsed PwmSettingXml in " + parseDuration.toString() + ", loads=" + LOAD_COUNTER.getAndIncrement() );
-
-                xmlDocCache = newDoc;
-
-                // clear cached dom after 30 seconds.
-                final Thread t = new Thread( "PwmSettingXml static cache thread" )
-                {
-                    @Override
-                    public void run( )
-                    {
-                        try
-                        {
-                            Thread.sleep( 30_000 );
-                        }
-                        catch ( InterruptedException e )
-                        {
-                            //ignored
-                        }
-                        LOGGER.trace( () -> "cached PwmSettingXml discarded" );
-                        xmlDocCache = null;
-                    }
-                };
-                t.setDaemon( true );
-                t.start();
+                LOGGER.trace( () -> "parsed PwmSettingXml in " + parseDuration.asCompactString() + ", loads=" + LOAD_COUNTER.getAndIncrement() );
+
+                xmlDocCache = new WeakReference<>( newDoc );
 
                 return newDoc;
             }

+ 4 - 169
server/src/main/java/password/pwm/config/profile/PwmPasswordPolicy.java

@@ -24,18 +24,15 @@ package password.pwm.config.profile;
 
 import com.novell.ldapchai.ChaiPasswordPolicy;
 import com.novell.ldapchai.ChaiPasswordRule;
-import com.novell.ldapchai.util.DefaultChaiPasswordPolicy;
-import com.novell.ldapchai.util.PasswordRuleHelper;
 import com.novell.ldapchai.util.StringHelper;
 import password.pwm.config.option.ADPolicyComplexity;
 import password.pwm.config.value.data.UserPermission;
 import password.pwm.health.HealthMessage;
 import password.pwm.health.HealthRecord;
-import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.JsonUtil;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.logging.PwmLogger;
-import password.pwm.util.macro.MacroMachine;
+import password.pwm.util.password.PasswordRuleReaderHelper;
 
 import java.io.Serializable;
 import java.util.ArrayList;
@@ -48,7 +45,6 @@ import java.util.Locale;
 import java.util.Map;
 import java.util.Set;
 import java.util.regex.Pattern;
-import java.util.regex.PatternSyntaxException;
 
 
 /**
@@ -146,9 +142,9 @@ public class PwmPasswordPolicy implements Profile, Serializable
         return chaiPasswordPolicy;
     }
 
-    public RuleHelper getRuleHelper( )
+    public PasswordRuleReaderHelper getRuleHelper( )
     {
-        return new RuleHelper( this );
+        return new PasswordRuleReaderHelper( this );
     }
 
     public String getValue( final PwmPasswordRule rule )
@@ -313,167 +309,6 @@ public class PwmPasswordPolicy implements Profile, Serializable
         return createPwmPasswordPolicy( policyMap, null );
     }
 
-    public static class RuleHelper
-    {
-        public enum Flag
-        {
-            KeepThresholds
-        }
-
-        private final PwmPasswordPolicy passwordPolicy;
-        private final PasswordRuleHelper chaiRuleHelper;
-
-        public RuleHelper( final PwmPasswordPolicy passwordPolicy )
-        {
-            this.passwordPolicy = passwordPolicy;
-            chaiRuleHelper = DefaultChaiPasswordPolicy.createDefaultChaiPasswordPolicy( passwordPolicy.policyMap ).getRuleHelper();
-        }
-
-        public List<String> getDisallowedValues( )
-        {
-            return chaiRuleHelper.getDisallowedValues();
-        }
-
-        public List<String> getDisallowedAttributes( final Flag... flags )
-        {
-            final List<String> disallowedAttributes = chaiRuleHelper.getDisallowedAttributes();
-
-            if ( JavaHelper.enumArrayContainsValue( flags, Flag.KeepThresholds ) )
-            {
-                return disallowedAttributes;
-            }
-            else
-            {
-                // Strip off any thresholds from attribute (specified as: "attributeName:N", where N is a numeric value).
-                final List<String> strippedDisallowedAttributes = new ArrayList<String>();
-
-                if ( disallowedAttributes != null )
-                {
-                    for ( final String disallowedAttribute : disallowedAttributes )
-                    {
-                        if ( disallowedAttribute != null )
-                        {
-                            final int indexOfColon = disallowedAttribute.indexOf( ':' );
-                            if ( indexOfColon > 0 )
-                            {
-                                strippedDisallowedAttributes.add( disallowedAttribute.substring( 0, indexOfColon ) );
-                            }
-                            else
-                            {
-                                strippedDisallowedAttributes.add( disallowedAttribute );
-                            }
-                        }
-                    }
-                }
-
-                return strippedDisallowedAttributes;
-            }
-        }
-
-        public List<Pattern> getRegExMatch( final MacroMachine macroMachine )
-        {
-            return readRegExSetting( PwmPasswordRule.RegExMatch, macroMachine );
-        }
-
-        public List<Pattern> getRegExNoMatch( final MacroMachine macroMachine )
-        {
-            return readRegExSetting( PwmPasswordRule.RegExNoMatch, macroMachine );
-        }
-
-        public List<Pattern> getCharGroupValues( )
-        {
-            return readRegExSetting( PwmPasswordRule.CharGroupsValues, null );
-        }
-
-
-        public int readIntValue( final PwmPasswordRule rule )
-        {
-            if (
-                    ( rule.getRuleType() != ChaiPasswordRule.RuleType.MIN )
-                            && ( rule.getRuleType() != ChaiPasswordRule.RuleType.MAX )
-                            && ( rule.getRuleType() != ChaiPasswordRule.RuleType.NUMERIC )
-                    )
-            {
-                throw new IllegalArgumentException( "attempt to read non-numeric rule value as int for rule " + rule );
-            }
-
-            final String value = passwordPolicy.policyMap.get( rule.getKey() );
-            final int defaultValue = StringHelper.convertStrToInt( rule.getDefaultValue(), 0 );
-            return StringHelper.convertStrToInt( value, defaultValue );
-        }
-
-        public boolean readBooleanValue( final PwmPasswordRule rule )
-        {
-            if ( rule.getRuleType() != ChaiPasswordRule.RuleType.BOOLEAN )
-            {
-                throw new IllegalArgumentException( "attempt to read non-boolean rule value as boolean for rule " + rule );
-            }
-
-            final String value = passwordPolicy.policyMap.get( rule.getKey() );
-            return StringHelper.convertStrToBoolean( value );
-        }
-
-        private List<Pattern> readRegExSetting( final PwmPasswordRule rule, final MacroMachine macroMachine )
-        {
-            final String input = passwordPolicy.policyMap.get( rule.getKey() );
-
-            return readRegExSetting( rule, macroMachine, input );
-        }
-
-        List<Pattern> readRegExSetting( final PwmPasswordRule rule, final MacroMachine macroMachine, final String input )
-        {
-            if ( input == null )
-            {
-                return Collections.emptyList();
-            }
-
-            final String separator = ( rule == PwmPasswordRule.RegExMatch || rule == PwmPasswordRule.RegExNoMatch ) ? ";;;" : "\n";
-            final List<String> values = new ArrayList<>( StringHelper.tokenizeString( input, separator ) );
-            final List<Pattern> patterns = new ArrayList<>();
-
-            for ( final String value : values )
-            {
-                if ( value != null && value.length() > 0 )
-                {
-                    String valueToCompile = value;
-
-                    if ( macroMachine != null && readBooleanValue( PwmPasswordRule.AllowMacroInRegExSetting ) )
-                    {
-                        valueToCompile = macroMachine.expandMacros( value );
-                    }
-
-                    try
-                    {
-                        final Pattern loopPattern = Pattern.compile( valueToCompile );
-                        patterns.add( loopPattern );
-                    }
-                    catch ( PatternSyntaxException e )
-                    {
-                        LOGGER.warn( "reading password rule value '" + valueToCompile + "' for rule " + rule.getKey() + " is not a valid regular expression " + e.getMessage() );
-                    }
-                }
-            }
-
-            return patterns;
-        }
-
-        public String getChangeMessage( )
-        {
-            final String changeMessage = passwordPolicy.getValue( PwmPasswordRule.ChangeMessage );
-            return changeMessage == null ? "" : changeMessage;
-        }
-
-        public ADPolicyComplexity getADComplexityLevel( )
-        {
-            final String strLevel = passwordPolicy.getValue( PwmPasswordRule.ADComplexityLevel );
-            if ( strLevel == null || strLevel.isEmpty() )
-            {
-                return ADPolicyComplexity.NONE;
-            }
-            return ADPolicyComplexity.valueOf( strLevel );
-        }
-    }
-
     public Map<String, String> getPolicyMap( )
     {
         return Collections.unmodifiableMap( policyMap );
@@ -493,7 +328,7 @@ public class PwmPasswordPolicy implements Profile, Serializable
 
     public List<HealthRecord> health( final Locale locale )
     {
-        final RuleHelper ruleHelper = this.getRuleHelper();
+        final PasswordRuleReaderHelper ruleHelper = this.getRuleHelper();
         final List<HealthRecord> returnList = new ArrayList<>();
         final Map<PwmPasswordRule, PwmPasswordRule> rulePairs = new LinkedHashMap<>();
         rulePairs.put( PwmPasswordRule.MinimumLength, PwmPasswordRule.MaximumLength );

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

@@ -246,13 +246,13 @@ public class StoredConfigurationImpl implements StoredConfiguration
             // remove existing element
             {
                 final XmlElement propertyElement  = xmlHelper.xpathForConfigProperty( propertyName );
-                propertyElement.detach();
+                if ( propertyElement != null )
+                {
+                    propertyElement.detach();
+                }
             }
 
             // add new property
-            {
-
-            }
             final XmlElement propertyElement = xmlHelper.getXmlFactory().newElement( XML_ELEMENT_PROPERTY );
             propertyElement.setAttribute( XML_ATTRIBUTE_KEY, propertyName.getKey() );
             propertyElement.addText( value );

+ 22 - 0
server/src/main/java/password/pwm/error/ErrorInformation.java

@@ -29,7 +29,12 @@ import password.pwm.http.PwmSession;
 import java.io.Serializable;
 import java.time.Instant;
 import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
 import java.util.Locale;
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Collectors;
 
 /**
  * An ErrorInformation is a package of error data generated within PWM.  Error information includes an error code
@@ -175,4 +180,21 @@ public class ErrorInformation implements Serializable
         return new ErrorInformation( pwmError, this.getDetailedErrorMsg() );
 
     }
+
+    public static boolean listsContainSameErrors( final List<ErrorInformation> errorInformation1, final List<ErrorInformation> errorInformation2 )
+    {
+        Objects.requireNonNull( errorInformation1 );
+        Objects.requireNonNull( errorInformation2 );
+        return extractErrorSet( errorInformation1 ).equals( extractErrorSet( errorInformation2 ) );
+    }
+
+    private static Set<PwmError> extractErrorSet( final List<ErrorInformation> errors )
+    {
+        if ( errors != null )
+        {
+            return errors.stream().map( ErrorInformation::getError ).collect( Collectors.toSet() );
+        }
+
+        return Collections.emptySet();
+    }
 }

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

@@ -56,7 +56,7 @@ import password.pwm.ldap.LdapOperationsHelper;
 import password.pwm.ldap.UserInfo;
 import password.pwm.ldap.UserInfoFactory;
 import password.pwm.util.PasswordData;
-import password.pwm.util.RandomPasswordGenerator;
+import password.pwm.util.password.RandomPasswordGenerator;
 import password.pwm.util.i18n.LocaleHelper;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.StringUtil;

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

@@ -178,7 +178,7 @@ public class PwmHttpResponseWrapper
         }
 
         final boolean httpOnlyEnabled = Boolean.parseBoolean( configuration.readAppProperty( AppProperty.HTTP_COOKIE_HTTPONLY_ENABLE ) );
-        final boolean httpOnly = httpOnlyEnabled && JavaHelper.enumArrayContainsValue( flags, Flag.NonHttpOnly );
+        final boolean httpOnly = httpOnlyEnabled && !JavaHelper.enumArrayContainsValue( flags, Flag.NonHttpOnly );
 
         final String value;
         {

+ 117 - 134
server/src/main/java/password/pwm/http/filter/ConfigAccessFilter.java

@@ -22,6 +22,8 @@
 
 package password.pwm.http.filter;
 
+import com.google.gson.annotations.SerializedName;
+import lombok.Value;
 import password.pwm.AppProperty;
 import password.pwm.Permission;
 import password.pwm.PwmApplication;
@@ -40,6 +42,7 @@ import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.http.ContextManager;
 import password.pwm.http.JspUrl;
 import password.pwm.http.ProcessStatus;
+import password.pwm.http.PwmHttpResponseWrapper;
 import password.pwm.http.PwmRequest;
 import password.pwm.http.PwmRequestAttribute;
 import password.pwm.http.PwmSession;
@@ -48,12 +51,10 @@ import password.pwm.http.bean.ConfigManagerBean;
 import password.pwm.svc.intruder.RecordType;
 import password.pwm.svc.sessiontrack.UserAgentUtils;
 import password.pwm.util.java.JavaHelper;
-import password.pwm.util.java.JsonUtil;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.java.TimeDuration;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.secure.PwmHashAlgorithm;
-import password.pwm.util.secure.PwmSecurityKey;
 import password.pwm.util.secure.SecureEngine;
 
 import javax.servlet.ServletException;
@@ -68,6 +69,8 @@ public class ConfigAccessFilter extends AbstractPwmFilter
 {
     private static final PwmLogger LOGGER = PwmLogger.forClass( ConfigAccessFilter.class );
 
+    private static final String COOKIE_NAME = PwmConstants.COOKIE_PERSISTENT_CONFIG_LOGIN;
+    private static final PwmHttpResponseWrapper.CookiePath COOKIE_PATH = PwmHttpResponseWrapper.CookiePath.Private;
 
     @Override
     void processFilter( final PwmApplicationMode mode, final PwmRequest pwmRequest, final PwmFilterChain filterChain ) throws PwmException, IOException, ServletException
@@ -139,70 +142,41 @@ public class ConfigAccessFilter extends AbstractPwmFilter
         }
 
         final boolean persistentLoginEnabled = persistentLoginEnabled( pwmRequest );
-        final boolean persistentLoginAccepted = checkPersistentLoginCookie( pwmRequest, storedConfig );
 
-
-
-        final String password = pwmRequest.readParameterAsString( "password" );
-        boolean passwordAccepted = false;
-        if ( !persistentLoginAccepted )
+        if ( persistentLoginEnabled )
         {
-            if ( password != null && password.length() > 0 )
+            final boolean persistentLoginPassed = checkPersistentLoginCookie( pwmRequest, storedConfig );
+            if ( persistentLoginPassed )
             {
-                if ( storedConfig.verifyPassword( password, pwmRequest.getConfig() ) )
-                {
-                    passwordAccepted = true;
-                    LOGGER.trace( pwmRequest, () -> "valid configuration password accepted" );
-                    updateLoginHistory( pwmRequest, pwmRequest.getUserInfoIfLoggedIn(), true );
-                }
-                else
-                {
-                    LOGGER.trace( pwmRequest, () -> "configuration password is not correct" );
-                    pwmApplication.getIntruderManager().convenience().markAddressAndSession( pwmSession );
-                    pwmApplication.getIntruderManager().mark( RecordType.USERNAME, PwmConstants.CONFIGMANAGER_INTRUDER_USERNAME, pwmSession.getLabel() );
-                    final ErrorInformation errorInformation = new ErrorInformation( PwmError.ERROR_PASSWORD_ONLY_BAD );
-                    updateLoginHistory( pwmRequest, pwmRequest.getUserInfoIfLoggedIn(), false );
-                    return denyAndError( pwmRequest, errorInformation );
-                }
+                return processLoginSuccess( pwmRequest, persistentLoginEnabled );
             }
         }
 
-        if ( ( persistentLoginAccepted || passwordAccepted ) )
+        final String password = pwmRequest.readParameterAsString( "password" );
+
+        boolean passwordAccepted = false;
+        if ( !StringUtil.isEmpty( password ) )
         {
-            configManagerBean.setPasswordVerified( true );
-            pwmApplication.getIntruderManager().convenience().clearAddressAndSession( pwmSession );
-            pwmApplication.getIntruderManager().clear( RecordType.USERNAME, PwmConstants.CONFIGMANAGER_INTRUDER_USERNAME );
-            pwmRequest.getPwmSession().getSessionStateBean().setSessionIdRecycleNeeded( true );
-            if ( persistentLoginEnabled && !persistentLoginAccepted && "on".equals( pwmRequest.readParameterAsString( "remember" ) ) )
+            if ( storedConfig.verifyPassword( password, pwmRequest.getConfig() ) )
             {
-                final int persistentSeconds = figureMaxLoginSeconds( pwmRequest );
-                if ( persistentSeconds > 0 )
-                {
-                    final Instant expirationDate = Instant.ofEpochMilli( System.currentTimeMillis() + ( persistentSeconds * 1000 ) );
-                    final String persistentLoginValue = makePersistentLoginValue( pwmRequest, storedConfig );
-                    final PersistentLoginInfo persistentLoginInfo = new PersistentLoginInfo( expirationDate, persistentLoginValue );
-                    final String jsonPersistentLoginInfo = JsonUtil.serialize( persistentLoginInfo );
-                    final String cookieValue = pwmApplication.getSecureService().encryptToString( jsonPersistentLoginInfo );
-                    pwmRequest.getPwmResponse().writeCookie(
-                            PwmConstants.COOKIE_PERSISTENT_CONFIG_LOGIN,
-                            cookieValue,
-                            persistentSeconds
-                    );
-                    LOGGER.debug( pwmRequest, () -> "set persistent config login cookie (expires "
-                            + JavaHelper.toIsoDate( expirationDate )
-                            + ")"
-                    );
-                }
+                passwordAccepted = true;
+                LOGGER.trace( pwmRequest, () -> "valid configuration password accepted" );
+                updateLoginHistory( pwmRequest, pwmRequest.getUserInfoIfLoggedIn(), true );
             }
-
-            if ( configManagerBean.getPrePasswordEntryUrl() != null )
+            else
             {
-                final String originalUrl = configManagerBean.getPrePasswordEntryUrl();
-                configManagerBean.setPrePasswordEntryUrl( null );
-                pwmRequest.getPwmResponse().sendRedirect( originalUrl );
-                return ProcessStatus.Halt;
+                LOGGER.trace( pwmRequest, () -> "configuration password is not correct" );
+                pwmApplication.getIntruderManager().convenience().markAddressAndSession( pwmSession );
+                pwmApplication.getIntruderManager().mark( RecordType.USERNAME, PwmConstants.CONFIGMANAGER_INTRUDER_USERNAME, pwmSession.getLabel() );
+                final ErrorInformation errorInformation = new ErrorInformation( PwmError.ERROR_PASSWORD_ONLY_BAD );
+                updateLoginHistory( pwmRequest, pwmRequest.getUserInfoIfLoggedIn(), false );
+                return denyAndError( pwmRequest, errorInformation );
             }
-            return ProcessStatus.Continue;
+        }
+
+        if ( passwordAccepted )
+        {
+            return processLoginSuccess( pwmRequest, persistentLoginEnabled );
         }
 
         configManagerBean.setPrePasswordEntryUrl( pwmRequest.getHttpServletRequest().getRequestURL().toString() );
@@ -211,29 +185,50 @@ public class ConfigAccessFilter extends AbstractPwmFilter
         return ProcessStatus.Halt;
     }
 
+    private static void writePersistentLoginCookie( final PwmRequest pwmRequest )
+            throws PwmUnrecoverableException
+    {
+        final int persistentSeconds = figureMaxLoginSeconds( pwmRequest );
+
+        if ( persistentSeconds > 0 )
+        {
+            final TimeDuration persistenceDuration = TimeDuration.of( persistentSeconds, TimeDuration.Unit.SECONDS );
+            final Instant expirationDate = persistenceDuration.incrementFromInstant( Instant.now() );
+            final StoredConfigurationImpl storedConfig = pwmRequest.getConfig().getStoredConfiguration();
+            final String persistentLoginValue = makePersistentLoginPassword( pwmRequest, storedConfig );
+            final PersistentLoginInfo persistentLoginInfo = new PersistentLoginInfo( expirationDate, persistentLoginValue );
+            final String cookieValue = pwmRequest.getPwmApplication().getSecureService().encryptObjectToString( persistentLoginInfo );
+            pwmRequest.getPwmResponse().writeCookie(
+                    COOKIE_NAME,
+                    cookieValue,
+                    persistentSeconds,
+                    COOKIE_PATH
+            );
+            LOGGER.debug( pwmRequest, () -> "set persistent config login cookie (expires "
+                    + JavaHelper.toIsoDate( expirationDate )
+                    + ")"
+            );
+        }
+    }
+
     private static boolean checkPersistentLoginCookie(
             final PwmRequest pwmRequest,
             final StoredConfiguration storedConfig
 
     )
-            throws PwmUnrecoverableException
     {
-        final PwmSecurityKey securityKey = pwmRequest.getConfig().getSecurityKey();
-        final String cookieStr = pwmRequest.readCookie( PwmConstants.COOKIE_PERSISTENT_CONFIG_LOGIN );
-        if ( securityKey != null && cookieStr != null && !cookieStr.isEmpty() )
+        try
         {
-            try
+            final String cookieValue = pwmRequest.readCookie( COOKIE_NAME );
+            if ( !StringUtil.isEmpty( cookieValue ) )
             {
-                final String persistentLoginValue = makePersistentLoginValue( pwmRequest, storedConfig );
-
-                final String jsonStr = pwmRequest.getPwmApplication().getSecureService().decryptStringValue( cookieStr );
-                final PersistentLoginInfo persistentLoginInfo = JsonUtil.deserialize( jsonStr, PersistentLoginInfo.class );
-                if ( persistentLoginInfo != null && persistentLoginValue != null )
+                final PersistentLoginInfo persistentLoginInfo = pwmRequest.getPwmApplication().getSecureService().decryptObject( cookieValue, PersistentLoginInfo.class );
+                if ( persistentLoginInfo != null )
                 {
                     if ( persistentLoginInfo.getExpireDate().isAfter( Instant.now() ) )
                     {
-
-                        if ( persistentLoginValue.equals( persistentLoginInfo.getPassword() ) )
+                        final String persistentLoginPassword = makePersistentLoginPassword( pwmRequest, storedConfig );
+                        if ( StringUtil.nullSafeEquals( persistentLoginPassword, persistentLoginInfo.getPassword() ) )
                         {
                             LOGGER.debug( pwmRequest, () -> "accepting persistent config login from cookie (expires "
                                     + JavaHelper.toIsoDate( persistentLoginInfo.getExpireDate() )
@@ -242,17 +237,15 @@ public class ConfigAccessFilter extends AbstractPwmFilter
                             return true;
                         }
                     }
+
+                    pwmRequest.getPwmResponse().removeCookie( COOKIE_NAME, COOKIE_PATH );
+                    LOGGER.debug( pwmRequest, () -> "removing non-working persistent config login cookie" );
                 }
             }
-            catch ( Exception e )
-            {
-                LOGGER.error( pwmRequest, "error examining persistent config login cookie: " + e.getMessage() );
-            }
-            if ( !StringUtil.isEmpty( cookieStr ) )
-            {
-                pwmRequest.getPwmResponse().removeCookie( PwmConstants.COOKIE_PERSISTENT_CONFIG_LOGIN, null );
-                LOGGER.debug( pwmRequest, () -> "removing non-working persistent config login cookie" );
-            }
+        }
+        catch ( Exception e )
+        {
+            LOGGER.error( pwmRequest, "error examining persistent config login cookie: " + e.getMessage() );
         }
 
         return false;
@@ -304,27 +297,22 @@ public class ConfigAccessFilter extends AbstractPwmFilter
         return true;
     }
 
-    private static String makePersistentLoginValue(
+    private static String makePersistentLoginPassword(
             final PwmRequest pwmRequest,
             final StoredConfiguration storedConfig
     )
             throws PwmUnrecoverableException
     {
+        final int hashChars = 32;
+        String hashValue = storedConfig.readConfigProperty( ConfigurationProperty.PASSWORD_HASH );
+
         if ( PwmApplicationMode.RUNNING == pwmRequest.getPwmApplication().getApplicationMode() )
         {
             final PwmSession pwmSession = pwmRequest.getPwmSession();
-            return SecureEngine.hash(
-                    storedConfig.readConfigProperty( ConfigurationProperty.PASSWORD_HASH )
-                            + pwmSession.getUserInfo().getUserIdentity().toDelimitedKey(),
-                    PwmHashAlgorithm.SHA512 );
-
-        }
-        else
-        {
-            return SecureEngine.hash(
-                    storedConfig.readConfigProperty( ConfigurationProperty.PASSWORD_HASH ),
-                    PwmHashAlgorithm.SHA512 );
+            hashValue += pwmSession.getUserInfo().getUserIdentity().toDelimitedKey();
         }
+
+        return StringUtil.truncate( SecureEngine.hash( hashValue, PwmHashAlgorithm.SHA512 ), hashChars );
     }
 
     private static void forwardToJsp( final PwmRequest pwmRequest )
@@ -362,36 +350,21 @@ public class ConfigAccessFilter extends AbstractPwmFilter
         pwmRequest.getPwmApplication().writeAppAttribute( PwmApplication.AppAttribute.CONFIG_LOGIN_HISTORY, configLoginHistory );
     }
 
+    @Value
     private static class PersistentLoginInfo implements Serializable
     {
+        @SerializedName( "e" )
         private Instant expireDate;
-        private String password;
-
-        private PersistentLoginInfo(
-                final Instant expireDate,
-                final String password
-        )
-        {
-            this.expireDate = expireDate;
-            this.password = password;
-        }
-
-        public Instant getExpireDate( )
-        {
-            return expireDate;
-        }
 
-        public String getPassword( )
-        {
-            return password;
-        }
+        @SerializedName( "p" )
+        private String password;
     }
 
-
+    @Value
     public static class ConfigLoginHistory implements Serializable
     {
-        private final List<ConfigLoginEvent> successEvents = new ArrayList<>();
-        private final List<ConfigLoginEvent> failedEvents = new ArrayList<>();
+        private List<ConfigLoginEvent> successEvents = new ArrayList<>();
+        private List<ConfigLoginEvent> failedEvents = new ArrayList<>();
 
         void addEvent( final ConfigLoginEvent event, final int maxEvents, final boolean successful )
         {
@@ -417,38 +390,20 @@ public class ConfigAccessFilter extends AbstractPwmFilter
         }
     }
 
+    @Value
     public static class ConfigLoginEvent implements Serializable
     {
         private final String userIdentity;
         private final Instant date;
         private final String networkAddress;
-
-        public ConfigLoginEvent( final String userIdentity, final Instant date, final String networkAddress )
-        {
-            this.userIdentity = userIdentity;
-            this.date = date;
-            this.networkAddress = networkAddress;
-        }
-
-        public String getUserIdentity( )
-        {
-            return userIdentity;
-        }
-
-        public Instant getDate( )
-        {
-            return date;
-        }
-
-        public String getNetworkAddress( )
-        {
-            return networkAddress;
-        }
     }
 
-    static int figureMaxLoginSeconds( final PwmRequest pwmRequest )
+    private static int figureMaxLoginSeconds( final PwmRequest pwmRequest )
     {
-        return Integer.parseInt( pwmRequest.getConfig().readAppProperty( AppProperty.CONFIG_MAX_PERSISTENT_LOGIN_SECONDS ) );
+        return JavaHelper.silentParseInt(
+                pwmRequest.getConfig().readAppProperty( AppProperty.CONFIG_MAX_PERSISTENT_LOGIN_SECONDS ),
+                (int) TimeDuration.HOUR.as( TimeDuration.Unit.SECONDS )
+        );
     }
 
 
@@ -459,4 +414,32 @@ public class ConfigAccessFilter extends AbstractPwmFilter
         forwardToJsp( pwmRequest );
         return ProcessStatus.Halt;
     }
+
+    private static ProcessStatus processLoginSuccess( final PwmRequest pwmRequest, final boolean persistentLoginEnabled )
+            throws PwmUnrecoverableException, IOException
+    {
+        final ConfigManagerBean configManagerBean = pwmRequest.getPwmApplication().getSessionStateService().getBean( pwmRequest, ConfigManagerBean.class );
+        final PwmApplication pwmApplication = pwmRequest.getPwmApplication();
+        final PwmSession pwmSession = pwmRequest.getPwmSession();
+
+        configManagerBean.setPasswordVerified( true );
+        pwmApplication.getIntruderManager().convenience().clearAddressAndSession( pwmSession );
+        pwmApplication.getIntruderManager().clear( RecordType.USERNAME, PwmConstants.CONFIGMANAGER_INTRUDER_USERNAME );
+        pwmRequest.getPwmSession().getSessionStateBean().setSessionIdRecycleNeeded( true );
+        if ( persistentLoginEnabled && "on".equals( pwmRequest.readParameterAsString( "remember" ) ) )
+        {
+            writePersistentLoginCookie( pwmRequest );
+        }
+
+        if ( configManagerBean.getPrePasswordEntryUrl() != null )
+        {
+            final String originalUrl = configManagerBean.getPrePasswordEntryUrl();
+            configManagerBean.setPrePasswordEntryUrl( null );
+            pwmRequest.getPwmResponse().sendRedirect( originalUrl );
+            return ProcessStatus.Halt;
+        }
+
+        pwmRequest.sendRedirect( pwmRequest.getURLwithQueryString() );
+        return ProcessStatus.Continue;
+    }
 }

+ 34 - 0
server/src/main/java/password/pwm/http/filter/ObsoleteUrlFilter.java

@@ -23,6 +23,7 @@
 package password.pwm.http.filter;
 
 import password.pwm.PwmApplicationMode;
+import password.pwm.PwmConstants;
 import password.pwm.error.PwmException;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.http.HttpMethod;
@@ -32,16 +33,30 @@ import password.pwm.http.PwmURL;
 import password.pwm.http.servlet.PwmServletDefinition;
 import password.pwm.svc.stats.Statistic;
 import password.pwm.svc.stats.StatisticsManager;
+import password.pwm.util.java.StringUtil;
 import password.pwm.util.logging.PwmLogger;
 
 import javax.servlet.ServletException;
 import java.io.IOException;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
 
 public class ObsoleteUrlFilter extends AbstractPwmFilter
 {
 
     private static final PwmLogger LOGGER = PwmLogger.forClass( ObsoleteUrlFilter.class );
 
+    private static final Map<String, String> STATIC_REDIRECTS;
+
+    static
+    {
+        final Map<String, String> staticRedirects = new HashMap<>();
+        staticRedirects.put( PwmConstants.URL_PREFIX_PRIVATE, PwmConstants.URL_PREFIX_PRIVATE + "/" );
+        STATIC_REDIRECTS = Collections.unmodifiableMap( staticRedirects );
+    }
+
+
     @Override
     void processFilter( final PwmApplicationMode mode, final PwmRequest pwmRequest, final PwmFilterChain filterChain )
             throws PwmException, IOException, ServletException
@@ -107,6 +122,25 @@ public class ObsoleteUrlFilter extends AbstractPwmFilter
 
         }
 
+        return doStaticMapRedirects( pwmRequest );
+    }
+
+    private ProcessStatus doStaticMapRedirects( final PwmRequest pwmRequest )
+            throws IOException, PwmUnrecoverableException
+    {
+        final String requestUrl = pwmRequest.getURLwithQueryString();
+
+        for ( final Map.Entry<String, String> entry : STATIC_REDIRECTS.entrySet() )
+        {
+            final String testUrl = pwmRequest.getContextPath() + entry.getKey();
+            if ( StringUtil.nullSafeEquals( requestUrl, testUrl ) )
+            {
+                final String nextUrl = pwmRequest.getContextPath() + entry.getValue();
+                pwmRequest.sendRedirect( nextUrl );
+                return ProcessStatus.Halt;
+            }
+        }
+
         return ProcessStatus.Continue;
     }
 

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

@@ -56,7 +56,7 @@ import password.pwm.ldap.search.UserSearchEngine;
 import password.pwm.svc.stats.Statistic;
 import password.pwm.util.FormMap;
 import password.pwm.util.PasswordData;
-import password.pwm.util.RandomPasswordGenerator;
+import password.pwm.util.password.RandomPasswordGenerator;
 import password.pwm.util.form.FormUtility;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.logging.PwmLogger;

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

@@ -278,7 +278,7 @@ public class LoginServlet extends ControlledPwmServlet
         final LoginServletBean loginServletBean = pwmRequest.getPwmApplication().getSessionStateService().getBean( pwmRequest, LoginServletBean.class );
         final String decryptedValue = loginServletBean.getNextUrl();
 
-        if ( decryptedValue != null && !decryptedValue.isEmpty() )
+        if ( !StringUtil.isEmpty( decryptedValue ) )
         {
             final PwmURL originalPwmURL = new PwmURL( URI.create( decryptedValue ), pwmRequest.getContextPath() );
             if ( !originalPwmURL.isLoginServlet() )

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

@@ -55,8 +55,8 @@ import password.pwm.svc.event.AuditRecord;
 import password.pwm.svc.event.AuditRecordFactory;
 import password.pwm.svc.stats.AvgStatistic;
 import password.pwm.util.PasswordData;
-import password.pwm.util.PwmPasswordRuleValidator;
-import password.pwm.util.RandomPasswordGenerator;
+import password.pwm.util.password.PwmPasswordRuleValidator;
+import password.pwm.util.password.RandomPasswordGenerator;
 import password.pwm.util.form.FormUtility;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.JsonUtil;
@@ -429,7 +429,11 @@ public abstract class ChangePasswordServlet extends ControlledPwmServlet
     @ActionHandler( action = "randomPassword" )
     private ProcessStatus processRandomPasswordAction( final PwmRequest pwmRequest ) throws IOException, PwmUnrecoverableException, ChaiUnavailableException
     {
-        final PasswordData passwordData = RandomPasswordGenerator.createRandomPassword( pwmRequest.getPwmSession(), pwmRequest.getPwmApplication() );
+        final PasswordData passwordData = RandomPasswordGenerator.createRandomPassword(
+                pwmRequest.getSessionLabel(),
+                pwmRequest.getPwmSession().getUserInfo().getPasswordPolicy(),
+                pwmRequest.getPwmApplication() );
+
         final RestRandomPasswordServer.JsonOutput jsonOutput = new RestRandomPasswordServer.JsonOutput();
         jsonOutput.setPassword( passwordData.getStringValue() );
         final RestResultBean restResultBean = RestResultBean.withData( jsonOutput );

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

@@ -74,7 +74,7 @@ import password.pwm.i18n.Message;
 import password.pwm.i18n.PwmLocaleBundle;
 import password.pwm.ldap.LdapBrowser;
 import password.pwm.util.PasswordData;
-import password.pwm.util.RandomPasswordGenerator;
+import password.pwm.util.password.RandomPasswordGenerator;
 import password.pwm.util.java.JsonUtil;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.java.TimeDuration;

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

@@ -494,6 +494,7 @@ public class ConfigGuideServlet extends ControlledPwmServlet
             }
             catch ( Exception e )
             {
+                LOGGER.error( pwmRequest, "error during save: " + e.getMessage(), e );
                 final RestResultBean restResultBean = RestResultBean.fromError( new ErrorInformation(
                         PwmError.ERROR_INTERNAL,
                         "error during save: " + e.getMessage()

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

@@ -66,7 +66,7 @@ import password.pwm.svc.stats.StatisticsManager;
 import password.pwm.svc.token.TokenType;
 import password.pwm.svc.token.TokenUtil;
 import password.pwm.util.PasswordData;
-import password.pwm.util.RandomPasswordGenerator;
+import password.pwm.util.password.RandomPasswordGenerator;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.macro.MacroMachine;

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

@@ -77,7 +77,7 @@ import password.pwm.svc.stats.StatisticsManager;
 import password.pwm.svc.token.TokenService;
 import password.pwm.svc.token.TokenUtil;
 import password.pwm.util.PasswordData;
-import password.pwm.util.RandomPasswordGenerator;
+import password.pwm.util.password.RandomPasswordGenerator;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.JsonUtil;
 import password.pwm.util.java.StringUtil;

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

@@ -64,7 +64,7 @@ import password.pwm.svc.stats.Statistic;
 import password.pwm.svc.token.TokenType;
 import password.pwm.svc.token.TokenUtil;
 import password.pwm.util.PasswordData;
-import password.pwm.util.RandomPasswordGenerator;
+import password.pwm.util.password.RandomPasswordGenerator;
 import password.pwm.util.form.FormUtility;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.JsonUtil;

+ 2 - 7
server/src/main/java/password/pwm/http/servlet/resource/ResourceServletService.java

@@ -32,13 +32,11 @@ import password.pwm.error.PwmException;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.health.HealthRecord;
 import password.pwm.http.PwmRequest;
-import password.pwm.http.bean.ImmutableByteArray;
 import password.pwm.svc.PwmService;
 import password.pwm.util.EventRateMeter;
 import password.pwm.util.java.FileSystemUtility;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.Percent;
-import password.pwm.util.java.StringUtil;
 import password.pwm.util.java.TimeDuration;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.secure.ChecksumOutputStream;
@@ -177,7 +175,6 @@ public class ResourceServletService implements PwmService
     private String makeResourcePathNonce( )
             throws IOException
     {
-        final int nonceLength = Integer.parseInt( pwmApplication.getConfig().readAppProperty( AppProperty.HTTP_RESOURCES_PATH_NONCE_LENGTH ) );
         final boolean enablePathNonce = Boolean.parseBoolean( pwmApplication.getConfig().readAppProperty( AppProperty.HTTP_RESOURCES_ENABLE_PATH_NONCE ) );
         if ( !enablePathNonce )
         {
@@ -185,9 +182,7 @@ public class ResourceServletService implements PwmService
         }
 
         final Instant startTime = Instant.now();
-        final ImmutableByteArray checksumBytes = checksumAllResources( pwmApplication );
-
-        final String nonce = StringUtil.truncate( JavaHelper.byteArrayToHexString( checksumBytes.copyOf() ).toLowerCase(), nonceLength );
+        final String nonce = checksumAllResources( pwmApplication );
         LOGGER.debug( () -> "completed generation of nonce '" + nonce + "' in " + TimeDuration.fromCurrent( startTime ).asCompactString() );
 
         final String noncePrefix = pwmApplication.getConfig().readAppProperty( AppProperty.HTTP_RESOURCES_NONCE_PATH_PREFIX );
@@ -240,7 +235,7 @@ public class ResourceServletService implements PwmService
         return false;
     }
 
-    private ImmutableByteArray checksumAllResources( final PwmApplication pwmApplication )
+    private String checksumAllResources( final PwmApplication pwmApplication )
             throws IOException
     {
         try ( ChecksumOutputStream checksumStream = new ChecksumOutputStream( new NullOutputStream() ) )

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

@@ -28,6 +28,7 @@ import password.pwm.config.option.ADPolicyComplexity;
 import password.pwm.config.profile.NewUserProfile;
 import password.pwm.config.profile.PwmPasswordPolicy;
 import password.pwm.config.profile.PwmPasswordRule;
+import password.pwm.util.password.PasswordRuleReaderHelper;
 import password.pwm.error.PwmException;
 import password.pwm.http.PwmRequest;
 import password.pwm.http.PwmSession;
@@ -71,7 +72,7 @@ public class PasswordRequirementsTag extends TagSupport
         final ADPolicyComplexity adPolicyLevel = pwordPolicy.getRuleHelper().getADComplexityLevel();
 
 
-        final PwmPasswordPolicy.RuleHelper ruleHelper = pwordPolicy.getRuleHelper();
+        final PasswordRuleReaderHelper ruleHelper = pwordPolicy.getRuleHelper();
 
         if ( ruleHelper.readBooleanValue( PwmPasswordRule.CaseSensitive ) )
         {

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

@@ -289,6 +289,7 @@ public enum Display implements PwmDisplayBundle
     Title_MainPage,
     Title_NewUser,
     Title_OrgChart,
+    Title_Settings,
     Title_PasswordGuide,
     Title_PasswordPolicy,
     Title_PasswordStrength,

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

@@ -54,7 +54,7 @@ import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.svc.PwmService;
 import password.pwm.svc.pwnotify.PwNotifyUserStatus;
 import password.pwm.util.PasswordData;
-import password.pwm.util.PwmPasswordRuleValidator;
+import password.pwm.util.password.PwmPasswordRuleValidator;
 import password.pwm.util.form.FormUtility;
 import password.pwm.util.i18n.LocaleHelper;
 import password.pwm.util.java.CachingProxyWrapper;

+ 1 - 1
server/src/main/java/password/pwm/ldap/auth/LDAPAuthenticationRequest.java

@@ -57,7 +57,7 @@ import password.pwm.svc.stats.EpsStatistic;
 import password.pwm.svc.stats.Statistic;
 import password.pwm.svc.stats.StatisticsManager;
 import password.pwm.util.PasswordData;
-import password.pwm.util.RandomPasswordGenerator;
+import password.pwm.util.password.RandomPasswordGenerator;
 import password.pwm.util.java.AtomicLoopIntIncrementer;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.TimeDuration;

+ 1 - 4
server/src/main/java/password/pwm/svc/sessiontrack/SessionTrackService.java

@@ -119,10 +119,7 @@ public class SessionTrackService implements PwmService
 
     private Set<PwmSession> copyOfSessionSet( )
     {
-        final Set<PwmSession> newSet = new HashSet<>();
-        newSet.addAll( pwmSessions.keySet() );
-        return newSet;
-
+        return new HashSet<>( pwmSessions.keySet() );
     }
 
     public Map<DebugKey, String> getDebugData( )

+ 14 - 16
server/src/main/java/password/pwm/svc/sessiontrack/UserAgentUtils.java

@@ -31,6 +31,7 @@ import password.pwm.error.PwmError;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.http.HttpHeader;
 import password.pwm.http.PwmRequest;
+import password.pwm.util.java.LazySoftReference;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.java.TimeDuration;
 import password.pwm.util.logging.PwmLogger;
@@ -42,30 +43,27 @@ public class UserAgentUtils
 {
     private static final PwmLogger LOGGER = PwmLogger.forClass( UserAgentUtils.class );
 
-    private static UserAgentParser cachedParser;
+    private static final LazySoftReference<UserAgentParser> CACHED_PARSER = new LazySoftReference<>( UserAgentUtils::loadUserAgentParser );
 
-    private static UserAgentParser getUserAgentParser( ) throws PwmUnrecoverableException
+    private static UserAgentParser loadUserAgentParser( )
     {
-        if ( cachedParser == null )
+        try
         {
-            try
-            {
-                cachedParser = new UserAgentService().loadParser();
-            }
-            catch ( IOException | ParseException e )
-            {
-                final String msg = "error loading user-agent parser: " + e.getMessage();
-                LOGGER.error( msg, e );
-                throw new PwmUnrecoverableException( PwmError.ERROR_INTERNAL, msg );
-            }
+            return new UserAgentService().loadParser();
+        }
+        catch ( IOException | ParseException e )
+        {
+            final String msg = "error loading user-agent parser: " + e.getMessage();
+            LOGGER.error( msg, e );
         }
-        return cachedParser;
+
+        return null;
     }
 
     public static void initializeCache() throws PwmUnrecoverableException
     {
         final Instant startTime = Instant.now();
-        getUserAgentParser();
+        CACHED_PARSER.get();
         LOGGER.trace( () -> "loaded useragent parser in " + TimeDuration.compactFromCurrent( startTime ) );
     }
 
@@ -79,7 +77,7 @@ public class UserAgentUtils
 
         boolean badBrowser = false;
 
-        final UserAgentParser userAgentParser = getUserAgentParser();
+        final UserAgentParser userAgentParser = CACHED_PARSER.get();
         final Capabilities capabilities = userAgentParser.parse( userAgentString );
         final String browser = capabilities.getBrowser();
         final String browserMajorVersion = capabilities.getBrowserMajorVersion();

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

@@ -162,7 +162,7 @@ class WordlistSource
                 return null;
             }
 
-            final String hash = JavaHelper.binaryArrayToHex( checksumInputStream.readUntilEndAndChecksum().copyOf() );
+            final String hash = checksumInputStream.checksum();
 
             final WordlistSourceInfo wordlistSourceInfo = new WordlistSourceInfo(
                     hash,

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

@@ -26,7 +26,6 @@ import org.apache.commons.io.input.CountingInputStream;
 import password.pwm.PwmConstants;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmUnrecoverableException;
-import password.pwm.util.java.JavaHelper;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.secure.ChecksumInputStream;
 
@@ -161,6 +160,6 @@ class WordlistZipReader implements AutoCloseable, Closeable
 
     String getChecksum()
     {
-        return JavaHelper.binaryArrayToHex( checksumInputStream.checksum().copyOf() );
+        return checksumInputStream.checksum();
     }
 }

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

@@ -119,10 +119,10 @@ public class CaptchaUtility
         final PasswordData privateKey = pwmApplication.getConfig().readSettingAsPassword( PwmSetting.RECAPTCHA_KEY_PRIVATE );
 
         final String bodyText = "secret=" + StringUtil.urlEncode( privateKey.getStringValue() )
-                        + "&"
-                        + "remoteip=" + StringUtil.urlEncode( pwmRequest.getSessionLabel().getSrcAddress() )
-                        + "&"
-                        + "response=" + StringUtil.urlEncode( recaptchaResponse );
+                + "&"
+                + "remoteip=" + StringUtil.urlEncode( pwmRequest.getSessionLabel().getSrcAddress() )
+                + "&"
+                + "response=" + StringUtil.urlEncode( recaptchaResponse );
 
         try
         {
@@ -337,11 +337,11 @@ public class CaptchaUtility
             return true;
         }
 
-        final String skipCaptcha = pwmRequest.readParameterAsString( PwmConstants.PARAM_SKIP_CAPTCHA );
-        if ( skipCaptcha != null && skipCaptcha.length() > 0 )
+        final String configValue = pwmRequest.getConfig().readSettingAsString( PwmSetting.CAPTCHA_SKIP_PARAM );
+        if ( !StringUtil.isEmpty( configValue ) )
         {
-            final String configValue = pwmRequest.getConfig().readSettingAsString( PwmSetting.CAPTCHA_SKIP_PARAM );
-            if ( configValue != null && configValue.equals( skipCaptcha ) )
+            final String skipCaptcha = pwmRequest.readParameterAsString( PwmConstants.PARAM_SKIP_CAPTCHA );
+            if ( StringUtil.nullSafeEquals( configValue, skipCaptcha ) )
             {
                 LOGGER.trace( pwmRequest, () -> "valid skipCaptcha value in request, skipping captcha check for this session" );
                 pwmRequest.getPwmSession().getSessionStateBean().setCaptchaBypassedViaParameter( true );

+ 0 - 1044
server/src/main/java/password/pwm/util/PwmPasswordRuleValidator.java

@@ -1,1044 +0,0 @@
-/*
- * Password Management Servlets (PWM)
- * http://www.pwm-project.org
- *
- * Copyright (c) 2006-2009 Novell, Inc.
- * Copyright (c) 2009-2018 The PWM Project
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
- */
-
-package password.pwm.util;
-
-import com.google.gson.reflect.TypeToken;
-import com.novell.ldapchai.ChaiUser;
-import com.novell.ldapchai.exception.ChaiError;
-import com.novell.ldapchai.exception.ChaiPasswordPolicyException;
-import com.novell.ldapchai.exception.ChaiUnavailableException;
-import org.apache.commons.lang3.StringUtils;
-import org.apache.commons.lang3.math.NumberUtils;
-import password.pwm.AppProperty;
-import password.pwm.PwmApplication;
-import password.pwm.PwmConstants;
-import password.pwm.bean.SessionLabel;
-import password.pwm.bean.pub.PublicUserInfoBean;
-import password.pwm.config.Configuration;
-import password.pwm.config.PwmSetting;
-import password.pwm.config.option.ADPolicyComplexity;
-import password.pwm.config.profile.PwmPasswordPolicy;
-import password.pwm.config.profile.PwmPasswordPolicy.RuleHelper;
-import password.pwm.config.profile.PwmPasswordRule;
-import password.pwm.error.ErrorInformation;
-import password.pwm.error.PwmDataValidationException;
-import password.pwm.error.PwmError;
-import password.pwm.error.PwmOperationalException;
-import password.pwm.error.PwmUnrecoverableException;
-import password.pwm.ldap.UserInfo;
-import password.pwm.svc.PwmService;
-import password.pwm.svc.stats.Statistic;
-import password.pwm.util.java.JavaHelper;
-import password.pwm.util.java.JsonUtil;
-import password.pwm.util.java.StringUtil;
-import password.pwm.util.logging.PwmLogger;
-import password.pwm.util.macro.MacroMachine;
-import password.pwm.util.operations.PasswordUtility;
-import password.pwm.ws.client.rest.RestClientHelper;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Locale;
-import java.util.Map;
-import java.util.Set;
-import java.util.regex.Pattern;
-
-public class PwmPasswordRuleValidator
-{
-
-    private static final PwmLogger LOGGER = PwmLogger.forClass( PwmPasswordRuleValidator.class );
-
-    private static final boolean EXTRA_LOGGING = false;
-
-    private final PwmApplication pwmApplication;
-    private final PwmPasswordPolicy policy;
-    private final Locale locale;
-    private final Flag[] flags;
-
-
-    public enum Flag
-    {
-        FailFast,
-        BypassLdapRuleCheck,
-    }
-
-    public PwmPasswordRuleValidator( final PwmApplication pwmApplication, final PwmPasswordPolicy policy, final Flag... flags )
-    {
-        this.pwmApplication = pwmApplication;
-        this.policy = policy;
-        this.locale = PwmConstants.DEFAULT_LOCALE;
-        this.flags = flags;
-    }
-
-    public PwmPasswordRuleValidator(
-            final PwmApplication pwmApplication,
-            final PwmPasswordPolicy policy,
-            final Locale locale,
-            final Flag... flags
-    )
-    {
-        this.pwmApplication = pwmApplication;
-        this.policy = policy;
-        this.locale = locale;
-        this.flags = flags;
-    }
-
-    public boolean testPassword(
-            final PasswordData password,
-            final PasswordData oldPassword,
-            final UserInfo userInfo,
-            final ChaiUser user
-    )
-            throws PwmDataValidationException, ChaiUnavailableException, PwmUnrecoverableException
-    {
-        final List<ErrorInformation> errorResults = validate( password, oldPassword, userInfo );
-
-        if ( !errorResults.isEmpty() )
-        {
-            throw new PwmDataValidationException( errorResults.iterator().next() );
-        }
-
-        if ( user != null && !JavaHelper.enumArrayContainsValue( flags, Flag.BypassLdapRuleCheck ) )
-        {
-            try
-            {
-                LOGGER.trace( () -> "calling chai directory password validation checker" );
-                user.testPasswordPolicy( password.getStringValue() );
-            }
-            catch ( UnsupportedOperationException e )
-            {
-                LOGGER.trace( () -> "Unsupported operation was thrown while validating password: " + e.toString() );
-            }
-            catch ( ChaiUnavailableException e )
-            {
-                pwmApplication.getStatisticsManager().incrementValue( Statistic.LDAP_UNAVAILABLE_COUNT );
-                LOGGER.warn( "ChaiUnavailableException was thrown while validating password: " + e.toString() );
-                throw e;
-            }
-            catch ( ChaiPasswordPolicyException e )
-            {
-                final ChaiError passwordError = e.getErrorCode();
-                final PwmError pwmError = PwmError.forChaiError( passwordError );
-                final ErrorInformation info = new ErrorInformation( pwmError == null ? PwmError.PASSWORD_UNKNOWN_VALIDATION : pwmError );
-                LOGGER.trace( () -> "ChaiPasswordPolicyException was thrown while validating password: " + e.toString() );
-                errorResults.add( info );
-            }
-        }
-
-        if ( !errorResults.isEmpty() )
-        {
-            throw new PwmDataValidationException( errorResults.iterator().next() );
-        }
-
-        return true;
-    }
-
-
-    /**
-     * Validates a password against the configured rules of PWM.  No directory operations
-     * are performed here.
-     *
-     * @param password desired new password
-     * @return true if the password is okay, never returns false.
-     */
-    private List<ErrorInformation> validate(
-            final PasswordData password,
-            final PasswordData oldPassword,
-            final UserInfo userInfo
-    )
-            throws PwmUnrecoverableException
-    {
-        final List<ErrorInformation> internalResults = internalPwmPolicyValidator( password, oldPassword, userInfo );
-        if ( pwmApplication != null )
-        {
-            final List<ErrorInformation> externalResults = invokeExternalRuleMethods(
-                    pwmApplication.getConfig(),
-                    policy,
-                    password,
-                    userInfo
-            );
-            internalResults.addAll( externalResults );
-        }
-        return internalResults;
-    }
-
-    public List<ErrorInformation> internalPwmPolicyValidator(
-            final PasswordData password,
-            final PasswordData oldPassword,
-            final UserInfo userInfo
-    )
-            throws PwmUnrecoverableException
-    {
-        final String passwordString = password == null ? "" : password.getStringValue();
-        final String oldPasswordString = oldPassword == null ? null : oldPassword.getStringValue();
-        return internalPwmPolicyValidator( passwordString, oldPasswordString, userInfo );
-    }
-
-    @SuppressWarnings( "checkstyle:MethodLength" )
-    public List<ErrorInformation> internalPwmPolicyValidator(
-            final String passwordString,
-            final String oldPasswordString,
-            final UserInfo userInfo
-    )
-            throws PwmUnrecoverableException
-    {
-        final boolean failFast = JavaHelper.enumArrayContainsValue( flags, Flag.FailFast );
-
-        // null check
-        if ( passwordString == null )
-        {
-            return Collections.singletonList( new ErrorInformation(
-                    PwmError.ERROR_INTERNAL,
-                    "empty (null) new password" ) );
-        }
-
-        final List<ErrorInformation> errorList = new ArrayList<>();
-        final PwmPasswordPolicy.RuleHelper ruleHelper = policy.getRuleHelper();
-        final MacroMachine macroMachine = userInfo == null || userInfo.getUserIdentity() == null
-                ? MacroMachine.forNonUserSpecific( pwmApplication, SessionLabel.SYSTEM_LABEL )
-                : MacroMachine.forUser(
-                pwmApplication,
-                PwmConstants.DEFAULT_LOCALE,
-                SessionLabel.SYSTEM_LABEL,
-                userInfo.getUserIdentity()
-        );
-
-        //check against old password
-        if ( oldPasswordString != null
-                && oldPasswordString.length() > 0
-                && ruleHelper.readBooleanValue( PwmPasswordRule.DisallowCurrent ) )
-        {
-            if ( oldPasswordString.length() > 0 )
-            {
-                if ( oldPasswordString.equalsIgnoreCase( passwordString ) )
-                {
-                    errorList.add( new ErrorInformation( PwmError.PASSWORD_SAMEASOLD ) );
-                }
-            }
-
-            //check chars from old password
-            final int maxOldAllowed = ruleHelper.readIntValue( PwmPasswordRule.MaximumOldChars );
-            if ( maxOldAllowed > 0 )
-            {
-                if ( oldPasswordString.length() > 0 )
-                {
-                    final String lPassword = passwordString.toLowerCase();
-                    final Set<Character> dupeChars = new HashSet<>();
-
-                    //add all dupes to the set.
-                    for ( final char loopChar : oldPasswordString.toLowerCase().toCharArray() )
-                    {
-                        if ( lPassword.indexOf( loopChar ) != -1 )
-                        {
-                            dupeChars.add( loopChar );
-                        }
-                    }
-
-                    //count the number of (unique) set elements.
-                    if ( dupeChars.size() >= maxOldAllowed )
-                    {
-                        errorList.add( new ErrorInformation( PwmError.PASSWORD_TOO_MANY_OLD_CHARS ) );
-                    }
-                }
-            }
-        }
-
-        if ( failFast && errorList.size() > 1 )
-        {
-            return errorList;
-        }
-
-        errorList.addAll( basicSyntaxRuleChecks( passwordString, policy, userInfo ) );
-
-        if ( failFast && errorList.size() > 1 )
-        {
-            return errorList;
-        }
-
-        // check against disallowed values;
-        if ( !ruleHelper.getDisallowedValues().isEmpty() )
-        {
-            final String lcasePwd = passwordString.toLowerCase();
-            final Set<String> paramValues = new HashSet<>( ruleHelper.getDisallowedValues() );
-
-            for ( final String loopValue : paramValues )
-            {
-                if ( loopValue != null && loopValue.length() > 0 )
-                {
-                    final String expandedValue = macroMachine.expandMacros( loopValue );
-                    if ( StringUtils.isNotBlank( expandedValue ) )
-                    {
-                        final String loweredLoop = expandedValue.toLowerCase();
-                        if ( lcasePwd.contains( loweredLoop ) )
-                        {
-                            errorList.add( new ErrorInformation( PwmError.PASSWORD_USING_DISALLOWED ) );
-                        }
-                    }
-                }
-            }
-        }
-
-        if ( failFast && errorList.size() > 1 )
-        {
-            return errorList;
-        }
-
-        // check disallowed attributes.
-        if ( !policy.getRuleHelper().getDisallowedAttributes().isEmpty() )
-        {
-            final List<String> paramConfigs = policy.getRuleHelper().getDisallowedAttributes( RuleHelper.Flag.KeepThresholds );
-            if ( userInfo != null )
-            {
-                final Map<String, String> userValues = userInfo.getCachedPasswordRuleAttributes();
-
-                for ( final String paramConfig : paramConfigs )
-                {
-                    final String[] parts = paramConfig.split( ":" );
-
-                    final String attrName = parts[ 0 ];
-                    final String disallowedValue = StringUtils.defaultString( userValues.get( attrName ) );
-                    final int threshold = parts.length > 1 ? NumberUtils.toInt( parts[ 1 ] ) : 0;
-
-                    if ( containsDisallowedValue( passwordString, disallowedValue, threshold ) )
-                    {
-                        LOGGER.trace( () -> "password rejected, same as user attr " + attrName );
-                        errorList.add( new ErrorInformation( PwmError.PASSWORD_SAMEASATTR ) );
-                    }
-                }
-            }
-        }
-
-        if ( failFast && errorList.size() > 1 )
-        {
-            return errorList;
-        }
-
-        {
-            // check password strength
-            final int requiredPasswordStrength = ruleHelper.readIntValue( PwmPasswordRule.MinimumStrength );
-            if ( requiredPasswordStrength > 0 )
-            {
-                if ( pwmApplication != null )
-                {
-                    final int passwordStrength = PasswordUtility.judgePasswordStrength(
-                            pwmApplication.getConfig(),
-                            passwordString
-                    );
-                    if ( passwordStrength < requiredPasswordStrength )
-                    {
-                        errorList.add( new ErrorInformation( PwmError.PASSWORD_TOO_WEAK ) );
-                        if ( EXTRA_LOGGING )
-                        {
-                            LOGGER.trace( () -> "password rejected, password strength of "
-                                    + passwordStrength + " is lower than policy requirement of "
-                                    + requiredPasswordStrength );
-                        }
-                    }
-                }
-            }
-        }
-
-        if ( failFast && errorList.size() > 1 )
-        {
-            return errorList;
-        }
-
-        // check regex matches.
-        for ( final Pattern pattern : ruleHelper.getRegExMatch( macroMachine ) )
-        {
-            if ( !pattern.matcher( passwordString ).matches() )
-            {
-                errorList.add( new ErrorInformation( PwmError.PASSWORD_INVALID_CHAR ) );
-                if ( EXTRA_LOGGING )
-                {
-                    LOGGER.trace( () -> "password rejected, does not match configured regex pattern: " + pattern.toString() );
-                }
-            }
-        }
-
-        if ( failFast && errorList.size() > 1 )
-        {
-            return errorList;
-        }
-
-        // check no-regex matches.
-        for ( final Pattern pattern : ruleHelper.getRegExNoMatch( macroMachine ) )
-        {
-            if ( pattern.matcher( passwordString ).matches() )
-            {
-                errorList.add( new ErrorInformation( PwmError.PASSWORD_INVALID_CHAR ) );
-                if ( EXTRA_LOGGING )
-                {
-                    LOGGER.trace( () -> "password rejected, matches configured no-regex pattern: " + pattern.toString() );
-                }
-            }
-        }
-
-        if ( failFast && errorList.size() > 1 )
-        {
-            return errorList;
-        }
-
-        // check char group matches
-        if ( ruleHelper.readIntValue( PwmPasswordRule.CharGroupsMinMatch ) > 0 )
-        {
-            final List<Pattern> ruleGroups = ruleHelper.getCharGroupValues();
-            if ( ruleGroups != null && !ruleGroups.isEmpty() )
-            {
-                final int requiredMatches = ruleHelper.readIntValue( PwmPasswordRule.CharGroupsMinMatch );
-                int matches = 0;
-                for ( final Pattern pattern : ruleGroups )
-                {
-                    if ( pattern.matcher( passwordString ).find() )
-                    {
-                        matches++;
-                    }
-                }
-                if ( matches < requiredMatches )
-                {
-                    errorList.add( new ErrorInformation( PwmError.PASSWORD_NOT_ENOUGH_GROUPS ) );
-                }
-            }
-            if ( failFast && errorList.size() > 1 )
-            {
-                return errorList;
-            }
-        }
-
-        if ( failFast && errorList.size() > 1 )
-        {
-            return errorList;
-        }
-
-        // check if the password is in the dictionary.
-        if ( ruleHelper.readBooleanValue( PwmPasswordRule.EnableWordlist ) )
-        {
-            if ( pwmApplication != null )
-            {
-                if ( pwmApplication.getWordlistManager() != null && pwmApplication.getWordlistManager().status() == PwmService.STATUS.OPEN )
-                {
-                    final boolean found = pwmApplication.getWordlistManager().containsWord( passwordString );
-
-                    if ( found )
-                    {
-                        //LOGGER.trace(pwmSession, "password rejected, in wordlist file");
-                        errorList.add( new ErrorInformation( PwmError.PASSWORD_INWORDLIST ) );
-                    }
-                }
-                else
-                {
-                    /* noop */
-                    //LOGGER.warn(pwmSession, "password wordlist checking enabled, but wordlist is not available, skipping wordlist check");
-                }
-            }
-            if ( failFast && errorList.size() > 1 )
-            {
-                return errorList;
-            }
-        }
-
-        if ( failFast && errorList.size() > 1 )
-        {
-            return errorList;
-        }
-
-        // check for shared (global) password history
-        if ( pwmApplication != null )
-        {
-            if ( pwmApplication.getConfig().readSettingAsBoolean( PwmSetting.PASSWORD_SHAREDHISTORY_ENABLE )
-                    && pwmApplication.getSharedHistoryManager().status() == PwmService.STATUS.OPEN )
-            {
-                final boolean found = pwmApplication.getSharedHistoryManager().containsWord( passwordString );
-
-                if ( found )
-                {
-                    //LOGGER.trace(pwmSession, "password rejected, in global shared history");
-                    errorList.add( new ErrorInformation( PwmError.PASSWORD_INWORDLIST ) );
-                }
-            }
-            if ( failFast && errorList.size() > 1 )
-            {
-                return errorList;
-            }
-        }
-
-        return errorList;
-    }
-
-    static boolean containsDisallowedValue( final String password, final String disallowedValue, final int threshold )
-    {
-        if ( StringUtils.isNotBlank( disallowedValue ) )
-        {
-            if ( threshold > 0 )
-            {
-                if ( disallowedValue.length() >= threshold )
-                {
-                    final String[] disallowedValueChunks = StringUtil.createStringChunks( disallowedValue, threshold );
-                    for ( final String chunk : disallowedValueChunks )
-                    {
-                        if ( StringUtils.containsIgnoreCase( password, chunk ) )
-                        {
-                            return true;
-                        }
-                    }
-                }
-            }
-            else
-            {
-                // No threshold?  Then the password can't contain the whole disallowed value
-                return StringUtils.containsIgnoreCase( password, disallowedValue );
-            }
-        }
-
-        return false;
-    }
-
-    /**
-     * Check a supplied password for it's validity according to AD complexity rules.
-     * - Not contain the user's account name or parts of the user's full name that exceed two consecutive characters
-     * - Be at least six characters in length
-     * - Contain characters from three of the following five categories:
-     * - English uppercase characters (A through Z)
-     * - English lowercase characters (a through z)
-     * - Base 10 digits (0 through 9)
-     * - Non-alphabetic characters (for example, !, $, #, %)
-     * - Any character categorized as an alphabetic but is not uppercase or lowercase.
-     * <p/>
-     * See this article: http://technet.microsoft.com/en-us/library/cc786468%28WS.10%29.aspx
-     *
-     * @param userInfo    userInfoBean
-     * @param password    password to test
-     * @param charCounter associated charCounter for the password.
-     * @return list of errors if the password does not meet requirements, or an empty list if the password complies
-     *         with AD requirements
-     */
-
-    private static List<ErrorInformation> checkPasswordForADComplexity(
-            final ADPolicyComplexity complexityLevel,
-            final UserInfo userInfo,
-            final String password,
-            final PasswordCharCounter charCounter,
-            final int maxGroupViolationCount
-    ) throws PwmUnrecoverableException
-    {
-        final List<ErrorInformation> errorList = new ArrayList<>();
-
-        if ( password == null || password.length() < 6 )
-        {
-            errorList.add( new ErrorInformation( PwmError.PASSWORD_TOO_SHORT ) );
-            return errorList;
-        }
-
-        final int maxLength = complexityLevel == ADPolicyComplexity.AD2003 ? 128 : 512;
-        if ( password.length() > maxLength )
-        {
-            errorList.add( new ErrorInformation( PwmError.PASSWORD_TOO_LONG ) );
-            return errorList;
-        }
-
-        if ( userInfo != null && userInfo.getCachedPasswordRuleAttributes() != null )
-        {
-            final Map<String, String> userAttrs = userInfo.getCachedPasswordRuleAttributes();
-            final String samAccountName = userAttrs.get( "sAMAccountName" );
-            if ( samAccountName != null
-                    && samAccountName.length() > 2
-                    && samAccountName.length() >= password.length() )
-            {
-                if ( password.toLowerCase().contains( samAccountName.toLowerCase() ) )
-                {
-                    errorList.add( new ErrorInformation( PwmError.PASSWORD_INWORDLIST ) );
-                    LOGGER.trace( () -> "Password violation due to ADComplexity check: Password contains sAMAccountName" );
-                }
-            }
-            final String displayName = userAttrs.get( "displayName" );
-            if ( displayName != null && displayName.length() > 2 )
-            {
-                if ( checkContainsTokens( password, displayName ) )
-                {
-                    errorList.add( new ErrorInformation( PwmError.PASSWORD_INWORDLIST ) );
-                    LOGGER.trace( () -> "Password violation due to ADComplexity check: Tokens from displayName used in password" );
-                }
-            }
-        }
-
-        int complexityPoints = 0;
-        if ( charCounter.getUpperCharCount() > 0 )
-        {
-            complexityPoints++;
-        }
-        if ( charCounter.getLowerCharCount() > 0 )
-        {
-            complexityPoints++;
-        }
-        if ( charCounter.getNumericCharCount() > 0 )
-        {
-            complexityPoints++;
-        }
-        switch ( complexityLevel )
-        {
-            case AD2003:
-                if ( charCounter.getSpecialCharsCount() > 0 || charCounter.getOtherLetterCharCount() > 0 )
-                {
-                    complexityPoints++;
-                }
-                break;
-
-            case AD2008:
-                if ( charCounter.getSpecialCharsCount() > 0 )
-                {
-                    complexityPoints++;
-                }
-                if ( charCounter.getOtherLetterCharCount() > 0 )
-                {
-                    complexityPoints++;
-                }
-                break;
-
-            default:
-                JavaHelper.unhandledSwitchStatement( complexityLevel );
-        }
-
-        switch ( complexityLevel )
-        {
-            case AD2008:
-                final int totalGroups = 5;
-                final int violations = totalGroups - complexityPoints;
-                if ( violations <= maxGroupViolationCount )
-                {
-                    return errorList;
-                }
-                break;
-
-            case AD2003:
-                if ( complexityPoints >= 3 )
-                {
-                    return errorList;
-                }
-                break;
-
-            default:
-                JavaHelper.unhandledSwitchStatement( complexityLevel );
-        }
-
-        if ( charCounter.getUpperCharCount() < 1 )
-        {
-            errorList.add( new ErrorInformation( PwmError.PASSWORD_NOT_ENOUGH_UPPER ) );
-        }
-        if ( charCounter.getLowerCharCount() < 1 )
-        {
-            errorList.add( new ErrorInformation( PwmError.PASSWORD_NOT_ENOUGH_LOWER ) );
-        }
-        if ( charCounter.getNumericCharCount() < 1 )
-        {
-            errorList.add( new ErrorInformation( PwmError.PASSWORD_NOT_ENOUGH_NUM ) );
-        }
-        if ( charCounter.getSpecialCharsCount() < 1 )
-        {
-            errorList.add( new ErrorInformation( PwmError.PASSWORD_NOT_ENOUGH_SPECIAL ) );
-        }
-        if ( charCounter.getOtherLetterCharCount() < 1 )
-        {
-            errorList.add( new ErrorInformation( PwmError.PASSWORD_UNKNOWN_VALIDATION ) );
-        }
-
-        return errorList;
-    }
-
-    // escape characters permitted because they match the exact AD specification
-    @SuppressWarnings( "checkstyle:avoidescapedunicodecharacters" )
-    private static boolean checkContainsTokens( final String baseValue, final String checkPattern )
-    {
-        if ( baseValue == null || baseValue.length() == 0 )
-        {
-            return false;
-        }
-
-        if ( checkPattern == null || checkPattern.length() == 0 )
-        {
-            return false;
-        }
-
-        final String baseValueLower = baseValue.toLowerCase();
-
-        final String[] tokens = checkPattern.toLowerCase().split( "[,\\.\\-\u2013\u2014_ \u00a3\\t]+" );
-
-        if ( tokens != null && tokens.length > 0 )
-        {
-            for ( final String token : tokens )
-            {
-                if ( token.length() > 2 )
-                {
-                    if ( baseValueLower.contains( token ) )
-                    {
-                        return true;
-                    }
-                }
-            }
-        }
-        return false;
-    }
-
-    private static final String REST_RESPONSE_KEY_ERROR = "error";
-    private static final String REST_RESPONSE_KEY_ERROR_MSG = "errorMessage";
-
-    public List<ErrorInformation> invokeExternalRuleMethods(
-            final Configuration config,
-            final PwmPasswordPolicy pwmPasswordPolicy,
-            final PasswordData password,
-            final UserInfo userInfo
-    )
-            throws PwmUnrecoverableException
-    {
-        final List<ErrorInformation> returnedErrors = new ArrayList<>();
-        final String restURL = config.readSettingAsString( PwmSetting.EXTERNAL_PWCHECK_REST_URLS );
-        final boolean haltOnError = Boolean.parseBoolean( config.readAppProperty( AppProperty.WS_REST_CLIENT_PWRULE_HALTONERROR ) );
-        final Map<String, Object> sendData = new LinkedHashMap<>();
-
-
-        if ( restURL == null || restURL.isEmpty() )
-        {
-            return Collections.emptyList();
-        }
-
-        {
-            final String passwordStr = password == null ? "" : password.getStringValue();
-            sendData.put( "password", passwordStr );
-        }
-
-        if ( pwmPasswordPolicy != null )
-        {
-            final LinkedHashMap<String, Object> policyData = new LinkedHashMap<>();
-            for ( final PwmPasswordRule rule : PwmPasswordRule.values() )
-            {
-                policyData.put( rule.name(), pwmPasswordPolicy.getValue( rule ) );
-            }
-            sendData.put( "policy", policyData );
-        }
-        if ( userInfo != null )
-        {
-            final MacroMachine macroMachine = MacroMachine.forUser( pwmApplication, PwmConstants.DEFAULT_LOCALE, SessionLabel.SYSTEM_LABEL, userInfo.getUserIdentity() );
-            final PublicUserInfoBean publicUserInfoBean = PublicUserInfoBean.fromUserInfoBean( userInfo, pwmApplication.getConfig(), locale, macroMachine );
-            sendData.put( "userInfo", publicUserInfoBean );
-        }
-
-        final String jsonRequestBody = JsonUtil.serializeMap( sendData );
-        try
-        {
-            final String responseBody = RestClientHelper.makeOutboundRestWSCall( pwmApplication, locale, restURL,
-                    jsonRequestBody );
-            final Map<String, Object> responseMap = JsonUtil.deserialize( responseBody,
-                    new TypeToken<Map<String, Object>>()
-                    {
-                    }
-            );
-            if ( responseMap.containsKey( REST_RESPONSE_KEY_ERROR ) && Boolean.parseBoolean( responseMap.get(
-                    REST_RESPONSE_KEY_ERROR ).toString() ) )
-            {
-                if ( responseMap.containsKey( REST_RESPONSE_KEY_ERROR_MSG ) )
-                {
-                    final String errorMessage = responseMap.get( REST_RESPONSE_KEY_ERROR_MSG ).toString();
-                    LOGGER.trace( () -> "external web service reported error: " + errorMessage );
-                    returnedErrors.add( new ErrorInformation( PwmError.PASSWORD_CUSTOM_ERROR, errorMessage, errorMessage, null ) );
-                }
-                else
-                {
-                    LOGGER.trace( () -> "external web service reported error without specifying an errorMessage" );
-                    returnedErrors.add( new ErrorInformation( PwmError.PASSWORD_CUSTOM_ERROR ) );
-                }
-            }
-            else
-            {
-                LOGGER.trace( () -> "external web service did not report an error" );
-            }
-
-        }
-        catch ( PwmOperationalException e )
-        {
-            final String errorMsg = "error executing external rule REST call: " + e.getMessage();
-            LOGGER.error( errorMsg );
-            if ( haltOnError )
-            {
-                throw new PwmUnrecoverableException( e.getErrorInformation(), e );
-            }
-            throw new IllegalStateException( "http response error code: " + e.getMessage() );
-        }
-        return returnedErrors;
-    }
-
-    @SuppressWarnings( "checkstyle:MethodLength" )
-    private static List<ErrorInformation> basicSyntaxRuleChecks(
-            final String password,
-            final PwmPasswordPolicy policy,
-            final UserInfo userInfo
-    ) throws PwmUnrecoverableException
-    {
-        final List<ErrorInformation> errorList = new ArrayList<>();
-        final PwmPasswordPolicy.RuleHelper ruleHelper = policy.getRuleHelper();
-        final PasswordCharCounter charCounter = new PasswordCharCounter( password );
-
-        final int passwordLength = password.length();
-
-        //Check minimum length
-        if ( passwordLength < ruleHelper.readIntValue( PwmPasswordRule.MinimumLength ) )
-        {
-            errorList.add( new ErrorInformation( PwmError.PASSWORD_TOO_SHORT ) );
-        }
-
-        //Check maximum length
-        {
-            final int passwordMaximumLength = ruleHelper.readIntValue( PwmPasswordRule.MaximumLength );
-
-            if ( passwordMaximumLength > 0 && passwordLength > passwordMaximumLength )
-            {
-                errorList.add( new ErrorInformation( PwmError.PASSWORD_TOO_LONG ) );
-            }
-        }
-
-        //check number of numeric characters
-        {
-            final int numberOfNumericChars = charCounter.getNumericCharCount();
-            if ( ruleHelper.readBooleanValue( PwmPasswordRule.AllowNumeric ) )
-            {
-                if ( numberOfNumericChars < ruleHelper.readIntValue( PwmPasswordRule.MinimumNumeric ) )
-                {
-                    errorList.add( new ErrorInformation( PwmError.PASSWORD_NOT_ENOUGH_NUM ) );
-                }
-
-                final int maxNumeric = ruleHelper.readIntValue( PwmPasswordRule.MaximumNumeric );
-                if ( maxNumeric > 0 && numberOfNumericChars > maxNumeric )
-                {
-                    errorList.add( new ErrorInformation( PwmError.PASSWORD_TOO_MANY_NUMERIC ) );
-                }
-
-                if ( !ruleHelper.readBooleanValue(
-                        PwmPasswordRule.AllowFirstCharNumeric ) && charCounter.isFirstNumeric() )
-                {
-                    errorList.add( new ErrorInformation( PwmError.PASSWORD_FIRST_IS_NUMERIC ) );
-                }
-
-                if ( !ruleHelper.readBooleanValue(
-                        PwmPasswordRule.AllowLastCharNumeric ) && charCounter.isLastNumeric() )
-                {
-                    errorList.add( new ErrorInformation( PwmError.PASSWORD_LAST_IS_NUMERIC ) );
-                }
-            }
-            else
-            {
-                if ( numberOfNumericChars > 0 )
-                {
-                    errorList.add( new ErrorInformation( PwmError.PASSWORD_TOO_MANY_NUMERIC ) );
-                }
-            }
-        }
-
-        //check number of upper characters
-        {
-            final int numberOfUpperChars = charCounter.getUpperCharCount();
-            if ( numberOfUpperChars < ruleHelper.readIntValue( PwmPasswordRule.MinimumUpperCase ) )
-            {
-                errorList.add( new ErrorInformation( PwmError.PASSWORD_NOT_ENOUGH_UPPER ) );
-            }
-
-            final int maxUpper = ruleHelper.readIntValue( PwmPasswordRule.MaximumUpperCase );
-            if ( maxUpper > 0 && numberOfUpperChars > maxUpper )
-            {
-                errorList.add( new ErrorInformation( PwmError.PASSWORD_TOO_MANY_UPPER ) );
-            }
-        }
-
-        //check number of alpha characters
-        {
-            final int numberOfAlphaChars = charCounter.getAlphaCharCount();
-            if ( numberOfAlphaChars < ruleHelper.readIntValue( PwmPasswordRule.MinimumAlpha ) )
-            {
-                errorList.add( new ErrorInformation( PwmError.PASSWORD_NOT_ENOUGH_ALPHA ) );
-            }
-
-            final int maxAlpha = ruleHelper.readIntValue( PwmPasswordRule.MaximumAlpha );
-            if ( maxAlpha > 0 && numberOfAlphaChars > maxAlpha )
-            {
-                errorList.add( new ErrorInformation( PwmError.PASSWORD_TOO_MANY_ALPHA ) );
-            }
-        }
-
-        //check number of non-alpha characters
-        {
-            final int numberOfNonAlphaChars = charCounter.getNonAlphaCharCount();
-
-            if ( ruleHelper.readBooleanValue( PwmPasswordRule.AllowNonAlpha ) )
-            {
-                if ( numberOfNonAlphaChars < ruleHelper.readIntValue( PwmPasswordRule.MinimumNonAlpha ) )
-                {
-                    errorList.add( new ErrorInformation( PwmError.PASSWORD_NOT_ENOUGH_NONALPHA ) );
-                }
-
-                final int maxNonAlpha = ruleHelper.readIntValue( PwmPasswordRule.MaximumNonAlpha );
-                if ( maxNonAlpha > 0 && numberOfNonAlphaChars > maxNonAlpha )
-                {
-                    errorList.add( new ErrorInformation( PwmError.PASSWORD_TOO_MANY_NONALPHA ) );
-                }
-            }
-            else
-            {
-                if ( numberOfNonAlphaChars > 0 )
-                {
-                    errorList.add( new ErrorInformation( PwmError.PASSWORD_TOO_MANY_NONALPHA ) );
-                }
-            }
-        }
-
-        //check number of lower characters
-        {
-            final int numberOfLowerChars = charCounter.getLowerCharCount();
-            if ( numberOfLowerChars < ruleHelper.readIntValue( PwmPasswordRule.MinimumLowerCase ) )
-            {
-                errorList.add( new ErrorInformation( PwmError.PASSWORD_NOT_ENOUGH_LOWER ) );
-            }
-
-            final int maxLower = ruleHelper.readIntValue( PwmPasswordRule.MaximumLowerCase );
-            if ( maxLower > 0 && numberOfLowerChars > maxLower )
-            {
-                errorList.add( new ErrorInformation( PwmError.PASSWORD_TOO_MANY_UPPER ) );
-            }
-        }
-
-        //check number of special characters
-        {
-            final int numberOfSpecialChars = charCounter.getSpecialCharsCount();
-            if ( ruleHelper.readBooleanValue( PwmPasswordRule.AllowSpecial ) )
-            {
-                if ( numberOfSpecialChars < ruleHelper.readIntValue( PwmPasswordRule.MinimumSpecial ) )
-                {
-                    errorList.add( new ErrorInformation( PwmError.PASSWORD_NOT_ENOUGH_SPECIAL ) );
-                }
-
-                final int maxSpecial = ruleHelper.readIntValue( PwmPasswordRule.MaximumSpecial );
-                if ( maxSpecial > 0 && numberOfSpecialChars > maxSpecial )
-                {
-                    errorList.add( new ErrorInformation( PwmError.PASSWORD_TOO_MANY_SPECIAL ) );
-                }
-
-                if ( !ruleHelper.readBooleanValue(
-                        PwmPasswordRule.AllowFirstCharSpecial ) && charCounter.isFirstSpecial() )
-                {
-                    errorList.add( new ErrorInformation( PwmError.PASSWORD_FIRST_IS_SPECIAL ) );
-                }
-
-                if ( !ruleHelper.readBooleanValue(
-                        PwmPasswordRule.AllowLastCharSpecial ) && charCounter.isLastSpecial() )
-                {
-                    errorList.add( new ErrorInformation( PwmError.PASSWORD_LAST_IS_SPECIAL ) );
-                }
-            }
-            else
-            {
-                if ( numberOfSpecialChars > 0 )
-                {
-                    errorList.add( new ErrorInformation( PwmError.PASSWORD_TOO_MANY_SPECIAL ) );
-                }
-            }
-        }
-
-        //Check maximum character repeats (sequential)
-        {
-            final int maxSequentialRepeat = ruleHelper.readIntValue( PwmPasswordRule.MaximumSequentialRepeat );
-            if ( maxSequentialRepeat > 0 && charCounter.getSequentialRepeatedChars() > maxSequentialRepeat )
-            {
-                errorList.add( new ErrorInformation( PwmError.PASSWORD_TOO_MANY_REPEAT ) );
-            }
-
-            //Check maximum character repeats (overall)
-            final int maxRepeat = ruleHelper.readIntValue( PwmPasswordRule.MaximumRepeat );
-            if ( maxRepeat > 0 && charCounter.getRepeatedChars() > maxRepeat )
-            {
-                errorList.add( new ErrorInformation( PwmError.PASSWORD_TOO_MANY_REPEAT ) );
-            }
-        }
-
-        //Check minimum unique character
-        {
-            final int minUnique = ruleHelper.readIntValue( PwmPasswordRule.MinimumUnique );
-            if ( minUnique > 0 && charCounter.getUniqueChars() < minUnique )
-            {
-                errorList.add( new ErrorInformation( PwmError.PASSWORD_NOT_ENOUGH_UNIQUE ) );
-            }
-        }
-
-        // check ad-complexity
-        {
-            final ADPolicyComplexity complexityLevel = ruleHelper.getADComplexityLevel();
-            if ( complexityLevel == ADPolicyComplexity.AD2003 || complexityLevel == ADPolicyComplexity.AD2008 )
-            {
-                final int maxGroupViolations = ruleHelper.readIntValue( PwmPasswordRule.ADComplexityMaxViolations );
-                errorList.addAll( checkPasswordForADComplexity( complexityLevel, userInfo, password, charCounter,
-                        maxGroupViolations ) );
-            }
-        }
-
-        // check consecutive characters
-        {
-            final int maximumConsecutive = ruleHelper.readIntValue( PwmPasswordRule.MaximumConsecutive );
-            if ( tooManyConsecutiveChars( password, maximumConsecutive ) )
-            {
-                errorList.add( new ErrorInformation( PwmError.PASSWORD_TOO_MANY_CONSECUTIVE ) );
-            }
-        }
-
-        return errorList;
-    }
-
-    public static boolean tooManyConsecutiveChars( final String str, final int maximumConsecutive )
-    {
-        if ( str != null && maximumConsecutive > 1 && str.length() >= maximumConsecutive )
-        {
-            final int[] codePoints = StringUtil.toCodePointArray( str.toLowerCase() );
-
-            int lastCodePoint = -1;
-            int consecutiveCharCount = 1;
-
-            for ( int i = 0; i < codePoints.length; i++ )
-            {
-                if ( codePoints[ i ] == lastCodePoint + 1 )
-                {
-                    consecutiveCharCount++;
-                }
-                else
-                {
-                    consecutiveCharCount = 1;
-                }
-
-                lastCodePoint = codePoints[ i ];
-
-                if ( consecutiveCharCount == maximumConsecutive )
-                {
-                    return true;
-                }
-            }
-        }
-
-        return false;
-    }
-}

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

@@ -25,6 +25,7 @@ package password.pwm.util;
 import org.jetbrains.annotations.NotNull;
 import password.pwm.PwmApplication;
 import password.pwm.PwmConstants;
+import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.java.TimeDuration;
 import password.pwm.util.logging.PwmLogger;
@@ -66,16 +67,15 @@ public class PwmScheduler
     {
         Objects.requireNonNull( runnable );
 
-        final ExecutorService executor = makeSingleThreadExecutorService( instanceID, runnable.getClass() );
+        final ScheduledExecutorService executor = makeSingleThreadExecutorService( instanceID, runnable.getClass() );
 
         if ( applicationExecutorService.isShutdown() )
         {
             return null;
         }
 
-        final WrappedRunner wrappedRunner = new WrappedRunner( runnable, executor );
-        applicationExecutorService.schedule( wrappedRunner, 0, TimeUnit.MILLISECONDS );
-        executor.shutdown();
+        final WrappedRunner wrappedRunner = new WrappedRunner( runnable, executor, WrappedRunner.Flag.ShutdownExecutorAfterExecution );
+        applicationExecutorService.submit( wrappedRunner );
         return wrappedRunner.getFuture();
     }
 
@@ -222,13 +222,20 @@ public class PwmScheduler
     {
         private final Runnable runnable;
         private final ExecutorService executor;
+        private final Flag[] flags;
         private volatile Future innerFuture;
         private volatile boolean hasFailed;
 
-        WrappedRunner( final Runnable runnable, final ExecutorService executor )
+        enum Flag
+        {
+            ShutdownExecutorAfterExecution,
+        }
+
+        WrappedRunner( final Runnable runnable, final ExecutorService executor, final Flag... flags )
         {
             this.runnable = runnable;
             this.executor = executor;
+            this.flags = flags;
         }
 
         Future getFuture()
@@ -287,6 +294,11 @@ public class PwmScheduler
                 LOGGER.error( "unexpected error running scheduled job: " + t.getMessage(), t );
                 hasFailed = true;
             }
+
+            if ( JavaHelper.enumArrayContainsValue( flags, Flag.ShutdownExecutorAfterExecution ) )
+            {
+                executor.shutdown();
+            }
         }
     }
 

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

@@ -47,6 +47,7 @@ import java.lang.management.MonitorInfo;
 import java.lang.management.ThreadInfo;
 import java.lang.reflect.Method;
 import java.net.URI;
+import java.nio.ByteBuffer;
 import java.text.DateFormat;
 import java.text.SimpleDateFormat;
 import java.time.Instant;
@@ -644,13 +645,6 @@ public class JavaHelper
 
     public static byte[] longToBytes( final long input )
     {
-        final byte[] result = new byte[Byte.SIZE];
-        long shift = input;
-        for ( int i = Byte.SIZE - 1; i >= 0; i-- )
-        {
-            result[i] = (byte) ( shift & 0xFF );
-            shift >>= Byte.SIZE;
-        }
-        return result;
+        return ByteBuffer.allocate( 8 ).putLong( input ).array();
     }
 }

+ 48 - 0
server/src/main/java/password/pwm/util/java/LazySoftReference.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.util.java;
+
+import java.lang.ref.SoftReference;
+import java.util.function.Supplier;
+
+public class LazySoftReference<E>
+{
+    private volatile SoftReference<E> reference = new SoftReference<>( null );
+    private final Supplier<E> supplier;
+
+    public LazySoftReference( final Supplier<E> supplier )
+    {
+        this.supplier = supplier;
+    }
+
+    public synchronized E get()
+    {
+        E localValue = reference.get();
+        if ( localValue == null )
+        {
+            localValue = supplier.get();
+            reference = new SoftReference<>( localValue );
+        }
+        return localValue;
+    }
+}

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

@@ -274,7 +274,7 @@ public interface XmlElement
         public String getText()
         {
             final String value = element.getTextContent();
-            return StringUtil.isEmpty( value ) ? null : value;
+            return value == null ? "" : value;
         }
 
         @Override

+ 32 - 4
server/src/main/java/password/pwm/util/java/XmlFactory.java

@@ -27,7 +27,7 @@ import org.jdom2.input.SAXBuilder;
 import org.jdom2.input.sax.XMLReaders;
 import org.jdom2.output.Format;
 import org.jdom2.output.XMLOutputter;
-import org.w3c.dom.Element;
+import org.w3c.dom.Node;
 import org.w3c.dom.NodeList;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmError;
@@ -53,11 +53,17 @@ import java.util.List;
 
 public interface XmlFactory
 {
+    enum FactoryType
+    {
+        JDOM,
+        W3C,
+    }
+
     XmlDocument parseXml( InputStream inputStream )
             throws PwmUnrecoverableException;
 
     void outputDocument( XmlDocument document, OutputStream outputStream )
-                    throws IOException;
+            throws IOException;
 
     XmlDocument newDocument( String rootElementName );
 
@@ -65,10 +71,28 @@ public interface XmlFactory
 
     static XmlFactory getFactory()
     {
-        //return new XmlFactoryW3c();
         return new XmlFactoryJDOM();
     }
 
+    static XmlFactory getFactory( final FactoryType factoryType )
+    {
+        switch ( factoryType )
+        {
+            case JDOM:
+                return new XmlFactoryJDOM();
+
+            case W3C:
+                return new XmlFactoryW3c();
+
+            default:
+                JavaHelper.unhandledSwitchStatement( factoryType );
+
+        }
+
+        return null;
+    }
+
+
     class XmlFactoryJDOM implements XmlFactory
     {
         private static final Charset STORAGE_CHARSET = Charset.forName( "UTF8" );
@@ -222,7 +246,11 @@ public interface XmlFactory
             {
                 for ( int i = 0; i < nodeList.getLength(); i++ )
                 {
-                    returnList.add( new XmlElement.XmlElementW3c( ( Element ) nodeList.item( i ) ) );
+                    final Node node = nodeList.item( i );
+                    if ( node.getNodeType() == Node.ELEMENT_NODE )
+                    {
+                        returnList.add( new XmlElement.XmlElementW3c( ( org.w3c.dom.Element ) node ) );
+                    }
                 }
                 return returnList;
             }

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

@@ -79,10 +79,10 @@ import password.pwm.svc.event.HelpdeskAuditRecord;
 import password.pwm.svc.stats.AvgStatistic;
 import password.pwm.svc.stats.EpsStatistic;
 import password.pwm.svc.stats.Statistic;
-import password.pwm.util.PasswordCharCounter;
+import password.pwm.util.password.PasswordCharCounter;
 import password.pwm.util.PasswordData;
 import password.pwm.util.PostChangePasswordAction;
-import password.pwm.util.PwmPasswordRuleValidator;
+import password.pwm.util.password.PwmPasswordRuleValidator;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.JsonUtil;
 import password.pwm.util.java.TimeDuration;

+ 1 - 1
server/src/main/java/password/pwm/util/PasswordCharCounter.java → server/src/main/java/password/pwm/util/password/PasswordCharCounter.java

@@ -20,7 +20,7 @@
  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
  */
 
-package password.pwm.util;
+package password.pwm.util.password;
 
 public class PasswordCharCounter
 {

+ 781 - 0
server/src/main/java/password/pwm/util/password/PasswordRuleChecks.java

@@ -0,0 +1,781 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2018 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+package password.pwm.util.password;
+
+import lombok.Builder;
+import lombok.Data;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.math.NumberUtils;
+import password.pwm.AppProperty;
+import password.pwm.PwmApplication;
+import password.pwm.PwmConstants;
+import password.pwm.bean.SessionLabel;
+import password.pwm.config.PwmSetting;
+import password.pwm.config.option.ADPolicyComplexity;
+import password.pwm.config.profile.PwmPasswordPolicy;
+import password.pwm.config.profile.PwmPasswordRule;
+import password.pwm.error.ErrorInformation;
+import password.pwm.error.PwmError;
+import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.ldap.UserInfo;
+import password.pwm.svc.PwmService;
+import password.pwm.util.java.JavaHelper;
+import password.pwm.util.java.StringUtil;
+import password.pwm.util.logging.PwmLogger;
+import password.pwm.util.macro.MacroMachine;
+import password.pwm.util.operations.PasswordUtility;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Pattern;
+
+public class PasswordRuleChecks
+{
+    private static final PwmLogger LOGGER = PwmLogger.forClass( PasswordRuleChecks.class );
+
+    private static final boolean EXTRA_LOGGING = false;
+
+    @Data
+    @Builder
+    private static class RuleCheckData
+    {
+        private PwmApplication pwmApplication;
+        private PwmPasswordPolicy policy;
+        private UserInfo userInfo;
+        private PasswordRuleReaderHelper ruleHelper;
+        private PasswordCharCounter charCounter;
+        private MacroMachine macroMachine;
+    }
+
+    private interface RuleChecker
+    {
+        List<ErrorInformation> test(
+                String password,
+                String oldPassword,
+                RuleCheckData ruleCheckData
+        )
+                throws PwmUnrecoverableException;
+    }
+
+    private static final List<RuleChecker> RULE_CHECKS = Collections.unmodifiableList( Arrays.asList(
+            new OldPasswordRuleChecker(),
+            new MinimumLengthRuleChecker(),
+            new MaximumLengthRuleChecker(),
+            new NumericLimitsRuleChecker(),
+            new AlphaLimitsRuleChecker(),
+            new CasingLimitsRuleChecker(),
+            new SpecialLimitsRuleChecker(),
+            new UniqueCharRuleChecker(),
+            new CharSequenceRuleChecker(),
+            new ActiveDirectoryRuleChecker(),
+            new DisallowedValueRuleChecker(),
+            new DisallowedAttributeRuleChecker(),
+            new PasswordStrengthRuleChecker(),
+            new RegexPatternsRuleChecker(),
+            new CharGroupRuleChecker(),
+            new DictionaryRuleChecker(),
+            new SharedHistoryRuleChecker()
+    ) );
+
+
+        public static List<ErrorInformation> extendedPolicyRuleChecker(
+            final PwmApplication pwmApplication,
+            final PwmPasswordPolicy policy,
+            final String password,
+            final String oldPassword,
+            final UserInfo userInfo,
+            final PwmPasswordRuleValidator.Flag... flags
+
+    )
+            throws PwmUnrecoverableException
+    {
+        final boolean failFast = JavaHelper.enumArrayContainsValue( flags, PwmPasswordRuleValidator.Flag.FailFast );
+
+        // null check
+        if ( password == null )
+        {
+            return Collections.singletonList( new ErrorInformation(
+                    PwmError.ERROR_INTERNAL,
+                    "empty (null) new password" ) );
+        }
+
+        final List<ErrorInformation> errorList = new ArrayList<>();
+        final MacroMachine macroMachine = userInfo == null || userInfo.getUserIdentity() == null
+                ? MacroMachine.forNonUserSpecific( pwmApplication, SessionLabel.SYSTEM_LABEL )
+                : MacroMachine.forUser(
+                pwmApplication,
+                PwmConstants.DEFAULT_LOCALE,
+                SessionLabel.SYSTEM_LABEL,
+                userInfo.getUserIdentity()
+        );
+
+        final RuleCheckData ruleCheckData = RuleCheckData.builder()
+                .pwmApplication( pwmApplication )
+                .policy( policy )
+                .userInfo( userInfo )
+                .ruleHelper( policy.getRuleHelper() )
+                .macroMachine( macroMachine )
+                .charCounter( new PasswordCharCounter( password ) )
+                .build();
+
+        for ( final RuleChecker ruleChecker : RULE_CHECKS )
+        {
+            errorList.addAll( ruleChecker.test( password, oldPassword, ruleCheckData ) );
+
+            if ( failFast && !errorList.isEmpty() )
+            {
+                return errorList;
+            }
+        }
+
+        return errorList;
+    }
+
+    private static class OldPasswordRuleChecker implements RuleChecker
+    {
+        @Override
+        public List<ErrorInformation> test( final String password, final String oldPassword, final RuleCheckData ruleCheckData )
+                throws PwmUnrecoverableException
+        {
+            final List<ErrorInformation> errorList = new ArrayList<>();
+            final PasswordRuleReaderHelper ruleHelper = ruleCheckData.getRuleHelper();
+
+            //check against old password
+            if ( !StringUtil.isEmpty( oldPassword ) && ruleHelper.readBooleanValue( PwmPasswordRule.DisallowCurrent ) )
+            {
+                if ( oldPassword.equalsIgnoreCase( password ) )
+                {
+                    errorList.add( new ErrorInformation( PwmError.PASSWORD_SAMEASOLD ) );
+                }
+
+                //check chars from old password
+                final int maxOldAllowed = ruleHelper.readIntValue( PwmPasswordRule.MaximumOldChars );
+                if ( maxOldAllowed > 0 )
+                {
+                    final String lPassword = password.toLowerCase();
+                    final Set<Character> dupeChars = new HashSet<>();
+
+                    //add all dupes to the set.
+                    for ( final char loopChar : oldPassword.toLowerCase().toCharArray() )
+                    {
+                        if ( lPassword.indexOf( loopChar ) != -1 )
+                        {
+                            dupeChars.add( loopChar );
+                        }
+                    }
+
+                    //count the number of (unique) set elements.
+                    if ( dupeChars.size() >= maxOldAllowed )
+                    {
+                        errorList.add( new ErrorInformation( PwmError.PASSWORD_TOO_MANY_OLD_CHARS ) );
+                    }
+                }
+            }
+
+            return Collections.unmodifiableList( errorList );
+        }
+    }
+
+    private static class MinimumLengthRuleChecker implements RuleChecker
+    {
+        @Override
+        public List<ErrorInformation> test( final String password, final String oldPassword, final RuleCheckData ruleCheckData )
+                throws PwmUnrecoverableException
+        {
+            //Check minimum length
+            if ( password.length() < ruleCheckData.getRuleHelper().readIntValue( PwmPasswordRule.MinimumLength ) )
+            {
+                return Collections.singletonList( new ErrorInformation( PwmError.PASSWORD_TOO_SHORT ) );
+            }
+            return Collections.emptyList();
+        }
+    }
+
+    private static class MaximumLengthRuleChecker implements RuleChecker
+    {
+        @Override
+        public List<ErrorInformation> test( final String password, final String oldPasswordString, final RuleCheckData ruleCheckData )
+                throws PwmUnrecoverableException
+        {
+            //Check maximum length
+            {
+                final int passwordMaximumLength = ruleCheckData.getRuleHelper().readIntValue( PwmPasswordRule.MaximumLength );
+
+                if ( passwordMaximumLength > 0 && password.length() > passwordMaximumLength )
+                {
+                    return Collections.singletonList( new ErrorInformation( PwmError.PASSWORD_TOO_LONG ) );
+                }
+            }
+            return Collections.emptyList();
+        }
+    }
+
+    private static class NumericLimitsRuleChecker implements RuleChecker
+    {
+        @Override
+        public List<ErrorInformation> test( final String password, final String oldPassword, final RuleCheckData ruleCheckData )
+                throws PwmUnrecoverableException
+        {
+            //check number of numeric characters
+            final List<ErrorInformation> errorList = new ArrayList<>();
+            final PasswordRuleReaderHelper ruleHelper = ruleCheckData.getRuleHelper();
+            final PasswordCharCounter charCounter = ruleCheckData.getCharCounter();
+            {
+                final int numberOfNumericChars = charCounter.getNumericCharCount();
+                if ( ruleHelper.readBooleanValue( PwmPasswordRule.AllowNumeric ) )
+                {
+                    if ( numberOfNumericChars < ruleHelper.readIntValue( PwmPasswordRule.MinimumNumeric ) )
+                    {
+                        errorList.add( new ErrorInformation( PwmError.PASSWORD_NOT_ENOUGH_NUM ) );
+                    }
+
+                    final int maxNumeric = ruleHelper.readIntValue( PwmPasswordRule.MaximumNumeric );
+                    if ( maxNumeric > 0 && numberOfNumericChars > maxNumeric )
+                    {
+                        errorList.add( new ErrorInformation( PwmError.PASSWORD_TOO_MANY_NUMERIC ) );
+                    }
+
+                    if ( !ruleHelper.readBooleanValue(
+                            PwmPasswordRule.AllowFirstCharNumeric ) && charCounter.isFirstNumeric() )
+                    {
+                        errorList.add( new ErrorInformation( PwmError.PASSWORD_FIRST_IS_NUMERIC ) );
+                    }
+
+                    if ( !ruleHelper.readBooleanValue(
+                            PwmPasswordRule.AllowLastCharNumeric ) && charCounter.isLastNumeric() )
+                    {
+                        errorList.add( new ErrorInformation( PwmError.PASSWORD_LAST_IS_NUMERIC ) );
+                    }
+                }
+                else
+                {
+                    if ( numberOfNumericChars > 0 )
+                    {
+                        errorList.add( new ErrorInformation( PwmError.PASSWORD_TOO_MANY_NUMERIC ) );
+                    }
+                }
+            }
+            return Collections.unmodifiableList( errorList );
+        }
+    }
+
+    private static class CasingLimitsRuleChecker implements RuleChecker
+    {
+        @Override
+        public List<ErrorInformation> test( final String password, final String oldPassword, final RuleCheckData ruleCheckData )
+                throws PwmUnrecoverableException
+        {
+            final List<ErrorInformation> errorList = new ArrayList<>();
+            final PasswordRuleReaderHelper ruleHelper = ruleCheckData.getRuleHelper();
+            final PasswordCharCounter charCounter = ruleCheckData.getCharCounter();
+
+            //check number of upper characters
+            {
+                final int numberOfUpperChars = charCounter.getUpperCharCount();
+                if ( numberOfUpperChars < ruleHelper.readIntValue( PwmPasswordRule.MinimumUpperCase ) )
+                {
+                    errorList.add( new ErrorInformation( PwmError.PASSWORD_NOT_ENOUGH_UPPER ) );
+                }
+
+                final int maxUpper = ruleHelper.readIntValue( PwmPasswordRule.MaximumUpperCase );
+                if ( maxUpper > 0 && numberOfUpperChars > maxUpper )
+                {
+                    errorList.add( new ErrorInformation( PwmError.PASSWORD_TOO_MANY_UPPER ) );
+                }
+            }
+
+            //check number of lower characters
+            {
+                final int numberOfLowerChars = charCounter.getLowerCharCount();
+                if ( numberOfLowerChars < ruleHelper.readIntValue( PwmPasswordRule.MinimumLowerCase ) )
+                {
+                    errorList.add( new ErrorInformation( PwmError.PASSWORD_NOT_ENOUGH_LOWER ) );
+                }
+
+                final int maxLower = ruleHelper.readIntValue( PwmPasswordRule.MaximumLowerCase );
+                if ( maxLower > 0 && numberOfLowerChars > maxLower )
+                {
+                    errorList.add( new ErrorInformation( PwmError.PASSWORD_TOO_MANY_UPPER ) );
+                }
+            }
+            return Collections.unmodifiableList( errorList );
+        }
+    }
+
+    private static class AlphaLimitsRuleChecker implements RuleChecker
+    {
+        @Override
+        public List<ErrorInformation> test( final String password, final String oldPassword, final RuleCheckData ruleCheckData )
+                throws PwmUnrecoverableException
+        {
+            final List<ErrorInformation> errorList = new ArrayList<>();
+            final PasswordRuleReaderHelper ruleHelper = ruleCheckData.getRuleHelper();
+            final PasswordCharCounter charCounter = ruleCheckData.getCharCounter();
+
+            //check number of alpha characters
+            {
+                final int numberOfAlphaChars = charCounter.getAlphaCharCount();
+                if ( numberOfAlphaChars < ruleHelper.readIntValue( PwmPasswordRule.MinimumAlpha ) )
+                {
+                    errorList.add( new ErrorInformation( PwmError.PASSWORD_NOT_ENOUGH_ALPHA ) );
+                }
+
+                final int maxAlpha = ruleHelper.readIntValue( PwmPasswordRule.MaximumAlpha );
+                if ( maxAlpha > 0 && numberOfAlphaChars > maxAlpha )
+                {
+                    errorList.add( new ErrorInformation( PwmError.PASSWORD_TOO_MANY_ALPHA ) );
+                }
+            }
+
+            //check number of non-alpha characters
+            {
+                final int numberOfNonAlphaChars = charCounter.getNonAlphaCharCount();
+
+                if ( ruleHelper.readBooleanValue( PwmPasswordRule.AllowNonAlpha ) )
+                {
+                    if ( numberOfNonAlphaChars < ruleHelper.readIntValue( PwmPasswordRule.MinimumNonAlpha ) )
+                    {
+                        errorList.add( new ErrorInformation( PwmError.PASSWORD_NOT_ENOUGH_NONALPHA ) );
+                    }
+
+                    final int maxNonAlpha = ruleHelper.readIntValue( PwmPasswordRule.MaximumNonAlpha );
+                    if ( maxNonAlpha > 0 && numberOfNonAlphaChars > maxNonAlpha )
+                    {
+                        errorList.add( new ErrorInformation( PwmError.PASSWORD_TOO_MANY_NONALPHA ) );
+                    }
+                }
+                else
+                {
+                    if ( numberOfNonAlphaChars > 0 )
+                    {
+                        errorList.add( new ErrorInformation( PwmError.PASSWORD_TOO_MANY_NONALPHA ) );
+                    }
+                }
+            }
+            return Collections.unmodifiableList( errorList );
+        }
+    }
+
+    private static class SpecialLimitsRuleChecker implements RuleChecker
+    {
+        @Override
+        public List<ErrorInformation> test( final String password, final String oldPassword, final RuleCheckData ruleCheckData )
+                throws PwmUnrecoverableException
+        {
+            final List<ErrorInformation> errorList = new ArrayList<>();
+            final PasswordRuleReaderHelper ruleHelper = ruleCheckData.getRuleHelper();
+            final PasswordCharCounter charCounter = ruleCheckData.getCharCounter();
+
+            //check number of special characters
+            {
+                final int numberOfSpecialChars = charCounter.getSpecialCharsCount();
+                if ( ruleHelper.readBooleanValue( PwmPasswordRule.AllowSpecial ) )
+                {
+                    if ( numberOfSpecialChars < ruleHelper.readIntValue( PwmPasswordRule.MinimumSpecial ) )
+                    {
+                        errorList.add( new ErrorInformation( PwmError.PASSWORD_NOT_ENOUGH_SPECIAL ) );
+                    }
+
+                    final int maxSpecial = ruleHelper.readIntValue( PwmPasswordRule.MaximumSpecial );
+                    if ( maxSpecial > 0 && numberOfSpecialChars > maxSpecial )
+                    {
+                        errorList.add( new ErrorInformation( PwmError.PASSWORD_TOO_MANY_SPECIAL ) );
+                    }
+
+                    if ( !ruleHelper.readBooleanValue(
+                            PwmPasswordRule.AllowFirstCharSpecial ) && charCounter.isFirstSpecial() )
+                    {
+                        errorList.add( new ErrorInformation( PwmError.PASSWORD_FIRST_IS_SPECIAL ) );
+                    }
+
+                    if ( !ruleHelper.readBooleanValue(
+                            PwmPasswordRule.AllowLastCharSpecial ) && charCounter.isLastSpecial() )
+                    {
+                        errorList.add( new ErrorInformation( PwmError.PASSWORD_LAST_IS_SPECIAL ) );
+                    }
+                }
+                else
+                {
+                    if ( numberOfSpecialChars > 0 )
+                    {
+                        errorList.add( new ErrorInformation( PwmError.PASSWORD_TOO_MANY_SPECIAL ) );
+                    }
+                }
+            }
+
+            return Collections.unmodifiableList( errorList );
+        }
+    }
+
+    private static class CharSequenceRuleChecker implements RuleChecker
+    {
+        @Override
+        public List<ErrorInformation> test( final String password, final String oldPassword, final RuleCheckData ruleCheckData )
+                throws PwmUnrecoverableException
+        {
+            final List<ErrorInformation> errorList = new ArrayList<>();
+            final PasswordRuleReaderHelper ruleHelper = ruleCheckData.getRuleHelper();
+            final PasswordCharCounter charCounter = ruleCheckData.getCharCounter();
+
+            //Check maximum character repeats (sequential)
+            {
+                final int maxSequentialRepeat = ruleHelper.readIntValue( PwmPasswordRule.MaximumSequentialRepeat );
+                if ( maxSequentialRepeat > 0 && charCounter.getSequentialRepeatedChars() > maxSequentialRepeat )
+                {
+                    errorList.add( new ErrorInformation( PwmError.PASSWORD_TOO_MANY_REPEAT ) );
+                }
+
+                //Check maximum character repeats (overall)
+                final int maxRepeat = ruleHelper.readIntValue( PwmPasswordRule.MaximumRepeat );
+                if ( maxRepeat > 0 && charCounter.getRepeatedChars() > maxRepeat )
+                {
+                    errorList.add( new ErrorInformation( PwmError.PASSWORD_TOO_MANY_REPEAT ) );
+                }
+            }
+
+            // check consecutive characters
+            {
+                final int maximumConsecutive = ruleHelper.readIntValue( PwmPasswordRule.MaximumConsecutive );
+                if ( PwmPasswordRuleUtil.tooManyConsecutiveChars( password, maximumConsecutive ) )
+                {
+                    errorList.add( new ErrorInformation( PwmError.PASSWORD_TOO_MANY_CONSECUTIVE ) );
+                }
+            }
+
+            return Collections.unmodifiableList( errorList );
+        }
+    }
+
+    private static class UniqueCharRuleChecker implements RuleChecker
+    {
+        @Override
+        public List<ErrorInformation> test( final String password, final String oldPassword, final RuleCheckData ruleCheckData )
+                throws PwmUnrecoverableException
+        {
+            final List<ErrorInformation> errorList = new ArrayList<>();
+            final PasswordRuleReaderHelper ruleHelper = ruleCheckData.getRuleHelper();
+            final PasswordCharCounter charCounter = ruleCheckData.getCharCounter();
+
+            //Check minimum unique character
+            {
+                final int minUnique = ruleHelper.readIntValue( PwmPasswordRule.MinimumUnique );
+                if ( minUnique > 0 && charCounter.getUniqueChars() < minUnique )
+                {
+                    errorList.add( new ErrorInformation( PwmError.PASSWORD_NOT_ENOUGH_UNIQUE ) );
+                }
+            }
+
+            return Collections.unmodifiableList( errorList );
+        }
+    }
+
+    private static class ActiveDirectoryRuleChecker implements RuleChecker
+    {
+        @Override
+        public List<ErrorInformation> test( final String password, final String oldPassword, final RuleCheckData ruleCheckData )
+                throws PwmUnrecoverableException
+        {
+            final List<ErrorInformation> errorList = new ArrayList<>();
+            final PasswordRuleReaderHelper ruleHelper = ruleCheckData.getRuleHelper();
+            final PasswordCharCounter charCounter = ruleCheckData.getCharCounter();
+
+            // check ad-complexity
+            {
+                final ADPolicyComplexity complexityLevel = ruleHelper.getADComplexityLevel();
+                if ( complexityLevel == ADPolicyComplexity.AD2003 || complexityLevel == ADPolicyComplexity.AD2008 )
+                {
+                    final int maxGroupViolations = ruleHelper.readIntValue( PwmPasswordRule.ADComplexityMaxViolations );
+                    errorList.addAll( PwmPasswordRuleUtil.checkPasswordForADComplexity(
+                            complexityLevel,
+                            ruleCheckData.getUserInfo(),
+                            password,
+                            charCounter,
+                            maxGroupViolations ) );
+                }
+            }
+
+            return Collections.unmodifiableList( errorList );
+        }
+    }
+
+    private static class DisallowedValueRuleChecker implements RuleChecker
+    {
+        @Override
+        public List<ErrorInformation> test( final String password, final String oldPassword, final RuleCheckData ruleCheckData )
+                throws PwmUnrecoverableException
+        {
+            final List<ErrorInformation> errorList = new ArrayList<>();
+            final PasswordRuleReaderHelper ruleHelper = ruleCheckData.getRuleHelper();
+
+            // check against disallowed values;
+            if ( !ruleHelper.getDisallowedValues().isEmpty() )
+            {
+                final String lcasePwd = password.toLowerCase();
+                final Set<String> paramValues = new HashSet<>( ruleHelper.getDisallowedValues() );
+
+                for ( final String loopValue : paramValues )
+                {
+                    if ( loopValue != null && loopValue.length() > 0 )
+                    {
+                        final MacroMachine macroMachine = ruleCheckData.getMacroMachine();
+                        final String expandedValue = macroMachine.expandMacros( loopValue );
+                        if ( StringUtils.isNotBlank( expandedValue ) )
+                        {
+                            final String loweredLoop = expandedValue.toLowerCase();
+                            if ( lcasePwd.contains( loweredLoop ) )
+                            {
+                                errorList.add( new ErrorInformation( PwmError.PASSWORD_USING_DISALLOWED ) );
+                            }
+                        }
+                    }
+                }
+            }
+
+            return Collections.unmodifiableList( errorList );
+        }
+    }
+
+    private static class DisallowedAttributeRuleChecker implements RuleChecker
+    {
+        @Override
+        public List<ErrorInformation> test( final String password, final String oldPassword, final RuleCheckData ruleCheckData )
+                throws PwmUnrecoverableException
+        {
+            final List<ErrorInformation> errorList = new ArrayList<>();
+            final UserInfo userInfo = ruleCheckData.getUserInfo();
+            final PwmPasswordPolicy policy = ruleCheckData.getPolicy();
+
+            // check disallowed attributes.
+            if ( !policy.getRuleHelper().getDisallowedAttributes().isEmpty() )
+            {
+                final List<String> paramConfigs = policy.getRuleHelper().getDisallowedAttributes( PasswordRuleReaderHelper.Flag.KeepThresholds );
+                if ( userInfo != null )
+                {
+                    final Map<String, String> userValues = userInfo.getCachedPasswordRuleAttributes();
+
+                    for ( final String paramConfig : paramConfigs )
+                    {
+                        final String[] parts = paramConfig.split( ":" );
+
+                        final String attrName = parts[ 0 ];
+                        final String disallowedValue = StringUtils.defaultString( userValues.get( attrName ) );
+                        final int threshold = parts.length > 1 ? NumberUtils.toInt( parts[ 1 ] ) : 0;
+
+                        if ( PwmPasswordRuleUtil.containsDisallowedValue( password, disallowedValue, threshold ) )
+                        {
+                            LOGGER.trace( () -> "password rejected, same as user attr " + attrName );
+                            errorList.add( new ErrorInformation( PwmError.PASSWORD_SAMEASATTR ) );
+                        }
+                    }
+                }
+            }
+
+            return Collections.unmodifiableList( errorList );
+        }
+    }
+
+    private static class PasswordStrengthRuleChecker implements RuleChecker
+    {
+        @Override
+        public List<ErrorInformation> test( final String password, final String oldPassword, final RuleCheckData ruleCheckData )
+                throws PwmUnrecoverableException
+        {
+            final List<ErrorInformation> errorList = new ArrayList<>();
+            final PwmApplication pwmApplication = ruleCheckData.getPwmApplication();
+
+            // check password strength
+            final int requiredPasswordStrength = ruleCheckData.getRuleHelper().readIntValue( PwmPasswordRule.MinimumStrength );
+            if ( requiredPasswordStrength > 0 )
+            {
+                if ( pwmApplication != null )
+                {
+                    final int passwordStrength = PasswordUtility.judgePasswordStrength(
+                            pwmApplication.getConfig(),
+                            password
+                    );
+                    if ( passwordStrength < requiredPasswordStrength )
+                    {
+                        errorList.add( new ErrorInformation( PwmError.PASSWORD_TOO_WEAK ) );
+                        if ( EXTRA_LOGGING )
+                        {
+                            LOGGER.trace( () -> "password rejected, password strength of "
+                                    + passwordStrength + " is lower than policy requirement of "
+                                    + requiredPasswordStrength );
+                        }
+                    }
+                }
+            }
+
+            return Collections.unmodifiableList( errorList );
+        }
+    }
+
+    private static class RegexPatternsRuleChecker implements RuleChecker
+    {
+        @Override
+        public List<ErrorInformation> test( final String password, final String oldPassword, final RuleCheckData ruleCheckData )
+                throws PwmUnrecoverableException
+        {
+            final List<ErrorInformation> errorList = new ArrayList<>();
+            final MacroMachine macroMachine = ruleCheckData.getMacroMachine();
+            final PasswordRuleReaderHelper ruleHelper = ruleCheckData.getRuleHelper();
+
+            // check regex matches.
+            for ( final Pattern pattern : ruleHelper.getRegExMatch( macroMachine ) )
+            {
+                if ( !pattern.matcher( password ).matches() )
+                {
+                    errorList.add( new ErrorInformation( PwmError.PASSWORD_INVALID_CHAR ) );
+                    if ( EXTRA_LOGGING )
+                    {
+                        LOGGER.trace( () -> "password rejected, does not match configured regex pattern: " + pattern.toString() );
+                    }
+                }
+            }
+
+            // check no-regex matches.
+            for ( final Pattern pattern : ruleHelper.getRegExNoMatch( macroMachine ) )
+            {
+                if ( pattern.matcher( password ).matches() )
+                {
+                    errorList.add( new ErrorInformation( PwmError.PASSWORD_INVALID_CHAR ) );
+                    if ( EXTRA_LOGGING )
+                    {
+                        LOGGER.trace( () -> "password rejected, matches configured no-regex pattern: " + pattern.toString() );
+                    }
+                }
+            }
+
+            return Collections.unmodifiableList( errorList );
+        }
+    }
+
+    private static class CharGroupRuleChecker implements RuleChecker
+    {
+        @Override
+        public List<ErrorInformation> test( final String password, final String oldPassword, final RuleCheckData ruleCheckData )
+                throws PwmUnrecoverableException
+        {
+            final List<ErrorInformation> errorList = new ArrayList<>();
+            final PasswordRuleReaderHelper ruleHelper = ruleCheckData.getRuleHelper();
+
+            // check char group matches
+            if ( ruleHelper.readIntValue( PwmPasswordRule.CharGroupsMinMatch ) > 0 )
+            {
+                final List<Pattern> ruleGroups = ruleHelper.getCharGroupValues();
+                if ( ruleGroups != null && !ruleGroups.isEmpty() )
+                {
+                    final int requiredMatches = ruleHelper.readIntValue( PwmPasswordRule.CharGroupsMinMatch );
+                    int matches = 0;
+                    for ( final Pattern pattern : ruleGroups )
+                    {
+                        if ( pattern.matcher( password ).find() )
+                        {
+                            matches++;
+                        }
+                    }
+                    if ( matches < requiredMatches )
+                    {
+                        errorList.add( new ErrorInformation( PwmError.PASSWORD_NOT_ENOUGH_GROUPS ) );
+                    }
+                }
+            }
+
+            return Collections.unmodifiableList( errorList );
+        }
+    }
+
+    private static class DictionaryRuleChecker implements RuleChecker
+    {
+        @Override
+        public List<ErrorInformation> test( final String password, final String oldPassword, final RuleCheckData ruleCheckData )
+                throws PwmUnrecoverableException
+        {
+            final List<ErrorInformation> errorList = new ArrayList<>();
+            final PwmApplication pwmApplication = ruleCheckData.getPwmApplication();
+            final PasswordRuleReaderHelper ruleHelper = ruleCheckData.getRuleHelper();
+
+            // check if the password is in the dictionary.
+            if ( ruleHelper.readBooleanValue( PwmPasswordRule.EnableWordlist ) )
+            {
+                if ( pwmApplication != null )
+                {
+                    if ( pwmApplication.getWordlistManager() != null && pwmApplication.getWordlistManager().status() == PwmService.STATUS.OPEN )
+                    {
+                        final boolean found = pwmApplication.getWordlistManager().containsWord( password );
+
+                        if ( found )
+                        {
+                            //LOGGER.trace(pwmSession, "password rejected, in wordlist file");
+                            errorList.add( new ErrorInformation( PwmError.PASSWORD_INWORDLIST ) );
+                        }
+                    }
+                    else
+                    {
+                        final boolean failWhenClosed = Boolean.parseBoolean( pwmApplication.getConfig().readAppProperty( AppProperty.PASSWORD_RULE_WORDLIST_FAIL_WHEN_CLOSED ) );
+                        if ( failWhenClosed )
+                        {
+                            throw PwmUnrecoverableException.newException( PwmError.ERROR_SERVICE_NOT_AVAILABLE, "wordlist service is not available" );
+                        }
+                    }
+                }
+            }
+
+            return Collections.unmodifiableList( errorList );
+        }
+    }
+
+    private static class SharedHistoryRuleChecker implements RuleChecker
+    {
+        @Override
+        public List<ErrorInformation> test( final String password, final String oldPassword, final RuleCheckData ruleCheckData )
+                throws PwmUnrecoverableException
+        {
+            final List<ErrorInformation> errorList = new ArrayList<>();
+            final PwmApplication pwmApplication = ruleCheckData.getPwmApplication();
+
+            // check for shared (global) password history
+            if ( pwmApplication != null )
+            {
+                if ( pwmApplication.getConfig().readSettingAsBoolean( PwmSetting.PASSWORD_SHAREDHISTORY_ENABLE )
+                        && pwmApplication.getSharedHistoryManager().status() == PwmService.STATUS.OPEN )
+                {
+                    final boolean found = pwmApplication.getSharedHistoryManager().containsWord( password );
+
+                    if ( found )
+                    {
+                        //LOGGER.trace(pwmSession, "password rejected, in global shared history");
+                        errorList.add( new ErrorInformation( PwmError.PASSWORD_INWORDLIST ) );
+                    }
+                }
+            }
+
+            return Collections.unmodifiableList( errorList );
+        }
+    }
+}

+ 202 - 0
server/src/main/java/password/pwm/util/password/PasswordRuleReaderHelper.java

@@ -0,0 +1,202 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2018 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+package password.pwm.util.password;
+
+import com.novell.ldapchai.ChaiPasswordRule;
+import com.novell.ldapchai.util.DefaultChaiPasswordPolicy;
+import com.novell.ldapchai.util.StringHelper;
+import password.pwm.config.option.ADPolicyComplexity;
+import password.pwm.config.profile.PwmPasswordPolicy;
+import password.pwm.config.profile.PwmPasswordRule;
+import password.pwm.util.java.JavaHelper;
+import password.pwm.util.logging.PwmLogger;
+import password.pwm.util.macro.MacroMachine;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.regex.Pattern;
+import java.util.regex.PatternSyntaxException;
+
+public class PasswordRuleReaderHelper
+{
+    private static final PwmLogger LOGGER = PwmLogger.forClass( PasswordRuleReaderHelper.class );
+
+    public enum Flag
+    {
+        KeepThresholds
+    }
+
+    private final PwmPasswordPolicy passwordPolicy;
+    private final com.novell.ldapchai.util.PasswordRuleHelper chaiRuleHelper;
+
+    public PasswordRuleReaderHelper( final PwmPasswordPolicy passwordPolicy )
+    {
+        this.passwordPolicy = passwordPolicy;
+        chaiRuleHelper = DefaultChaiPasswordPolicy.createDefaultChaiPasswordPolicy( passwordPolicy.getPolicyMap() ).getRuleHelper();
+    }
+
+    public List<String> getDisallowedValues( )
+    {
+        return chaiRuleHelper.getDisallowedValues();
+    }
+
+    public List<String> getDisallowedAttributes( final Flag... flags )
+    {
+        final List<String> disallowedAttributes = chaiRuleHelper.getDisallowedAttributes();
+
+        if ( JavaHelper.enumArrayContainsValue( flags, Flag.KeepThresholds ) )
+        {
+            return disallowedAttributes;
+        }
+        else
+        {
+            // Strip off any thresholds from attribute (specified as: "attributeName:N", where N is a numeric value).
+            final List<String> strippedDisallowedAttributes = new ArrayList<String>();
+
+            if ( disallowedAttributes != null )
+            {
+                for ( final String disallowedAttribute : disallowedAttributes )
+                {
+                    if ( disallowedAttribute != null )
+                    {
+                        final int indexOfColon = disallowedAttribute.indexOf( ':' );
+                        if ( indexOfColon > 0 )
+                        {
+                            strippedDisallowedAttributes.add( disallowedAttribute.substring( 0, indexOfColon ) );
+                        }
+                        else
+                        {
+                            strippedDisallowedAttributes.add( disallowedAttribute );
+                        }
+                    }
+                }
+            }
+
+            return strippedDisallowedAttributes;
+        }
+    }
+
+    public List<Pattern> getRegExMatch( final MacroMachine macroMachine )
+    {
+        return readRegExSetting( PwmPasswordRule.RegExMatch, macroMachine );
+    }
+
+    public List<Pattern> getRegExNoMatch( final MacroMachine macroMachine )
+    {
+        return readRegExSetting( PwmPasswordRule.RegExNoMatch, macroMachine );
+    }
+
+    public List<Pattern> getCharGroupValues( )
+    {
+        return readRegExSetting( PwmPasswordRule.CharGroupsValues, null );
+    }
+
+
+    public int readIntValue( final PwmPasswordRule rule )
+    {
+        if (
+                ( rule.getRuleType() != ChaiPasswordRule.RuleType.MIN )
+                        && ( rule.getRuleType() != ChaiPasswordRule.RuleType.MAX )
+                        && ( rule.getRuleType() != ChaiPasswordRule.RuleType.NUMERIC )
+                )
+        {
+            throw new IllegalArgumentException( "attempt to read non-numeric rule value as int for rule " + rule );
+        }
+
+        final String value = passwordPolicy.getPolicyMap().get( rule.getKey() );
+        final int defaultValue = StringHelper.convertStrToInt( rule.getDefaultValue(), 0 );
+        return StringHelper.convertStrToInt( value, defaultValue );
+    }
+
+    public boolean readBooleanValue( final PwmPasswordRule rule )
+    {
+        if ( rule.getRuleType() != ChaiPasswordRule.RuleType.BOOLEAN )
+        {
+            throw new IllegalArgumentException( "attempt to read non-boolean rule value as boolean for rule " + rule );
+        }
+
+        final String value = passwordPolicy.getPolicyMap().get( rule.getKey() );
+        return StringHelper.convertStrToBoolean( value );
+    }
+
+    private List<Pattern> readRegExSetting( final PwmPasswordRule rule, final MacroMachine macroMachine )
+    {
+        final String input = passwordPolicy.getPolicyMap().get( rule.getKey() );
+
+        return readRegExSetting( rule, macroMachine, input );
+    }
+
+    public List<Pattern> readRegExSetting( final PwmPasswordRule rule, final MacroMachine macroMachine, final String input )
+    {
+        if ( input == null )
+        {
+            return Collections.emptyList();
+        }
+
+        final String separator = ( rule == PwmPasswordRule.RegExMatch || rule == PwmPasswordRule.RegExNoMatch ) ? ";;;" : "\n";
+        final List<String> values = new ArrayList<>( StringHelper.tokenizeString( input, separator ) );
+        final List<Pattern> patterns = new ArrayList<>();
+
+        for ( final String value : values )
+        {
+            if ( value != null && value.length() > 0 )
+            {
+                String valueToCompile = value;
+
+                if ( macroMachine != null && readBooleanValue( PwmPasswordRule.AllowMacroInRegExSetting ) )
+                {
+                    valueToCompile = macroMachine.expandMacros( value );
+                }
+
+                try
+                {
+                    final Pattern loopPattern = Pattern.compile( valueToCompile );
+                    patterns.add( loopPattern );
+                }
+                catch ( PatternSyntaxException e )
+                {
+                    LOGGER.warn( "reading password rule value '" + valueToCompile + "' for rule " + rule.getKey() + " is not a valid regular expression " + e.getMessage() );
+                }
+            }
+        }
+
+        return patterns;
+    }
+
+    public String getChangeMessage( )
+    {
+        final String changeMessage = passwordPolicy.getValue( PwmPasswordRule.ChangeMessage );
+        return changeMessage == null ? "" : changeMessage;
+    }
+
+    public ADPolicyComplexity getADComplexityLevel( )
+    {
+        final String strLevel = passwordPolicy.getValue( PwmPasswordRule.ADComplexityLevel );
+        if ( strLevel == null || strLevel.isEmpty() )
+        {
+            return ADPolicyComplexity.NONE;
+        }
+        return ADPolicyComplexity.valueOf( strLevel );
+    }
+}

+ 292 - 0
server/src/main/java/password/pwm/util/password/PwmPasswordRuleUtil.java

@@ -0,0 +1,292 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2018 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+package password.pwm.util.password;
+
+import org.apache.commons.lang3.StringUtils;
+import password.pwm.config.option.ADPolicyComplexity;
+import password.pwm.error.ErrorInformation;
+import password.pwm.error.PwmError;
+import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.ldap.UserInfo;
+import password.pwm.util.java.JavaHelper;
+import password.pwm.util.java.StringUtil;
+import password.pwm.util.logging.PwmLogger;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+public class PwmPasswordRuleUtil
+{
+    private static final PwmLogger LOGGER = PwmLogger.forClass( PwmPasswordRuleUtil.class );
+
+    private PwmPasswordRuleUtil()
+    {
+    }
+
+    /**
+     * Check a supplied password for it's validity according to AD complexity rules.
+     * - Not contain the user's account name or parts of the user's full name that exceed two consecutive characters
+     * - Be at least six characters in length
+     * - Contain characters from three of the following five categories:
+     * - English uppercase characters (A through Z)
+     * - English lowercase characters (a through z)
+     * - Base 10 digits (0 through 9)
+     * - Non-alphabetic characters (for example, !, $, #, %)
+     * - Any character categorized as an alphabetic but is not uppercase or lowercase.
+     * <p/>
+     * See this article: http://technet.microsoft.com/en-us/library/cc786468%28WS.10%29.aspx
+     *
+     * @param userInfo    userInfoBean
+     * @param password    password to test
+     * @param charCounter associated charCounter for the password.
+     * @return list of errors if the password does not meet requirements, or an empty list if the password complies
+     *         with AD requirements
+     */
+
+    static List<ErrorInformation> checkPasswordForADComplexity(
+            final ADPolicyComplexity complexityLevel,
+            final UserInfo userInfo,
+            final String password,
+            final PasswordCharCounter charCounter,
+            final int maxGroupViolationCount
+    )
+            throws PwmUnrecoverableException
+    {
+        final List<ErrorInformation> errorList = new ArrayList<>();
+
+        if ( password == null || password.length() < 6 )
+        {
+            errorList.add( new ErrorInformation( PwmError.PASSWORD_TOO_SHORT ) );
+            return errorList;
+        }
+
+        final int maxLength = complexityLevel == ADPolicyComplexity.AD2003 ? 128 : 512;
+        if ( password.length() > maxLength )
+        {
+            errorList.add( new ErrorInformation( PwmError.PASSWORD_TOO_LONG ) );
+            return errorList;
+        }
+
+        if ( userInfo != null && userInfo.getCachedPasswordRuleAttributes() != null )
+        {
+            final Map<String, String> userAttrs = userInfo.getCachedPasswordRuleAttributes();
+            final String samAccountName = userAttrs.get( "sAMAccountName" );
+            if ( samAccountName != null
+                    && samAccountName.length() > 2
+                    && samAccountName.length() >= password.length() )
+            {
+                if ( password.toLowerCase().contains( samAccountName.toLowerCase() ) )
+                {
+                    errorList.add( new ErrorInformation( PwmError.PASSWORD_INWORDLIST ) );
+                    LOGGER.trace( () -> "Password violation due to ADComplexity check: Password contains sAMAccountName" );
+                }
+            }
+            final String displayName = userAttrs.get( "displayName" );
+            if ( displayName != null && displayName.length() > 2 )
+            {
+                if ( checkContainsTokens( password, displayName ) )
+                {
+                    errorList.add( new ErrorInformation( PwmError.PASSWORD_INWORDLIST ) );
+                    LOGGER.trace( () -> "Password violation due to ADComplexity check: Tokens from displayName used in password" );
+                }
+            }
+        }
+
+        int complexityPoints = 0;
+        if ( charCounter.getUpperCharCount() > 0 )
+        {
+            complexityPoints++;
+        }
+        if ( charCounter.getLowerCharCount() > 0 )
+        {
+            complexityPoints++;
+        }
+        if ( charCounter.getNumericCharCount() > 0 )
+        {
+            complexityPoints++;
+        }
+        switch ( complexityLevel )
+        {
+            case AD2003:
+                if ( charCounter.getSpecialCharsCount() > 0 || charCounter.getOtherLetterCharCount() > 0 )
+                {
+                    complexityPoints++;
+                }
+                break;
+
+            case AD2008:
+                if ( charCounter.getSpecialCharsCount() > 0 )
+                {
+                    complexityPoints++;
+                }
+                if ( charCounter.getOtherLetterCharCount() > 0 )
+                {
+                    complexityPoints++;
+                }
+                break;
+
+            default:
+                JavaHelper.unhandledSwitchStatement( complexityLevel );
+        }
+
+        switch ( complexityLevel )
+        {
+            case AD2008:
+                final int totalGroups = 5;
+                final int violations = totalGroups - complexityPoints;
+                if ( violations <= maxGroupViolationCount )
+                {
+                    return errorList;
+                }
+                break;
+
+            case AD2003:
+                if ( complexityPoints >= 3 )
+                {
+                    return errorList;
+                }
+                break;
+
+            default:
+                JavaHelper.unhandledSwitchStatement( complexityLevel );
+        }
+
+        if ( charCounter.getUpperCharCount() < 1 )
+        {
+            errorList.add( new ErrorInformation( PwmError.PASSWORD_NOT_ENOUGH_UPPER ) );
+        }
+        if ( charCounter.getLowerCharCount() < 1 )
+        {
+            errorList.add( new ErrorInformation( PwmError.PASSWORD_NOT_ENOUGH_LOWER ) );
+        }
+        if ( charCounter.getNumericCharCount() < 1 )
+        {
+            errorList.add( new ErrorInformation( PwmError.PASSWORD_NOT_ENOUGH_NUM ) );
+        }
+        if ( charCounter.getSpecialCharsCount() < 1 )
+        {
+            errorList.add( new ErrorInformation( PwmError.PASSWORD_NOT_ENOUGH_SPECIAL ) );
+        }
+        if ( charCounter.getOtherLetterCharCount() < 1 )
+        {
+            errorList.add( new ErrorInformation( PwmError.PASSWORD_UNKNOWN_VALIDATION ) );
+        }
+
+        return errorList;
+    }
+
+    // escape characters permitted because they match the exact AD specification
+    @SuppressWarnings( "checkstyle:avoidescapedunicodecharacters" )
+    private static boolean checkContainsTokens( final String baseValue, final String checkPattern )
+    {
+        if ( baseValue == null || baseValue.length() == 0 )
+        {
+            return false;
+        }
+
+        if ( checkPattern == null || checkPattern.length() == 0 )
+        {
+            return false;
+        }
+
+        final String baseValueLower = baseValue.toLowerCase();
+
+        final String[] tokens = checkPattern.toLowerCase().split( "[,\\.\\-\u2013\u2014_ \u00a3\\t]+" );
+
+        if ( tokens != null && tokens.length > 0 )
+        {
+            for ( final String token : tokens )
+            {
+                if ( token.length() > 2 )
+                {
+                    if ( baseValueLower.contains( token ) )
+                    {
+                        return true;
+                    }
+                }
+            }
+        }
+        return false;
+    }
+
+    public static boolean tooManyConsecutiveChars( final String str, final int maximumConsecutive )
+    {
+        if ( str != null && maximumConsecutive > 1 && str.length() >= maximumConsecutive )
+        {
+            final int[] codePoints = StringUtil.toCodePointArray( str.toLowerCase() );
+
+            int lastCodePoint = -1;
+            int consecutiveCharCount = 1;
+
+            for ( int i = 0; i < codePoints.length; i++ )
+            {
+                if ( codePoints[ i ] == lastCodePoint + 1 )
+                {
+                    consecutiveCharCount++;
+                }
+                else
+                {
+                    consecutiveCharCount = 1;
+                }
+
+                lastCodePoint = codePoints[ i ];
+
+                if ( consecutiveCharCount == maximumConsecutive )
+                {
+                    return true;
+                }
+            }
+        }
+
+        return false;
+    }
+
+    public static boolean containsDisallowedValue( final String password, final String disallowedValue, final int threshold )
+    {
+        if ( !StringUtil.isEmpty( disallowedValue ) )
+        {
+            if ( threshold > 0 )
+            {
+                if ( disallowedValue.length() >= threshold )
+                {
+                    final String[] disallowedValueChunks = StringUtil.createStringChunks( disallowedValue, threshold );
+                    for ( final String chunk : disallowedValueChunks )
+                    {
+                        if ( StringUtils.containsIgnoreCase( password, chunk ) )
+                        {
+                            return true;
+                        }
+                    }
+                }
+            }
+            else
+            {
+                // No threshold?  Then the password can't contain the whole disallowed value
+                return StringUtils.containsIgnoreCase( password, disallowedValue );
+            }
+        }
+
+        return false;
+    }
+}

+ 291 - 0
server/src/main/java/password/pwm/util/password/PwmPasswordRuleValidator.java

@@ -0,0 +1,291 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2018 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+package password.pwm.util.password;
+
+import com.google.gson.reflect.TypeToken;
+import com.novell.ldapchai.ChaiUser;
+import com.novell.ldapchai.exception.ChaiError;
+import com.novell.ldapchai.exception.ChaiPasswordPolicyException;
+import com.novell.ldapchai.exception.ChaiUnavailableException;
+import password.pwm.AppProperty;
+import password.pwm.PwmApplication;
+import password.pwm.PwmConstants;
+import password.pwm.bean.SessionLabel;
+import password.pwm.bean.pub.PublicUserInfoBean;
+import password.pwm.config.Configuration;
+import password.pwm.config.PwmSetting;
+import password.pwm.config.profile.PwmPasswordPolicy;
+import password.pwm.config.profile.PwmPasswordRule;
+import password.pwm.error.ErrorInformation;
+import password.pwm.error.PwmDataValidationException;
+import password.pwm.error.PwmError;
+import password.pwm.error.PwmOperationalException;
+import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.ldap.UserInfo;
+import password.pwm.svc.stats.Statistic;
+import password.pwm.util.PasswordData;
+import password.pwm.util.java.JavaHelper;
+import password.pwm.util.java.JsonUtil;
+import password.pwm.util.logging.PwmLogger;
+import password.pwm.util.macro.MacroMachine;
+import password.pwm.ws.client.rest.RestClientHelper;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+public class PwmPasswordRuleValidator
+{
+
+    private static final PwmLogger LOGGER = PwmLogger.forClass( PwmPasswordRuleValidator.class );
+
+    private final PwmApplication pwmApplication;
+    private final PwmPasswordPolicy policy;
+    private final Locale locale;
+    private final Flag[] flags;
+
+
+    public enum Flag
+    {
+        FailFast,
+        BypassLdapRuleCheck,
+    }
+
+    public PwmPasswordRuleValidator(
+            final PwmApplication pwmApplication,
+            final PwmPasswordPolicy policy,
+            final Flag... flags
+    )
+    {
+        this.pwmApplication = pwmApplication;
+        this.policy = policy;
+        this.locale = PwmConstants.DEFAULT_LOCALE;
+        this.flags = flags;
+    }
+
+    public PwmPasswordRuleValidator(
+            final PwmApplication pwmApplication,
+            final PwmPasswordPolicy policy,
+            final Locale locale,
+            final Flag... flags
+    )
+    {
+        this.pwmApplication = pwmApplication;
+        this.policy = policy;
+        this.locale = locale;
+        this.flags = flags;
+    }
+
+    public boolean testPassword(
+            final PasswordData password,
+            final PasswordData oldPassword,
+            final UserInfo userInfo,
+            final ChaiUser user
+    )
+            throws PwmDataValidationException, ChaiUnavailableException, PwmUnrecoverableException
+    {
+        final List<ErrorInformation> errorResults = validate( password, oldPassword, userInfo );
+
+        if ( !errorResults.isEmpty() )
+        {
+            throw new PwmDataValidationException( errorResults.iterator().next() );
+        }
+
+        if ( user != null && !JavaHelper.enumArrayContainsValue( flags, Flag.BypassLdapRuleCheck ) )
+        {
+            try
+            {
+                LOGGER.trace( () -> "calling chai directory password validation checker" );
+                user.testPasswordPolicy( password.getStringValue() );
+            }
+            catch ( UnsupportedOperationException e )
+            {
+                LOGGER.trace( () -> "Unsupported operation was thrown while validating password: " + e.toString() );
+            }
+            catch ( ChaiUnavailableException e )
+            {
+                pwmApplication.getStatisticsManager().incrementValue( Statistic.LDAP_UNAVAILABLE_COUNT );
+                LOGGER.warn( "ChaiUnavailableException was thrown while validating password: " + e.toString() );
+                throw e;
+            }
+            catch ( ChaiPasswordPolicyException e )
+            {
+                final ChaiError passwordError = e.getErrorCode();
+                final PwmError pwmError = PwmError.forChaiError( passwordError );
+                final ErrorInformation info = new ErrorInformation( pwmError == null ? PwmError.PASSWORD_UNKNOWN_VALIDATION : pwmError );
+                LOGGER.trace( () -> "ChaiPasswordPolicyException was thrown while validating password: " + e.toString() );
+                errorResults.add( info );
+            }
+        }
+
+        if ( !errorResults.isEmpty() )
+        {
+            throw new PwmDataValidationException( errorResults.iterator().next() );
+        }
+
+        return true;
+    }
+
+
+    /**
+     * Validates a password against the configured rules of PWM.  No directory operations
+     * are performed here.
+     *
+     * @param password desired new password
+     * @return true if the password is okay, never returns false.
+     */
+    private List<ErrorInformation> validate(
+            final PasswordData password,
+            final PasswordData oldPassword,
+            final UserInfo userInfo
+    )
+            throws PwmUnrecoverableException
+    {
+        final List<ErrorInformation> internalResults = internalPwmPolicyValidator( password, oldPassword, userInfo );
+        if ( pwmApplication != null )
+        {
+            final List<ErrorInformation> externalResults = invokeExternalRuleMethods(
+                    pwmApplication.getConfig(),
+                    policy,
+                    password,
+                    userInfo
+            );
+            internalResults.addAll( externalResults );
+        }
+        return internalResults;
+    }
+
+    public List<ErrorInformation> internalPwmPolicyValidator(
+            final PasswordData password,
+            final PasswordData oldPassword,
+            final UserInfo userInfo
+    )
+            throws PwmUnrecoverableException
+    {
+        final String passwordString = password == null ? "" : password.getStringValue();
+        final String oldPasswordString = oldPassword == null ? null : oldPassword.getStringValue();
+        return PasswordRuleChecks.extendedPolicyRuleChecker( pwmApplication, policy, passwordString, oldPasswordString, userInfo, flags );
+    }
+
+    public List<ErrorInformation> internalPwmPolicyValidator(
+            final String password,
+            final String oldPassword,
+            final UserInfo userInfo
+    )
+            throws PwmUnrecoverableException
+    {
+        return PasswordRuleChecks.extendedPolicyRuleChecker( pwmApplication, policy, password, oldPassword, userInfo, flags );
+    }
+
+
+    private static final String REST_RESPONSE_KEY_ERROR = "error";
+    private static final String REST_RESPONSE_KEY_ERROR_MSG = "errorMessage";
+
+    public List<ErrorInformation> invokeExternalRuleMethods(
+            final Configuration config,
+            final PwmPasswordPolicy pwmPasswordPolicy,
+            final PasswordData password,
+            final UserInfo userInfo
+    )
+            throws PwmUnrecoverableException
+    {
+        final List<ErrorInformation> returnedErrors = new ArrayList<>();
+        final String restURL = config.readSettingAsString( PwmSetting.EXTERNAL_PWCHECK_REST_URLS );
+        final boolean haltOnError = Boolean.parseBoolean( config.readAppProperty( AppProperty.WS_REST_CLIENT_PWRULE_HALTONERROR ) );
+        final Map<String, Object> sendData = new LinkedHashMap<>();
+
+
+        if ( restURL == null || restURL.isEmpty() )
+        {
+            return Collections.emptyList();
+        }
+
+        {
+            final String passwordStr = password == null ? "" : password.getStringValue();
+            sendData.put( "password", passwordStr );
+        }
+
+        if ( pwmPasswordPolicy != null )
+        {
+            final LinkedHashMap<String, Object> policyData = new LinkedHashMap<>();
+            for ( final PwmPasswordRule rule : PwmPasswordRule.values() )
+            {
+                policyData.put( rule.name(), pwmPasswordPolicy.getValue( rule ) );
+            }
+            sendData.put( "policy", policyData );
+        }
+        if ( userInfo != null )
+        {
+            final MacroMachine macroMachine = MacroMachine.forUser( pwmApplication, PwmConstants.DEFAULT_LOCALE, SessionLabel.SYSTEM_LABEL, userInfo.getUserIdentity() );
+            final PublicUserInfoBean publicUserInfoBean = PublicUserInfoBean.fromUserInfoBean( userInfo, pwmApplication.getConfig(), locale, macroMachine );
+            sendData.put( "userInfo", publicUserInfoBean );
+        }
+
+        final String jsonRequestBody = JsonUtil.serializeMap( sendData );
+        try
+        {
+            final String responseBody = RestClientHelper.makeOutboundRestWSCall( pwmApplication, locale, restURL,
+                    jsonRequestBody );
+            final Map<String, Object> responseMap = JsonUtil.deserialize( responseBody,
+                    new TypeToken<Map<String, Object>>()
+                    {
+                    }
+            );
+            if ( responseMap.containsKey( REST_RESPONSE_KEY_ERROR ) && Boolean.parseBoolean( responseMap.get(
+                    REST_RESPONSE_KEY_ERROR ).toString() ) )
+            {
+                if ( responseMap.containsKey( REST_RESPONSE_KEY_ERROR_MSG ) )
+                {
+                    final String errorMessage = responseMap.get( REST_RESPONSE_KEY_ERROR_MSG ).toString();
+                    LOGGER.trace( () -> "external web service reported error: " + errorMessage );
+                    returnedErrors.add( new ErrorInformation( PwmError.PASSWORD_CUSTOM_ERROR, errorMessage, errorMessage, null ) );
+                }
+                else
+                {
+                    LOGGER.trace( () -> "external web service reported error without specifying an errorMessage" );
+                    returnedErrors.add( new ErrorInformation( PwmError.PASSWORD_CUSTOM_ERROR ) );
+                }
+            }
+            else
+            {
+                LOGGER.trace( () -> "external web service did not report an error" );
+            }
+
+        }
+        catch ( PwmOperationalException e )
+        {
+            final String errorMsg = "error executing external rule REST call: " + e.getMessage();
+            LOGGER.error( errorMsg );
+            if ( haltOnError )
+            {
+                throw new PwmUnrecoverableException( e.getErrorInformation(), e );
+            }
+            throw new IllegalStateException( "http response error code: " + e.getMessage() );
+        }
+        return returnedErrors;
+    }
+}
+

+ 2 - 12
server/src/main/java/password/pwm/util/RandomPasswordGenerator.java → server/src/main/java/password/pwm/util/password/RandomPasswordGenerator.java

@@ -20,7 +20,7 @@
  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
  */
 
-package password.pwm.util;
+package password.pwm.util.password;
 
 import com.novell.ldapchai.exception.ImpossiblePasswordPolicyException;
 import lombok.Builder;
@@ -35,11 +35,11 @@ import password.pwm.config.profile.PwmPasswordRule;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmUnrecoverableException;
-import password.pwm.http.PwmSession;
 import password.pwm.svc.PwmService;
 import password.pwm.svc.stats.Statistic;
 import password.pwm.svc.stats.StatisticsManager;
 import password.pwm.svc.wordlist.SeedlistService;
+import password.pwm.util.PasswordData;
 import password.pwm.util.java.TimeDuration;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.operations.PasswordUtility;
@@ -78,16 +78,6 @@ public class RandomPasswordGenerator
 
     private static final PwmLogger LOGGER = PwmLogger.forClass( RandomPasswordGenerator.class );
 
-    public static PasswordData createRandomPassword(
-            final PwmSession pwmSession,
-            final PwmApplication pwmApplication
-    )
-            throws PwmUnrecoverableException
-    {
-        final PwmPasswordPolicy userPasswordPolicy = pwmSession.getUserInfo().getPasswordPolicy();
-        return createRandomPassword( pwmSession.getLabel(), userPasswordPolicy, pwmApplication );
-    }
-
     public static PasswordData createRandomPassword(
             final SessionLabel sessionLabel,
             final PwmPasswordPolicy passwordPolicy,

+ 3 - 6
server/src/main/java/password/pwm/util/secure/ChecksumInputStream.java

@@ -22,9 +22,6 @@
 
 package password.pwm.util.secure;
 
-import password.pwm.http.bean.ImmutableByteArray;
-import password.pwm.util.java.JavaHelper;
-
 import java.io.IOException;
 import java.io.InputStream;
 import java.util.zip.CRC32;
@@ -108,12 +105,12 @@ public class ChecksumInputStream extends InputStream
         return false;
     }
 
-    public ImmutableByteArray checksum( )
+    public String checksum( )
     {
-        return ImmutableByteArray.of( JavaHelper.longToBytes( crc32.getValue() ) );
+        return ChecksumOutputStream.stringifyChecksum( crc32.getValue() );
     }
 
-    public ImmutableByteArray readUntilEndAndChecksum( ) throws IOException
+    public String readUntilEndAndChecksum( ) throws IOException
     {
         final byte[] buffer = new byte[ 1024 ];
 

+ 12 - 11
server/src/main/java/password/pwm/util/secure/ChecksumOutputStream.java

@@ -22,9 +22,6 @@
 
 package password.pwm.util.secure;
 
-import password.pwm.http.bean.ImmutableByteArray;
-import password.pwm.util.java.JavaHelper;
-
 import java.io.IOException;
 import java.io.OutputStream;
 import java.util.zip.CRC32;
@@ -46,21 +43,20 @@ public class ChecksumOutputStream extends OutputStream
     }
 
     @Override
-    public void write( final byte[] b ) throws IOException
+    public void write( final byte[] bytes ) throws IOException
     {
-        crc32.update( b );
-        wrappedStream.write( b );
+        write( bytes, 0, bytes.length );
     }
 
     @Override
-    public void write( final byte[] b, final int off, final int len ) throws IOException
+    public void write( final byte[] bytes, final int off, final int len ) throws IOException
     {
         if ( len > 0 )
         {
-            crc32.update( b, off, len );
+            crc32.update( bytes, off, len );
         }
 
-        wrappedStream.write( b, off, len );
+        wrappedStream.write( bytes, off, len );
     }
 
     @Override
@@ -76,8 +72,13 @@ public class ChecksumOutputStream extends OutputStream
         wrappedStream.write( b );
     }
 
-    public ImmutableByteArray checksum( )
+    public String checksum( )
+    {
+        return stringifyChecksum( crc32.getValue() );
+    }
+
+    static String stringifyChecksum( final long value )
     {
-        return ImmutableByteArray.of( JavaHelper.longToBytes( crc32.getValue() ) );
+        return Long.toString( value, 36 ).toLowerCase();
     }
 }

+ 1 - 1
server/src/main/java/password/pwm/ws/server/rest/RestRandomPasswordServer.java

@@ -36,7 +36,7 @@ import password.pwm.http.PwmHttpRequestWrapper;
 import password.pwm.svc.stats.Statistic;
 import password.pwm.svc.stats.StatisticsManager;
 import password.pwm.util.PasswordData;
-import password.pwm.util.RandomPasswordGenerator;
+import password.pwm.util.password.RandomPasswordGenerator;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.operations.PasswordUtility;

+ 1 - 1
server/src/main/java/password/pwm/ws/server/rest/RestSetPasswordServer.java

@@ -40,7 +40,7 @@ import password.pwm.svc.stats.Statistic;
 import password.pwm.svc.stats.StatisticsManager;
 import password.pwm.util.BasicAuthInfo;
 import password.pwm.util.PasswordData;
-import password.pwm.util.RandomPasswordGenerator;
+import password.pwm.util.password.RandomPasswordGenerator;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.operations.PasswordUtility;
 import password.pwm.ws.server.RestMethodHandler;

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

@@ -255,6 +255,7 @@ password.strength.threshold.strong=75
 password.strength.threshold.good=45
 password.strength.threshold.weak=20
 password.strength.threshold.veryWeak=0
+password.rule.wordlist.failWhenClosed=false
 peoplesearch.export.csv.maxDepth=1
 peoplesearch.export.csv.maxItems=1000
 peoplesearch.export.csv.maxSeconds=600

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

@@ -94,11 +94,11 @@
         <flag>Select_AllowUserInput</flag>
         <regex>^/.+</regex>
         <default>
-            <value>/private</value>
+            <value>/private/</value>
         </default>
         <options>
-            <option value="/private">/private</option>
-            <option value="/public">/public</option>
+            <option value="/private/">/private</option>
+            <option value="/public/">/public</option>
         </options>
     </setting>
     <setting hidden="false" key="idleTimeoutSeconds" level="1" required="true">
@@ -1315,9 +1315,7 @@
         <default/>
     </setting>
     <setting hidden="false" key="password.policy.ruleText" level="2">
-        <default>
-            <value />
-        </default>
+        <default/>
     </setting>
     <setting hidden="false" key="password.policy.disallowCurrent" level="2" required="true">
         <default>
@@ -1629,8 +1627,7 @@
     </setting>
     <setting hidden="false" key="security.cspHeader" level="2">
         <default>
-            <!--<value><![CDATA[]]></value>-->
-            <value><![CDATA[default-src 'self'; object-src 'none'; img-src 'self' data:; style-src 'self'; script-src https://www.google.com/recaptcha/ https://www.gstatic.com/recaptcha/ 'self' 'nonce-%NONCE%' ; frame-src https://www.google.com/recaptcha/ https://www.gstatic.com/recaptcha/; report-uri /sspr/public/command/cspReport]]></value>
+            <value><![CDATA[default-src 'self'; object-src 'none'; img-src 'self' data:; style-src 'self' 'unsafe-inline'; script-src https://www.google.com/recaptcha/ https://www.gstatic.com/recaptcha/ 'self' 'unsafe-eval' 'nonce-%NONCE%'; frame-src https://www.google.com/recaptcha/ https://www.gstatic.com/recaptcha/; report-uri /sspr/public/command/cspReport]]></value>
         </default>
     </setting>
     <setting hidden="false" key="email.adminAlert.toAddress" level="1">
@@ -3074,9 +3071,7 @@
         </default>
     </setting>
     <setting hidden="false" key="updateAttributes.customLinks" level="1">
-        <default>
-            <value></value>
-        </default>
+        <default/>
     </setting>
     <setting hidden="false" key="updateAttributes.token.lifetime" level="1" required="true">
         <default>

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

@@ -330,6 +330,7 @@ Title_LogoutPublic=Inactive Timeout
 Title_MainPage=Main Menu
 Title_NewUser=New User Registration
 Title_OrgChart=Organizational Chart
+Title_Settings=Settings
 Title_PasswordGuide=Password Guide
 Title_PasswordPolicy=Password Policy
 Title_PasswordStrength=Password Strength

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

@@ -27,6 +27,7 @@ import org.junit.Test;
 import password.pwm.PwmConstants;
 import password.pwm.error.PwmOperationalException;
 import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.util.secure.PwmSecurityKey;
 
 import java.util.Collections;
 import java.util.HashSet;
@@ -38,12 +39,20 @@ public class PwmSettingTest
     @Test
     public void testDefaultValues() throws PwmUnrecoverableException, PwmOperationalException
     {
+        final PwmSecurityKey pwmSecurityKey = new PwmSecurityKey( "abcdefghijklmnopqrstuvwxyz" );
         for ( final PwmSetting pwmSetting : PwmSetting.values() )
         {
             for ( final PwmSettingTemplate template : PwmSettingTemplate.values() )
             {
                 final PwmSettingTemplateSet templateSet = new PwmSettingTemplateSet( Collections.singleton( template ) );
-                pwmSetting.getDefaultValue( templateSet );
+                final StoredValue storedValue = pwmSetting.getDefaultValue( templateSet );
+                storedValue.toNativeObject();
+                storedValue.toDebugString( PwmConstants.DEFAULT_LOCALE );
+                storedValue.toDebugJsonObject( PwmConstants.DEFAULT_LOCALE );
+                storedValue.toXmlValues( "value", pwmSecurityKey );
+                storedValue.validateValue( pwmSetting );
+                storedValue.requiresStoredUpdate();
+                Assert.assertNotNull( storedValue.valueHash() );
             }
         }
     }

+ 3 - 3
server/src/test/java/password/pwm/config/profile/RuleHelperTest.java → server/src/test/java/password/pwm/config/profile/PasswordRuleReaderHelperTest.java

@@ -30,13 +30,13 @@ import org.mockito.ArgumentMatchers;
 import org.mockito.Mockito;
 import org.mockito.invocation.InvocationOnMock;
 import org.mockito.stubbing.Answer;
-import password.pwm.config.profile.PwmPasswordPolicy.RuleHelper;
 import password.pwm.util.macro.MacroMachine;
+import password.pwm.util.password.PasswordRuleReaderHelper;
 
 import java.util.List;
 import java.util.regex.Pattern;
 
-public class RuleHelperTest
+public class PasswordRuleReaderHelperTest
 {
     private static final String[][] MACRO_MAP = new String[][] {
             {"@User:ID@", "fflintstone"},
@@ -46,7 +46,7 @@ public class RuleHelperTest
     };
 
     private MacroMachine macroMachine = Mockito.mock( MacroMachine.class );
-    private RuleHelper ruleHelper = Mockito.mock( RuleHelper.class );
+    private PasswordRuleReaderHelper ruleHelper = Mockito.mock( PasswordRuleReaderHelper.class );
 
     @Before
     public void setUp() throws Exception

+ 0 - 122
server/src/test/java/password/pwm/util/PwmPasswordRuleValidatorTest.java

@@ -1,122 +0,0 @@
-/*
- * Password Management Servlets (PWM)
- * http://www.pwm-project.org
- *
- * Copyright (c) 2006-2009 Novell, Inc.
- * Copyright (c) 2009-2018 The PWM Project
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
- */
-
-package password.pwm.util;
-
-import org.junit.Assert;
-import org.junit.Test;
-
-public class PwmPasswordRuleValidatorTest
-{
-    @Test
-    public void testContainsDisallowedValue() throws Exception
-    {
-        // containsDisallowedValue([new password], [disallowed value], [character match threshold])
-
-        Assert.assertTrue( PwmPasswordRuleValidator.containsDisallowedValue( "n", "n", 0 ) );
-        Assert.assertTrue( PwmPasswordRuleValidator.containsDisallowedValue( "N", "n", 0 ) );
-        Assert.assertTrue( PwmPasswordRuleValidator.containsDisallowedValue( "n", "N", 0 ) );
-        Assert.assertTrue( PwmPasswordRuleValidator.containsDisallowedValue( "novell", "N", 0 ) );
-        Assert.assertTrue( PwmPasswordRuleValidator.containsDisallowedValue( "novell", "o", 0 ) );
-        Assert.assertTrue( PwmPasswordRuleValidator.containsDisallowedValue( "novell", "V", 0 ) );
-        Assert.assertTrue( PwmPasswordRuleValidator.containsDisallowedValue( "novell", "e", 0 ) );
-        Assert.assertTrue( PwmPasswordRuleValidator.containsDisallowedValue( "novell", "l", 0 ) );
-        Assert.assertFalse( PwmPasswordRuleValidator.containsDisallowedValue( "n", "n", 10 ) );
-
-        Assert.assertTrue( PwmPasswordRuleValidator.containsDisallowedValue( "novell", "novell", 0 ) );
-        Assert.assertTrue( PwmPasswordRuleValidator.containsDisallowedValue( "novell", "novell", 5 ) );
-        Assert.assertTrue( PwmPasswordRuleValidator.containsDisallowedValue( "novell", "novell", 6 ) );
-        Assert.assertFalse( PwmPasswordRuleValidator.containsDisallowedValue( "novell", "novell", 7 ) );
-        Assert.assertFalse( PwmPasswordRuleValidator.containsDisallowedValue( "novell", "foo", 0 ) );
-        Assert.assertFalse( PwmPasswordRuleValidator.containsDisallowedValue( "novell", "", 0 ) );
-
-        Assert.assertTrue( PwmPasswordRuleValidator.containsDisallowedValue( "love", "novell", 1 ) );
-        Assert.assertTrue( PwmPasswordRuleValidator.containsDisallowedValue( "love", "novell", 2 ) );
-        Assert.assertTrue( PwmPasswordRuleValidator.containsDisallowedValue( "love", "novell", 3 ) );
-        Assert.assertFalse( PwmPasswordRuleValidator.containsDisallowedValue( "love", "novell", 4 ) );
-        Assert.assertFalse( PwmPasswordRuleValidator.containsDisallowedValue( "love", "novell", 5 ) );
-        Assert.assertFalse( PwmPasswordRuleValidator.containsDisallowedValue( "love", "novell", 6 ) );
-
-        // Case shouldn't matter
-        Assert.assertTrue( PwmPasswordRuleValidator.containsDisallowedValue( "LOVE", "novell", 1 ) );
-        Assert.assertTrue( PwmPasswordRuleValidator.containsDisallowedValue( "LOVE", "novell", 2 ) );
-        Assert.assertTrue( PwmPasswordRuleValidator.containsDisallowedValue( "LOVE", "novell", 3 ) );
-        Assert.assertFalse( PwmPasswordRuleValidator.containsDisallowedValue( "LOVE", "novell", 4 ) );
-        Assert.assertFalse( PwmPasswordRuleValidator.containsDisallowedValue( "LOVE", "novell", 5 ) );
-        Assert.assertFalse( PwmPasswordRuleValidator.containsDisallowedValue( "LOVE", "novell", 6 ) );
-        Assert.assertTrue( PwmPasswordRuleValidator.containsDisallowedValue( "love", "NOVELL", 1 ) );
-        Assert.assertTrue( PwmPasswordRuleValidator.containsDisallowedValue( "love", "NOVELL", 2 ) );
-        Assert.assertTrue( PwmPasswordRuleValidator.containsDisallowedValue( "love", "NOVELL", 3 ) );
-        Assert.assertFalse( PwmPasswordRuleValidator.containsDisallowedValue( "love", "NOVELL", 4 ) );
-        Assert.assertFalse( PwmPasswordRuleValidator.containsDisallowedValue( "love", "NOVELL", 5 ) );
-        Assert.assertFalse( PwmPasswordRuleValidator.containsDisallowedValue( "love", "NOVELL", 6 ) );
-
-        // Play around the threshold boundaries
-        Assert.assertTrue( PwmPasswordRuleValidator.containsDisallowedValue( "foo-nove-bar", "novell", 4 ) );
-        Assert.assertTrue( PwmPasswordRuleValidator.containsDisallowedValue( "foo-ovel-bar", "novell", 4 ) );
-        Assert.assertTrue( PwmPasswordRuleValidator.containsDisallowedValue( "foo-vell-bar", "novell", 4 ) );
-        Assert.assertTrue( PwmPasswordRuleValidator.containsDisallowedValue( "foo-nove", "novell", 4 ) );
-        Assert.assertTrue( PwmPasswordRuleValidator.containsDisallowedValue( "foo-ovel", "novell", 4 ) );
-        Assert.assertTrue( PwmPasswordRuleValidator.containsDisallowedValue( "foo-vell", "novell", 4 ) );
-        Assert.assertTrue( PwmPasswordRuleValidator.containsDisallowedValue( "nove-bar", "novell", 4 ) );
-        Assert.assertTrue( PwmPasswordRuleValidator.containsDisallowedValue( "ovel-bar", "novell", 4 ) );
-        Assert.assertTrue( PwmPasswordRuleValidator.containsDisallowedValue( "vell-bar", "novell", 4 ) );
-    }
-
-    @Test
-    public void testTooManyConsecutiveChars()
-    {
-        Assert.assertFalse( PwmPasswordRuleValidator.tooManyConsecutiveChars( null, 4 ) );
-        Assert.assertFalse( PwmPasswordRuleValidator.tooManyConsecutiveChars( "", 4 ) );
-
-        Assert.assertFalse( PwmPasswordRuleValidator.tooManyConsecutiveChars( "12345678", 0 ) );
-        Assert.assertFalse( PwmPasswordRuleValidator.tooManyConsecutiveChars( "novell", 0 ) );
-
-        Assert.assertFalse( PwmPasswordRuleValidator.tooManyConsecutiveChars( "novell", 1 ) );
-        // 'n' and 'o' are consecutive
-        Assert.assertTrue( PwmPasswordRuleValidator.tooManyConsecutiveChars( "novell", 2 ) );
-        Assert.assertFalse( PwmPasswordRuleValidator.tooManyConsecutiveChars( "novell", 3 ) );
-        Assert.assertFalse( PwmPasswordRuleValidator.tooManyConsecutiveChars( "novell", 4 ) );
-        Assert.assertFalse( PwmPasswordRuleValidator.tooManyConsecutiveChars( "novell", 5 ) );
-        Assert.assertFalse( PwmPasswordRuleValidator.tooManyConsecutiveChars( "novell", 6 ) );
-
-        Assert.assertTrue( PwmPasswordRuleValidator.tooManyConsecutiveChars( "xyznovell", 3 ) );
-        Assert.assertTrue( PwmPasswordRuleValidator.tooManyConsecutiveChars( "novellabc", 3 ) );
-        Assert.assertTrue( PwmPasswordRuleValidator.tooManyConsecutiveChars( "novfghell", 3 ) );
-
-        Assert.assertFalse( PwmPasswordRuleValidator.tooManyConsecutiveChars( "Novell1235", 4 ) );
-        Assert.assertTrue( PwmPasswordRuleValidator.tooManyConsecutiveChars( "Novell1234", 4 ) );
-        Assert.assertTrue( PwmPasswordRuleValidator.tooManyConsecutiveChars( "1234Novell", 4 ) );
-        Assert.assertTrue( PwmPasswordRuleValidator.tooManyConsecutiveChars( "Nov1234ell", 4 ) );
-
-        Assert.assertFalse( PwmPasswordRuleValidator.tooManyConsecutiveChars( "123novabcellxyz", 4 ) );
-        Assert.assertTrue( PwmPasswordRuleValidator.tooManyConsecutiveChars( "123novabcellxyz", 3 ) );
-
-        Assert.assertFalse( PwmPasswordRuleValidator.tooManyConsecutiveChars( "abcdefghijklmnopqrstuvwxyz", -1 ) );
-        Assert.assertFalse( PwmPasswordRuleValidator.tooManyConsecutiveChars( "abcdefghijklmnopqrstuvwxyz", 0 ) );
-        Assert.assertFalse( PwmPasswordRuleValidator.tooManyConsecutiveChars( "abcdefghijklmnopqrstuvwxyz", 1 ) );
-        Assert.assertFalse( PwmPasswordRuleValidator.tooManyConsecutiveChars( "abcdefghijklmnopqrstuvwxyz", 27 ) );
-        Assert.assertTrue( PwmPasswordRuleValidator.tooManyConsecutiveChars( "abcdefghijklmnopqrstuvwxyz", 26 ) );
-        Assert.assertTrue( PwmPasswordRuleValidator.tooManyConsecutiveChars( "abcdefghijklmnopqrstuvwxyz", 25 ) );
-        Assert.assertTrue( PwmPasswordRuleValidator.tooManyConsecutiveChars( "abcdefghijklmnopqrstuvwxyz", 2 ) );
-    }
-}

+ 82 - 0
server/src/test/java/password/pwm/util/java/XmlFactoryBenchmarkExtendedTest.java

@@ -0,0 +1,82 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2018 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+package password.pwm.util.java;
+
+import org.apache.commons.io.output.NullOutputStream;
+import org.junit.Test;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.runner.Runner;
+import org.openjdk.jmh.runner.options.Options;
+import org.openjdk.jmh.runner.options.OptionsBuilder;
+import org.openjdk.jmh.runner.options.TimeValue;
+
+import java.io.InputStream;
+import java.util.concurrent.TimeUnit;
+
+public class XmlFactoryBenchmarkExtendedTest
+{
+    @Test
+    public void
+    launchBenchmark()
+            throws Exception
+    {
+        final Options opt = new OptionsBuilder()
+                .include( this.getClass().getName() + ".*" )
+                .mode ( Mode.AverageTime )
+                .timeUnit( TimeUnit.MILLISECONDS )
+                .warmupTime( TimeValue.seconds( 10 ) )
+                .measurementIterations( 10 )
+                .threads( 1 )
+                .forks( 1 )
+                .shouldFailOnError( true )
+                .shouldDoGC( true )
+                .jvmArgs( "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n" )
+                .build();
+
+        new Runner( opt ).run();
+    }
+
+    @Benchmark
+    public void benchmarkW3c ()
+            throws Exception
+    {
+        benchmarkImpl( XmlFactory.FactoryType.W3C );
+    }
+
+    @Benchmark
+    public void benchmarkJDom ()
+            throws Exception
+    {
+        benchmarkImpl( XmlFactory.FactoryType.JDOM );
+    }
+
+    private void benchmarkImpl ( final XmlFactory.FactoryType factoryType )
+            throws Exception
+    {
+        final XmlFactory xmlFactory = XmlFactory.getFactory( factoryType );
+        final InputStream xmlFactoryTestXmlFile = XmlFactoryTest.class.getResourceAsStream( "XmlFactoryTest.xml" );
+        final XmlDocument xmlDocument = xmlFactory.parseXml( xmlFactoryTestXmlFile );
+        xmlFactory.outputDocument( xmlDocument, new NullOutputStream() );
+    }
+}

+ 21 - 2
server/src/test/java/password/pwm/util/localdb/TestHelper.java

@@ -29,6 +29,16 @@ import org.apache.log4j.Level;
 import org.apache.log4j.Logger;
 import org.apache.log4j.PatternLayout;
 import password.pwm.PwmApplication;
+import password.pwm.PwmApplicationMode;
+import password.pwm.PwmEnvironment;
+import password.pwm.config.Configuration;
+import password.pwm.config.PwmSetting;
+import password.pwm.config.stored.StoredConfigurationImpl;
+import password.pwm.config.value.StringValue;
+import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.util.logging.PwmLogLevel;
+
+import java.io.File;
 
 public class TestHelper
 {
@@ -47,8 +57,17 @@ public class TestHelper
         chaiPackageLogger.setLevel( level );
     }
 
-    public static void t()
+    public static PwmApplication makeTestPwmApplication( final File tempFolder )
+            throws PwmUnrecoverableException
     {
-
+        Logger.getRootLogger().setLevel( Level.OFF );
+        final StoredConfigurationImpl storedConfiguration = StoredConfigurationImpl.newStoredConfiguration();
+        storedConfiguration.writeSetting( PwmSetting.EVENTS_JAVA_STDOUT_LEVEL, new StringValue( PwmLogLevel.FATAL.toString() ), null );
+        final Configuration configuration = new Configuration( storedConfiguration );
+        final PwmEnvironment pwmEnvironment = new PwmEnvironment.Builder( configuration, tempFolder )
+                .setApplicationMode( PwmApplicationMode.READ_ONLY )
+                .setInternalRuntimeInstance( true )
+                .createPwmEnvironment();
+        return new PwmApplication( pwmEnvironment );
     }
 }

+ 187 - 0
server/src/test/java/password/pwm/util/password/PasswordRuleChecksTest.java

@@ -0,0 +1,187 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2018 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+package password.pwm.util.password;
+
+import org.junit.Assert;
+import org.junit.Test;
+import password.pwm.config.profile.PwmPasswordPolicy;
+import password.pwm.config.profile.PwmPasswordRule;
+import password.pwm.error.ErrorInformation;
+import password.pwm.error.PwmError;
+import password.pwm.error.PwmUnrecoverableException;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class PasswordRuleChecksTest
+{
+    @Test
+    public void minimumLengthTest()
+            throws Exception
+    {
+        final Map<String, String> policyMap = new HashMap<>();
+        policyMap.put( PwmPasswordRule.MinimumLength.getKey(), "7" );
+        policyMap.put( PwmPasswordRule.AllowNumeric.getKey(), "true" );
+
+        {
+            final List<ErrorInformation> expectedErrors = new ArrayList<>();
+            expectedErrors.add( new ErrorInformation( PwmError.PASSWORD_TOO_SHORT, null ) );
+
+            Assert.assertTrue( doCompareTest( policyMap, "123", expectedErrors ) );
+            Assert.assertTrue( doCompareTest( policyMap, "1234", expectedErrors ) );
+            Assert.assertTrue( doCompareTest( policyMap, "12345", expectedErrors ) );
+            Assert.assertTrue( doCompareTest( policyMap, "123456", expectedErrors ) );
+
+            Assert.assertFalse( doCompareTest( policyMap, "1234567", expectedErrors ) );
+            Assert.assertFalse( doCompareTest( policyMap, "12345678", expectedErrors ) );
+        }
+    }
+
+    @Test
+    public void maximumLengthTest()
+            throws Exception
+    {
+        final Map<String, String> policyMap = new HashMap<>();
+        policyMap.put( PwmPasswordRule.MaximumLength.getKey(), "7" );
+        policyMap.put( PwmPasswordRule.AllowNumeric.getKey(), "true" );
+
+        {
+            final List<ErrorInformation> expectedErrors = new ArrayList<>();
+            expectedErrors.add( new ErrorInformation( PwmError.PASSWORD_TOO_LONG, null ) );
+
+            Assert.assertFalse( doCompareTest( policyMap, "123", expectedErrors ) );
+            Assert.assertFalse( doCompareTest( policyMap, "1234", expectedErrors ) );
+            Assert.assertFalse( doCompareTest( policyMap, "12345", expectedErrors ) );
+            Assert.assertFalse( doCompareTest( policyMap, "123456", expectedErrors ) );
+            Assert.assertFalse( doCompareTest( policyMap, "1234567", expectedErrors ) );
+
+            Assert.assertTrue( doCompareTest( policyMap, "12345678", expectedErrors ) );
+            Assert.assertTrue( doCompareTest( policyMap, "123456789", expectedErrors ) );
+        }
+    }
+
+    @Test
+    public void minimumUniqueTest()
+            throws Exception
+    {
+        final Map<String, String> policyMap = new HashMap<>();
+        policyMap.put( PwmPasswordRule.MinimumUnique.getKey(), "4" );
+        policyMap.put( PwmPasswordRule.AllowNumeric.getKey(), "true" );
+
+        {
+            final List<ErrorInformation> expectedErrors = new ArrayList<>();
+            expectedErrors.add( new ErrorInformation( PwmError.PASSWORD_NOT_ENOUGH_UNIQUE, null ) );
+
+            Assert.assertTrue( doCompareTest( policyMap, "aaa", expectedErrors ) );
+            Assert.assertTrue( doCompareTest( policyMap, "aaa2", expectedErrors ) );
+            Assert.assertTrue( doCompareTest( policyMap, "aaa23", expectedErrors ) );
+
+            Assert.assertFalse( doCompareTest( policyMap, "aaa234", expectedErrors ) );
+            Assert.assertFalse( doCompareTest( policyMap, "aaa2345", expectedErrors ) );
+        }
+    }
+
+    @Test
+    public void allowNumericTest()
+            throws Exception
+    {
+        {
+            final Map<String, String> policyMap = new HashMap<>();
+            policyMap.put( PwmPasswordRule.AllowNumeric.getKey(), "true" );
+
+            {
+                final List<ErrorInformation> expectedErrors = new ArrayList<>();
+                expectedErrors.add( new ErrorInformation( PwmError.PASSWORD_TOO_MANY_NUMERIC, null ) );
+
+                Assert.assertFalse( doCompareTest( policyMap, "aaa", expectedErrors ) );
+                Assert.assertFalse( doCompareTest( policyMap, "aaa2", expectedErrors ) );
+            }
+        }
+        {
+            final Map<String, String> policyMap = new HashMap<>();
+            policyMap.put( PwmPasswordRule.AllowNumeric.getKey(), "false" );
+
+            {
+                final List<ErrorInformation> expectedErrors = new ArrayList<>();
+                expectedErrors.add( new ErrorInformation( PwmError.PASSWORD_TOO_MANY_NUMERIC, null ) );
+
+                Assert.assertFalse( doCompareTest( policyMap, "aaa", expectedErrors ) );
+                Assert.assertTrue( doCompareTest( policyMap, "aaa2", expectedErrors ) );
+            }
+        }
+    }
+
+    @Test
+    public void allowSpecialTest()
+            throws Exception
+    {
+        {
+            final Map<String, String> policyMap = new HashMap<>();
+            policyMap.put( PwmPasswordRule.AllowSpecial.getKey(), "true" );
+
+            {
+                final List<ErrorInformation> expectedErrors = new ArrayList<>();
+                expectedErrors.add( new ErrorInformation( PwmError.PASSWORD_TOO_MANY_SPECIAL, null ) );
+
+                Assert.assertFalse( doCompareTest( policyMap, "aaa", expectedErrors ) );
+                Assert.assertFalse( doCompareTest( policyMap, "aaa^", expectedErrors ) );
+            }
+        }
+        {
+            final Map<String, String> policyMap = new HashMap<>();
+            policyMap.put( PwmPasswordRule.AllowNumeric.getKey(), "false" );
+
+            {
+                final List<ErrorInformation> expectedErrors = new ArrayList<>();
+                expectedErrors.add( new ErrorInformation( PwmError.PASSWORD_TOO_MANY_SPECIAL, null ) );
+
+                Assert.assertFalse( doCompareTest( policyMap, "aaa", expectedErrors ) );
+                Assert.assertTrue( doCompareTest( policyMap, "aaa^", expectedErrors ) );
+            }
+        }
+    }
+
+    private static List<ErrorInformation> doTest( final Map<String, String> policy, final String password )
+            throws PwmUnrecoverableException
+    {
+        final Map<String, String> policyMap = new HashMap<>( PwmPasswordPolicy.defaultPolicy().getPolicyMap() );
+        policyMap.putAll( policy );
+        final PwmPasswordPolicy pwmPasswordPolicy = PwmPasswordPolicy.createPwmPasswordPolicy( policyMap );
+        return PasswordRuleChecks.extendedPolicyRuleChecker( null, pwmPasswordPolicy, password, null, null );
+    }
+
+    private static boolean doCompareTest(
+            final Map<String, String> policyMap,
+            final String password,
+            final List<ErrorInformation> expectedErrors
+    )
+            throws PwmUnrecoverableException
+    {
+        return ErrorInformation.listsContainSameErrors(
+                doTest( policyMap, password ),
+                expectedErrors );
+    }
+
+}

+ 122 - 0
server/src/test/java/password/pwm/util/password/PwmPasswordRuleValidatorTest.java

@@ -0,0 +1,122 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2018 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+package password.pwm.util.password;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+public class PwmPasswordRuleValidatorTest
+{
+    @Test
+    public void testContainsDisallowedValue() throws Exception
+    {
+        // containsDisallowedValue([new password], [disallowed value], [character match threshold])
+
+        Assert.assertTrue( PwmPasswordRuleUtil.containsDisallowedValue( "n", "n", 0 ) );
+        Assert.assertTrue( PwmPasswordRuleUtil.containsDisallowedValue( "N", "n", 0 ) );
+        Assert.assertTrue( PwmPasswordRuleUtil.containsDisallowedValue( "n", "N", 0 ) );
+        Assert.assertTrue( PwmPasswordRuleUtil.containsDisallowedValue( "novell", "N", 0 ) );
+        Assert.assertTrue( PwmPasswordRuleUtil.containsDisallowedValue( "novell", "o", 0 ) );
+        Assert.assertTrue( PwmPasswordRuleUtil.containsDisallowedValue( "novell", "V", 0 ) );
+        Assert.assertTrue( PwmPasswordRuleUtil.containsDisallowedValue( "novell", "e", 0 ) );
+        Assert.assertTrue( PwmPasswordRuleUtil.containsDisallowedValue( "novell", "l", 0 ) );
+        Assert.assertFalse( PwmPasswordRuleUtil.containsDisallowedValue( "n", "n", 10 ) );
+
+        Assert.assertTrue( PwmPasswordRuleUtil.containsDisallowedValue( "novell", "novell", 0 ) );
+        Assert.assertTrue( PwmPasswordRuleUtil.containsDisallowedValue( "novell", "novell", 5 ) );
+        Assert.assertTrue( PwmPasswordRuleUtil.containsDisallowedValue( "novell", "novell", 6 ) );
+        Assert.assertFalse( PwmPasswordRuleUtil.containsDisallowedValue( "novell", "novell", 7 ) );
+        Assert.assertFalse( PwmPasswordRuleUtil.containsDisallowedValue( "novell", "foo", 0 ) );
+        Assert.assertFalse( PwmPasswordRuleUtil.containsDisallowedValue( "novell", "", 0 ) );
+
+        Assert.assertTrue( PwmPasswordRuleUtil.containsDisallowedValue( "love", "novell", 1 ) );
+        Assert.assertTrue( PwmPasswordRuleUtil.containsDisallowedValue( "love", "novell", 2 ) );
+        Assert.assertTrue( PwmPasswordRuleUtil.containsDisallowedValue( "love", "novell", 3 ) );
+        Assert.assertFalse( PwmPasswordRuleUtil.containsDisallowedValue( "love", "novell", 4 ) );
+        Assert.assertFalse( PwmPasswordRuleUtil.containsDisallowedValue( "love", "novell", 5 ) );
+        Assert.assertFalse( PwmPasswordRuleUtil.containsDisallowedValue( "love", "novell", 6 ) );
+
+        // Case shouldn't matter
+        Assert.assertTrue( PwmPasswordRuleUtil.containsDisallowedValue( "LOVE", "novell", 1 ) );
+        Assert.assertTrue( PwmPasswordRuleUtil.containsDisallowedValue( "LOVE", "novell", 2 ) );
+        Assert.assertTrue( PwmPasswordRuleUtil.containsDisallowedValue( "LOVE", "novell", 3 ) );
+        Assert.assertFalse( PwmPasswordRuleUtil.containsDisallowedValue( "LOVE", "novell", 4 ) );
+        Assert.assertFalse( PwmPasswordRuleUtil.containsDisallowedValue( "LOVE", "novell", 5 ) );
+        Assert.assertFalse( PwmPasswordRuleUtil.containsDisallowedValue( "LOVE", "novell", 6 ) );
+        Assert.assertTrue( PwmPasswordRuleUtil.containsDisallowedValue( "love", "NOVELL", 1 ) );
+        Assert.assertTrue( PwmPasswordRuleUtil.containsDisallowedValue( "love", "NOVELL", 2 ) );
+        Assert.assertTrue( PwmPasswordRuleUtil.containsDisallowedValue( "love", "NOVELL", 3 ) );
+        Assert.assertFalse( PwmPasswordRuleUtil.containsDisallowedValue( "love", "NOVELL", 4 ) );
+        Assert.assertFalse( PwmPasswordRuleUtil.containsDisallowedValue( "love", "NOVELL", 5 ) );
+        Assert.assertFalse( PwmPasswordRuleUtil.containsDisallowedValue( "love", "NOVELL", 6 ) );
+
+        // Play around the threshold boundaries
+        Assert.assertTrue( PwmPasswordRuleUtil.containsDisallowedValue( "foo-nove-bar", "novell", 4 ) );
+        Assert.assertTrue( PwmPasswordRuleUtil.containsDisallowedValue( "foo-ovel-bar", "novell", 4 ) );
+        Assert.assertTrue( PwmPasswordRuleUtil.containsDisallowedValue( "foo-vell-bar", "novell", 4 ) );
+        Assert.assertTrue( PwmPasswordRuleUtil.containsDisallowedValue( "foo-nove", "novell", 4 ) );
+        Assert.assertTrue( PwmPasswordRuleUtil.containsDisallowedValue( "foo-ovel", "novell", 4 ) );
+        Assert.assertTrue( PwmPasswordRuleUtil.containsDisallowedValue( "foo-vell", "novell", 4 ) );
+        Assert.assertTrue( PwmPasswordRuleUtil.containsDisallowedValue( "nove-bar", "novell", 4 ) );
+        Assert.assertTrue( PwmPasswordRuleUtil.containsDisallowedValue( "ovel-bar", "novell", 4 ) );
+        Assert.assertTrue( PwmPasswordRuleUtil.containsDisallowedValue( "vell-bar", "novell", 4 ) );
+    }
+
+    @Test
+    public void testTooManyConsecutiveChars()
+    {
+        Assert.assertFalse( PwmPasswordRuleUtil.tooManyConsecutiveChars( null, 4 ) );
+        Assert.assertFalse( PwmPasswordRuleUtil.tooManyConsecutiveChars( "", 4 ) );
+
+        Assert.assertFalse( PwmPasswordRuleUtil.tooManyConsecutiveChars( "12345678", 0 ) );
+        Assert.assertFalse( PwmPasswordRuleUtil.tooManyConsecutiveChars( "novell", 0 ) );
+
+        Assert.assertFalse( PwmPasswordRuleUtil.tooManyConsecutiveChars( "novell", 1 ) );
+        // 'n' and 'o' are consecutive
+        Assert.assertTrue( PwmPasswordRuleUtil.tooManyConsecutiveChars( "novell", 2 ) );
+        Assert.assertFalse( PwmPasswordRuleUtil.tooManyConsecutiveChars( "novell", 3 ) );
+        Assert.assertFalse( PwmPasswordRuleUtil.tooManyConsecutiveChars( "novell", 4 ) );
+        Assert.assertFalse( PwmPasswordRuleUtil.tooManyConsecutiveChars( "novell", 5 ) );
+        Assert.assertFalse( PwmPasswordRuleUtil.tooManyConsecutiveChars( "novell", 6 ) );
+
+        Assert.assertTrue( PwmPasswordRuleUtil.tooManyConsecutiveChars( "xyznovell", 3 ) );
+        Assert.assertTrue( PwmPasswordRuleUtil.tooManyConsecutiveChars( "novellabc", 3 ) );
+        Assert.assertTrue( PwmPasswordRuleUtil.tooManyConsecutiveChars( "novfghell", 3 ) );
+
+        Assert.assertFalse( PwmPasswordRuleUtil.tooManyConsecutiveChars( "Novell1235", 4 ) );
+        Assert.assertTrue( PwmPasswordRuleUtil.tooManyConsecutiveChars( "Novell1234", 4 ) );
+        Assert.assertTrue( PwmPasswordRuleUtil.tooManyConsecutiveChars( "1234Novell", 4 ) );
+        Assert.assertTrue( PwmPasswordRuleUtil.tooManyConsecutiveChars( "Nov1234ell", 4 ) );
+
+        Assert.assertFalse( PwmPasswordRuleUtil.tooManyConsecutiveChars( "123novabcellxyz", 4 ) );
+        Assert.assertTrue( PwmPasswordRuleUtil.tooManyConsecutiveChars( "123novabcellxyz", 3 ) );
+
+        Assert.assertFalse( PwmPasswordRuleUtil.tooManyConsecutiveChars( "abcdefghijklmnopqrstuvwxyz", -1 ) );
+        Assert.assertFalse( PwmPasswordRuleUtil.tooManyConsecutiveChars( "abcdefghijklmnopqrstuvwxyz", 0 ) );
+        Assert.assertFalse( PwmPasswordRuleUtil.tooManyConsecutiveChars( "abcdefghijklmnopqrstuvwxyz", 1 ) );
+        Assert.assertFalse( PwmPasswordRuleUtil.tooManyConsecutiveChars( "abcdefghijklmnopqrstuvwxyz", 27 ) );
+        Assert.assertTrue( PwmPasswordRuleUtil.tooManyConsecutiveChars( "abcdefghijklmnopqrstuvwxyz", 26 ) );
+        Assert.assertTrue( PwmPasswordRuleUtil.tooManyConsecutiveChars( "abcdefghijklmnopqrstuvwxyz", 25 ) );
+        Assert.assertTrue( PwmPasswordRuleUtil.tooManyConsecutiveChars( "abcdefghijklmnopqrstuvwxyz", 2 ) );
+    }
+}

+ 75 - 0
server/src/test/java/password/pwm/util/password/RandomPasswordGeneratorTest.java

@@ -0,0 +1,75 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2018 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+package password.pwm.util.password;
+
+import org.junit.Assert;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import password.pwm.PwmApplication;
+import password.pwm.config.profile.PwmPasswordPolicy;
+import password.pwm.config.profile.PwmPasswordRule;
+import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.util.PasswordData;
+import password.pwm.util.localdb.TestHelper;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+public class RandomPasswordGeneratorTest
+{
+    @Rule
+    public TemporaryFolder temporaryFolder = new TemporaryFolder();
+
+
+    @Test
+    public void generateRandomPasswordsTest()
+            throws PwmUnrecoverableException, IOException
+    {
+        final PwmApplication pwmApplication = TestHelper.makeTestPwmApplication( temporaryFolder.newFolder() );
+        final Map<String, String> policyMap = new HashMap<>( PwmPasswordPolicy.defaultPolicy().getPolicyMap() );
+        policyMap.put( PwmPasswordRule.AllowNumeric.getKey(), "true" );
+        final PwmPasswordPolicy pwmPasswordPolicy = PwmPasswordPolicy.createPwmPasswordPolicy( policyMap );
+
+        final int loopCount = 1_000;
+        final Set<String> seenValues = new HashSet<>();
+
+        for ( int i = 0; i < loopCount; i++ )
+        {
+            final PasswordData passwordData = RandomPasswordGenerator.createRandomPassword(
+                    null,
+                    pwmPasswordPolicy,
+                    pwmApplication );
+
+            final String passwordString = passwordData.getStringValue();
+            if ( seenValues.contains( passwordString ) )
+            {
+                Assert.fail( "repeated random generated password" );
+            }
+            seenValues.add( passwordString );
+        }
+    }
+}

+ 3 - 1
webapp/pom.xml

@@ -98,7 +98,8 @@
                 <version>3.2.2</version>
                 <configuration>
                     <archiveClasses>true</archiveClasses>
-                    <packagingExcludes>WEB-INF/classes</packagingExcludes>
+                    <packagingExcludes>**/*.jsp</packagingExcludes>
+                    <webXml>${project.build.directory}/web.xml</webXml>
                     <archive>
                         <manifestEntries>
                             <Implementation-Archive-Name>${warArtifactID}</Implementation-Archive-Name>
@@ -127,6 +128,7 @@
                         </goals>
                         <phase>compile</phase>
                         <configuration>
+                            <trimSpaces>true</trimSpaces>
                             <compilerVersion>${maven.compiler.source}</compilerVersion>
                             <keepSources>false</keepSources>
                         </configuration>

+ 1 - 1
webapp/src/main/webapp/WEB-INF/jsp/fragment/footer.jsp

@@ -68,7 +68,7 @@
 </pwm:if>
 <pwm:script>
     <script type="text/javascript">
-        var dojoConfig = { has: { "csp-restrictions":false }, async:true }
+        var dojoConfig = { has: { "csp-restrictions":true }, async:true}
     </script>
 </pwm:script>
 <pwm:if test="<%=PwmIfTest.hasCustomJavascript%>">

+ 1 - 1
webapp/src/main/webapp/WEB-INF/jsp/fragment/ldap-selector.jsp

@@ -65,7 +65,7 @@
     </select>
 </div>
 <% } %>
-<div style="display: <%=showContextSelector?"inherit":"none"%>" id="contextSelectorWrapper">
+<div <%=showContextSelector?"":"class=\"display-none\" "%>" id="contextSelectorWrapper">
     <h2 class="loginFieldLabel"><label for="<%=PwmConstants.PARAM_CONTEXT%>"><pwm:display key="Field_Location"/></label></h2>
     <div class="formFieldWrapper">
         <select name="<%=PwmConstants.PARAM_CONTEXT%>" id="<%=PwmConstants.PARAM_CONTEXT%>" class="selectfield" title="<pwm:display key="Field_Location"/>">

+ 2 - 2
webapp/src/main/webapp/WEB-INF/jsp/fragment/message.jsp

@@ -35,7 +35,7 @@
     <span id="errorCode" style="display: none"><%=requestError.getError().getErrorCode()%></span>
     <span id="errorName" style="display: none"><%=requestError.getError().toString()%></span>
 <% } else { %>
-    <span style="display:none" id="message" class="message">&nbsp;</span>
+    <span id="message" class="message display-none">&nbsp;</span>
 <% } %>
-    <div id="capslockwarning" style="display:none;"><pwm:display key="Display_CapsLockIsOn"/></div>
+    <div id="capslockwarning" class="display-none"><pwm:display key="Display_CapsLockIsOn"/></div>
 </div>

+ 8 - 8
webapp/src/main/webapp/public/resources/js/main.js

@@ -31,7 +31,7 @@ PWM_API.formatDate = function(dateObj) {
     return PWM_MAIN.TimestampHandler.formatDate(dateObj);
 };
 
-PWM_MAIN.ajaxTimeout = 120 * 1000;
+PWM_MAIN.ajaxTimeout = 60 * 1000;
 
 PWM_MAIN.pageLoadHandler = function() {
     PWM_GLOBAL['localeBundle']=PWM_GLOBAL['localeBundle'] || [];
@@ -474,19 +474,19 @@ PWM_MAIN.checkForCapsLock = function(e) {
 
         if(dojo.isIE){
             if (capsLockKeyDetected) {
-                capsLockWarningElement.style.display = 'block';
+                PWM_MAIN.removeCssClass('capslockwarning','display-none');
                 PWM_GLOBAL['lastCapsLockErrorTime'] = (new Date().getTime());
                 setTimeout(function(){
                     if ((new Date().getTime() - PWM_GLOBAL['lastCapsLockErrorTime'] > displayDuration)) {
-                        capsLockWarningElement.style.display = 'none';
+                        PWM_MAIN.addCssClass('capslockwarning','display-none');
                     }
                 },displayDuration + 500);
             } else {
-                capsLockWarningElement.style.display = 'none';
+                PWM_MAIN.addCssClass('capslockwarning','display-none');
             }
         } else {
             if (capsLockKeyDetected) {
-                capsLockWarningElement.style.display = null;
+                PWM_MAIN.removeCssClass('capslockwarning','display-none');
                 fx.fadeIn(fadeInArgs).play();
                 PWM_GLOBAL['lastCapsLockErrorTime'] = (new Date().getTime());
                 setTimeout(function(){
@@ -494,7 +494,7 @@ PWM_MAIN.checkForCapsLock = function(e) {
                         dojo.fadeOut(fadeOutArgs).play();
                         setTimeout(function(){
                             if ((new Date().getTime() - PWM_GLOBAL['lastCapsLockErrorTime'] > displayDuration)) {
-                                capsLockWarningElement.style.display = 'none';
+                                PWM_MAIN.addCssClass('capslockwarning','display-none');
                             }
                         },5 * 1000);
                     }
@@ -1392,10 +1392,10 @@ PWM_MAIN.updateLoginContexts = function() {
         var selectedProfile = ldapProfileElement.options[ldapProfileElement.selectedIndex].value;
         var contextList = PWM_GLOBAL['ldapProfiles'][selectedProfile];
         if (PWM_MAIN.JSLibrary.isEmpty(contextList)) {
-            PWM_MAIN.getObject('contextSelectorWrapper').style.display = 'none';
+            PWM_MAIN.addCssClass( 'contentSelectorWrapper', 'display-none' );
         } else {
             contextElement.innerHTML = '';
-            PWM_MAIN.getObject('contextSelectorWrapper').style.display = 'inherit';
+            PWM_MAIN.removeCssClass( 'contentSelectorWrapper', 'display-none' );
             for (var iter in contextList) {
                 (function (key) {
                     var display = contextList[key];