소스 검색

refactor long methods

Jason Rivard 4 년 전
부모
커밋
68b1cc22fc

+ 3 - 1
build/checkstyle.xml

@@ -167,7 +167,9 @@
 
 
         <!-- Checks for Size Violations.                    -->
         <!-- Checks for Size Violations.                    -->
         <!-- See http://checkstyle.sf.net/config_sizes.html -->
         <!-- See http://checkstyle.sf.net/config_sizes.html -->
-        <module name="MethodLength"/>
+        <module name="MethodLength">
+            <property name="max" value="150"/>
+        </module>
         <module name="ParameterNumber"/>
         <module name="ParameterNumber"/>
 
 
         <!-- Checks for whitespace                               -->
         <!-- Checks for whitespace                               -->

+ 74 - 55
server/src/main/java/password/pwm/http/servlet/peoplesearch/PeopleSearchDataReader.java

@@ -752,69 +752,22 @@ class PeopleSearchDataReader
 
 
         final Instant startTime = Instant.now();
         final Instant startTime = Instant.now();
 
 
-        final SearchRequestBean.SearchMode searchMode = searchRequest.getMode() == null
-                ? SearchRequestBean.SearchMode.simple
-                : searchRequest.getMode();
-
         final SearchConfiguration searchConfiguration;
         final SearchConfiguration searchConfiguration;
         {
         {
-            final SearchConfiguration.SearchConfigurationBuilder builder = SearchConfiguration.builder();
-            builder.contexts( this.peopleSearchConfiguration.getLdapBase() );
-            builder.enableContextValidation( false );
-            builder.enableValueEscaping( false );
-            builder.enableSplitWhitespace( true );
 
 
-            if ( !useProxy() )
+
+            final Optional<SearchConfiguration> optionalSearchConfiguration = makeSearchConfiguration( searchRequest );
+            if ( optionalSearchConfiguration.isPresent() )
             {
             {
-                builder.ldapProfile( pwmRequest.getPwmSession().getUserInfo().getUserIdentity().getLdapProfileID() );
-                builder.chaiProvider( pwmRequest.getPwmSession().getSessionManager().getChaiProvider() );
+                searchConfiguration = optionalSearchConfiguration.get();
             }
             }
-
-            switch ( searchMode )
+            else
             {
             {
-                case simple:
-                {
-                    if ( StringUtil.isEmpty( searchRequest.getUsername() ) )
-                    {
-                        return SearchResultBean.builder().searchResults( Collections.emptyList() ).build();
-                    }
-
-                    builder.filter( makeSimpleSearchFilter() );
-                    builder.username( searchRequest.getUsername() );
-                }
-                break;
-
-                case advanced:
-                {
-                    if ( CollectionUtil.isEmpty( searchRequest.nonEmptySearchValues() ) )
-                    {
-                        return SearchResultBean.builder().searchResults( Collections.emptyList() ).build();
-                    }
-
-                    final Map<FormConfiguration, String> formValues = new LinkedHashMap<>();
-                    final Map<String, String> requestSearchValues = SearchRequestBean.searchValueToMap( searchRequest.getSearchValues() );
-                    for ( final FormConfiguration formConfiguration : peopleSearchConfiguration.getSearchForm() )
-                    {
-                        final String attribute = formConfiguration.getName();
-                        final String value = requestSearchValues.get( attribute );
-                        if ( StringUtil.notEmpty( value ) )
-                        {
-                            formValues.put( formConfiguration, value );
-                        }
-                    }
-
-                    builder.filter( makeAdvancedFilter( requestSearchValues ) );
-                    builder.formValues( formValues );
-                }
-                break;
-
-                default:
-                    JavaHelper.unhandledSwitchStatement( searchMode );
+                return SearchResultBean.builder().searchResults( Collections.emptyList() ).build();
             }
             }
-
-            searchConfiguration = builder.build();
         }
         }
 
 
+
         final UserSearchEngine userSearchEngine = pwmRequest.getPwmDomain().getUserSearchEngine();
         final UserSearchEngine userSearchEngine = pwmRequest.getPwmDomain().getUserSearchEngine();
 
 
         final UserSearchResults results;
         final UserSearchResults results;
@@ -830,7 +783,7 @@ class PeopleSearchDataReader
         catch ( final PwmOperationalException e )
         catch ( final PwmOperationalException e )
         {
         {
             final ErrorInformation errorInformation = e.getErrorInformation();
             final ErrorInformation errorInformation = e.getErrorInformation();
-            LOGGER.error( pwmRequest.getLabel(), () -> errorInformation.toDebugStr() );
+            LOGGER.error( pwmRequest.getLabel(), errorInformation::toDebugStr );
             throw new PwmUnrecoverableException( errorInformation );
             throw new PwmUnrecoverableException( errorInformation );
         }
         }
 
 
@@ -872,6 +825,72 @@ class PeopleSearchDataReader
                 .build();
                 .build();
     }
     }
 
 
+    private Optional<SearchConfiguration> makeSearchConfiguration(
+            final SearchRequestBean searchRequest
+    )
+            throws PwmUnrecoverableException
+    {
+        final SearchConfiguration.SearchConfigurationBuilder builder = SearchConfiguration.builder();
+        builder.contexts( this.peopleSearchConfiguration.getLdapBase() );
+        builder.enableContextValidation( false );
+        builder.enableValueEscaping( false );
+        builder.enableSplitWhitespace( true );
+
+        if ( !useProxy() )
+        {
+            builder.ldapProfile( pwmRequest.getPwmSession().getUserInfo().getUserIdentity().getLdapProfileID() );
+            builder.chaiProvider( pwmRequest.getPwmSession().getSessionManager().getChaiProvider() );
+        }
+
+        final SearchRequestBean.SearchMode searchMode = searchRequest.getMode() == null
+                ? SearchRequestBean.SearchMode.simple
+                : searchRequest.getMode();
+
+        switch ( searchMode )
+        {
+            case simple:
+            {
+                if ( StringUtil.isEmpty( searchRequest.getUsername() ) )
+                {
+                    return Optional.empty();
+                }
+
+                builder.filter( makeSimpleSearchFilter() );
+                builder.username( searchRequest.getUsername() );
+            }
+            break;
+
+            case advanced:
+            {
+                if ( CollectionUtil.isEmpty( searchRequest.nonEmptySearchValues() ) )
+                {
+                    return Optional.empty();
+                }
+
+                final Map<FormConfiguration, String> formValues = new LinkedHashMap<>();
+                final Map<String, String> requestSearchValues = SearchRequestBean.searchValueToMap( searchRequest.getSearchValues() );
+                for ( final FormConfiguration formConfiguration : peopleSearchConfiguration.getSearchForm() )
+                {
+                    final String attribute = formConfiguration.getName();
+                    final String value = requestSearchValues.get( attribute );
+                    if ( StringUtil.notEmpty( value ) )
+                    {
+                        formValues.put( formConfiguration, value );
+                    }
+                }
+
+                builder.filter( makeAdvancedFilter( requestSearchValues ) );
+                builder.formValues( formValues );
+            }
+            break;
+
+            default:
+                JavaHelper.unhandledSwitchStatement( searchMode );
+        }
+
+        return Optional.of( builder.build() );
+    }
+
     private String readUserAttribute(
     private String readUserAttribute(
             final UserIdentity userIdentity,
             final UserIdentity userIdentity,
             final String attribute
             final String attribute

+ 83 - 70
server/src/main/java/password/pwm/svc/cr/CrService.java

@@ -58,6 +58,7 @@ import password.pwm.svc.PwmService;
 import password.pwm.svc.wordlist.WordlistService;
 import password.pwm.svc.wordlist.WordlistService;
 import password.pwm.util.java.CollectionUtil;
 import password.pwm.util.java.CollectionUtil;
 import password.pwm.util.java.JsonUtil;
 import password.pwm.util.java.JsonUtil;
+import password.pwm.util.java.StringUtil;
 import password.pwm.util.java.TimeDuration;
 import password.pwm.util.java.TimeDuration;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.logging.PwmLogger;
 
 
@@ -277,78 +278,11 @@ public class CrService extends AbstractPwmService implements PwmService
         //strip null keys from responseMap;
         //strip null keys from responseMap;
         responseMap.keySet().removeIf( Objects::isNull );
         responseMap.keySet().removeIf( Objects::isNull );
 
 
-        {
-            // check for missing question texts
-            for ( final Challenge challenge : responseMap.keySet() )
-            {
-                if ( !challenge.isAdminDefined() )
-                {
-                    final String text = challenge.getChallengeText();
-                    if ( text == null || text.length() < 1 )
-                    {
-                        final ErrorInformation errorInformation = new ErrorInformation( PwmError.ERROR_MISSING_CHALLENGE_TEXT );
-                        throw new PwmDataValidationException( errorInformation );
-                    }
-                }
-            }
-        }
+        checkResponsesForMissingQuestionsText( responseMap );
 
 
-        {
-            // check responses against wordlist
-            final WordlistService wordlistManager = pwmDomain.getPwmApplication().getWordlistService();
-            if ( wordlistManager.status() == PwmService.STATUS.OPEN )
-            {
-                for ( final Map.Entry<Challenge, String> entry : responseMap.entrySet() )
-                {
-                    final Challenge loopChallenge = entry.getKey();
-                    if ( loopChallenge.isEnforceWordlist() )
-                    {
-                        final String answer = entry.getValue();
-                        if ( wordlistManager.containsWord( answer ) )
-                        {
-                            final ErrorInformation errorInfo = new ErrorInformation(
-                                    PwmError.ERROR_RESPONSE_WORDLIST,
-                                    null,
-                                    new String[]
-                                            {
-                                                    loopChallenge.getChallengeText(),
-                                                    }
-                            );
-                            throw new PwmDataValidationException( errorInfo );
-                        }
-                    }
-                }
-            }
-        }
+        checkResponsesAgainstWordlist( responseMap );
 
 
-        {
-            // check for duplicate questions.  need to check the actual req params because the following dupes wont populate duplicates
-            final Set<String> userQuestionTexts = new HashSet<>();
-            for ( final Challenge challenge : responseMap.keySet() )
-            {
-                final String text = challenge.getChallengeText();
-                if ( text != null )
-                {
-                    if ( userQuestionTexts.contains( text.toLowerCase() ) )
-                    {
-                        final String errorMsg = "duplicate challenge text: " + text;
-                        final ErrorInformation errorInformation = new ErrorInformation(
-                                PwmError.ERROR_CHALLENGE_DUPLICATE,
-                                errorMsg,
-                                new String[]
-                                        {
-                                                text,
-                                                }
-                        );
-                        throw new PwmDataValidationException( errorInformation );
-                    }
-                    else
-                    {
-                        userQuestionTexts.add( text.toLowerCase() );
-                    }
-                }
-            }
-        }
+        checkForDuplicateQuestions( responseMap );
 
 
         int randomCount = 0;
         int randomCount = 0;
         for ( final Challenge loopChallenge : responseMap.keySet() )
         for ( final Challenge loopChallenge : responseMap.keySet() )
@@ -385,6 +319,85 @@ public class CrService extends AbstractPwmService implements PwmService
         }
         }
     }
     }
 
 
+    private void checkResponsesForMissingQuestionsText( final Map<Challenge, String> responseMap )
+            throws PwmDataValidationException
+    {
+        for ( final Challenge challenge : responseMap.keySet() )
+        {
+            if ( !challenge.isAdminDefined() )
+            {
+                if ( StringUtil.isEmpty( challenge.getChallengeText() ) )
+                {
+                    final ErrorInformation errorInformation = new ErrorInformation( PwmError.ERROR_MISSING_CHALLENGE_TEXT );
+                    throw new PwmDataValidationException( errorInformation );
+                }
+            }
+        }
+
+    }
+
+    private void checkResponsesAgainstWordlist( final Map<Challenge, String> responseMap )
+            throws PwmDataValidationException, PwmUnrecoverableException
+    {
+        final WordlistService wordlistManager = pwmDomain.getPwmApplication().getWordlistService();
+        if ( wordlistManager == null || wordlistManager.status() != PwmService.STATUS.OPEN )
+        {
+            return;
+        }
+
+        for ( final Map.Entry<Challenge, String> entry : responseMap.entrySet() )
+        {
+            final Challenge loopChallenge = entry.getKey();
+            if ( loopChallenge.isEnforceWordlist() )
+            {
+                final String answer = entry.getValue();
+                if ( wordlistManager.containsWord( answer ) )
+                {
+                    final ErrorInformation errorInfo = new ErrorInformation(
+                            PwmError.ERROR_RESPONSE_WORDLIST,
+                            null,
+                            new String[]
+                                    {
+                                            loopChallenge.getChallengeText(),
+                                    }
+                    );
+                    throw new PwmDataValidationException( errorInfo );
+                }
+            }
+        }
+    }
+
+    private void checkForDuplicateQuestions( final Map<Challenge, String> responseMap )
+            throws PwmDataValidationException
+    {
+        // check for duplicate questions.  need to check the actual req params because the following dupes wont populate duplicates
+        final Set<String> userQuestionTexts = new HashSet<>();
+        for ( final Challenge challenge : responseMap.keySet() )
+        {
+            final String text = challenge.getChallengeText();
+            if ( text != null )
+            {
+                if ( userQuestionTexts.contains( text.toLowerCase() ) )
+                {
+                    final String errorMsg = "duplicate challenge text: " + text;
+                    final ErrorInformation errorInformation = new ErrorInformation(
+                            PwmError.ERROR_CHALLENGE_DUPLICATE,
+                            errorMsg,
+                            new String[]
+                                    {
+                                            text,
+                                    }
+                    );
+                    throw new PwmDataValidationException( errorInformation );
+                }
+                else
+                {
+                    userQuestionTexts.add( text.toLowerCase() );
+                }
+            }
+        }
+    }
+
     public Optional<ResponseInfoBean> readUserResponseInfo(
     public Optional<ResponseInfoBean> readUserResponseInfo(
             final SessionLabel sessionLabel,
             final SessionLabel sessionLabel,
             final UserIdentity userIdentity,
             final UserIdentity userIdentity,

+ 70 - 64
server/src/main/java/password/pwm/svc/report/ReportService.java

@@ -423,6 +423,8 @@ public class ReportService extends AbstractPwmService implements PwmService
 
 
     private class ProcessWorkQueueTask implements Runnable
     private class ProcessWorkQueueTask implements Runnable
     {
     {
+        private final Lock updateTimeLock = new ReentrantLock();
+
         @Override
         @Override
         public void run( )
         public void run( )
         {
         {
@@ -484,7 +486,6 @@ public class ReportService extends AbstractPwmService implements PwmService
 
 
             final boolean pauseBetweenIterations = settings.getReportJobIntensity() == ReportSettings.JobIntensity.LOW;
             final boolean pauseBetweenIterations = settings.getReportJobIntensity() == ReportSettings.JobIntensity.LOW;
 
 
-            final Lock updateTimeLock = new ReentrantLock();
 
 
             try
             try
             {
             {
@@ -501,69 +502,14 @@ public class ReportService extends AbstractPwmService implements PwmService
                     }
                     }
                     threadService.blockingSubmit( ( ) ->
                     threadService.blockingSubmit( ( ) ->
                     {
                     {
-                        if ( getPwmApplication().getConfig().isDevDebugMode() )
-                        {
-                            LOGGER.trace( getSessionLabel(), () -> "start " + Instant.now().toString()
-                                    + " size=" + threadService.getQueue().size() );
-                        }
-                        try
-                        {
-                            final Instant startUpdateTime = Instant.now();
-                            updateCachedRecordFromLdap( userIdentity );
-                            reportStatus.updateAndGet( reportStatusInfo -> reportStatusInfo.toBuilder()
-                                    .count( reportStatusInfo.getCount() + 1 )
-                                    .build() );
-                            final TimeDuration totalUpdateTime = TimeDuration.fromCurrent( startUpdateTime );
-                            avgTracker.addSample( totalUpdateTime.asMillis() );
-
-                            updateTimeLock.lock();
-                            try
-                            {
-                                final TimeDuration scaledTime = TimeDuration.of( totalUpdateTime.asMillis() / threadCount, TimeDuration.Unit.MILLISECONDS );
-                                reportStatus.updateAndGet( reportStatusInfo -> reportStatusInfo.toBuilder()
-                                        .jobDuration( reportStatusInfo.getJobDuration().add( scaledTime ) )
-                                        .build() );
-                            }
-                            finally
-                            {
-                                updateTimeLock.unlock();
-                            }
-
-                            if ( pauseBetweenIterations )
-                            {
-                                TimeDuration.of( avgTracker.avgAsLong(), TimeDuration.Unit.MILLISECONDS ).pause();
-                            }
-                        }
-                        catch ( final PwmUnrecoverableException e )
-                        {
-                            LOGGER.debug( () -> "unexpected error reading report data: " + e.getMessage() );
-                        }
-                        catch ( final Exception e )
-                        {
-                            String errorMsg = "error while updating report cache for " + userIdentity.toString() + ", cause: ";
-                            errorMsg += e instanceof PwmException
-                                    ? ( ( PwmException ) e ).getErrorInformation().toDebugStr()
-                                    : e.getMessage();
-                            final ErrorInformation errorInformation = new ErrorInformation( PwmError.ERROR_REPORTING_ERROR, errorMsg );
-                            if ( e instanceof PwmException )
-                            {
-                                LOGGER.error( getSessionLabel(), errorInformation::toDebugStr );
-                            }
-                            else
-                            {
-                                LOGGER.error( getSessionLabel(), () ->
-                                        errorInformation.toDebugStr(), e );
-                            }
-                            reportStatus.updateAndGet( reportStatusInfo -> reportStatusInfo.toBuilder()
-                                    .lastError( errorInformation )
-                                    .errors( reportStatusInfo.getErrors() + 1 )
-                                    .build() );
-                        }
-                        if ( getPwmApplication().getConfig().isDevDebugMode() )
-                        {
-                            LOGGER.trace( getSessionLabel(), () -> "finish " + Instant.now().toString()
-                                    + " size=" + threadService.getQueue().size() );
-                        }
+                        LOGGER.traceDevDebug( getSessionLabel(), () -> "start " + Instant.now().toString()
+                                + " size=" + threadService.getQueue().size() );
+
+                        processRecord( userIdentity, pauseBetweenIterations, threadCount );
+
+                        LOGGER.traceDevDebug( getSessionLabel(), () -> "finish " + Instant.now().toString()
+                                + " size=" + threadService.getQueue().size() );
+
                     } );
                     } );
                 }
                 }
                 if ( getPwmApplication().getConfig().isDevDebugMode() )
                 if ( getPwmApplication().getConfig().isDevDebugMode() )
@@ -617,6 +563,66 @@ public class ReportService extends AbstractPwmService implements PwmService
 
 
             LOGGER.trace( getSessionLabel(), () -> "stored cache for " + userIdentity, () -> TimeDuration.fromCurrent( startTime ) );
             LOGGER.trace( getSessionLabel(), () -> "stored cache for " + userIdentity, () -> TimeDuration.fromCurrent( startTime ) );
         }
         }
+
+        private void processRecord(
+                final UserIdentity userIdentity,
+                final boolean pauseBetweenIterations,
+                final int threadCount
+        )
+        {
+            try
+            {
+                final Instant startUpdateTime = Instant.now();
+                updateCachedRecordFromLdap( userIdentity );
+                reportStatus.updateAndGet( reportStatusInfo -> reportStatusInfo.toBuilder()
+                        .count( reportStatusInfo.getCount() + 1 )
+                        .build() );
+                final TimeDuration totalUpdateTime = TimeDuration.fromCurrent( startUpdateTime );
+                avgTracker.addSample( totalUpdateTime.asMillis() );
+
+                updateTimeLock.lock();
+                try
+                {
+                    final TimeDuration scaledTime = TimeDuration.of( totalUpdateTime.asMillis() / threadCount, TimeDuration.Unit.MILLISECONDS );
+                    reportStatus.updateAndGet( reportStatusInfo -> reportStatusInfo.toBuilder()
+                            .jobDuration( reportStatusInfo.getJobDuration().add( scaledTime ) )
+                            .build() );
+                }
+                finally
+                {
+                    updateTimeLock.unlock();
+                }
+
+                if ( pauseBetweenIterations )
+                {
+                    TimeDuration.of( avgTracker.avgAsLong(), TimeDuration.Unit.MILLISECONDS ).pause();
+                }
+            }
+            catch ( final PwmUnrecoverableException e )
+            {
+                LOGGER.debug( () -> "unexpected error reading report data: " + e.getMessage() );
+            }
+            catch ( final Exception e )
+            {
+                String errorMsg = "error while updating report cache for " + userIdentity.toString() + ", cause: ";
+                errorMsg += e instanceof PwmException
+                        ? ( ( PwmException ) e ).getErrorInformation().toDebugStr()
+                        : e.getMessage();
+                final ErrorInformation errorInformation = new ErrorInformation( PwmError.ERROR_REPORTING_ERROR, errorMsg );
+                if ( e instanceof PwmException )
+                {
+                    LOGGER.error( getSessionLabel(), errorInformation::toDebugStr );
+                }
+                else
+                {
+                    LOGGER.error( getSessionLabel(), errorInformation::toDebugStr, e );
+                }
+                reportStatus.updateAndGet( reportStatusInfo -> reportStatusInfo.toBuilder()
+                        .lastError( errorInformation )
+                        .errors( reportStatusInfo.getErrors() + 1 )
+                        .build() );
+            }
+        }
     }
     }
 
 
     private class DailyJobExecuteTask implements Runnable
     private class DailyJobExecuteTask implements Runnable

+ 179 - 56
server/src/main/java/password/pwm/svc/report/ReportSummaryData.java

@@ -45,6 +45,7 @@ import java.util.TreeMap;
 import java.util.TreeSet;
 import java.util.TreeSet;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.atomic.LongAdder;
 import java.util.concurrent.atomic.LongAdder;
+import java.util.function.BiConsumer;
 import java.util.stream.Collectors;
 import java.util.stream.Collectors;
 
 
 @Value
 @Value
@@ -123,103 +124,226 @@ public class ReportSummaryData
     {
     {
         totalUsers.increment();
         totalUsers.increment();
 
 
-        if ( userReportRecord.isHasResponses() )
-        {
-            hasResponses.increment();
-        }
+        Updaters.UPDATERS.forEach( updater -> updater.accept( userReportRecord, this ) );
+    }
 
 
-        if ( userReportRecord.isHasHelpdeskResponses() )
+    private static class Updaters
+    {
+        private static final List<BiConsumer<UserReportRecord, ReportSummaryData>> UPDATERS = List.of(
+                new UpdateHasResponses(),
+                new UpdateHasHelpdeskResponses(),
+                new HasResponseSetTime(),
+                new UpdatePasswordExpirationTime(),
+                new UpdateAccountExpirationTime(),
+                new UpdateLastLoginTime(),
+                new UpdatePwChangeTime(),
+                new UpdatePwExpiredNotification(),
+                new UpdatePasswordStatus(),
+                new UpdateResponseStorageMethod(),
+                new UpdateLdapProfile(),
+                new UpdateResponseFormatType(),
+                new UpdateHasOtpSecret(),
+                new UpdateOtpSecretSetTime()
+        );
+
+        private static class UpdateHasResponses implements BiConsumer<UserReportRecord, ReportSummaryData>
         {
         {
-            hasHelpdeskResponses.increment();
-        }
+            @Override
+            public void accept( final UserReportRecord userReportRecord, final ReportSummaryData reportSummaryData )
+            {
+                if ( userReportRecord.isHasResponses() )
+                {
+                    reportSummaryData.hasResponses.increment();
+                }
 
 
-        if ( userReportRecord.getResponseSetTime() != null )
-        {
-            hasResponseSetTime.increment();
-            incrementIfWithinTimeWindow( userReportRecord, responseSetDays );
+            }
         }
         }
 
 
-        if ( userReportRecord.getPasswordExpirationTime() != null )
+        private static class UpdateHasHelpdeskResponses implements BiConsumer<UserReportRecord, ReportSummaryData>
         {
         {
-            hasPasswordExpirationTime.increment();
-            incrementIfWithinTimeWindow( userReportRecord, pwExpireDays );
+            @Override
+            public void accept( final UserReportRecord userReportRecord, final ReportSummaryData reportSummaryData )
+            {
+                if ( userReportRecord.isHasHelpdeskResponses() )
+                {
+                    reportSummaryData.hasHelpdeskResponses.increment();
+                }
+
+            }
         }
         }
 
 
-        if ( userReportRecord.getAccountExpirationTime() != null )
+        private static class HasResponseSetTime implements BiConsumer<UserReportRecord, ReportSummaryData>
         {
         {
-            hasAccountExpirationTime.increment();
-            incrementIfWithinTimeWindow( userReportRecord, accountExpireDays );
+            @Override
+            public void accept( final UserReportRecord userReportRecord, final ReportSummaryData reportSummaryData )
+            {
+                if ( userReportRecord.getResponseSetTime() != null )
+                {
+                    reportSummaryData.hasResponseSetTime.increment();
+                    reportSummaryData.incrementIfWithinTimeWindow( userReportRecord, reportSummaryData.responseSetDays );
+                }
+            }
         }
         }
 
 
-        if ( userReportRecord.getLastLoginTime() != null )
+        private static class UpdatePasswordExpirationTime implements BiConsumer<UserReportRecord, ReportSummaryData>
         {
         {
-            hasLoginTime.increment();
-            incrementIfWithinTimeWindow( userReportRecord, loginDays );
+            @Override
+            public void accept( final UserReportRecord userReportRecord, final ReportSummaryData reportSummaryData )
+            {
+                if ( userReportRecord.getPasswordExpirationTime() != null )
+                {
+                    reportSummaryData.hasPasswordExpirationTime.increment();
+                    reportSummaryData.incrementIfWithinTimeWindow( userReportRecord, reportSummaryData.pwExpireDays );
+                }
+            }
         }
         }
 
 
-        if ( userReportRecord.getPasswordChangeTime() != null )
+        private static class UpdateAccountExpirationTime implements BiConsumer<UserReportRecord, ReportSummaryData>
         {
         {
-            hasChangePwTime.increment();
-            incrementIfWithinTimeWindow( userReportRecord, changePwDays );
+            @Override
+            public void accept( final UserReportRecord userReportRecord, final ReportSummaryData reportSummaryData )
+            {
+                if ( userReportRecord.getAccountExpirationTime() != null )
+                {
+                    reportSummaryData.hasAccountExpirationTime.increment();
+                    reportSummaryData.incrementIfWithinTimeWindow( userReportRecord, reportSummaryData.accountExpireDays );
+                }
+            }
         }
         }
 
 
-        if ( userReportRecord.getPasswordExpirationNoticeSendTime() != null )
+        private static class UpdateLastLoginTime implements BiConsumer<UserReportRecord, ReportSummaryData>
         {
         {
-            hasReceivedPwExpireNotification.increment();
-            incrementIfWithinTimeWindow( userReportRecord, pwExpireNotificationDays );
+            @Override
+            public void accept( final UserReportRecord userReportRecord, final ReportSummaryData reportSummaryData )
+            {
+                if ( userReportRecord.getLastLoginTime() != null )
+                {
+                    reportSummaryData.hasLoginTime.increment();
+                    reportSummaryData.incrementIfWithinTimeWindow( userReportRecord, reportSummaryData.loginDays );
+                }
+            }
         }
         }
 
 
-        if ( userReportRecord.getPasswordStatus() != null )
+        private static class UpdatePwChangeTime implements BiConsumer<UserReportRecord, ReportSummaryData>
         {
         {
-            if ( userReportRecord.getPasswordStatus().isExpired() )
+            @Override
+            public void accept( final UserReportRecord userReportRecord, final ReportSummaryData reportSummaryData )
             {
             {
-                pwExpired.increment();
+                if ( userReportRecord.getPasswordChangeTime() != null )
+                {
+                    reportSummaryData.hasChangePwTime.increment();
+                    reportSummaryData.incrementIfWithinTimeWindow( userReportRecord, reportSummaryData.changePwDays );
+                }
             }
             }
-            if ( userReportRecord.getPasswordStatus().isPreExpired() )
+        }
+
+        private static class UpdatePwExpiredNotification implements BiConsumer<UserReportRecord, ReportSummaryData>
+        {
+            @Override
+            public void accept( final UserReportRecord userReportRecord, final ReportSummaryData reportSummaryData )
             {
             {
-                pwPreExpired.increment();
+                if ( userReportRecord.getPasswordExpirationNoticeSendTime() != null )
+                {
+                    reportSummaryData.hasReceivedPwExpireNotification.increment();
+                    reportSummaryData.incrementIfWithinTimeWindow( userReportRecord, reportSummaryData.pwExpireNotificationDays );
+                }
             }
             }
-            if ( userReportRecord.getPasswordStatus().isWarnPeriod() )
+        }
+
+        private static class UpdatePasswordStatus implements BiConsumer<UserReportRecord, ReportSummaryData>
+        {
+            @Override
+            public void accept( final UserReportRecord userReportRecord, final ReportSummaryData reportSummaryData )
             {
             {
-                pwWarnPeriod.increment();
+                if ( userReportRecord.getPasswordStatus() != null )
+                {
+                    if ( userReportRecord.getPasswordStatus().isExpired() )
+                    {
+                        reportSummaryData.pwExpired.increment();
+                    }
+                    if ( userReportRecord.getPasswordStatus().isPreExpired() )
+                    {
+                        reportSummaryData.pwPreExpired.increment();
+                    }
+                    if ( userReportRecord.getPasswordStatus().isWarnPeriod() )
+                    {
+                        reportSummaryData.pwWarnPeriod.increment();
+                    }
+                }
             }
             }
         }
         }
 
 
-        if ( userReportRecord.getResponseStorageMethod() != null )
+        private static class UpdateResponseStorageMethod implements BiConsumer<UserReportRecord, ReportSummaryData>
         {
         {
-            final DataStorageMethod method = userReportRecord.getResponseStorageMethod();
-            responseStorage
-                    .computeIfAbsent( method, dataStorageMethod -> new LongAdder() )
-                    .increment();
+            @Override
+            public void accept( final UserReportRecord userReportRecord, final ReportSummaryData reportSummaryData )
+            {
+                if ( userReportRecord.getResponseStorageMethod() != null )
+                {
+                    final DataStorageMethod method = userReportRecord.getResponseStorageMethod();
+                    reportSummaryData.responseStorage
+                            .computeIfAbsent( method, dataStorageMethod -> new LongAdder() )
+                            .increment();
+                }
+
+            }
         }
         }
 
 
-        if ( userReportRecord.getLdapProfile() != null )
+        private static class UpdateLdapProfile implements BiConsumer<UserReportRecord, ReportSummaryData>
         {
         {
-            final DomainID domainID = userReportRecord.getDomainID();
-            final String userProfile = userReportRecord.getLdapProfile();
-            ldapProfile
-                    .computeIfAbsent( domainID, type -> new ConcurrentHashMap<>() )
-                    .computeIfAbsent( userProfile, type -> new LongAdder() )
-                    .increment();
+            @Override
+            public void accept( final UserReportRecord userReportRecord, final ReportSummaryData reportSummaryData )
+            {
+                if ( userReportRecord.getLdapProfile() != null )
+                {
+                    final DomainID domainID = userReportRecord.getDomainID();
+                    final String userProfile = userReportRecord.getLdapProfile();
+                    reportSummaryData.ldapProfile
+                            .computeIfAbsent( domainID, type -> new ConcurrentHashMap<>() )
+                            .computeIfAbsent( userProfile, type -> new LongAdder() )
+                            .increment();
+                }
+            }
         }
         }
 
 
-        if ( userReportRecord.getResponseFormatType() != null )
+        private static class UpdateResponseFormatType implements BiConsumer<UserReportRecord, ReportSummaryData>
         {
         {
-            final Answer.FormatType type = userReportRecord.getResponseFormatType();
-            responseFormatType
-                    .computeIfAbsent( type, formatType -> new LongAdder() )
-                    .increment();
+            @Override
+            public void accept( final UserReportRecord userReportRecord, final ReportSummaryData reportSummaryData )
+            {
+                if ( userReportRecord.getResponseFormatType() != null )
+                {
+                    final Answer.FormatType type = userReportRecord.getResponseFormatType();
+                    reportSummaryData.responseFormatType
+                            .computeIfAbsent( type, formatType -> new LongAdder() )
+                            .increment();
+                }
+            }
         }
         }
 
 
-        if ( userReportRecord.isHasOtpSecret() )
+        private static class UpdateHasOtpSecret implements BiConsumer<UserReportRecord, ReportSummaryData>
         {
         {
-            hasOtpSecret.increment();
+            @Override
+            public void accept( final UserReportRecord userReportRecord, final ReportSummaryData reportSummaryData )
+            {
+                if ( userReportRecord.isHasOtpSecret() )
+                {
+                    reportSummaryData.hasOtpSecret.increment();
+                }
+            }
         }
         }
 
 
-        if ( userReportRecord.getOtpSecretSetTime() != null )
+        private static class UpdateOtpSecretSetTime implements BiConsumer<UserReportRecord, ReportSummaryData>
         {
         {
-            hasOtpSecretSetTime.increment();
-            incrementIfWithinTimeWindow( userReportRecord, otpSetDays );
+            @Override
+            public void accept( final UserReportRecord userReportRecord, final ReportSummaryData reportSummaryData )
+            {
+                if ( userReportRecord.getOtpSecretSetTime() != null )
+                {
+                    reportSummaryData.hasOtpSecretSetTime.increment();
+                    reportSummaryData.incrementIfWithinTimeWindow( userReportRecord, reportSummaryData.otpSetDays );
+                }
+            }
         }
         }
     }
     }
 
 
@@ -251,7 +375,6 @@ public class ReportSummaryData
         }
         }
     }
     }
 
 
-
     public List<PresentationRow> asPresentableCollection( final AppConfig config, final Locale locale )
     public List<PresentationRow> asPresentableCollection( final AppConfig config, final Locale locale )
     {
     {
         final ArrayList<PresentationRow> returnCollection = new ArrayList<>();
         final ArrayList<PresentationRow> returnCollection = new ArrayList<>();

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

@@ -90,7 +90,7 @@ class WordlistZipReader implements AutoCloseable, Closeable
         final int count = b.length;
         final int count = b.length;
         eventRateMeter.markEvents( count );
         eventRateMeter.markEvents( count );
         byteCounter += count;
         byteCounter += count;
-        messageDigest.digest( b );
+        messageDigest.update( b );
     }
     }
 
 
     private void nextZipEntry( )
     private void nextZipEntry( )

+ 10 - 0
server/src/main/java/password/pwm/util/logging/PwmLogger.java

@@ -307,6 +307,16 @@ public class PwmLogger
         doLogEvent( PwmLogLevel.TRACE, sessionLabel, message, null );
         doLogEvent( PwmLogLevel.TRACE, sessionLabel, message, null );
     }
     }
 
 
+    public void traceDevDebug( final SessionLabel sessionLabel, final Supplier<CharSequence> message )
+    {
+        if ( pwmApplication == null || !pwmApplication.getConfig().isDevDebugMode() )
+        {
+            return;
+        }
+
+        doLogEvent( PwmLogLevel.TRACE, sessionLabel, message, null );
+    }
+
     public void trace( final SessionLabel sessionLabel, final Supplier<CharSequence> message, final Supplier<TimeDuration> timeDuration )
     public void trace( final SessionLabel sessionLabel, final Supplier<CharSequence> message, final Supplier<TimeDuration> timeDuration )
     {
     {
         doLogEvent( PwmLogLevel.TRACE, sessionLabel, message, null, timeDuration );
         doLogEvent( PwmLogLevel.TRACE, sessionLabel, message, null, timeDuration );

+ 72 - 54
server/src/main/java/password/pwm/util/macro/UserMacros.java

@@ -81,16 +81,6 @@ public class UserMacros
         )
         )
                 throws MacroParseException
                 throws MacroParseException
         {
         {
-            final UserInfo userInfo;
-            {
-                final Optional<UserInfo> optionalUserInfo = loadUserInfo( macroRequest );
-                if ( optionalUserInfo.isEmpty() )
-                {
-                    return "";
-                }
-                userInfo = optionalUserInfo.get();
-            }
-
             final List<String> parameters = splitMacroParameters( matchValue, ignoreWords() );
             final List<String> parameters = splitMacroParameters( matchValue, ignoreWords() );
 
 
             final String ldapAttr;
             final String ldapAttr;
@@ -103,37 +93,7 @@ public class UserMacros
                 throw new MacroParseException( "required attribute name parameter is missing" );
                 throw new MacroParseException( "required attribute name parameter is missing" );
             }
             }
 
 
-            final int length;
-            if ( parameters.size() > 1 && !parameters.get( 1 ).isEmpty() )
-            {
-                try
-                {
-                    length = Integer.parseInt( parameters.get( 1 ) );
-                }
-                catch ( final NumberFormatException e )
-                {
-                    throw new MacroParseException( "error parsing length parameter: " + e.getMessage() );
-                }
-
-                final int maxLengthPermitted = Integer.parseInt(
-                        macroRequest.getPwmApplication() != null
-                                ?  macroRequest.getPwmApplication().getConfig().readAppProperty( AppProperty.MACRO_LDAP_ATTR_CHAR_MAX_LENGTH )
-                                :  AppProperty.MACRO_LDAP_ATTR_CHAR_MAX_LENGTH.getDefaultValue()
-                );
-
-                if ( length > maxLengthPermitted )
-                {
-                    throw new MacroParseException( "maximum permitted length of LDAP attribute (" + maxLengthPermitted + ") exceeded" );
-                }
-                else if ( length <= 0 )
-                {
-                    throw new MacroParseException( "length parameter must be greater than zero" );
-                }
-            }
-            else
-            {
-                length = 0;
-            }
+            final int length = readLengthParam( parameters, macroRequest );
 
 
             final String paddingChar;
             final String paddingChar;
             if ( parameters.size() > 2 && !parameters.get( 2 ).isEmpty() )
             if ( parameters.size() > 2 && !parameters.get( 2 ).isEmpty() )
@@ -150,6 +110,46 @@ public class UserMacros
                 throw new MacroParseException( "too many parameters" );
                 throw new MacroParseException( "too many parameters" );
             }
             }
 
 
+           final String ldapValue = readLdapValue( macroRequest, ldapAttr, matchValue );
+
+            final StringBuilder returnValue = new StringBuilder();
+            returnValue.append( ldapValue == null
+                    ? ""
+                    : ldapValue );
+
+            if ( length > 0 && length < returnValue.length() )
+            {
+                returnValue.delete( length, returnValue.length() );
+            }
+
+            if ( length > 0 && paddingChar.length() > 0 )
+            {
+                while ( returnValue.length() < length )
+                {
+                    returnValue.append( paddingChar );
+                }
+            }
+
+            return returnValue.toString();
+        }
+
+        private String readLdapValue(
+                final MacroRequest macroRequest,
+                final String ldapAttr,
+                final String matchValue
+
+        )
+        {
+            final UserInfo userInfo;
+            {
+                final Optional<UserInfo> optionalUserInfo = loadUserInfo( macroRequest );
+                if ( optionalUserInfo.isEmpty() )
+                {
+                    return "";
+                }
+                userInfo = optionalUserInfo.get();
+            }
+
             final String ldapValue;
             final String ldapValue;
             if ( "dn".equalsIgnoreCase( ldapAttr ) )
             if ( "dn".equalsIgnoreCase( ldapAttr ) )
             {
             {
@@ -173,26 +173,44 @@ public class UserMacros
                     return "";
                     return "";
                 }
                 }
             }
             }
+            return ldapValue;
+        }
 
 
-            final StringBuilder returnValue = new StringBuilder();
-            returnValue.append( ldapValue == null
-                    ? ""
-                    : ldapValue );
-
-            if ( length > 0 && length < returnValue.length() )
+        private static int readLengthParam(
+                final List<String> parameters,
+                final MacroRequest macroRequest
+        )
+                throws MacroParseException
+        {
+            int length = 0;
+            if ( parameters.size() > 1 && !parameters.get( 1 ).isEmpty() )
             {
             {
-                returnValue.delete( length, returnValue.length() );
-            }
+                try
+                {
+                    length = Integer.parseInt( parameters.get( 1 ) );
+                }
+                catch ( final NumberFormatException e )
+                {
+                    throw new MacroParseException( "error parsing length parameter: " + e.getMessage() );
+                }
 
 
-            if ( length > 0 && paddingChar.length() > 0 )
-            {
-                while ( returnValue.length() < length )
+                final int maxLengthPermitted = Integer.parseInt(
+                        macroRequest.getPwmApplication() != null
+                                ?  macroRequest.getPwmApplication().getConfig().readAppProperty( AppProperty.MACRO_LDAP_ATTR_CHAR_MAX_LENGTH )
+                                :  AppProperty.MACRO_LDAP_ATTR_CHAR_MAX_LENGTH.getDefaultValue()
+                );
+
+                if ( length > maxLengthPermitted )
                 {
                 {
-                    returnValue.append( paddingChar );
+                    throw new MacroParseException( "maximum permitted length of LDAP attribute (" + maxLengthPermitted + ") exceeded" );
+                }
+                else if ( length <= 0 )
+                {
+                    throw new MacroParseException( "length parameter must be greater than zero" );
                 }
                 }
             }
             }
 
 
-            return returnValue.toString();
+            return length;
         }
         }
 
 
         abstract Optional<UserInfo> loadUserInfo( MacroRequest macroRequest );
         abstract Optional<UserInfo> loadUserInfo( MacroRequest macroRequest );

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

@@ -509,7 +509,7 @@ public class PasswordRuleChecks
                 if ( complexityLevel == ADPolicyComplexity.AD2003 || complexityLevel == ADPolicyComplexity.AD2008 )
                 if ( complexityLevel == ADPolicyComplexity.AD2003 || complexityLevel == ADPolicyComplexity.AD2008 )
                 {
                 {
                     final int maxGroupViolations = ruleHelper.readIntValue( PwmPasswordRule.ADComplexityMaxViolations );
                     final int maxGroupViolations = ruleHelper.readIntValue( PwmPasswordRule.ADComplexityMaxViolations );
-                    errorList.addAll( PwmPasswordRuleUtil.checkPasswordForADComplexity(
+                    errorList.addAll( PwmPasswordAdRuleUtil.checkPasswordForADComplexity(
                             complexityLevel,
                             complexityLevel,
                             ruleCheckData.getUserInfo(),
                             ruleCheckData.getUserInfo(),
                             password,
                             password,

+ 270 - 0
server/src/main/java/password/pwm/util/password/PwmPasswordAdRuleUtil.java

@@ -0,0 +1,270 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2020 The PWM Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package password.pwm.util.password;
+
+import password.pwm.config.option.ADPolicyComplexity;
+import password.pwm.error.ErrorInformation;
+import password.pwm.error.PwmError;
+import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.ldap.UserInfo;
+import password.pwm.util.java.JavaHelper;
+import password.pwm.util.logging.PwmLogger;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * AD Password Rule Utility for checking AD password complexity.
+ */
+class PwmPasswordAdRuleUtil
+{
+    private PwmPasswordAdRuleUtil()
+    {
+    }
+
+    private static final PwmLogger LOGGER = PwmLogger.forClass( PwmPasswordAdRuleUtil.class );
+
+    /**
+     * Check a supplied password for it's validity according to AD complexity rules.
+     * - Not contain the user's account name or parts of the user's full name that exceed two consecutive characters
+     * - Be at least six characters in length
+     * - Contain characters from three of the following five categories:
+     * - English uppercase characters (A through Z)
+     * - English lowercase characters (a through z)
+     * - Base 10 digits (0 through 9)
+     * - Non-alphabetic characters (for example, !, $, #, %)
+     * - Any character categorized as an alphabetic but is not uppercase or lowercase.
+     * <p/>
+     * See this article: http://technet.microsoft.com/en-us/library/cc786468%28WS.10%29.aspx
+     *
+     * @param userInfo    userInfoBean
+     * @param password    password to test
+     * @param charCounter associated charCounter for the password.
+     * @return list of errors if the password does not meet requirements, or an empty list if the password complies
+     *         with AD requirements
+     */
+
+    static List<ErrorInformation> checkPasswordForADComplexity(
+            final ADPolicyComplexity complexityLevel,
+            final UserInfo userInfo,
+            final String password,
+            final PasswordCharCounter charCounter,
+            final int maxGroupViolationCount
+    )
+            throws PwmUnrecoverableException
+    {
+        if ( password == null || password.length() < 6 )
+        {
+            return Collections.singletonList( new ErrorInformation( PwmError.PASSWORD_TOO_SHORT ) );
+        }
+
+        final int maxLength = complexityLevel == ADPolicyComplexity.AD2003 ? 128 : 512;
+        if ( password.length() > maxLength )
+        {
+            return Collections.singletonList( new ErrorInformation( PwmError.PASSWORD_TOO_LONG ) );
+        }
+
+        final List<ErrorInformation> errorList = new ArrayList<>( AdCheckHelper.checkPasswordRuleAttributes( userInfo, password ) );
+
+        final int complexityPoints = AdCheckHelper.calculateComplexity( charCounter, complexityLevel );
+
+        errorList.addAll( AdCheckHelper.makeComplexityViolationErrors( complexityPoints, maxGroupViolationCount, charCounter, complexityLevel ) );
+
+        return Collections.unmodifiableList( errorList );
+    }
+
+    private static class AdCheckHelper
+    {
+        private static List<ErrorInformation> makeComplexityViolationErrors(
+                final int complexityPoints,
+                final int maxGroupViolationCount,
+                final PasswordCharCounter charCounter,
+                final ADPolicyComplexity complexityLevel
+        )
+        {
+            // exit if complexity violations < max
+            switch ( complexityLevel )
+            {
+                case AD2008:
+                    final int totalGroups = 5;
+                    final int violations = totalGroups - complexityPoints;
+                    if ( violations <= maxGroupViolationCount )
+                    {
+                        return Collections.emptyList();
+                    }
+                    break;
+
+                case AD2003:
+                    if ( complexityPoints >= 3 )
+                    {
+                        return Collections.emptyList();
+                    }
+                    break;
+
+                default:
+                    JavaHelper.unhandledSwitchStatement( complexityLevel );
+            }
+
+            final List<ErrorInformation> errorList = new ArrayList<>();
+
+            // add errors complexity violations
+            if ( charCounter.getUpperCharCount() < 1 )
+            {
+                errorList.add( new ErrorInformation( PwmError.PASSWORD_NOT_ENOUGH_UPPER ) );
+            }
+            if ( charCounter.getLowerCharCount() < 1 )
+            {
+                errorList.add( new ErrorInformation( PwmError.PASSWORD_NOT_ENOUGH_LOWER ) );
+            }
+            if ( charCounter.getNumericCharCount() < 1 )
+            {
+                errorList.add( new ErrorInformation( PwmError.PASSWORD_NOT_ENOUGH_NUM ) );
+            }
+            if ( charCounter.getSpecialCharsCount() < 1 )
+            {
+                errorList.add( new ErrorInformation( PwmError.PASSWORD_NOT_ENOUGH_SPECIAL ) );
+            }
+            if ( charCounter.getOtherLetterCharCount() < 1 )
+            {
+                errorList.add( new ErrorInformation( PwmError.PASSWORD_UNKNOWN_VALIDATION ) );
+            }
+
+            return Collections.unmodifiableList( errorList );
+        }
+
+        private static List<ErrorInformation> checkPasswordRuleAttributes(
+                final UserInfo userInfo,
+                final String password
+        )
+                throws PwmUnrecoverableException
+        {
+            final List<ErrorInformation> errorList = new ArrayList<>();
+
+            if ( userInfo != null && userInfo.getCachedPasswordRuleAttributes() != null )
+            {
+                final Map<String, String> userAttrs = userInfo.getCachedPasswordRuleAttributes();
+                final String samAccountName = userAttrs.get( "sAMAccountName" );
+                if ( samAccountName != null
+                        && samAccountName.length() > 2
+                        && samAccountName.length() >= password.length() )
+                {
+                    if ( password.toLowerCase().contains( samAccountName.toLowerCase() ) )
+                    {
+                        errorList.add( new ErrorInformation( PwmError.PASSWORD_INWORDLIST ) );
+                        LOGGER.trace( () -> "Password violation due to ADComplexity check: Password contains sAMAccountName" );
+                    }
+                }
+                final String displayName = userAttrs.get( "displayName" );
+                if ( displayName != null && displayName.length() > 2 )
+                {
+                    if ( AdCheckHelper.checkContainsTokens( password, displayName ) )
+                    {
+                        errorList.add( new ErrorInformation( PwmError.PASSWORD_INWORDLIST ) );
+                        LOGGER.trace( () -> "Password violation due to ADComplexity check: Tokens from displayName used in password" );
+                    }
+                }
+            }
+
+            return Collections.unmodifiableList( errorList );
+        }
+
+        private static int calculateComplexity(
+                final PasswordCharCounter charCounter,
+                final ADPolicyComplexity complexityLevel
+        )
+        {
+            int complexityPoints = 0;
+            if ( charCounter.getUpperCharCount() > 0 )
+            {
+                complexityPoints++;
+            }
+            if ( charCounter.getLowerCharCount() > 0 )
+            {
+                complexityPoints++;
+            }
+            if ( charCounter.getNumericCharCount() > 0 )
+            {
+                complexityPoints++;
+            }
+            switch ( complexityLevel )
+            {
+                case AD2003:
+                    if ( charCounter.getSpecialCharsCount() > 0 || charCounter.getOtherLetterCharCount() > 0 )
+                    {
+                        complexityPoints++;
+                    }
+                    break;
+
+                case AD2008:
+                    if ( charCounter.getSpecialCharsCount() > 0 )
+                    {
+                        complexityPoints++;
+                    }
+                    if ( charCounter.getOtherLetterCharCount() > 0 )
+                    {
+                        complexityPoints++;
+                    }
+                    break;
+
+                default:
+                    JavaHelper.unhandledSwitchStatement( complexityLevel );
+            }
+
+            return complexityPoints;
+        }
+
+        // escape characters permitted because they match the exact AD specification
+        @SuppressWarnings( "checkstyle:avoidescapedunicodecharacters" )
+        private static boolean checkContainsTokens( final String baseValue, final String checkPattern )
+        {
+            if ( baseValue == null || baseValue.length() == 0 )
+            {
+                return false;
+            }
+
+            if ( checkPattern == null || checkPattern.length() == 0 )
+            {
+                return false;
+            }
+
+            final String baseValueLower = baseValue.toLowerCase();
+
+            final String[] tokens = checkPattern.toLowerCase().split( "[,\\.\\-\u2013\u2014_ \u00a3\\t]+" );
+
+            if ( tokens.length > 0 )
+            {
+                for ( final String token : tokens )
+                {
+                    if ( token.length() > 2 )
+                    {
+                        if ( baseValueLower.contains( token ) )
+                        {
+                            return true;
+                        }
+                    }
+                }
+            }
+            return false;
+        }
+    }
+}

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

@@ -21,213 +21,14 @@
 package password.pwm.util.password;
 package password.pwm.util.password;
 
 
 import org.apache.commons.lang3.StringUtils;
 import org.apache.commons.lang3.StringUtils;
-import password.pwm.config.option.ADPolicyComplexity;
-import password.pwm.error.ErrorInformation;
-import password.pwm.error.PwmError;
-import password.pwm.error.PwmUnrecoverableException;
-import password.pwm.ldap.UserInfo;
-import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.java.StringUtil;
-import password.pwm.util.logging.PwmLogger;
 
 
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
-
-public class PwmPasswordRuleUtil
+class PwmPasswordRuleUtil
 {
 {
-    private static final PwmLogger LOGGER = PwmLogger.forClass( PwmPasswordRuleUtil.class );
-
     private PwmPasswordRuleUtil()
     private PwmPasswordRuleUtil()
     {
     {
     }
     }
 
 
-    /**
-     * Check a supplied password for it's validity according to AD complexity rules.
-     * - Not contain the user's account name or parts of the user's full name that exceed two consecutive characters
-     * - Be at least six characters in length
-     * - Contain characters from three of the following five categories:
-     * - English uppercase characters (A through Z)
-     * - English lowercase characters (a through z)
-     * - Base 10 digits (0 through 9)
-     * - Non-alphabetic characters (for example, !, $, #, %)
-     * - Any character categorized as an alphabetic but is not uppercase or lowercase.
-     * <p/>
-     * See this article: http://technet.microsoft.com/en-us/library/cc786468%28WS.10%29.aspx
-     *
-     * @param userInfo    userInfoBean
-     * @param password    password to test
-     * @param charCounter associated charCounter for the password.
-     * @return list of errors if the password does not meet requirements, or an empty list if the password complies
-     *         with AD requirements
-     */
-
-    static List<ErrorInformation> checkPasswordForADComplexity(
-            final ADPolicyComplexity complexityLevel,
-            final UserInfo userInfo,
-            final String password,
-            final PasswordCharCounter charCounter,
-            final int maxGroupViolationCount
-    )
-            throws PwmUnrecoverableException
-    {
-        final List<ErrorInformation> errorList = new ArrayList<>();
-
-        if ( password == null || password.length() < 6 )
-        {
-            errorList.add( new ErrorInformation( PwmError.PASSWORD_TOO_SHORT ) );
-            return errorList;
-        }
-
-        final int maxLength = complexityLevel == ADPolicyComplexity.AD2003 ? 128 : 512;
-        if ( password.length() > maxLength )
-        {
-            errorList.add( new ErrorInformation( PwmError.PASSWORD_TOO_LONG ) );
-            return errorList;
-        }
-
-        if ( userInfo != null && userInfo.getCachedPasswordRuleAttributes() != null )
-        {
-            final Map<String, String> userAttrs = userInfo.getCachedPasswordRuleAttributes();
-            final String samAccountName = userAttrs.get( "sAMAccountName" );
-            if ( samAccountName != null
-                    && samAccountName.length() > 2
-                    && samAccountName.length() >= password.length() )
-            {
-                if ( password.toLowerCase().contains( samAccountName.toLowerCase() ) )
-                {
-                    errorList.add( new ErrorInformation( PwmError.PASSWORD_INWORDLIST ) );
-                    LOGGER.trace( () -> "Password violation due to ADComplexity check: Password contains sAMAccountName" );
-                }
-            }
-            final String displayName = userAttrs.get( "displayName" );
-            if ( displayName != null && displayName.length() > 2 )
-            {
-                if ( checkContainsTokens( password, displayName ) )
-                {
-                    errorList.add( new ErrorInformation( PwmError.PASSWORD_INWORDLIST ) );
-                    LOGGER.trace( () -> "Password violation due to ADComplexity check: Tokens from displayName used in password" );
-                }
-            }
-        }
-
-        int complexityPoints = 0;
-        if ( charCounter.getUpperCharCount() > 0 )
-        {
-            complexityPoints++;
-        }
-        if ( charCounter.getLowerCharCount() > 0 )
-        {
-            complexityPoints++;
-        }
-        if ( charCounter.getNumericCharCount() > 0 )
-        {
-            complexityPoints++;
-        }
-        switch ( complexityLevel )
-        {
-            case AD2003:
-                if ( charCounter.getSpecialCharsCount() > 0 || charCounter.getOtherLetterCharCount() > 0 )
-                {
-                    complexityPoints++;
-                }
-                break;
-
-            case AD2008:
-                if ( charCounter.getSpecialCharsCount() > 0 )
-                {
-                    complexityPoints++;
-                }
-                if ( charCounter.getOtherLetterCharCount() > 0 )
-                {
-                    complexityPoints++;
-                }
-                break;
-
-            default:
-                JavaHelper.unhandledSwitchStatement( complexityLevel );
-        }
-
-        switch ( complexityLevel )
-        {
-            case AD2008:
-                final int totalGroups = 5;
-                final int violations = totalGroups - complexityPoints;
-                if ( violations <= maxGroupViolationCount )
-                {
-                    return errorList;
-                }
-                break;
-
-            case AD2003:
-                if ( complexityPoints >= 3 )
-                {
-                    return errorList;
-                }
-                break;
-
-            default:
-                JavaHelper.unhandledSwitchStatement( complexityLevel );
-        }
-
-        if ( charCounter.getUpperCharCount() < 1 )
-        {
-            errorList.add( new ErrorInformation( PwmError.PASSWORD_NOT_ENOUGH_UPPER ) );
-        }
-        if ( charCounter.getLowerCharCount() < 1 )
-        {
-            errorList.add( new ErrorInformation( PwmError.PASSWORD_NOT_ENOUGH_LOWER ) );
-        }
-        if ( charCounter.getNumericCharCount() < 1 )
-        {
-            errorList.add( new ErrorInformation( PwmError.PASSWORD_NOT_ENOUGH_NUM ) );
-        }
-        if ( charCounter.getSpecialCharsCount() < 1 )
-        {
-            errorList.add( new ErrorInformation( PwmError.PASSWORD_NOT_ENOUGH_SPECIAL ) );
-        }
-        if ( charCounter.getOtherLetterCharCount() < 1 )
-        {
-            errorList.add( new ErrorInformation( PwmError.PASSWORD_UNKNOWN_VALIDATION ) );
-        }
-
-        return errorList;
-    }
-
-    // escape characters permitted because they match the exact AD specification
-    @SuppressWarnings( "checkstyle:avoidescapedunicodecharacters" )
-    private static boolean checkContainsTokens( final String baseValue, final String checkPattern )
-    {
-        if ( baseValue == null || baseValue.length() == 0 )
-        {
-            return false;
-        }
-
-        if ( checkPattern == null || checkPattern.length() == 0 )
-        {
-            return false;
-        }
-
-        final String baseValueLower = baseValue.toLowerCase();
-
-        final String[] tokens = checkPattern.toLowerCase().split( "[,\\.\\-\u2013\u2014_ \u00a3\\t]+" );
-
-        if ( tokens != null && tokens.length > 0 )
-        {
-            for ( final String token : tokens )
-            {
-                if ( token.length() > 2 )
-                {
-                    if ( baseValueLower.contains( token ) )
-                    {
-                        return true;
-                    }
-                }
-            }
-        }
-        return false;
-    }
-
     public static boolean tooManyConsecutiveChars( final String str, final int maximumConsecutive )
     public static boolean tooManyConsecutiveChars( final String str, final int maximumConsecutive )
     {
     {
         if ( str != null && maximumConsecutive > 1 && str.length() >= maximumConsecutive )
         if ( str != null && maximumConsecutive > 1 && str.length() >= maximumConsecutive )

+ 127 - 103
server/src/main/java/password/pwm/util/password/RandomPasswordGenerator.java

@@ -119,140 +119,164 @@ public class RandomPasswordGenerator
             throws PwmUnrecoverableException
             throws PwmUnrecoverableException
     {
     {
         final Instant startTime = Instant.now();
         final Instant startTime = Instant.now();
-        final PwmRandom pwmRandom = pwmDomain.getSecureService().pwmRandom();
 
 
         randomGeneratorConfig.validateSettings( pwmDomain );
         randomGeneratorConfig.validateSettings( pwmDomain );
 
 
-        final RandomGeneratorConfig effectiveConfig;
+        final RandomGeneratorConfig effectiveConfig = makeEffectiveConfig( randomGeneratorConfig, pwmDomain );
+        final PwmRandom pwmRandom = pwmDomain.getSecureService().pwmRandom();
+        final SeedMachine seedMachine = new SeedMachine( pwmRandom, normalizeSeeds( effectiveConfig.getSeedlistPhrases() ) );
+
+        // determine the password policy to use for random generation
+        final PwmPasswordPolicy randomGenPolicy = makeRandomGenPwdPolicy( effectiveConfig, pwmDomain );
+
+        // read a rule validator
+        // modify until it passes all the rules
+        final MutatorResult mutatorResult = passwordMutator( sessionLabel, pwmDomain, seedMachine, effectiveConfig, randomGenPolicy );
+
+        // report outcome
+
+        if ( mutatorResult.isValidPassword() )
+        {
+            LOGGER.trace( sessionLabel, () -> "finished random password generation after " + mutatorResult.getTryCount()
+                    + " tries.", () -> TimeDuration.fromCurrent( startTime ) );
+        }
+        else
         {
         {
-            if ( randomGeneratorConfig.getSeedlistPhrases() == null || randomGeneratorConfig.getSeedlistPhrases().isEmpty() )
+            if ( LOGGER.isEnabled( PwmLogLevel.ERROR ) )
             {
             {
-                Set<String> seeds = DEFAULT_SEED_PHRASES;
+                final PwmPasswordRuleValidator pwmPasswordRuleValidator = PwmPasswordRuleValidator.create( sessionLabel, pwmDomain, randomGenPolicy );
+                final int errors = pwmPasswordRuleValidator.internalPwmPolicyValidator( mutatorResult.getPassword(), null, null ).size();
+                final int judgeLevel = PasswordUtility.judgePasswordStrength( pwmDomain.getConfig(), mutatorResult.getPassword() );
+                LOGGER.error( sessionLabel, () -> "failed random password generation after "
+                                + mutatorResult.getTryCount() + " tries. " + "(errors=" + errors + ", judgeLevel=" + judgeLevel,
+                        () -> TimeDuration.fromCurrent( startTime ) );
+            }
+        }
+
+        StatisticsClient.incrementStat( pwmDomain, Statistic.GENERATED_PASSWORDS );
+
+        LOGGER.trace( sessionLabel, () -> "real-time random password generator called"
+                + " (" + TimeDuration.compactFromCurrent( startTime ) + ")" );
+
+        return new PasswordData( mutatorResult.getPassword() );
+    }
+
+    @Value
+    private static class MutatorResult
+    {
+        private final String password;
+        private final boolean validPassword;
+        private final int tryCount;
+    }
+
+    private static PwmPasswordPolicy makeRandomGenPwdPolicy(
+            final RandomGeneratorConfig effectiveConfig,
+            final PwmDomain pwmDomain
+    )
+    {
+        final Map<String, String> newPolicyMap = new HashMap<>( effectiveConfig.getPasswordPolicy().getPolicyMap() );
+
+        newPolicyMap.put( PwmPasswordRule.MaximumLength.getKey(), String.valueOf( effectiveConfig.getMaximumLength() ) );
+        if ( effectiveConfig.getMinimumLength() > effectiveConfig.getPasswordPolicy().getRuleHelper().readIntValue( PwmPasswordRule.MinimumLength ) )
+        {
+            newPolicyMap.put( PwmPasswordRule.MinimumLength.getKey(), String.valueOf( effectiveConfig.getMinimumLength() ) );
+        }
+        if ( effectiveConfig.getMaximumLength() < effectiveConfig.getPasswordPolicy().getRuleHelper().readIntValue( PwmPasswordRule.MaximumLength ) )
+        {
+            newPolicyMap.put( PwmPasswordRule.MaximumLength.getKey(), String.valueOf( effectiveConfig.getMaximumLength() ) );
+        }
+        if ( effectiveConfig.getMinimumStrength() > effectiveConfig.getPasswordPolicy().getRuleHelper().readIntValue( PwmPasswordRule.MinimumStrength ) )
+        {
+            newPolicyMap.put( PwmPasswordRule.MinimumStrength.getKey(), String.valueOf( effectiveConfig.getMinimumStrength() ) );
+        }
+        return  PwmPasswordPolicy.createPwmPasswordPolicy( pwmDomain.getDomainID(), newPolicyMap );
+    }
+
+    private static RandomGeneratorConfig makeEffectiveConfig(
+            final RandomGeneratorConfig randomGeneratorConfig,
+            final PwmDomain pwmDomain
+    )
+            throws PwmUnrecoverableException
+    {
+        if ( randomGeneratorConfig.getSeedlistPhrases() == null || randomGeneratorConfig.getSeedlistPhrases().isEmpty() )
+        {
+            Set<String> seeds = DEFAULT_SEED_PHRASES;
 
 
-                final SeedlistService seedlistManager = pwmDomain.getPwmApplication().getSeedlistManager();
-                if ( seedlistManager != null && seedlistManager.status() == PwmService.STATUS.OPEN && seedlistManager.size() > 0 )
+            final SeedlistService seedlistManager = pwmDomain.getPwmApplication().getSeedlistManager();
+            if ( seedlistManager != null && seedlistManager.status() == PwmService.STATUS.OPEN && seedlistManager.size() > 0 )
+            {
+                seeds = new HashSet<>();
+                int safetyCounter = 0;
+                while ( seeds.size() < 10 && safetyCounter < 100 )
                 {
                 {
-                    seeds = new HashSet<>();
-                    int safetyCounter = 0;
-                    while ( seeds.size() < 10 && safetyCounter < 100 )
+                    safetyCounter++;
+                    final String randomWord = seedlistManager.randomSeed();
+                    if ( randomWord != null )
                     {
                     {
-                        safetyCounter++;
-                        final String randomWord = seedlistManager.randomSeed();
-                        if ( randomWord != null )
-                        {
-                            seeds.add( randomWord );
-                        }
+                        seeds.add( randomWord );
                     }
                     }
                 }
                 }
-                effectiveConfig = randomGeneratorConfig.toBuilder()
-                        .seedlistPhrases( seeds )
-                        .build();
-            }
-            else
-            {
-                effectiveConfig = randomGeneratorConfig;
             }
             }
+            return randomGeneratorConfig.toBuilder()
+                    .seedlistPhrases( seeds )
+                    .build();
         }
         }
 
 
-        final SeedMachine seedMachine = new SeedMachine( pwmRandom, normalizeSeeds( effectiveConfig.getSeedlistPhrases() ) );
+        return randomGeneratorConfig;
 
 
-        int tryCount = 0;
-        final StringBuilder password = new StringBuilder();
+    }
 
 
-        // determine the password policy to use for random generation
-        final PwmPasswordPolicy randomGenPolicy;
-        {
-            final Map<String, String> newPolicyMap = new HashMap<>( effectiveConfig.getPasswordPolicy().getPolicyMap() );
+    private static MutatorResult passwordMutator(
+            final SessionLabel sessionLabel,
+            final PwmDomain pwmDomain,
+            final SeedMachine seedMachine,
+            final RandomGeneratorConfig effectiveConfig,
+            final PwmPasswordPolicy randomGenPolicy
 
 
-            newPolicyMap.put( PwmPasswordRule.MaximumLength.getKey(), String.valueOf( effectiveConfig.getMaximumLength() ) );
+    )
+            throws PwmUnrecoverableException
+    {
 
 
-            if ( effectiveConfig.getMinimumLength() > effectiveConfig.getPasswordPolicy().getRuleHelper().readIntValue( PwmPasswordRule.MinimumLength ) )
-            {
-                newPolicyMap.put( PwmPasswordRule.MinimumLength.getKey(), String.valueOf( effectiveConfig.getMinimumLength() ) );
-            }
-            if ( effectiveConfig.getMaximumLength() < effectiveConfig.getPasswordPolicy().getRuleHelper().readIntValue( PwmPasswordRule.MaximumLength ) )
-            {
-                newPolicyMap.put( PwmPasswordRule.MaximumLength.getKey(), String.valueOf( effectiveConfig.getMaximumLength() ) );
-            }
-            if ( effectiveConfig.getMinimumStrength() > effectiveConfig.getPasswordPolicy().getRuleHelper().readIntValue( PwmPasswordRule.MinimumStrength ) )
-            {
-                newPolicyMap.put( PwmPasswordRule.MinimumStrength.getKey(), String.valueOf( effectiveConfig.getMinimumStrength() ) );
-            }
-            randomGenPolicy = PwmPasswordPolicy.createPwmPasswordPolicy( pwmDomain.getDomainID(), newPolicyMap );
-        }
+        final int maxTryCount = Integer.parseInt( pwmDomain.getConfig().readAppProperty( AppProperty.PASSWORD_RANDOMGEN_MAX_ATTEMPTS ) );
+        final int jitterCount = Integer.parseInt( pwmDomain.getConfig().readAppProperty( AppProperty.PASSWORD_RANDOMGEN_JITTER_COUNT ) );
+        final PwmRandom pwmRandom = pwmDomain.getSecureService().pwmRandom();
 
 
-        // initial creation
+        final StringBuilder password = new StringBuilder();
         password.append( generateNewPassword( pwmRandom, seedMachine, effectiveConfig.getMinimumLength() ) );
         password.append( generateNewPassword( pwmRandom, seedMachine, effectiveConfig.getMinimumLength() ) );
 
 
-        boolean validPassword = false;
-
-        // read a rule validator
-        // modify until it passes all the rules
-        {
-            final int maxTryCount = Integer.parseInt( pwmDomain.getConfig().readAppProperty( AppProperty.PASSWORD_RANDOMGEN_MAX_ATTEMPTS ) );
-            final int jitterCount = Integer.parseInt( pwmDomain.getConfig().readAppProperty( AppProperty.PASSWORD_RANDOMGEN_JITTER_COUNT ) );
-            {
-                final PwmPasswordRuleValidator pwmPasswordRuleValidator = PwmPasswordRuleValidator.create(
-                        sessionLabel, pwmDomain, randomGenPolicy, PwmPasswordRuleValidator.Flag.FailFast );
 
 
-                while ( !validPassword && tryCount < maxTryCount )
-                {
-                    tryCount++;
-                    validPassword = true;
+        final PwmPasswordRuleValidator pwmPasswordRuleValidator = PwmPasswordRuleValidator.create(
+                sessionLabel, pwmDomain, randomGenPolicy, PwmPasswordRuleValidator.Flag.FailFast );
 
 
-                    if ( tryCount % jitterCount == 0 )
-                    {
-                        password.delete( 0, password.length() );
-                        password.append( generateNewPassword( pwmRandom, seedMachine, effectiveConfig.getMinimumLength() ) );
-                    }
+        int tryCount = 0;
+        boolean validPassword = false;
+        while ( !validPassword && tryCount < maxTryCount )
+        {
+            tryCount++;
+            validPassword = true;
 
 
-                    final List<ErrorInformation> errors = pwmPasswordRuleValidator.internalPwmPolicyValidator(
-                            password.toString(), null, null );
-                    if ( errors != null && !errors.isEmpty() )
-                    {
-                        validPassword = false;
-                        modifyPasswordBasedOnErrors( pwmRandom, password, errors, seedMachine );
-                    }
-                    else if ( checkPasswordAgainstDisallowedHttpValues( pwmDomain.getConfig(), password.toString() ) )
-                    {
-                        validPassword = false;
-                        password.delete( 0, password.length() );
-                        password.append( generateNewPassword( pwmRandom, seedMachine, effectiveConfig.getMinimumLength() ) );
-                    }
-                }
+            if ( tryCount % jitterCount == 0 )
+            {
+                password.delete( 0, password.length() );
+                password.append( generateNewPassword( pwmRandom, seedMachine, effectiveConfig.getMinimumLength() ) );
             }
             }
-        }
 
 
-        // report outcome
-        {
-            final PwmPasswordRuleValidator pwmPasswordRuleValidator = PwmPasswordRuleValidator.create( sessionLabel, pwmDomain, randomGenPolicy );
-            if ( validPassword )
+            final List<ErrorInformation> errors = pwmPasswordRuleValidator.internalPwmPolicyValidator(
+                    password.toString(), null, null );
+            if ( errors != null && !errors.isEmpty() )
             {
             {
-                final int finalTryCount = tryCount;
-                LOGGER.trace( sessionLabel, () -> "finished random password generation after " + finalTryCount
-                        + " tries.", () -> TimeDuration.fromCurrent( startTime ) );
+                validPassword = false;
+                modifyPasswordBasedOnErrors( pwmRandom, password, errors, seedMachine );
             }
             }
-            else
+            else if ( checkPasswordAgainstDisallowedHttpValues( pwmDomain.getConfig(), password.toString() ) )
             {
             {
-                if ( LOGGER.isEnabled( PwmLogLevel.ERROR ) )
-                {
-                    final int errors = pwmPasswordRuleValidator.internalPwmPolicyValidator( password.toString(), null, null ).size();
-                    final int judgeLevel = PasswordUtility.judgePasswordStrength( pwmDomain.getConfig(), password.toString() );
-                    final int finalTryCount = tryCount;
-                    LOGGER.error( sessionLabel, () -> "failed random password generation after "
-                            + finalTryCount + " tries. " + "(errors=" + errors + ", judgeLevel=" + judgeLevel,
-                            () -> TimeDuration.fromCurrent( startTime ) );
-                }
+                validPassword = false;
+                password.delete( 0, password.length() );
+                password.append( generateNewPassword( pwmRandom, seedMachine, effectiveConfig.getMinimumLength() ) );
             }
             }
         }
         }
 
 
-        StatisticsClient.incrementStat( pwmDomain, Statistic.GENERATED_PASSWORDS );
-
-        final String logText = "real-time random password generator called"
-                + " (" + TimeDuration.compactFromCurrent( startTime ) + ")";
-        LOGGER.trace( sessionLabel, () -> logText );
-
-        return new PasswordData( password.toString() );
+        return new MutatorResult( password.toString(), validPassword, tryCount );
     }
     }
 
 
     private static void modifyPasswordBasedOnErrors(
     private static void modifyPasswordBasedOnErrors(

+ 86 - 61
server/src/main/java/password/pwm/ws/server/RestServlet.java

@@ -79,8 +79,6 @@ public abstract class RestServlet extends HttpServlet
     {
     {
         final Instant startTime = Instant.now();
         final Instant startTime = Instant.now();
 
 
-        RestResultBean restResultBean = RestResultBean.fromError( new ErrorInformation( PwmError.ERROR_APP_UNAVAILABLE ), true );
-
         final PwmApplication pwmApplication;
         final PwmApplication pwmApplication;
         try
         try
         {
         {
@@ -88,17 +86,10 @@ public abstract class RestServlet extends HttpServlet
         }
         }
         catch ( final PwmUnrecoverableException e )
         catch ( final PwmUnrecoverableException e )
         {
         {
-            outputRestResultBean( restResultBean, req, resp );
+            outputRestResultBean( RestResultBean.fromError( new ErrorInformation( PwmError.ERROR_APP_UNAVAILABLE ), true ), req, resp );
             return;
             return;
         }
         }
 
 
-        final Locale locale;
-        {
-            final List<Locale> knownLocales = pwmApplication.getConfig().getKnownLocales();
-            locale = LocaleHelper.localeResolver( req.getLocale(), knownLocales );
-            resp.setHeader( HttpHeader.ContentLanguage.getHttpName(), LocaleHelper.getBrowserLocaleString( locale ) );
-        }
-
         final PwmDomain pwmDomain;
         final PwmDomain pwmDomain;
         try
         try
         {
         {
@@ -107,10 +98,11 @@ public abstract class RestServlet extends HttpServlet
         }
         }
         catch ( final PwmUnrecoverableException e )
         catch ( final PwmUnrecoverableException e )
         {
         {
-            outputRestResultBean( restResultBean, req, resp );
+            outputRestResultBean( RestResultBean.fromError( new ErrorInformation( PwmError.ERROR_APP_UNAVAILABLE ), true ), req, resp );
             return;
             return;
         }
         }
 
 
+        final Locale locale = readLocale( pwmApplication, req, resp );
 
 
         final SessionLabel sessionLabel;
         final SessionLabel sessionLabel;
         try
         try
@@ -124,7 +116,7 @@ public abstract class RestServlet extends HttpServlet
         }
         }
         catch ( final PwmUnrecoverableException e )
         catch ( final PwmUnrecoverableException e )
         {
         {
-            restResultBean = RestResultBean.fromError(
+            final RestResultBean restResultBean  = RestResultBean.fromError(
                     e.getErrorInformation(),
                     e.getErrorInformation(),
                     pwmDomain,
                     pwmDomain,
                     locale,
                     locale,
@@ -135,30 +127,18 @@ public abstract class RestServlet extends HttpServlet
             return;
             return;
         }
         }
 
 
-        try
-        {
-            if ( LOGGER.isEnabled( PwmLogLevel.TRACE ) )
-            {
-                final PwmHttpRequestWrapper httpRequestWrapper = new PwmHttpRequestWrapper( req, pwmApplication.getConfig() );
-                final String debutTxt = httpRequestWrapper.debugHttpRequestToString( null, true );
-                LOGGER.trace( sessionLabel, () -> "incoming HTTP REST request: " + debutTxt );
-            }
-        }
-        catch ( final PwmUnrecoverableException e )
-        {
-            LOGGER.error( () -> "error while trying to log HTTP request data " + e.getMessage(), e );
-        }
+        logHttpRequest( pwmApplication, req, sessionLabel );
 
 
         if ( pwmApplication.getApplicationMode() != PwmApplicationMode.RUNNING )
         if ( pwmApplication.getApplicationMode() != PwmApplicationMode.RUNNING )
         {
         {
-            outputRestResultBean( restResultBean, req, resp );
+            outputRestResultBean( RestResultBean.fromError( new ErrorInformation( PwmError.ERROR_APP_UNAVAILABLE ), true ), req, resp );
             return;
             return;
         }
         }
 
 
         if ( !pwmDomain.getConfig().readSettingAsBoolean( PwmSetting.ENABLE_EXTERNAL_WEBSERVICES ) )
         if ( !pwmDomain.getConfig().readSettingAsBoolean( PwmSetting.ENABLE_EXTERNAL_WEBSERVICES ) )
         {
         {
             final ErrorInformation errorInformation = new ErrorInformation( PwmError.ERROR_SERVICE_NOT_AVAILABLE, "webservices are not enabled" );
             final ErrorInformation errorInformation = new ErrorInformation( PwmError.ERROR_SERVICE_NOT_AVAILABLE, "webservices are not enabled" );
-            restResultBean = RestResultBean.fromError(
+            final RestResultBean restResultBean = RestResultBean.fromError(
                     errorInformation,
                     errorInformation,
                     pwmDomain,
                     pwmDomain,
                     locale,
                     locale,
@@ -168,47 +148,92 @@ public abstract class RestServlet extends HttpServlet
             return;
             return;
         }
         }
 
 
-        try
-        {
-            final RestAuthentication restAuthentication = new RestAuthenticationProcessor( pwmDomain, sessionLabel, req ).readRestAuthentication();
-            LOGGER.debug( sessionLabel, () -> "rest request authentication status: " + JsonUtil.serialize( restAuthentication ) );
-
-            final RestRequest restRequest = RestRequest.forRequest( pwmDomain, restAuthentication, sessionLabel, req );
+        final RestResultBean restResultBean = executeRequest( req, resp, locale, pwmApplication, pwmDomain, sessionLabel );
 
 
-            RequestInitializationFilter.addStaticResponseHeaders( pwmApplication, req, resp );
-
-            preCheck( restRequest );
-
-            preCheckRequest( restRequest );
+        outputRestResultBean( restResultBean, req, resp );
+        final boolean success = restResultBean != null && !restResultBean.isError();
+        LOGGER.trace( sessionLabel, () -> "completed rest invocation, success=" + success, () -> TimeDuration.fromCurrent( startTime ) );
+    }
 
 
-            restResultBean = invokeWebService( restRequest );
-        }
-        catch ( final PwmUnrecoverableException e )
+   private RestResultBean executeRequest(
+           final HttpServletRequest req,
+           final HttpServletResponse resp,
+           final Locale locale,
+           final PwmApplication pwmApplication,
+           final PwmDomain pwmDomain,
+           final SessionLabel sessionLabel
+   )
+   {
+       try
+       {
+           final RestAuthentication restAuthentication = new RestAuthenticationProcessor( pwmDomain, sessionLabel, req ).readRestAuthentication();
+           LOGGER.debug( sessionLabel, () -> "rest request authentication status: " + JsonUtil.serialize( restAuthentication ) );
+
+           final RestRequest restRequest = RestRequest.forRequest( pwmDomain, restAuthentication, sessionLabel, req );
+
+           RequestInitializationFilter.addStaticResponseHeaders( pwmApplication, req, resp );
+
+           preCheck( restRequest );
+
+           preCheckRequest( restRequest );
+
+           return invokeWebService( restRequest );
+       }
+       catch ( final PwmUnrecoverableException e )
+       {
+           return RestResultBean.fromError(
+                   e.getErrorInformation(),
+                   pwmDomain,
+                   locale,
+                   pwmDomain.getConfig(),
+                   pwmDomain.determineIfDetailErrorMsgShown()
+           );
+       }
+       catch ( final Throwable e )
+       {
+           final String errorMsg = "internal error during rest service invocation: " + e.getMessage();
+           final ErrorInformation errorInformation = new ErrorInformation( PwmError.ERROR_INTERNAL, errorMsg );
+           LOGGER.error( sessionLabel, errorInformation, e );
+           return RestResultBean.fromError(
+                   errorInformation,
+                   pwmDomain,
+                   locale,
+                   pwmDomain.getConfig(),
+                   pwmDomain.determineIfDetailErrorMsgShown() );
+       }
+   }
+
+    private static void logHttpRequest(
+            final PwmApplication pwmApplication,
+            final HttpServletRequest req,
+            final SessionLabel sessionLabel
+    )
+    {
+        try
         {
         {
-            restResultBean = RestResultBean.fromError(
-                    e.getErrorInformation(),
-                    pwmDomain,
-                    locale,
-                    pwmDomain.getConfig(),
-                    pwmDomain.determineIfDetailErrorMsgShown()
-            );
+            if ( LOGGER.isEnabled( PwmLogLevel.TRACE ) )
+            {
+                final PwmHttpRequestWrapper httpRequestWrapper = new PwmHttpRequestWrapper( req, pwmApplication.getConfig() );
+                final String debutTxt = httpRequestWrapper.debugHttpRequestToString( null, true );
+                LOGGER.trace( sessionLabel, () -> "incoming HTTP REST request: " + debutTxt );
+            }
         }
         }
-        catch ( final Throwable e )
+        catch ( final PwmUnrecoverableException e )
         {
         {
-            final String errorMsg = "internal error during rest service invocation: " + e.getMessage();
-            final ErrorInformation errorInformation = new ErrorInformation( PwmError.ERROR_INTERNAL, errorMsg );
-            restResultBean = RestResultBean.fromError(
-                    errorInformation,
-                    pwmDomain,
-                    locale,
-                    pwmDomain.getConfig(),
-                    pwmDomain.determineIfDetailErrorMsgShown() );
-            LOGGER.error( sessionLabel, errorInformation, e );
+            LOGGER.error( () -> "error while trying to log HTTP request data " + e.getMessage(), e );
         }
         }
+    }
 
 
-        outputRestResultBean( restResultBean, req, resp );
-        final boolean success = restResultBean != null && !restResultBean.isError();
-        LOGGER.trace( sessionLabel, () -> "completed rest invocation, success=" + success, () -> TimeDuration.fromCurrent( startTime ) );
+    private static Locale readLocale(
+            final PwmApplication pwmApplication,
+            final HttpServletRequest req,
+            final HttpServletResponse resp
+    )
+    {
+        final List<Locale> knownLocales = pwmApplication.getConfig().getKnownLocales();
+        final Locale locale = LocaleHelper.localeResolver( req.getLocale(), knownLocales );
+        resp.setHeader( HttpHeader.ContentLanguage.getHttpName(), LocaleHelper.getBrowserLocaleString( locale ) );
+        return locale;
     }
     }
 
 
     private RestResultBean invokeWebService( final RestRequest restRequest ) throws IOException, PwmUnrecoverableException
     private RestResultBean invokeWebService( final RestRequest restRequest ) throws IOException, PwmUnrecoverableException