Browse Source

Password rule verification test cases

jrivard@gmail.com 6 years ago
parent
commit
20f8eef1ea
28 changed files with 1126 additions and 735 deletions
  1. 1 0
      server/src/main/java/password/pwm/AppProperty.java
  2. 4 4
      server/src/main/java/password/pwm/config/profile/PwmPasswordPolicy.java
  3. 22 0
      server/src/main/java/password/pwm/error/ErrorInformation.java
  4. 1 1
      server/src/main/java/password/pwm/health/LDAPHealthChecker.java
  5. 1 1
      server/src/main/java/password/pwm/http/servlet/GuestRegistrationServlet.java
  6. 6 2
      server/src/main/java/password/pwm/http/servlet/changepw/ChangePasswordServlet.java
  7. 1 1
      server/src/main/java/password/pwm/http/servlet/configeditor/ConfigEditorServlet.java
  8. 1 0
      server/src/main/java/password/pwm/http/servlet/configguide/ConfigGuideServlet.java
  9. 1 1
      server/src/main/java/password/pwm/http/servlet/forgottenpw/ForgottenPasswordUtil.java
  10. 1 1
      server/src/main/java/password/pwm/http/servlet/helpdesk/HelpdeskServlet.java
  11. 1 1
      server/src/main/java/password/pwm/http/servlet/newuser/NewUserUtils.java
  12. 2 2
      server/src/main/java/password/pwm/http/tag/PasswordRequirementsTag.java
  13. 1 1
      server/src/main/java/password/pwm/ldap/auth/LDAPAuthenticationRequest.java
  14. 1 1
      server/src/main/java/password/pwm/util/operations/PasswordUtility.java
  15. 1 1
      server/src/main/java/password/pwm/util/password/PasswordCharCounter.java
  16. 781 0
      server/src/main/java/password/pwm/util/password/PasswordRuleChecks.java
  17. 3 3
      server/src/main/java/password/pwm/util/password/PasswordRuleReaderHelper.java
  18. 0 1
      server/src/main/java/password/pwm/util/password/PwmPasswordRuleUtil.java
  19. 5 671
      server/src/main/java/password/pwm/util/password/PwmPasswordRuleValidator.java
  20. 2 13
      server/src/main/java/password/pwm/util/password/RandomPasswordGenerator.java
  21. 1 1
      server/src/main/java/password/pwm/ws/server/rest/RestRandomPasswordServer.java
  22. 1 1
      server/src/main/java/password/pwm/ws/server/rest/RestSetPasswordServer.java
  23. 1 0
      server/src/main/resources/password/pwm/AppProperty.properties
  24. 3 3
      server/src/test/java/password/pwm/config/profile/PasswordRuleReaderHelperTest.java
  25. 21 2
      server/src/test/java/password/pwm/util/localdb/TestHelper.java
  26. 187 0
      server/src/test/java/password/pwm/util/password/PasswordRuleChecksTest.java
  27. 1 23
      server/src/test/java/password/pwm/util/password/PwmPasswordRuleValidatorTest.java
  28. 75 0
      server/src/test/java/password/pwm/util/password/RandomPasswordGeneratorTest.java

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

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

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

@@ -32,7 +32,7 @@ import password.pwm.health.HealthRecord;
 import password.pwm.util.java.JsonUtil;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.logging.PwmLogger;
-import password.pwm.util.password.PasswordRuleHelper;
+import password.pwm.util.password.PasswordRuleReaderHelper;
 
 import java.io.Serializable;
 import java.util.ArrayList;
@@ -142,9 +142,9 @@ public class PwmPasswordPolicy implements Profile, Serializable
         return chaiPasswordPolicy;
     }
 
-    public PasswordRuleHelper getRuleHelper( )
+    public PasswordRuleReaderHelper getRuleHelper( )
     {
-        return new PasswordRuleHelper( this );
+        return new PasswordRuleReaderHelper( this );
     }
 
     public String getValue( final PwmPasswordRule rule )
@@ -328,7 +328,7 @@ public class PwmPasswordPolicy implements Profile, Serializable
 
     public List<HealthRecord> health( final Locale locale )
     {
-        final PasswordRuleHelper ruleHelper = this.getRuleHelper();
+        final PasswordRuleReaderHelper ruleHelper = this.getRuleHelper();
         final List<HealthRecord> returnList = new ArrayList<>();
         final Map<PwmPasswordRule, PwmPasswordRule> rulePairs = new LinkedHashMap<>();
         rulePairs.put( PwmPasswordRule.MinimumLength, PwmPasswordRule.MaximumLength );

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@@ -28,7 +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.util.password.PasswordRuleReaderHelper;
 import password.pwm.error.PwmException;
 import password.pwm.http.PwmRequest;
 import password.pwm.http.PwmSession;
@@ -72,7 +72,7 @@ public class PasswordRequirementsTag extends TagSupport
         final ADPolicyComplexity adPolicyLevel = pwordPolicy.getRuleHelper().getADComplexityLevel();
 
 
-        final PasswordRuleHelper ruleHelper = pwordPolicy.getRuleHelper();
+        final PasswordRuleReaderHelper ruleHelper = pwordPolicy.getRuleHelper();
 
         if ( ruleHelper.readBooleanValue( PwmPasswordRule.CaseSensitive ) )
         {

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

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

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

@@ -79,7 +79,7 @@ import password.pwm.svc.event.HelpdeskAuditRecord;
 import password.pwm.svc.stats.AvgStatistic;
 import password.pwm.svc.stats.EpsStatistic;
 import password.pwm.svc.stats.Statistic;
-import password.pwm.util.PasswordCharCounter;
+import password.pwm.util.password.PasswordCharCounter;
 import password.pwm.util.PasswordData;
 import password.pwm.util.PostChangePasswordAction;
 import password.pwm.util.password.PwmPasswordRuleValidator;

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

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

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

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

+ 3 - 3
server/src/main/java/password/pwm/util/password/PasswordRuleHelper.java → server/src/main/java/password/pwm/util/password/PasswordRuleReaderHelper.java

@@ -38,9 +38,9 @@ import java.util.List;
 import java.util.regex.Pattern;
 import java.util.regex.PatternSyntaxException;
 
-public class PasswordRuleHelper
+public class PasswordRuleReaderHelper
 {
-    private static final PwmLogger LOGGER = PwmLogger.forClass( PasswordRuleHelper.class );
+    private static final PwmLogger LOGGER = PwmLogger.forClass( PasswordRuleReaderHelper.class );
 
     public enum Flag
     {
@@ -50,7 +50,7 @@ public class PasswordRuleHelper
     private final PwmPasswordPolicy passwordPolicy;
     private final com.novell.ldapchai.util.PasswordRuleHelper chaiRuleHelper;
 
-    public PasswordRuleHelper( final PwmPasswordPolicy passwordPolicy )
+    public PasswordRuleReaderHelper( final PwmPasswordPolicy passwordPolicy )
     {
         this.passwordPolicy = passwordPolicy;
         chaiRuleHelper = DefaultChaiPasswordPolicy.createDefaultChaiPasswordPolicy( passwordPolicy.getPolicyMap() ).getRuleHelper();

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

@@ -28,7 +28,6 @@ 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;

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

@@ -27,10 +27,6 @@ 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;
 import password.pwm.PwmApplication;
 import password.pwm.PwmConstants;
@@ -38,7 +34,6 @@ import password.pwm.bean.SessionLabel;
 import password.pwm.bean.pub.PublicUserInfoBean;
 import password.pwm.config.Configuration;
 import password.pwm.config.PwmSetting;
-import password.pwm.config.option.ADPolicyComplexity;
 import password.pwm.config.profile.PwmPasswordPolicy;
 import password.pwm.config.profile.PwmPasswordRule;
 import password.pwm.error.ErrorInformation;
@@ -47,35 +42,26 @@ import password.pwm.error.PwmError;
 import password.pwm.error.PwmOperationalException;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.ldap.UserInfo;
-import password.pwm.svc.PwmService;
 import password.pwm.svc.stats.Statistic;
-import password.pwm.util.PasswordCharCounter;
 import password.pwm.util.PasswordData;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.JsonUtil;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.macro.MacroMachine;
-import password.pwm.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;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
-import java.util.Set;
-import java.util.regex.Pattern;
 
 public class PwmPasswordRuleValidator
 {
 
     private static final PwmLogger LOGGER = PwmLogger.forClass( PwmPasswordRuleValidator.class );
 
-    private static final boolean EXTRA_LOGGING = false;
-
     private final PwmApplication pwmApplication;
     private final PwmPasswordPolicy policy;
     private final Locale locale;
@@ -201,300 +187,20 @@ public class PwmPasswordRuleValidator
     {
         final String passwordString = password == null ? "" : password.getStringValue();
         final String oldPasswordString = oldPassword == null ? null : oldPassword.getStringValue();
-        return internalPwmPolicyValidator( passwordString, oldPasswordString, userInfo );
+        return PasswordRuleChecks.extendedPolicyRuleChecker( pwmApplication, policy, passwordString, oldPasswordString, userInfo, flags );
     }
 
-    @SuppressWarnings( "checkstyle:MethodLength" )
     public List<ErrorInformation> internalPwmPolicyValidator(
-            final String passwordString,
-            final String oldPasswordString,
+            final String password,
+            final String oldPassword,
             final UserInfo userInfo
     )
             throws PwmUnrecoverableException
     {
-        final boolean failFast = JavaHelper.enumArrayContainsValue( flags, Flag.FailFast );
-
-        // null check
-        if ( passwordString == null )
-        {
-            return Collections.singletonList( new ErrorInformation(
-                    PwmError.ERROR_INTERNAL,
-                    "empty (null) new password" ) );
-        }
-
-        final List<ErrorInformation> errorList = new ArrayList<>();
-        final PasswordRuleHelper ruleHelper = policy.getRuleHelper();
-        final MacroMachine macroMachine = userInfo == null || userInfo.getUserIdentity() == null
-                ? MacroMachine.forNonUserSpecific( pwmApplication, SessionLabel.SYSTEM_LABEL )
-                : MacroMachine.forUser(
-                pwmApplication,
-                PwmConstants.DEFAULT_LOCALE,
-                SessionLabel.SYSTEM_LABEL,
-                userInfo.getUserIdentity()
-        );
-
-        //check against old password
-        if ( oldPasswordString != null
-                && oldPasswordString.length() > 0
-                && ruleHelper.readBooleanValue( PwmPasswordRule.DisallowCurrent ) )
-        {
-            if ( oldPasswordString.length() > 0 )
-            {
-                if ( oldPasswordString.equalsIgnoreCase( passwordString ) )
-                {
-                    errorList.add( new ErrorInformation( PwmError.PASSWORD_SAMEASOLD ) );
-                }
-            }
-
-            //check chars from old password
-            final int maxOldAllowed = ruleHelper.readIntValue( PwmPasswordRule.MaximumOldChars );
-            if ( maxOldAllowed > 0 )
-            {
-                if ( oldPasswordString.length() > 0 )
-                {
-                    final String lPassword = passwordString.toLowerCase();
-                    final Set<Character> dupeChars = new HashSet<>();
-
-                    //add all dupes to the set.
-                    for ( final char loopChar : oldPasswordString.toLowerCase().toCharArray() )
-                    {
-                        if ( lPassword.indexOf( loopChar ) != -1 )
-                        {
-                            dupeChars.add( loopChar );
-                        }
-                    }
-
-                    //count the number of (unique) set elements.
-                    if ( dupeChars.size() >= maxOldAllowed )
-                    {
-                        errorList.add( new ErrorInformation( PwmError.PASSWORD_TOO_MANY_OLD_CHARS ) );
-                    }
-                }
-            }
-        }
-
-        if ( failFast && errorList.size() > 1 )
-        {
-            return errorList;
-        }
-
-        errorList.addAll( basicSyntaxRuleChecks( passwordString, policy, userInfo ) );
-
-        if ( failFast && errorList.size() > 1 )
-        {
-            return errorList;
-        }
-
-        // check against disallowed values;
-        if ( !ruleHelper.getDisallowedValues().isEmpty() )
-        {
-            final String lcasePwd = passwordString.toLowerCase();
-            final Set<String> paramValues = new HashSet<>( ruleHelper.getDisallowedValues() );
-
-            for ( final String loopValue : paramValues )
-            {
-                if ( loopValue != null && loopValue.length() > 0 )
-                {
-                    final String expandedValue = macroMachine.expandMacros( loopValue );
-                    if ( StringUtils.isNotBlank( expandedValue ) )
-                    {
-                        final String loweredLoop = expandedValue.toLowerCase();
-                        if ( lcasePwd.contains( loweredLoop ) )
-                        {
-                            errorList.add( new ErrorInformation( PwmError.PASSWORD_USING_DISALLOWED ) );
-                        }
-                    }
-                }
-            }
-        }
-
-        if ( failFast && errorList.size() > 1 )
-        {
-            return errorList;
-        }
-
-        // check disallowed attributes.
-        if ( !policy.getRuleHelper().getDisallowedAttributes().isEmpty() )
-        {
-            final List<String> paramConfigs = policy.getRuleHelper().getDisallowedAttributes( PasswordRuleHelper.Flag.KeepThresholds );
-            if ( userInfo != null )
-            {
-                final Map<String, String> userValues = userInfo.getCachedPasswordRuleAttributes();
-
-                for ( final String paramConfig : paramConfigs )
-                {
-                    final String[] parts = paramConfig.split( ":" );
-
-                    final String attrName = parts[ 0 ];
-                    final String disallowedValue = StringUtils.defaultString( userValues.get( attrName ) );
-                    final int threshold = parts.length > 1 ? NumberUtils.toInt( parts[ 1 ] ) : 0;
-
-                    if ( PwmPasswordRuleUtil.containsDisallowedValue( passwordString, disallowedValue, threshold ) )
-                    {
-                        LOGGER.trace( () -> "password rejected, same as user attr " + attrName );
-                        errorList.add( new ErrorInformation( PwmError.PASSWORD_SAMEASATTR ) );
-                    }
-                }
-            }
-        }
-
-        if ( failFast && errorList.size() > 1 )
-        {
-            return errorList;
-        }
-
-        {
-            // check password strength
-            final int requiredPasswordStrength = ruleHelper.readIntValue( PwmPasswordRule.MinimumStrength );
-            if ( requiredPasswordStrength > 0 )
-            {
-                if ( pwmApplication != null )
-                {
-                    final int passwordStrength = PasswordUtility.judgePasswordStrength(
-                            pwmApplication.getConfig(),
-                            passwordString
-                    );
-                    if ( passwordStrength < requiredPasswordStrength )
-                    {
-                        errorList.add( new ErrorInformation( PwmError.PASSWORD_TOO_WEAK ) );
-                        if ( EXTRA_LOGGING )
-                        {
-                            LOGGER.trace( () -> "password rejected, password strength of "
-                                    + passwordStrength + " is lower than policy requirement of "
-                                    + requiredPasswordStrength );
-                        }
-                    }
-                }
-            }
-        }
-
-        if ( failFast && errorList.size() > 1 )
-        {
-            return errorList;
-        }
-
-        // check regex matches.
-        for ( final Pattern pattern : ruleHelper.getRegExMatch( macroMachine ) )
-        {
-            if ( !pattern.matcher( passwordString ).matches() )
-            {
-                errorList.add( new ErrorInformation( PwmError.PASSWORD_INVALID_CHAR ) );
-                if ( EXTRA_LOGGING )
-                {
-                    LOGGER.trace( () -> "password rejected, does not match configured regex pattern: " + pattern.toString() );
-                }
-            }
-        }
-
-        if ( failFast && errorList.size() > 1 )
-        {
-            return errorList;
-        }
-
-        // check no-regex matches.
-        for ( final Pattern pattern : ruleHelper.getRegExNoMatch( macroMachine ) )
-        {
-            if ( pattern.matcher( passwordString ).matches() )
-            {
-                errorList.add( new ErrorInformation( PwmError.PASSWORD_INVALID_CHAR ) );
-                if ( EXTRA_LOGGING )
-                {
-                    LOGGER.trace( () -> "password rejected, matches configured no-regex pattern: " + pattern.toString() );
-                }
-            }
-        }
-
-        if ( failFast && errorList.size() > 1 )
-        {
-            return errorList;
-        }
-
-        // check char group matches
-        if ( ruleHelper.readIntValue( PwmPasswordRule.CharGroupsMinMatch ) > 0 )
-        {
-            final List<Pattern> ruleGroups = ruleHelper.getCharGroupValues();
-            if ( ruleGroups != null && !ruleGroups.isEmpty() )
-            {
-                final int requiredMatches = ruleHelper.readIntValue( PwmPasswordRule.CharGroupsMinMatch );
-                int matches = 0;
-                for ( final Pattern pattern : ruleGroups )
-                {
-                    if ( pattern.matcher( passwordString ).find() )
-                    {
-                        matches++;
-                    }
-                }
-                if ( matches < requiredMatches )
-                {
-                    errorList.add( new ErrorInformation( PwmError.PASSWORD_NOT_ENOUGH_GROUPS ) );
-                }
-            }
-            if ( failFast && errorList.size() > 1 )
-            {
-                return errorList;
-            }
-        }
-
-        if ( failFast && errorList.size() > 1 )
-        {
-            return errorList;
-        }
-
-        // check if the password is in the dictionary.
-        if ( ruleHelper.readBooleanValue( PwmPasswordRule.EnableWordlist ) )
-        {
-            if ( pwmApplication != null )
-            {
-                if ( pwmApplication.getWordlistManager() != null && pwmApplication.getWordlistManager().status() == PwmService.STATUS.OPEN )
-                {
-                    final boolean found = pwmApplication.getWordlistManager().containsWord( passwordString );
-
-                    if ( found )
-                    {
-                        //LOGGER.trace(pwmSession, "password rejected, in wordlist file");
-                        errorList.add( new ErrorInformation( PwmError.PASSWORD_INWORDLIST ) );
-                    }
-                }
-                else
-                {
-                    /* noop */
-                    //LOGGER.warn(pwmSession, "password wordlist checking enabled, but wordlist is not available, skipping wordlist check");
-                }
-            }
-            if ( failFast && errorList.size() > 1 )
-            {
-                return errorList;
-            }
-        }
-
-        if ( failFast && errorList.size() > 1 )
-        {
-            return errorList;
-        }
-
-        // check for shared (global) password history
-        if ( pwmApplication != null )
-        {
-            if ( pwmApplication.getConfig().readSettingAsBoolean( PwmSetting.PASSWORD_SHAREDHISTORY_ENABLE )
-                    && pwmApplication.getSharedHistoryManager().status() == PwmService.STATUS.OPEN )
-            {
-                final boolean found = pwmApplication.getSharedHistoryManager().containsWord( passwordString );
-
-                if ( found )
-                {
-                    //LOGGER.trace(pwmSession, "password rejected, in global shared history");
-                    errorList.add( new ErrorInformation( PwmError.PASSWORD_INWORDLIST ) );
-                }
-            }
-            if ( failFast && errorList.size() > 1 )
-            {
-                return errorList;
-            }
-        }
-
-        return errorList;
+        return PasswordRuleChecks.extendedPolicyRuleChecker( pwmApplication, policy, password, oldPassword, userInfo, flags );
     }
 
+
     private static final String REST_RESPONSE_KEY_ERROR = "error";
     private static final String REST_RESPONSE_KEY_ERROR_MSG = "errorMessage";
 
@@ -581,377 +287,5 @@ public class PwmPasswordRuleValidator
         }
         return returnedErrors;
     }
-
-    public static List<ErrorInformation> basicSyntaxRuleChecks(
-            final String password,
-            final PwmPasswordPolicy policy,
-            final UserInfo userInfo
-    )
-            throws PwmUnrecoverableException
-    {
-        final List<ErrorInformation> errorList = new ArrayList<>();
-        final RuleCheckerHelper ruleCheckerHelper = new RuleCheckerHelper( policy, userInfo, policy.getRuleHelper(), new PasswordCharCounter( password ) );
-
-        for ( final RuleChecker ruleChecker : BASIC_RULE_CHECKERS )
-        {
-            errorList.addAll( ruleChecker.test( password, ruleCheckerHelper ) );
-        }
-
-        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;
-    }
-
-    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 ) )
-            {
-                return Collections.singletonList( new ErrorInformation( PwmError.PASSWORD_TOO_SHORT ) );
-            }
-            return Collections.emptyList();
-        }
-    }
-
-    private static class MaximumLengthRuleChecker implements RuleChecker
-    {
-        @Override
-        public List<ErrorInformation> test( final String password, final RuleCheckerHelper ruleCheckerHelper )
-                throws PwmUnrecoverableException
-        {
-            //Check maximum length
-            {
-                final int passwordMaximumLength = ruleCheckerHelper.getRuleHelper().readIntValue( PwmPasswordRule.MaximumLength );
-
-                if ( passwordMaximumLength > 0 && password.length() > passwordMaximumLength )
-                {
-                    return Collections.singletonList( new ErrorInformation( PwmError.PASSWORD_TOO_LONG ) );
-                }
-            }
-            return Collections.emptyList();
-        }
-    }
-
-    private static class NumericLimitsRuleChecker implements RuleChecker
-    {
-        @Override
-        public List<ErrorInformation> test( final String password, final RuleCheckerHelper ruleCheckerHelper )
-                throws PwmUnrecoverableException
-        {
-            //check number of numeric characters
-            final List<ErrorInformation> errorList = new ArrayList<>();
-            final PasswordRuleHelper ruleHelper = ruleCheckerHelper.getRuleHelper();
-            final PasswordCharCounter charCounter = ruleCheckerHelper.getCharCounter();
-            {
-                final int numberOfNumericChars = charCounter.getNumericCharCount();
-                if ( ruleHelper.readBooleanValue( PwmPasswordRule.AllowNumeric ) )
-                {
-                    if ( numberOfNumericChars < ruleHelper.readIntValue( PwmPasswordRule.MinimumNumeric ) )
-                    {
-                        errorList.add( new ErrorInformation( PwmError.PASSWORD_NOT_ENOUGH_NUM ) );
-                    }
-
-                    final int maxNumeric = ruleHelper.readIntValue( PwmPasswordRule.MaximumNumeric );
-                    if ( maxNumeric > 0 && numberOfNumericChars > maxNumeric )
-                    {
-                        errorList.add( new ErrorInformation( PwmError.PASSWORD_TOO_MANY_NUMERIC ) );
-                    }
-
-                    if ( !ruleHelper.readBooleanValue(
-                            PwmPasswordRule.AllowFirstCharNumeric ) && charCounter.isFirstNumeric() )
-                    {
-                        errorList.add( new ErrorInformation( PwmError.PASSWORD_FIRST_IS_NUMERIC ) );
-                    }
-
-                    if ( !ruleHelper.readBooleanValue(
-                            PwmPasswordRule.AllowLastCharNumeric ) && charCounter.isLastNumeric() )
-                    {
-                        errorList.add( new ErrorInformation( PwmError.PASSWORD_LAST_IS_NUMERIC ) );
-                    }
-                }
-                else
-                {
-                    if ( numberOfNumericChars > 0 )
-                    {
-                        errorList.add( new ErrorInformation( PwmError.PASSWORD_TOO_MANY_NUMERIC ) );
-                    }
-                }
-            }
-            return Collections.unmodifiableList( errorList );
-        }
-    }
-
-    private static class CasingLimitsRuleChecker implements RuleChecker
-    {
-        @Override
-        public List<ErrorInformation> test( final String password, final RuleCheckerHelper ruleCheckerHelper )
-                throws PwmUnrecoverableException
-        {
-            final List<ErrorInformation> errorList = new ArrayList<>();
-            final PasswordRuleHelper ruleHelper = ruleCheckerHelper.getRuleHelper();
-            final PasswordCharCounter charCounter = ruleCheckerHelper.getCharCounter();
-
-            //check number of upper characters
-            {
-                final int numberOfUpperChars = charCounter.getUpperCharCount();
-                if ( numberOfUpperChars < ruleHelper.readIntValue( PwmPasswordRule.MinimumUpperCase ) )
-                {
-                    errorList.add( new ErrorInformation( PwmError.PASSWORD_NOT_ENOUGH_UPPER ) );
-                }
-
-                final int maxUpper = ruleHelper.readIntValue( PwmPasswordRule.MaximumUpperCase );
-                if ( maxUpper > 0 && numberOfUpperChars > maxUpper )
-                {
-                    errorList.add( new ErrorInformation( PwmError.PASSWORD_TOO_MANY_UPPER ) );
-                }
-            }
-
-            //check number of lower characters
-            {
-                final int numberOfLowerChars = charCounter.getLowerCharCount();
-                if ( numberOfLowerChars < ruleHelper.readIntValue( PwmPasswordRule.MinimumLowerCase ) )
-                {
-                    errorList.add( new ErrorInformation( PwmError.PASSWORD_NOT_ENOUGH_LOWER ) );
-                }
-
-                final int maxLower = ruleHelper.readIntValue( PwmPasswordRule.MaximumLowerCase );
-                if ( maxLower > 0 && numberOfLowerChars > maxLower )
-                {
-                    errorList.add( new ErrorInformation( PwmError.PASSWORD_TOO_MANY_UPPER ) );
-                }
-            }
-            return Collections.unmodifiableList( errorList );
-        }
-    }
-
-    private static class AlphaLimitsRuleChecker implements RuleChecker
-    {
-        @Override
-        public List<ErrorInformation> test( final String password, final RuleCheckerHelper ruleCheckerHelper )
-                throws PwmUnrecoverableException
-        {
-            final List<ErrorInformation> errorList = new ArrayList<>();
-            final PasswordRuleHelper ruleHelper = ruleCheckerHelper.getRuleHelper();
-            final PasswordCharCounter charCounter = ruleCheckerHelper.getCharCounter();
-
-            //check number of alpha characters
-            {
-                final int numberOfAlphaChars = charCounter.getAlphaCharCount();
-                if ( numberOfAlphaChars < ruleHelper.readIntValue( PwmPasswordRule.MinimumAlpha ) )
-                {
-                    errorList.add( new ErrorInformation( PwmError.PASSWORD_NOT_ENOUGH_ALPHA ) );
-                }
-
-                final int maxAlpha = ruleHelper.readIntValue( PwmPasswordRule.MaximumAlpha );
-                if ( maxAlpha > 0 && numberOfAlphaChars > maxAlpha )
-                {
-                    errorList.add( new ErrorInformation( PwmError.PASSWORD_TOO_MANY_ALPHA ) );
-                }
-            }
-
-            //check number of non-alpha characters
-            {
-                final int numberOfNonAlphaChars = charCounter.getNonAlphaCharCount();
-
-                if ( ruleHelper.readBooleanValue( PwmPasswordRule.AllowNonAlpha ) )
-                {
-                    if ( numberOfNonAlphaChars < ruleHelper.readIntValue( PwmPasswordRule.MinimumNonAlpha ) )
-                    {
-                        errorList.add( new ErrorInformation( PwmError.PASSWORD_NOT_ENOUGH_NONALPHA ) );
-                    }
-
-                    final int maxNonAlpha = ruleHelper.readIntValue( PwmPasswordRule.MaximumNonAlpha );
-                    if ( maxNonAlpha > 0 && numberOfNonAlphaChars > maxNonAlpha )
-                    {
-                        errorList.add( new ErrorInformation( PwmError.PASSWORD_TOO_MANY_NONALPHA ) );
-                    }
-                }
-                else
-                {
-                    if ( numberOfNonAlphaChars > 0 )
-                    {
-                        errorList.add( new ErrorInformation( PwmError.PASSWORD_TOO_MANY_NONALPHA ) );
-                    }
-                }
-            }
-            return Collections.unmodifiableList( errorList );
-        }
-    }
-
-    private static class SpecialLimitsRuleChecker implements RuleChecker
-    {
-        @Override
-        public List<ErrorInformation> test( final String password, final RuleCheckerHelper ruleCheckerHelper )
-                throws PwmUnrecoverableException
-        {
-            final List<ErrorInformation> errorList = new ArrayList<>();
-            final PasswordRuleHelper ruleHelper = ruleCheckerHelper.getRuleHelper();
-            final PasswordCharCounter charCounter = ruleCheckerHelper.getCharCounter();
-
-            //check number of special characters
-            {
-                final int numberOfSpecialChars = charCounter.getSpecialCharsCount();
-                if ( ruleHelper.readBooleanValue( PwmPasswordRule.AllowSpecial ) )
-                {
-                    if ( numberOfSpecialChars < ruleHelper.readIntValue( PwmPasswordRule.MinimumSpecial ) )
-                    {
-                        errorList.add( new ErrorInformation( PwmError.PASSWORD_NOT_ENOUGH_SPECIAL ) );
-                    }
-
-                    final int maxSpecial = ruleHelper.readIntValue( PwmPasswordRule.MaximumSpecial );
-                    if ( maxSpecial > 0 && numberOfSpecialChars > maxSpecial )
-                    {
-                        errorList.add( new ErrorInformation( PwmError.PASSWORD_TOO_MANY_SPECIAL ) );
-                    }
-
-                    if ( !ruleHelper.readBooleanValue(
-                            PwmPasswordRule.AllowFirstCharSpecial ) && charCounter.isFirstSpecial() )
-                    {
-                        errorList.add( new ErrorInformation( PwmError.PASSWORD_FIRST_IS_SPECIAL ) );
-                    }
-
-                    if ( !ruleHelper.readBooleanValue(
-                            PwmPasswordRule.AllowLastCharSpecial ) && charCounter.isLastSpecial() )
-                    {
-                        errorList.add( new ErrorInformation( PwmError.PASSWORD_LAST_IS_SPECIAL ) );
-                    }
-                }
-                else
-                {
-                    if ( numberOfSpecialChars > 0 )
-                    {
-                        errorList.add( new ErrorInformation( PwmError.PASSWORD_TOO_MANY_SPECIAL ) );
-                    }
-                }
-            }
-
-            return Collections.unmodifiableList( errorList );
-        }
-    }
-
-    private static class CharSequenceRuleChecker implements RuleChecker
-    {
-        @Override
-        public List<ErrorInformation> test( final String password, final RuleCheckerHelper ruleCheckerHelper )
-                throws PwmUnrecoverableException
-        {
-            final List<ErrorInformation> errorList = new ArrayList<>();
-            final PasswordRuleHelper ruleHelper = ruleCheckerHelper.getRuleHelper();
-            final PasswordCharCounter charCounter = ruleCheckerHelper.getCharCounter();
-
-            //Check maximum character repeats (sequential)
-            {
-                final int maxSequentialRepeat = ruleHelper.readIntValue( PwmPasswordRule.MaximumSequentialRepeat );
-                if ( maxSequentialRepeat > 0 && charCounter.getSequentialRepeatedChars() > maxSequentialRepeat )
-                {
-                    errorList.add( new ErrorInformation( PwmError.PASSWORD_TOO_MANY_REPEAT ) );
-                }
-
-                //Check maximum character repeats (overall)
-                final int maxRepeat = ruleHelper.readIntValue( PwmPasswordRule.MaximumRepeat );
-                if ( maxRepeat > 0 && charCounter.getRepeatedChars() > maxRepeat )
-                {
-                    errorList.add( new ErrorInformation( PwmError.PASSWORD_TOO_MANY_REPEAT ) );
-                }
-            }
-
-            // check consecutive characters
-            {
-                final int maximumConsecutive = ruleHelper.readIntValue( PwmPasswordRule.MaximumConsecutive );
-                if ( PwmPasswordRuleUtil.tooManyConsecutiveChars( password, maximumConsecutive ) )
-                {
-                    errorList.add( new ErrorInformation( PwmError.PASSWORD_TOO_MANY_CONSECUTIVE ) );
-                }
-            }
-
-            return Collections.unmodifiableList( errorList );
-        }
-    }
-
-    private static class UniqueCharRuleChecker implements RuleChecker
-    {
-        @Override
-        public List<ErrorInformation> test( final String password, final RuleCheckerHelper ruleCheckerHelper )
-                throws PwmUnrecoverableException
-        {
-            final List<ErrorInformation> errorList = new ArrayList<>();
-            final PasswordRuleHelper ruleHelper = ruleCheckerHelper.getRuleHelper();
-            final PasswordCharCounter charCounter = ruleCheckerHelper.getCharCounter();
-
-            //Check minimum unique character
-            {
-                final int minUnique = ruleHelper.readIntValue( PwmPasswordRule.MinimumUnique );
-                if ( minUnique > 0 && charCounter.getUniqueChars() < minUnique )
-                {
-                    errorList.add( new ErrorInformation( PwmError.PASSWORD_NOT_ENOUGH_UNIQUE ) );
-                }
-            }
-
-            return Collections.unmodifiableList( errorList );
-        }
-    }
-
-    private static class ActiveDirectoryRuleChecker implements RuleChecker
-    {
-        @Override
-        public List<ErrorInformation> test( final String password, final 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 )
-                {
-                    final int maxGroupViolations = ruleHelper.readIntValue( PwmPasswordRule.ADComplexityMaxViolations );
-                    errorList.addAll( PwmPasswordRuleUtil.checkPasswordForADComplexity(
-                            complexityLevel,
-                            ruleCheckerHelper.getUserInfo(),
-                            password,
-                            charCounter,
-                            maxGroupViolations ) );
-                }
-            }
-
-            return Collections.unmodifiableList( errorList );
-        }
-    }
 }
 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

+ 1 - 23
server/src/test/java/password/pwm/util/PwmPasswordRuleValidatorTest.java → server/src/test/java/password/pwm/util/password/PwmPasswordRuleValidatorTest.java

@@ -20,35 +20,13 @@
  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
  */
 
-package password.pwm.util;
+package password.pwm.util.password;
 
 import org.junit.Assert;
 import org.junit.Test;
-import password.pwm.config.profile.PwmPasswordPolicy;
-import password.pwm.config.profile.PwmPasswordRule;
-import password.pwm.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
     {

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

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