Browse Source

-config login fixes
-replace jdom xml factory with w3c
-jsp precompiliation

jrivard@gmail.com 6 years ago
parent
commit
ba2991f230
42 changed files with 1353 additions and 945 deletions
  1. 1 0
      build/checkstyle-import.xml
  2. 1 1
      client/pom.xml
  3. 3 2
      docker/pom.xml
  4. 45 1
      pom.xml
  5. 0 26
      rest-test-service/pom.xml
  6. 1 33
      server/pom.xml
  7. 0 2
      server/src/main/java/password/pwm/PwmApplication.java
  8. 1 1
      server/src/main/java/password/pwm/PwmConstants.java
  9. 6 26
      server/src/main/java/password/pwm/config/PwmSettingXml.java
  10. 4 169
      server/src/main/java/password/pwm/config/profile/PwmPasswordPolicy.java
  11. 1 1
      server/src/main/java/password/pwm/http/PwmHttpResponseWrapper.java
  12. 117 134
      server/src/main/java/password/pwm/http/filter/ConfigAccessFilter.java
  13. 34 0
      server/src/main/java/password/pwm/http/filter/ObsoleteUrlFilter.java
  14. 1 1
      server/src/main/java/password/pwm/http/servlet/LoginServlet.java
  15. 1 1
      server/src/main/java/password/pwm/http/servlet/changepw/ChangePasswordServlet.java
  16. 2 7
      server/src/main/java/password/pwm/http/servlet/resource/ResourceServletService.java
  17. 2 1
      server/src/main/java/password/pwm/http/tag/PasswordRequirementsTag.java
  18. 1 1
      server/src/main/java/password/pwm/ldap/UserInfoReader.java
  19. 1 4
      server/src/main/java/password/pwm/svc/sessiontrack/SessionTrackService.java
  20. 14 16
      server/src/main/java/password/pwm/svc/sessiontrack/UserAgentUtils.java
  21. 1 1
      server/src/main/java/password/pwm/svc/wordlist/WordlistSource.java
  22. 1 2
      server/src/main/java/password/pwm/svc/wordlist/WordlistZipReader.java
  23. 8 8
      server/src/main/java/password/pwm/util/CaptchaUtility.java
  24. 17 5
      server/src/main/java/password/pwm/util/PwmScheduler.java
  25. 1 0
      server/src/main/java/password/pwm/util/RandomPasswordGenerator.java
  26. 2 8
      server/src/main/java/password/pwm/util/java/JavaHelper.java
  27. 48 0
      server/src/main/java/password/pwm/util/java/LazySoftReference.java
  28. 1 1
      server/src/main/java/password/pwm/util/java/XmlElement.java
  29. 32 4
      server/src/main/java/password/pwm/util/java/XmlFactory.java
  30. 1 1
      server/src/main/java/password/pwm/util/operations/PasswordUtility.java
  31. 202 0
      server/src/main/java/password/pwm/util/password/PasswordRuleHelper.java
  32. 293 0
      server/src/main/java/password/pwm/util/password/PwmPasswordRuleUtil.java
  33. 295 382
      server/src/main/java/password/pwm/util/password/PwmPasswordRuleValidator.java
  34. 3 6
      server/src/main/java/password/pwm/util/secure/ChecksumInputStream.java
  35. 12 11
      server/src/main/java/password/pwm/util/secure/ChecksumOutputStream.java
  36. 5 9
      server/src/main/resources/password/pwm/config/PwmSetting.xml
  37. 10 1
      server/src/test/java/password/pwm/config/PwmSettingTest.java
  38. 3 3
      server/src/test/java/password/pwm/config/profile/PasswordRuleHelperTest.java
  39. 96 74
      server/src/test/java/password/pwm/util/PwmPasswordRuleValidatorTest.java
  40. 82 0
      server/src/test/java/password/pwm/util/java/XmlFactoryBenchmarkExtendedTest.java
  41. 3 1
      webapp/pom.xml
  42. 1 1
      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 - 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>

+ 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>

+ 1 - 33
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>

+ 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.PasswordRuleHelper;
 
 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 PasswordRuleHelper getRuleHelper( )
     {
-        return new RuleHelper( this );
+        return new PasswordRuleHelper( 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 PasswordRuleHelper ruleHelper = this.getRuleHelper();
         final List<HealthRecord> returnList = new ArrayList<>();
         final Map<PwmPasswordRule, PwmPasswordRule> rulePairs = new LinkedHashMap<>();
         rulePairs.put( PwmPasswordRule.MinimumLength, PwmPasswordRule.MaximumLength );

+ 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/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() )

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

@@ -55,7 +55,7 @@ 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.password.PwmPasswordRuleValidator;
 import password.pwm.util.RandomPasswordGenerator;
 import password.pwm.util.form.FormUtility;
 import password.pwm.util.java.JavaHelper;

+ 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.PasswordRuleHelper;
 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 PasswordRuleHelper ruleHelper = pwordPolicy.getRuleHelper();
 
         if ( ruleHelper.readBooleanValue( PwmPasswordRule.CaseSensitive ) )
         {

+ 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 - 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 );

+ 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();
+            }
         }
     }
 

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

@@ -43,6 +43,7 @@ import password.pwm.svc.wordlist.SeedlistService;
 import password.pwm.util.java.TimeDuration;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.operations.PasswordUtility;
+import password.pwm.util.password.PwmPasswordRuleValidator;
 import password.pwm.util.secure.PwmRandom;
 
 import java.time.Instant;

+ 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;
             }

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

@@ -82,7 +82,7 @@ import password.pwm.svc.stats.Statistic;
 import password.pwm.util.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;

+ 202 - 0
server/src/main/java/password/pwm/util/password/PasswordRuleHelper.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 PasswordRuleHelper
+{
+    private static final PwmLogger LOGGER = PwmLogger.forClass( PasswordRuleHelper.class );
+
+    public enum Flag
+    {
+        KeepThresholds
+    }
+
+    private final PwmPasswordPolicy passwordPolicy;
+    private final com.novell.ldapchai.util.PasswordRuleHelper chaiRuleHelper;
+
+    public PasswordRuleHelper( 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 );
+    }
+}

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

@@ -0,0 +1,293 @@
+/*
+ * 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.PasswordCharCounter;
+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;
+    }
+}

+ 295 - 382
server/src/main/java/password/pwm/util/PwmPasswordRuleValidator.java → server/src/main/java/password/pwm/util/password/PwmPasswordRuleValidator.java

@@ -20,13 +20,15 @@
  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
  */
 
-package password.pwm.util;
+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 lombok.AllArgsConstructor;
+import lombok.Data;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.commons.lang3.math.NumberUtils;
 import password.pwm.AppProperty;
@@ -38,7 +40,6 @@ 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;
@@ -48,15 +49,17 @@ 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.PasswordCharCounter;
+import password.pwm.util.PasswordData;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.JsonUtil;
-import password.pwm.util.java.StringUtil;
 import password.pwm.util.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.Arrays;
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.LinkedHashMap;
@@ -85,7 +88,11 @@ public class PwmPasswordRuleValidator
         BypassLdapRuleCheck,
     }
 
-    public PwmPasswordRuleValidator( final PwmApplication pwmApplication, final PwmPasswordPolicy policy, final Flag... flags )
+    public PwmPasswordRuleValidator(
+            final PwmApplication pwmApplication,
+            final PwmPasswordPolicy policy,
+            final Flag... flags
+    )
     {
         this.pwmApplication = pwmApplication;
         this.policy = policy;
@@ -216,7 +223,7 @@ public class PwmPasswordRuleValidator
         }
 
         final List<ErrorInformation> errorList = new ArrayList<>();
-        final PwmPasswordPolicy.RuleHelper ruleHelper = policy.getRuleHelper();
+        final PasswordRuleHelper ruleHelper = policy.getRuleHelper();
         final MacroMachine macroMachine = userInfo == null || userInfo.getUserIdentity() == null
                 ? MacroMachine.forNonUserSpecific( pwmApplication, SessionLabel.SYSTEM_LABEL )
                 : MacroMachine.forUser(
@@ -309,7 +316,7 @@ public class PwmPasswordRuleValidator
         // check disallowed attributes.
         if ( !policy.getRuleHelper().getDisallowedAttributes().isEmpty() )
         {
-            final List<String> paramConfigs = policy.getRuleHelper().getDisallowedAttributes( RuleHelper.Flag.KeepThresholds );
+            final List<String> paramConfigs = policy.getRuleHelper().getDisallowedAttributes( PasswordRuleHelper.Flag.KeepThresholds );
             if ( userInfo != null )
             {
                 final Map<String, String> userValues = userInfo.getCachedPasswordRuleAttributes();
@@ -322,7 +329,7 @@ public class PwmPasswordRuleValidator
                     final String disallowedValue = StringUtils.defaultString( userValues.get( attrName ) );
                     final int threshold = parts.length > 1 ? NumberUtils.toInt( parts[ 1 ] ) : 0;
 
-                    if ( containsDisallowedValue( passwordString, disallowedValue, threshold ) )
+                    if ( PwmPasswordRuleUtil.containsDisallowedValue( passwordString, disallowedValue, threshold ) )
                     {
                         LOGGER.trace( () -> "password rejected, same as user attr " + attrName );
                         errorList.add( new ErrorInformation( PwmError.PASSWORD_SAMEASATTR ) );
@@ -488,219 +495,6 @@ public class PwmPasswordRuleValidator
         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";
 
@@ -788,257 +582,376 @@ public class PwmPasswordRuleValidator
         return returnedErrors;
     }
 
-    @SuppressWarnings( "checkstyle:MethodLength" )
-    private static List<ErrorInformation> basicSyntaxRuleChecks(
+    public static List<ErrorInformation> basicSyntaxRuleChecks(
             final String password,
             final PwmPasswordPolicy policy,
             final UserInfo userInfo
-    ) throws PwmUnrecoverableException
+    )
+            throws PwmUnrecoverableException
     {
         final List<ErrorInformation> errorList = new ArrayList<>();
-        final PwmPasswordPolicy.RuleHelper ruleHelper = policy.getRuleHelper();
-        final PasswordCharCounter charCounter = new PasswordCharCounter( password );
+        final RuleCheckerHelper ruleCheckerHelper = new RuleCheckerHelper( policy, userInfo, policy.getRuleHelper(), new PasswordCharCounter( password ) );
 
-        final int passwordLength = password.length();
-
-        //Check minimum length
-        if ( passwordLength < ruleHelper.readIntValue( PwmPasswordRule.MinimumLength ) )
+        for ( final RuleChecker ruleChecker : BASIC_RULE_CHECKERS )
         {
-            errorList.add( new ErrorInformation( PwmError.PASSWORD_TOO_SHORT ) );
+            errorList.addAll( ruleChecker.test( password, ruleCheckerHelper ) );
         }
 
-        //Check maximum length
-        {
-            final int passwordMaximumLength = ruleHelper.readIntValue( PwmPasswordRule.MaximumLength );
+        return errorList;
+    }
+
+
+    private interface RuleChecker
+    {
+        List<ErrorInformation> test(
+                String password,
+                RuleCheckerHelper ruleCheckerHelper
+        )
+                throws PwmUnrecoverableException;
+    }
+
+    @Data
+    @AllArgsConstructor
+    private static class RuleCheckerHelper
+    {
+        private PwmPasswordPolicy policy;
+        private UserInfo userInfo;
+        private PasswordRuleHelper ruleHelper;
+        private PasswordCharCounter charCounter;
+    }
 
-            if ( passwordMaximumLength > 0 && passwordLength > passwordMaximumLength )
+    private static final List<RuleChecker> BASIC_RULE_CHECKERS = Collections.unmodifiableList( Arrays.asList(
+            new MinimumLengthRuleChecker(),
+            new MaximumLengthRuleChecker(),
+            new NumericLimitsRuleChecker(),
+            new AlphaLimitsRuleChecker(),
+            new CasingLimitsRuleChecker(),
+            new SpecialLimitsRuleChecker(),
+            new UniqueCharRuleChecker(),
+            new CharSequenceRuleChecker(),
+            new ActiveDirectoryRuleChecker()
+    ) );
+
+    private static class MinimumLengthRuleChecker implements RuleChecker
+    {
+        @Override
+        public List<ErrorInformation> test( final String password, final RuleCheckerHelper ruleCheckerHelper )
+                throws PwmUnrecoverableException
+        {
+            //Check minimum length
+            if ( password.length() < ruleCheckerHelper.getRuleHelper().readIntValue( PwmPasswordRule.MinimumLength ) )
             {
-                errorList.add( new ErrorInformation( PwmError.PASSWORD_TOO_LONG ) );
+                return Collections.singletonList( new ErrorInformation( PwmError.PASSWORD_TOO_SHORT ) );
             }
+            return Collections.emptyList();
         }
+    }
 
-        //check number of numeric characters
+    private static class MaximumLengthRuleChecker implements RuleChecker
+    {
+        @Override
+        public List<ErrorInformation> test( final String password, final RuleCheckerHelper ruleCheckerHelper )
+                throws PwmUnrecoverableException
         {
-            final int numberOfNumericChars = charCounter.getNumericCharCount();
-            if ( ruleHelper.readBooleanValue( PwmPasswordRule.AllowNumeric ) )
+            //Check maximum length
             {
-                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 ) );
-                }
+                final int passwordMaximumLength = ruleCheckerHelper.getRuleHelper().readIntValue( PwmPasswordRule.MaximumLength );
 
-                if ( !ruleHelper.readBooleanValue(
-                        PwmPasswordRule.AllowLastCharNumeric ) && charCounter.isLastNumeric() )
-                {
-                    errorList.add( new ErrorInformation( PwmError.PASSWORD_LAST_IS_NUMERIC ) );
-                }
-            }
-            else
-            {
-                if ( numberOfNumericChars > 0 )
+                if ( passwordMaximumLength > 0 && password.length() > passwordMaximumLength )
                 {
-                    errorList.add( new ErrorInformation( PwmError.PASSWORD_TOO_MANY_NUMERIC ) );
+                    return Collections.singletonList( new ErrorInformation( PwmError.PASSWORD_TOO_LONG ) );
                 }
             }
+            return Collections.emptyList();
         }
+    }
 
-        //check number of upper characters
+    private static class NumericLimitsRuleChecker implements RuleChecker
+    {
+        @Override
+        public List<ErrorInformation> test( final String password, final RuleCheckerHelper ruleCheckerHelper )
+                throws PwmUnrecoverableException
         {
-            final int numberOfUpperChars = charCounter.getUpperCharCount();
-            if ( numberOfUpperChars < ruleHelper.readIntValue( PwmPasswordRule.MinimumUpperCase ) )
+            //check number of numeric characters
+            final List<ErrorInformation> errorList = new ArrayList<>();
+            final PasswordRuleHelper ruleHelper = ruleCheckerHelper.getRuleHelper();
+            final PasswordCharCounter charCounter = ruleCheckerHelper.getCharCounter();
             {
-                errorList.add( new ErrorInformation( PwmError.PASSWORD_NOT_ENOUGH_UPPER ) );
-            }
+                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 maxUpper = ruleHelper.readIntValue( PwmPasswordRule.MaximumUpperCase );
-            if ( maxUpper > 0 && numberOfUpperChars > maxUpper )
-            {
-                errorList.add( new ErrorInformation( PwmError.PASSWORD_TOO_MANY_UPPER ) );
-            }
-        }
+                    final int maxNumeric = ruleHelper.readIntValue( PwmPasswordRule.MaximumNumeric );
+                    if ( maxNumeric > 0 && numberOfNumericChars > maxNumeric )
+                    {
+                        errorList.add( new ErrorInformation( PwmError.PASSWORD_TOO_MANY_NUMERIC ) );
+                    }
 
-        //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 ) );
-            }
+                    if ( !ruleHelper.readBooleanValue(
+                            PwmPasswordRule.AllowFirstCharNumeric ) && charCounter.isFirstNumeric() )
+                    {
+                        errorList.add( new ErrorInformation( PwmError.PASSWORD_FIRST_IS_NUMERIC ) );
+                    }
 
-            final int maxAlpha = ruleHelper.readIntValue( PwmPasswordRule.MaximumAlpha );
-            if ( maxAlpha > 0 && numberOfAlphaChars > maxAlpha )
-            {
-                errorList.add( new ErrorInformation( PwmError.PASSWORD_TOO_MANY_ALPHA ) );
+                    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 );
         }
+    }
 
-        //check number of non-alpha characters
+    private static class CasingLimitsRuleChecker implements RuleChecker
+    {
+        @Override
+        public List<ErrorInformation> test( final String password, final RuleCheckerHelper ruleCheckerHelper )
+                throws PwmUnrecoverableException
         {
-            final int numberOfNonAlphaChars = charCounter.getNonAlphaCharCount();
+            final List<ErrorInformation> errorList = new ArrayList<>();
+            final PasswordRuleHelper ruleHelper = ruleCheckerHelper.getRuleHelper();
+            final PasswordCharCounter charCounter = ruleCheckerHelper.getCharCounter();
 
-            if ( ruleHelper.readBooleanValue( PwmPasswordRule.AllowNonAlpha ) )
+            //check number of upper characters
             {
-                if ( numberOfNonAlphaChars < ruleHelper.readIntValue( PwmPasswordRule.MinimumNonAlpha ) )
+                final int numberOfUpperChars = charCounter.getUpperCharCount();
+                if ( numberOfUpperChars < ruleHelper.readIntValue( PwmPasswordRule.MinimumUpperCase ) )
                 {
-                    errorList.add( new ErrorInformation( PwmError.PASSWORD_NOT_ENOUGH_NONALPHA ) );
+                    errorList.add( new ErrorInformation( PwmError.PASSWORD_NOT_ENOUGH_UPPER ) );
                 }
 
-                final int maxNonAlpha = ruleHelper.readIntValue( PwmPasswordRule.MaximumNonAlpha );
-                if ( maxNonAlpha > 0 && numberOfNonAlphaChars > maxNonAlpha )
+                final int maxUpper = ruleHelper.readIntValue( PwmPasswordRule.MaximumUpperCase );
+                if ( maxUpper > 0 && numberOfUpperChars > maxUpper )
                 {
-                    errorList.add( new ErrorInformation( PwmError.PASSWORD_TOO_MANY_NONALPHA ) );
+                    errorList.add( new ErrorInformation( PwmError.PASSWORD_TOO_MANY_UPPER ) );
                 }
             }
-            else
+
+            //check number of lower characters
             {
-                if ( numberOfNonAlphaChars > 0 )
+                final int numberOfLowerChars = charCounter.getLowerCharCount();
+                if ( numberOfLowerChars < ruleHelper.readIntValue( PwmPasswordRule.MinimumLowerCase ) )
                 {
-                    errorList.add( new ErrorInformation( PwmError.PASSWORD_TOO_MANY_NONALPHA ) );
+                    errorList.add( new ErrorInformation( PwmError.PASSWORD_NOT_ENOUGH_LOWER ) );
                 }
-            }
-        }
-
-        //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 ) );
+                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 );
         }
+    }
 
-        //check number of special characters
+    private static class AlphaLimitsRuleChecker implements RuleChecker
+    {
+        @Override
+        public List<ErrorInformation> test( final String password, final RuleCheckerHelper ruleCheckerHelper )
+                throws PwmUnrecoverableException
         {
-            final int numberOfSpecialChars = charCounter.getSpecialCharsCount();
-            if ( ruleHelper.readBooleanValue( PwmPasswordRule.AllowSpecial ) )
+            final List<ErrorInformation> errorList = new ArrayList<>();
+            final PasswordRuleHelper ruleHelper = ruleCheckerHelper.getRuleHelper();
+            final PasswordCharCounter charCounter = ruleCheckerHelper.getCharCounter();
+
+            //check number of alpha characters
             {
-                if ( numberOfSpecialChars < ruleHelper.readIntValue( PwmPasswordRule.MinimumSpecial ) )
+                final int numberOfAlphaChars = charCounter.getAlphaCharCount();
+                if ( numberOfAlphaChars < ruleHelper.readIntValue( PwmPasswordRule.MinimumAlpha ) )
                 {
-                    errorList.add( new ErrorInformation( PwmError.PASSWORD_NOT_ENOUGH_SPECIAL ) );
+                    errorList.add( new ErrorInformation( PwmError.PASSWORD_NOT_ENOUGH_ALPHA ) );
                 }
 
-                final int maxSpecial = ruleHelper.readIntValue( PwmPasswordRule.MaximumSpecial );
-                if ( maxSpecial > 0 && numberOfSpecialChars > maxSpecial )
+                final int maxAlpha = ruleHelper.readIntValue( PwmPasswordRule.MaximumAlpha );
+                if ( maxAlpha > 0 && numberOfAlphaChars > maxAlpha )
                 {
-                    errorList.add( new ErrorInformation( PwmError.PASSWORD_TOO_MANY_SPECIAL ) );
+                    errorList.add( new ErrorInformation( PwmError.PASSWORD_TOO_MANY_ALPHA ) );
                 }
+            }
 
-                if ( !ruleHelper.readBooleanValue(
-                        PwmPasswordRule.AllowFirstCharSpecial ) && charCounter.isFirstSpecial() )
-                {
-                    errorList.add( new ErrorInformation( PwmError.PASSWORD_FIRST_IS_SPECIAL ) );
-                }
+            //check number of non-alpha characters
+            {
+                final int numberOfNonAlphaChars = charCounter.getNonAlphaCharCount();
 
-                if ( !ruleHelper.readBooleanValue(
-                        PwmPasswordRule.AllowLastCharSpecial ) && charCounter.isLastSpecial() )
+                if ( ruleHelper.readBooleanValue( PwmPasswordRule.AllowNonAlpha ) )
                 {
-                    errorList.add( new ErrorInformation( PwmError.PASSWORD_LAST_IS_SPECIAL ) );
+                    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 ( numberOfSpecialChars > 0 )
+                else
                 {
-                    errorList.add( new ErrorInformation( PwmError.PASSWORD_TOO_MANY_SPECIAL ) );
+                    if ( numberOfNonAlphaChars > 0 )
+                    {
+                        errorList.add( new ErrorInformation( PwmError.PASSWORD_TOO_MANY_NONALPHA ) );
+                    }
                 }
             }
+            return Collections.unmodifiableList( errorList );
         }
+    }
 
-        //Check maximum character repeats (sequential)
+    private static class SpecialLimitsRuleChecker implements RuleChecker
+    {
+        @Override
+        public List<ErrorInformation> test( final String password, final RuleCheckerHelper ruleCheckerHelper )
+                throws PwmUnrecoverableException
         {
-            final int maxSequentialRepeat = ruleHelper.readIntValue( PwmPasswordRule.MaximumSequentialRepeat );
-            if ( maxSequentialRepeat > 0 && charCounter.getSequentialRepeatedChars() > maxSequentialRepeat )
-            {
-                errorList.add( new ErrorInformation( PwmError.PASSWORD_TOO_MANY_REPEAT ) );
-            }
+            final List<ErrorInformation> errorList = new ArrayList<>();
+            final PasswordRuleHelper ruleHelper = ruleCheckerHelper.getRuleHelper();
+            final PasswordCharCounter charCounter = ruleCheckerHelper.getCharCounter();
 
-            //Check maximum character repeats (overall)
-            final int maxRepeat = ruleHelper.readIntValue( PwmPasswordRule.MaximumRepeat );
-            if ( maxRepeat > 0 && charCounter.getRepeatedChars() > maxRepeat )
+            //check number of special characters
             {
-                errorList.add( new ErrorInformation( PwmError.PASSWORD_TOO_MANY_REPEAT ) );
-            }
-        }
+                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 ) );
+                    }
 
-        //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 ) );
+                    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 );
         }
+    }
 
-        // check ad-complexity
+    private static class CharSequenceRuleChecker implements RuleChecker
+    {
+        @Override
+        public List<ErrorInformation> test( final String password, final RuleCheckerHelper ruleCheckerHelper )
+                throws PwmUnrecoverableException
         {
-            final ADPolicyComplexity complexityLevel = ruleHelper.getADComplexityLevel();
-            if ( complexityLevel == ADPolicyComplexity.AD2003 || complexityLevel == ADPolicyComplexity.AD2008 )
+            final List<ErrorInformation> errorList = new ArrayList<>();
+            final PasswordRuleHelper ruleHelper = ruleCheckerHelper.getRuleHelper();
+            final PasswordCharCounter charCounter = ruleCheckerHelper.getCharCounter();
+
+            //Check maximum character repeats (sequential)
             {
-                final int maxGroupViolations = ruleHelper.readIntValue( PwmPasswordRule.ADComplexityMaxViolations );
-                errorList.addAll( checkPasswordForADComplexity( complexityLevel, userInfo, password, charCounter,
-                        maxGroupViolations ) );
+                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 ( tooManyConsecutiveChars( password, maximumConsecutive ) )
+            // check consecutive characters
             {
-                errorList.add( new ErrorInformation( PwmError.PASSWORD_TOO_MANY_CONSECUTIVE ) );
+                final int maximumConsecutive = ruleHelper.readIntValue( PwmPasswordRule.MaximumConsecutive );
+                if ( PwmPasswordRuleUtil.tooManyConsecutiveChars( password, maximumConsecutive ) )
+                {
+                    errorList.add( new ErrorInformation( PwmError.PASSWORD_TOO_MANY_CONSECUTIVE ) );
+                }
             }
-        }
 
-        return errorList;
+            return Collections.unmodifiableList( errorList );
+        }
     }
 
-    public static boolean tooManyConsecutiveChars( final String str, final int maximumConsecutive )
+    private static class UniqueCharRuleChecker implements RuleChecker
     {
-        if ( str != null && maximumConsecutive > 1 && str.length() >= maximumConsecutive )
+        @Override
+        public List<ErrorInformation> test( final String password, final RuleCheckerHelper ruleCheckerHelper )
+                throws PwmUnrecoverableException
         {
-            final int[] codePoints = StringUtil.toCodePointArray( str.toLowerCase() );
-
-            int lastCodePoint = -1;
-            int consecutiveCharCount = 1;
+            final List<ErrorInformation> errorList = new ArrayList<>();
+            final PasswordRuleHelper ruleHelper = ruleCheckerHelper.getRuleHelper();
+            final PasswordCharCounter charCounter = ruleCheckerHelper.getCharCounter();
 
-            for ( int i = 0; i < codePoints.length; i++ )
+            //Check minimum unique character
             {
-                if ( codePoints[ i ] == lastCodePoint + 1 )
-                {
-                    consecutiveCharCount++;
-                }
-                else
+                final int minUnique = ruleHelper.readIntValue( PwmPasswordRule.MinimumUnique );
+                if ( minUnique > 0 && charCounter.getUniqueChars() < minUnique )
                 {
-                    consecutiveCharCount = 1;
+                    errorList.add( new ErrorInformation( PwmError.PASSWORD_NOT_ENOUGH_UNIQUE ) );
                 }
+            }
 
-                lastCodePoint = codePoints[ i ];
+            return Collections.unmodifiableList( errorList );
+        }
+    }
 
-                if ( consecutiveCharCount == maximumConsecutive )
+    private static class ActiveDirectoryRuleChecker implements RuleChecker
+    {
+        @Override
+        public List<ErrorInformation> test( final String password, final RuleCheckerHelper ruleCheckerHelper )
+                throws PwmUnrecoverableException
+        {
+            final List<ErrorInformation> errorList = new ArrayList<>();
+            final PasswordRuleHelper ruleHelper = ruleCheckerHelper.getRuleHelper();
+            final PasswordCharCounter charCounter = ruleCheckerHelper.getCharCounter();
+
+            // check ad-complexity
+            {
+                final ADPolicyComplexity complexityLevel = ruleHelper.getADComplexityLevel();
+                if ( complexityLevel == ADPolicyComplexity.AD2003 || complexityLevel == ADPolicyComplexity.AD2008 )
                 {
-                    return true;
+                    final int maxGroupViolations = ruleHelper.readIntValue( PwmPasswordRule.ADComplexityMaxViolations );
+                    errorList.addAll( PwmPasswordRuleUtil.checkPasswordForADComplexity(
+                            complexityLevel,
+                            ruleCheckerHelper.getUserInfo(),
+                            password,
+                            charCounter,
+                            maxGroupViolations ) );
                 }
             }
-        }
 
-        return false;
+            return Collections.unmodifiableList( errorList );
+        }
     }
 }
+

+ 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();
     }
 }

+ 5 - 9
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>
@@ -3074,9 +3072,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>

+ 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/PasswordRuleHelperTest.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.PasswordRuleHelper;
 
 import java.util.List;
 import java.util.regex.Pattern;
 
-public class RuleHelperTest
+public class PasswordRuleHelperTest
 {
     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 PasswordRuleHelper ruleHelper = Mockito.mock( PasswordRuleHelper.class );
 
     @Before
     public void setUp() throws Exception

+ 96 - 74
server/src/test/java/password/pwm/util/PwmPasswordRuleValidatorTest.java

@@ -24,99 +24,121 @@ package password.pwm.util;
 
 import org.junit.Assert;
 import org.junit.Test;
+import password.pwm.config.profile.PwmPasswordPolicy;
+import password.pwm.config.profile.PwmPasswordRule;
+import password.pwm.util.password.PwmPasswordRuleUtil;
+import password.pwm.util.password.PwmPasswordRuleValidator;
+
+import java.util.HashMap;
+import java.util.Map;
 
 public class PwmPasswordRuleValidatorTest
 {
+    @Test
+    public void complexPolicyTest()
+            throws Exception
+    {
+        final Map<String, String> policyMap = new HashMap<>( PwmPasswordPolicy.defaultPolicy().getPolicyMap() );
+        policyMap.put( PwmPasswordRule.MinimumLength.getKey(), "3" );
+        policyMap.put( PwmPasswordRule.AllowNumeric.getKey(), "true" );
+
+        final PwmPasswordPolicy pwmPasswordPolicy = PwmPasswordPolicy.createPwmPasswordPolicy( policyMap );
+
+        final PwmPasswordRuleValidator pwmPasswordRuleValidator = new PwmPasswordRuleValidator( null, pwmPasswordPolicy );
+        Assert.assertTrue( pwmPasswordRuleValidator.testPassword( new PasswordData( "123" ), null, null, null ) );
+
+    }
+
     @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 ) );
+        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( 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 ) );
+        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( 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 ) );
+        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( PwmPasswordRuleValidator.tooManyConsecutiveChars( null, 4 ) );
-        Assert.assertFalse( PwmPasswordRuleValidator.tooManyConsecutiveChars( "", 4 ) );
+        Assert.assertFalse( PwmPasswordRuleUtil.tooManyConsecutiveChars( null, 4 ) );
+        Assert.assertFalse( PwmPasswordRuleUtil.tooManyConsecutiveChars( "", 4 ) );
 
-        Assert.assertFalse( PwmPasswordRuleValidator.tooManyConsecutiveChars( "12345678", 0 ) );
-        Assert.assertFalse( PwmPasswordRuleValidator.tooManyConsecutiveChars( "novell", 0 ) );
+        Assert.assertFalse( PwmPasswordRuleUtil.tooManyConsecutiveChars( "12345678", 0 ) );
+        Assert.assertFalse( PwmPasswordRuleUtil.tooManyConsecutiveChars( "novell", 0 ) );
 
-        Assert.assertFalse( PwmPasswordRuleValidator.tooManyConsecutiveChars( "novell", 1 ) );
+        Assert.assertFalse( PwmPasswordRuleUtil.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 ) );
+        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 ) );
     }
 }

+ 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() );
+    }
+}

+ 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/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'] || [];