Browse Source

service shutdown fix

Jason Rivard 3 years ago
parent
commit
9e951f8658
66 changed files with 532 additions and 467 deletions
  1. 2 2
      data-service/src/main/java/password/pwm/receiver/Settings.java
  2. 17 0
      lib-util/src/main/java/password/pwm/util/java/CollectionUtil.java
  3. 2 3
      lib-util/src/main/java/password/pwm/util/java/JavaHelper.java
  4. 5 0
      lib-util/src/main/java/password/pwm/util/java/LazySupplier.java
  5. 5 0
      lib-util/src/main/java/password/pwm/util/java/StringUtil.java
  6. 3 2
      server/src/main/java/password/pwm/AppProperty.java
  7. 1 3
      server/src/main/java/password/pwm/PwmAboutProperty.java
  8. 7 6
      server/src/main/java/password/pwm/PwmDomainUtil.java
  9. 1 2
      server/src/main/java/password/pwm/config/AppConfig.java
  10. 1 2
      server/src/main/java/password/pwm/config/PwmSettingCategory.java
  11. 2 2
      server/src/main/java/password/pwm/config/PwmSettingMetaData.java
  12. 3 2
      server/src/main/java/password/pwm/config/PwmSettingStats.java
  13. 2 3
      server/src/main/java/password/pwm/config/PwmSettingTemplateSet.java
  14. 3 0
      server/src/main/java/password/pwm/config/PwmSettingXml.java
  15. 1 2
      server/src/main/java/password/pwm/config/StoredSettingReader.java
  16. 2 4
      server/src/main/java/password/pwm/config/option/WebServiceUsage.java
  17. 1 1
      server/src/main/java/password/pwm/config/profile/LdapProfile.java
  18. 35 35
      server/src/main/java/password/pwm/config/profile/PwmPasswordPolicy.java
  19. 60 98
      server/src/main/java/password/pwm/config/profile/PwmPasswordRule.java
  20. 1 1
      server/src/main/java/password/pwm/config/stored/StoredConfigXmlSerializer.java
  21. 1 1
      server/src/main/java/password/pwm/config/stored/StoredConfigZipJsonSerializer.java
  22. 0 3
      server/src/main/java/password/pwm/config/stored/StoredConfigurationFactory.java
  23. 25 41
      server/src/main/java/password/pwm/config/stored/StoredConfigurationUtil.java
  24. 1 1
      server/src/main/java/password/pwm/config/value/ActionValue.java
  25. 1 1
      server/src/main/java/password/pwm/config/value/BooleanValue.java
  26. 1 1
      server/src/main/java/password/pwm/config/value/ChallengeValue.java
  27. 1 1
      server/src/main/java/password/pwm/config/value/CustomLinkValue.java
  28. 1 1
      server/src/main/java/password/pwm/config/value/EmailValue.java
  29. 1 1
      server/src/main/java/password/pwm/config/value/FileValue.java
  30. 1 1
      server/src/main/java/password/pwm/config/value/FormValue.java
  31. 1 1
      server/src/main/java/password/pwm/config/value/LocalizedStringArrayValue.java
  32. 1 1
      server/src/main/java/password/pwm/config/value/LocalizedStringValue.java
  33. 1 1
      server/src/main/java/password/pwm/config/value/NamedSecretValue.java
  34. 1 1
      server/src/main/java/password/pwm/config/value/NumericArrayValue.java
  35. 1 1
      server/src/main/java/password/pwm/config/value/NumericValue.java
  36. 1 1
      server/src/main/java/password/pwm/config/value/OptionListValue.java
  37. 1 1
      server/src/main/java/password/pwm/config/value/PasswordValue.java
  38. 1 1
      server/src/main/java/password/pwm/config/value/PrivateKeyValue.java
  39. 1 1
      server/src/main/java/password/pwm/config/value/RemoteWebServiceValue.java
  40. 1 1
      server/src/main/java/password/pwm/config/value/StoredValue.java
  41. 18 12
      server/src/main/java/password/pwm/config/value/StringArrayValue.java
  42. 1 1
      server/src/main/java/password/pwm/config/value/StringValue.java
  43. 1 1
      server/src/main/java/password/pwm/config/value/UserPermissionValue.java
  44. 1 1
      server/src/main/java/password/pwm/config/value/ValueFactory.java
  45. 1 1
      server/src/main/java/password/pwm/config/value/VerificationMethodValue.java
  46. 1 1
      server/src/main/java/password/pwm/config/value/X509CertificateValue.java
  47. 2 2
      server/src/main/java/password/pwm/http/PwmRequest.java
  48. 16 25
      server/src/main/java/password/pwm/http/servlet/ClientApiServlet.java
  49. 11 13
      server/src/main/java/password/pwm/http/servlet/admin/AdminServlet.java
  50. 5 5
      server/src/main/java/password/pwm/http/servlet/configguide/ConfigGuideForm.java
  51. 2 3
      server/src/main/java/password/pwm/http/state/CryptoCookieBeanImpl.java
  52. 3 3
      server/src/main/java/password/pwm/svc/AbstractPwmService.java
  53. 9 7
      server/src/main/java/password/pwm/svc/stats/StatisticsService.java
  54. 26 20
      server/src/main/java/password/pwm/svc/version/VersionCheckService.java
  55. 2 2
      server/src/main/java/password/pwm/svc/wordlist/WordlistStatistics.java
  56. 3 3
      server/src/main/java/password/pwm/util/PropertyConfigurationImporter.java
  57. 56 3
      server/src/main/java/password/pwm/util/SampleDataGenerator.java
  58. 27 6
      server/src/main/java/password/pwm/util/macro/MacroRequest.java
  59. 107 33
      server/src/main/java/password/pwm/util/macro/UserMacros.java
  60. 2 2
      server/src/main/java/password/pwm/ws/server/rest/RestStatisticsServer.java
  61. 19 87
      server/src/main/resources/password/pwm/config/PwmSetting.xml
  62. 9 0
      server/src/test/java/password/pwm/config/PwmSettingTest.java
  63. 2 2
      server/src/test/java/password/pwm/http/PwmURLTest.java
  64. 1 1
      server/src/test/java/password/pwm/http/client/PwmHttpClientTest.java
  65. 9 2
      server/src/test/java/password/pwm/util/macro/MacroTest.java
  66. 1 1
      webapp/src/main/webapp/public/resources/themes/pwm/style.css

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

@@ -21,6 +21,7 @@
 package password.pwm.receiver;
 
 import password.pwm.bean.VersionNumber;
+import password.pwm.util.java.CollectionUtil;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.java.TimeDuration;
 
@@ -31,7 +32,6 @@ import java.io.Reader;
 import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
 import java.nio.file.Path;
-import java.util.EnumSet;
 import java.util.Map;
 import java.util.Properties;
 import java.util.stream.Collectors;
@@ -87,7 +87,7 @@ public class Settings
         try ( Reader reader = new InputStreamReader( Files.newInputStream( path ), StandardCharsets.UTF_8 ) )
         {
             properties.load( reader );
-            final Map<Setting, String> returnMap = EnumSet.allOf( Setting.class ).stream()
+            final Map<Setting, String> returnMap = CollectionUtil.enumStream( Setting.class )
                     .collect( Collectors.toUnmodifiableMap(
                             setting -> setting,
                             setting -> properties.getProperty( setting.name(), setting.getDefaultValue() )

+ 17 - 0
lib-util/src/main/java/password/pwm/util/java/CollectionUtil.java

@@ -61,6 +61,18 @@ public class CollectionUtil
                 .collect( Collectors.toUnmodifiableList() );
     }
 
+    public static <V> Set<V> stripNulls( final Set<V> input )
+    {
+        if ( input == null )
+        {
+            return Collections.emptySet();
+        }
+
+        return input.stream()
+                .filter( Objects::nonNull )
+                .collect( Collectors.toUnmodifiableSet() );
+    }
+
     public static <K, V> Map<K, V> stripNulls( final Map<K, V> input )
     {
         if ( input == null )
@@ -206,4 +218,9 @@ public class CollectionUtil
                 () -> new EnumMap<>( keyClass )
         );
     }
+
+    public static <E extends Enum<E>> Stream<E> enumStream( final Class<E> enumClass )
+    {
+        return EnumSet.allOf( enumClass ).stream();
+    }
 }

+ 2 - 3
lib-util/src/main/java/password/pwm/util/java/JavaHelper.java

@@ -43,7 +43,6 @@ import java.time.Instant;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
-import java.util.EnumSet;
 import java.util.LinkedHashMap;
 import java.util.LinkedHashSet;
 import java.util.Map;
@@ -100,7 +99,7 @@ public class JavaHelper
             return Optional.empty();
         }
 
-        return EnumSet.allOf( enumClass ).stream().filter( match ).findFirst();
+        return CollectionUtil.enumStream( enumClass ).filter( match ).findFirst();
     }
 
     public static <E extends Enum<E>> Set<E> readEnumsFromPredicate( final Class<E> enumClass, final Predicate<E> match )
@@ -115,7 +114,7 @@ public class JavaHelper
             return Collections.emptySet();
         }
 
-        return EnumSet.allOf( enumClass ).stream().filter( match ).collect( Collectors.toUnmodifiableSet() );
+        return CollectionUtil.enumStream( enumClass ).filter( match ).collect( Collectors.toUnmodifiableSet() );
     }
 
     public static <E extends Enum<E>> Optional<E> readEnumFromString( final Class<E> enumClass, final String input )

+ 5 - 0
lib-util/src/main/java/password/pwm/util/java/LazySupplier.java

@@ -53,6 +53,11 @@ public class LazySupplier<T> implements Supplier<T>
         return value;
     }
 
+    public boolean isSupplied()
+    {
+        return supplied;
+    }
+
     @SuppressFBWarnings( "THROWS_METHOD_THROWS_CLAUSE_BASIC_EXCEPTION" )
     public interface CheckedSupplier<T, E extends Exception>
     {

+ 5 - 0
lib-util/src/main/java/password/pwm/util/java/StringUtil.java

@@ -542,6 +542,11 @@ public abstract class StringUtil
         return isEmpty( input ) || input.trim().length() == 0;
     }
 
+    public static boolean notTrimEmpty( final String input )
+    {
+        return input != null && !input.trim().isEmpty();
+    }
+
     public static String defaultString( final String input, final String defaultStr )
     {
         return StringUtils.defaultString( input, defaultStr );

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

@@ -20,7 +20,8 @@
 
 package password.pwm;
 
-import java.util.EnumSet;
+import password.pwm.util.java.CollectionUtil;
+
 import java.util.Objects;
 import java.util.Optional;
 import java.util.ResourceBundle;
@@ -438,7 +439,7 @@ public enum AppProperty
 
     public static Optional<AppProperty> forKey( final String key )
     {
-        return EnumSet.allOf( AppProperty.class ).stream()
+        return CollectionUtil.enumStream( AppProperty.class )
                 .filter( loopProperty -> Objects.equals( loopProperty.getKey(), key ) )
                 .findFirst();
     }

+ 1 - 3
server/src/main/java/password/pwm/PwmAboutProperty.java

@@ -38,7 +38,6 @@ import java.nio.charset.Charset;
 import java.security.NoSuchAlgorithmException;
 import java.time.Instant;
 import java.util.Collections;
-import java.util.EnumSet;
 import java.util.Map;
 import java.util.Optional;
 import java.util.TreeMap;
@@ -138,8 +137,7 @@ public enum PwmAboutProperty
             final PwmApplication pwmApplication
     )
     {
-        return Collections.unmodifiableMap( EnumSet.allOf( PwmAboutProperty.class )
-                .stream()
+        return Collections.unmodifiableMap( CollectionUtil.enumStream( PwmAboutProperty.class )
                 .map( aboutProp -> new Pair<>( aboutProp, readAboutValue( pwmApplication, aboutProp ) ) )
                 .filter( entry -> entry.getValue().isPresent() )
                 .collect( CollectionUtil.collectorToEnumMap( PwmAboutProperty.class,

+ 7 - 6
server/src/main/java/password/pwm/PwmDomainUtil.java

@@ -24,6 +24,7 @@ import password.pwm.bean.DomainID;
 import password.pwm.config.AppConfig;
 import password.pwm.config.DomainConfig;
 import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.util.java.CollectionUtil;
 import password.pwm.util.java.TimeDuration;
 import password.pwm.util.logging.PwmLogger;
 
@@ -211,13 +212,13 @@ class PwmDomainUtil
         {
             final Set<DomainID> obsoleteDomains = new HashSet<>( oldConfig.getDomainConfigs().keySet() );
             obsoleteDomains.removeAll( newConfig.getDomainConfigs().keySet() );
-            types.put( DomainModifyCategory.obsolete, Collections.unmodifiableSet( obsoleteDomains ) );
+            types.put( DomainModifyCategory.obsolete, CollectionUtil.stripNulls( obsoleteDomains ) );
         }
 
         {
             final Set<DomainID> createdDomains = new HashSet<>( newConfig.getDomainConfigs().keySet() );
             createdDomains.removeAll( oldConfig.getDomainConfigs().keySet() );
-            types.put( DomainModifyCategory.created, Collections.unmodifiableSet( createdDomains ) );
+            types.put( DomainModifyCategory.created, CollectionUtil.stripNulls( createdDomains ) );
         }
 
         final Set<DomainID> unchangedDomains = new HashSet<>();
@@ -225,9 +226,9 @@ class PwmDomainUtil
         for ( final DomainID domainID : newConfig.getDomainConfigs().keySet() )
         {
             final DomainConfig newDomainConfig = newConfig.getDomainConfigs().get( domainID );
-            final String oldValueHash = oldConfig.getDomainConfigs().get( newDomainConfig.getDomainID() ).getValueHash();
+            final DomainConfig oldDomainConfig = oldConfig.getDomainConfigs().get( newDomainConfig.getDomainID() );
 
-            if ( Objects.equals( oldValueHash, newDomainConfig.getValueHash() ) )
+            if ( newDomainConfig != null && oldDomainConfig != null && Objects.equals( oldDomainConfig.getValueHash(), newDomainConfig.getValueHash() ) )
             {
                 unchangedDomains.add( domainID );
             }
@@ -236,8 +237,8 @@ class PwmDomainUtil
                 modifiedDomains.add( domainID );
             }
         }
-        types.put( DomainModifyCategory.unchanged, Collections.unmodifiableSet( unchangedDomains ) );
-        types.put( DomainModifyCategory.modified, Collections.unmodifiableSet( modifiedDomains ) );
+        types.put( DomainModifyCategory.unchanged, CollectionUtil.stripNulls( unchangedDomains ) );
+        types.put( DomainModifyCategory.modified, CollectionUtil.stripNulls( modifiedDomains ) );
         return Collections.unmodifiableMap( types );
     }
 }

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

@@ -53,7 +53,6 @@ import java.security.cert.X509Certificate;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.EnumMap;
-import java.util.EnumSet;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Locale;
@@ -187,7 +186,7 @@ public class AppConfig implements SettingReader
 
     public Map<AppProperty, String> readAllAppProperties()
     {
-          return Collections.unmodifiableMap( EnumSet.allOf( AppProperty.class ).stream()
+          return Collections.unmodifiableMap( CollectionUtil.enumStream( AppProperty.class )
                   .collect( CollectionUtil.collectorToLinkedMap(
                           Function.identity(),
                           this::readAppProperty

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

@@ -509,8 +509,7 @@ public enum PwmSettingCategory
 
         public static Set<PwmSettingCategory> readChildren( final PwmSettingCategory category )
         {
-            final Set<PwmSettingCategory> categories = EnumSet.allOf( PwmSettingCategory.class )
-                    .stream()
+            final Set<PwmSettingCategory> categories = CollectionUtil.enumStream( PwmSettingCategory.class )
                     .filter( ( loopCategory ) -> loopCategory.getParent() == category )
                     .collect( Collectors.toUnmodifiableSet() );
             return Collections.unmodifiableSet( CollectionUtil.copyToEnumSet( categories, PwmSettingCategory.class ) );

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

@@ -100,8 +100,8 @@ class PwmSettingMetaData
         private static Set<PwmSettingFlag> readFlags( final XmlElement settingElement )
         {
             final Set<PwmSettingFlag> returnObj = EnumSet.noneOf( PwmSettingFlag.class );
-            settingElement.getChild( "flag" ).ifPresent( flagElement ->
-                    flagElement.getChildren( "flags" ).forEach( flagsElement ->
+            settingElement.getChild( PwmSettingXml.XML_ELEMENT_FLAGS ).ifPresent( flagElement ->
+                    flagElement.getChildren( PwmSettingXml.XML_ELEMENT_FLAG ).forEach( flagsElement ->
                     {
                         final String value = flagsElement.getText().orElse( "" ).trim();
                         JavaHelper.readEnumFromString( PwmSettingFlag.class, value ).ifPresent( returnObj::add );

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

@@ -20,8 +20,9 @@
 
 package password.pwm.config;
 
+import password.pwm.util.java.CollectionUtil;
+
 import java.util.Arrays;
-import java.util.EnumSet;
 import java.util.LinkedHashMap;
 import java.util.Map;
 import java.util.stream.Collectors;
@@ -41,7 +42,7 @@ public class PwmSettingStats
 
         returnObj.put( SettingStat.Total, PwmSetting.values().length );
 
-        returnObj.put( SettingStat.hasProfile, EnumSet.allOf( PwmSetting.class ).stream()
+        returnObj.put( SettingStat.hasProfile, CollectionUtil.enumStream( PwmSetting.class )
                 .filter( pwmSetting -> pwmSetting.getCategory().hasProfiles() )
                 .count() );
 

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

@@ -24,7 +24,6 @@ import lombok.Value;
 import password.pwm.util.java.CollectionUtil;
 
 import java.io.Serializable;
-import java.util.EnumSet;
 import java.util.List;
 import java.util.Set;
 import java.util.stream.Collectors;
@@ -42,7 +41,7 @@ public class PwmSettingTemplateSet implements Serializable
                 .map( PwmSettingTemplate::getType )
                 .collect( Collectors.toSet() );
 
-        workingSet.addAll( EnumSet.allOf( PwmSettingTemplate.Type.class ).stream()
+        workingSet.addAll( CollectionUtil.enumStream( PwmSettingTemplate.Type.class )
                 .filter( type -> !seenTypes.contains( type ) )
                 .map( PwmSettingTemplate.Type::getDefaultValue )
                 .collect( Collectors.toUnmodifiableSet( ) ) );
@@ -71,7 +70,7 @@ public class PwmSettingTemplateSet implements Serializable
      */
     public static List<PwmSettingTemplateSet> allValues()
     {
-        return EnumSet.allOf( PwmSettingTemplate.class ).stream()
+        return CollectionUtil.enumStream( PwmSettingTemplate.class )
                 .map( pwmSettingTemplate -> new PwmSettingTemplateSet( Set.of( pwmSettingTemplate ) ) )
                 .collect( Collectors.toUnmodifiableList() );
     }

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

@@ -57,6 +57,9 @@ public class PwmSettingXml
     static final String XML_ELEMENT_LEVEL = "level";
     static final String XML_ELEMENT_PROPERTIES = "properties";
     static final String XML_ELEMENT_PROPERTY = "property";
+    static final String XML_ELEMENT_FLAG = "flag";
+    static final String XML_ELEMENT_FLAGS = "flags";
+
     static final String XML_ATTRIBUTE_KEY = "key";
     static final String XML_ELEMENT_VALUE = "value";
     static final String XML_ELEMENT_OPTION = "option";

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

@@ -59,7 +59,6 @@ import java.security.cert.X509Certificate;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.EnumMap;
-import java.util.EnumSet;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
@@ -242,7 +241,7 @@ public class StoredSettingReader implements SettingReader
         )
         {
             final Map<ProfileDefinition, Map<String, Profile>> returnMap = new EnumMap<>( ProfileDefinition.class );
-            returnMap.putAll( EnumSet.allOf( ProfileDefinition.class ).stream()
+            returnMap.putAll( CollectionUtil.enumStream( ProfileDefinition.class )
                     .filter( profileDefinition -> domainID.inScope( profileDefinition.getCategory().getScope() ) )
                     .collect( CollectionUtil.collectorToLinkedMap(
                             profileDefinition -> profileDefinition,

+ 2 - 4
server/src/main/java/password/pwm/config/option/WebServiceUsage.java

@@ -20,13 +20,13 @@
 
 package password.pwm.config.option;
 
+import password.pwm.util.java.JavaHelper;
 import password.pwm.ws.server.RestAuthenticationType;
 
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.EnumSet;
 import java.util.Set;
-import java.util.stream.Collectors;
 
 public enum WebServiceUsage
 {
@@ -59,8 +59,6 @@ public enum WebServiceUsage
 
     public static Set<WebServiceUsage> forType( final RestAuthenticationType type )
     {
-        return EnumSet.allOf( WebServiceUsage.class ).stream()
-                .filter( webServiceUsage -> webServiceUsage.getTypes().contains( type ) )
-                .collect( Collectors.toUnmodifiableSet() );
+        return JavaHelper.readEnumsFromPredicate( WebServiceUsage.class, webServiceUsage -> webServiceUsage.getTypes().contains( type ) );
     }
 }

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

@@ -106,7 +106,7 @@ public class LdapProfile extends AbstractProfile implements Profile
     public String getDisplayName( final Locale locale )
     {
         final String displayName = readSettingAsLocalizedString( PwmSetting.LDAP_PROFILE_DISPLAY_NAME, locale );
-        return displayName == null || displayName.length() < 1 ? getIdentifier() : displayName;
+        return StringUtil.isTrimEmpty( displayName ) ? getIdentifier() : displayName;
     }
 
     public String getUsernameAttribute( )

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

@@ -44,7 +44,6 @@ import password.pwm.util.password.PasswordRuleReaderHelper;
 import java.io.Serializable;
 import java.util.ArrayList;
 import java.util.Collections;
-import java.util.EnumSet;
 import java.util.HashMap;
 import java.util.LinkedHashMap;
 import java.util.List;
@@ -52,7 +51,6 @@ import java.util.Locale;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Optional;
-import java.util.Set;
 import java.util.TreeMap;
 import java.util.function.Supplier;
 import java.util.regex.Pattern;
@@ -73,12 +71,10 @@ public class PwmPasswordPolicy implements Profile, Serializable
     private final transient Supplier<List<HealthRecord>> healthChecker = new LazySupplier<>( () -> doHealthChecks( this ) );
     private final transient ChaiPasswordPolicy chaiPasswordPolicy;
 
-    private final DomainID domainID;
     private final Map<String, String> policyMap;
     private final PolicyMetaData policyMetaData;
 
     private PwmPasswordPolicy(
-            final DomainID domainID,
             final Map<String, String> policyMap,
             final ChaiPasswordPolicy chaiPasswordPolicy,
             final PolicyMetaData policyMetaData
@@ -101,10 +97,9 @@ public class PwmPasswordPolicy implements Profile, Serializable
             }
         }
 
-        this.domainID = domainID;
         this.chaiPasswordPolicy = chaiPasswordPolicy;
         this.policyMetaData = policyMetaData == null ? PolicyMetaData.builder().build() : policyMetaData;
-        this.policyMap = Map.copyOf( effectivePolicyMap );
+        this.policyMap = Collections.unmodifiableMap( new TreeMap<>( effectivePolicyMap ) );
     }
 
     public static PwmPasswordPolicy createPwmPasswordPolicy(
@@ -119,17 +114,17 @@ public class PwmPasswordPolicy implements Profile, Serializable
             final ChaiPasswordPolicy chaiPasswordPolicy
     )
     {
-        return new PwmPasswordPolicy( domainID, policyMap, chaiPasswordPolicy, null );
+        final PolicyMetaData policyMetaData = PolicyMetaData.builder().domainID( domainID ).build();
+        return new PwmPasswordPolicy( policyMap, chaiPasswordPolicy, policyMetaData );
     }
 
     public static PwmPasswordPolicy createPwmPasswordPolicy(
-            final DomainID domainID,
             final Map<String, String> policyMap,
             final ChaiPasswordPolicy chaiPasswordPolicy,
             final PolicyMetaData policyMetaData
     )
     {
-        return new PwmPasswordPolicy( domainID, policyMap, chaiPasswordPolicy, policyMetaData );
+        return new PwmPasswordPolicy( policyMap, chaiPasswordPolicy, policyMetaData );
     }
 
     public static PwmPasswordPolicy createPwmPasswordPolicy(
@@ -158,14 +153,15 @@ public class PwmPasswordPolicy implements Profile, Serializable
         }
 
         // set pwm-specific values
-        final PwmPasswordPolicy.PolicyMetaData policyMetaData = PwmPasswordPolicy.PolicyMetaData.builder()
+        final PwmPasswordPolicy.PolicyMetaData policyMetaData = PolicyMetaData.builder()
                 .profileID( profileID )
+                .domainID( domainConfig.getDomainID() )
                 .userPermissions( settingReader.readSettingAsUserPermission( PwmSetting.PASSWORD_POLICY_QUERY_MATCH ) )
                 .ruleText( readLocalizedSetting( PwmSetting.PASSWORD_POLICY_RULE_TEXT, domainConfig, settingReader ) )
                 .changePasswordText( readLocalizedSetting( PwmSetting.PASSWORD_POLICY_CHANGE_MESSAGE, domainConfig, settingReader ) )
                 .build();
 
-        return PwmPasswordPolicy.createPwmPasswordPolicy( domainConfig.getDomainID(), passwordPolicySettings, null, policyMetaData );
+        return PwmPasswordPolicy.createPwmPasswordPolicy( passwordPolicySettings, null, policyMetaData );
     }
 
 
@@ -207,7 +203,7 @@ public class PwmPasswordPolicy implements Profile, Serializable
 
     public DomainID getDomainID()
     {
-        return domainID;
+        return policyMetaData == null ? null : policyMetaData.getDomainID();
     }
 
     private static PwmPasswordPolicy makeDefaultPolicy()
@@ -215,9 +211,10 @@ public class PwmPasswordPolicy implements Profile, Serializable
         PwmPasswordPolicy newDefaultPolicy = null;
         try
         {
-            final Map<String, String> defaultPolicyMap = EnumSet.allOf( PwmPasswordRule.class ).stream().collect( Collectors.toUnmodifiableMap(
-                    PwmPasswordRule::getKey,
-                    PwmPasswordRule::getDefaultValue ) );
+            final Map<String, String> defaultPolicyMap = CollectionUtil.enumStream( PwmPasswordRule.class )
+                    .collect( Collectors.toUnmodifiableMap(
+                            PwmPasswordRule::getKey,
+                            PwmPasswordRule::getDefaultValue ) );
 
             newDefaultPolicy = createPwmPasswordPolicy( DomainID.systemId(), defaultPolicyMap, null );
         }
@@ -288,26 +285,29 @@ public class PwmPasswordPolicy implements Profile, Serializable
             return this;
         }
 
-        final Set<PwmPasswordRule> pwmPasswordRules = EnumSet.allOf( PwmPasswordRule.class );
-        final Map<String, String> newPasswordPolicies = new HashMap<>( pwmPasswordRules.size() );
-
-        for ( final PwmPasswordRule rule : pwmPasswordRules )
-        {
-            final String ruleKey = rule.getKey();
-            final String value1 = this.policyMap.get( ruleKey );
-            final String value2 = otherPolicy.policyMap.get( ruleKey );
-
-            if ( StringUtil.notEmpty( value1 ) || StringUtil.notEmpty( value2 ) )
-            {
-                final PwmPasswordRuleFunctions.RuleMergeFunction ruleMergeFunction
-                        = PwmPasswordRuleFunctions.RULE_MERGE_FUNCTIONS.getOrDefault( rule, PwmPasswordRuleFunctions.DEFAULT_RULE_MERGE_SINGLETON );
-                ruleMergeFunction.apply( rule, value1, value2 ).ifPresent( value -> newPasswordPolicies.put( ruleKey, value ) );
-            }
-        }
+        final Map<String, String> newPasswordPolicies = CollectionUtil.enumStream( PwmPasswordRule.class )
+                .map( rule -> Map.entry( rule, mergeValue( otherPolicy, rule ) ) )
+                .filter( entry -> entry.getValue().isPresent() )
+                .collect( Collectors.toUnmodifiableMap( entry -> entry.getKey().getKey(), entry -> entry.getValue().get() ) );
 
         final ChaiPasswordPolicy backingPolicy = this.chaiPasswordPolicy != null ? chaiPasswordPolicy : otherPolicy.chaiPasswordPolicy;
         final PolicyMetaData metaData = getPolicyMetaData().merge( otherPolicy.getPolicyMetaData() );
-        return new PwmPasswordPolicy( domainID, newPasswordPolicies, backingPolicy, metaData );
+        return new PwmPasswordPolicy( newPasswordPolicies, backingPolicy, metaData );
+    }
+
+    private Optional<String> mergeValue( final PwmPasswordPolicy otherPolicy, final PwmPasswordRule rule )
+    {
+        final String ruleKey = rule.getKey();
+        final String thisValue = this.policyMap.get( ruleKey );
+        final String otherValue = otherPolicy.policyMap.get( ruleKey );
+
+        if ( thisValue != null || otherValue != null )
+        {
+            final PwmPasswordRuleFunctions.RuleMergeFunction ruleMergeFunction
+                    = PwmPasswordRuleFunctions.RULE_MERGE_FUNCTIONS.getOrDefault( rule, PwmPasswordRuleFunctions.DEFAULT_RULE_MERGE_SINGLETON );
+            return ruleMergeFunction.apply( rule, thisValue, otherValue );
+        }
+        return Optional.empty();
     }
 
     private PolicyMetaData getPolicyMetaData()
@@ -315,9 +315,6 @@ public class PwmPasswordPolicy implements Profile, Serializable
         return policyMetaData;
     }
 
-
-
-
     public Map<String, String> getPolicyMap( )
     {
         return policyMap;
@@ -401,6 +398,8 @@ public class PwmPasswordPolicy implements Profile, Serializable
     @Builder
     public static class PolicyMetaData implements Serializable
     {
+        private final DomainID domainID;
+
         private final String profileID;
 
         @Builder.Default
@@ -418,6 +417,7 @@ public class PwmPasswordPolicy implements Profile, Serializable
                     .changePasswordText( CollectionUtil.isEmpty( changePasswordText ) ? otherPolicy.changePasswordText : changePasswordText )
                     .userPermissions( CollectionUtil.isEmpty( userPermissions ) ? otherPolicy.userPermissions : userPermissions )
                     .profileID( StringUtil.isEmpty( profileID ) ? otherPolicy.profileID : profileID )
+                    .domainID( domainID == null ? otherPolicy.domainID : domainID )
                     .build();
         }
     }

+ 60 - 98
server/src/main/java/password/pwm/config/profile/PwmPasswordRule.java

@@ -26,6 +26,7 @@ import password.pwm.config.DomainConfig;
 import password.pwm.config.PwmSetting;
 import password.pwm.i18n.Message;
 import password.pwm.util.i18n.LocaleHelper;
+import password.pwm.util.java.JavaHelper;
 
 import java.util.List;
 import java.util.Locale;
@@ -44,259 +45,228 @@ public enum PwmPasswordRule
             null,
             ChaiPasswordRule.PolicyEnabled.getRuleType(),
             ChaiPasswordRule.PolicyEnabled.getDefaultValue(),
-            true ),
+            Flag.positiveBooleanMerge ),
 
     MinimumLength(
             ChaiPasswordRule.MinimumLength,
             PwmSetting.PASSWORD_POLICY_MINIMUM_LENGTH,
             ChaiPasswordRule.MinimumLength.getRuleType(),
-            ChaiPasswordRule.MinimumLength.getDefaultValue(),
-            false ),
+            ChaiPasswordRule.MinimumLength.getDefaultValue() ),
 
     MaximumLength(
             ChaiPasswordRule.MaximumLength,
             PwmSetting.PASSWORD_POLICY_MAXIMUM_LENGTH,
             ChaiPasswordRule.MaximumLength.getRuleType(),
-            ChaiPasswordRule.MaximumLength.getDefaultValue(),
-            false ),
+            ChaiPasswordRule.MaximumLength.getDefaultValue() ),
 
     MinimumUpperCase(
             ChaiPasswordRule.MinimumUpperCase,
             PwmSetting.PASSWORD_POLICY_MINIMUM_UPPERCASE,
             ChaiPasswordRule.MinimumUpperCase.getRuleType(),
-            ChaiPasswordRule.MinimumUpperCase.getDefaultValue(),
-            false ),
+            ChaiPasswordRule.MinimumUpperCase.getDefaultValue() ),
 
     MaximumUpperCase(
             ChaiPasswordRule.MaximumUpperCase,
             PwmSetting.PASSWORD_POLICY_MAXIMUM_UPPERCASE,
             ChaiPasswordRule.MaximumUpperCase.getRuleType(),
-            ChaiPasswordRule.MaximumUpperCase.getDefaultValue(),
-            false ),
+            ChaiPasswordRule.MaximumUpperCase.getDefaultValue() ),
 
     MinimumLowerCase(
             ChaiPasswordRule.MinimumLowerCase,
             PwmSetting.PASSWORD_POLICY_MINIMUM_LOWERCASE,
             ChaiPasswordRule.MinimumLowerCase.getRuleType(),
-            ChaiPasswordRule.MinimumLowerCase.getDefaultValue(),
-            false ),
+            ChaiPasswordRule.MinimumLowerCase.getDefaultValue() ),
 
     MaximumLowerCase(
             ChaiPasswordRule.MaximumLowerCase,
             PwmSetting.PASSWORD_POLICY_MAXIMUM_LOWERCASE,
             ChaiPasswordRule.MaximumLowerCase.getRuleType(),
-            ChaiPasswordRule.MaximumLowerCase.getDefaultValue(),
-            false ),
+            ChaiPasswordRule.MaximumLowerCase.getDefaultValue() ),
 
     AllowNumeric(
             ChaiPasswordRule.AllowNumeric,
             PwmSetting.PASSWORD_POLICY_ALLOW_NUMERIC,
             ChaiPasswordRule.AllowNumeric.getRuleType(),
-            ChaiPasswordRule.AllowNumeric.getDefaultValue(),
-            false ),
+            ChaiPasswordRule.AllowNumeric.getDefaultValue() ),
 
     MinimumNumeric(
             ChaiPasswordRule.MinimumNumeric,
             PwmSetting.PASSWORD_POLICY_MINIMUM_NUMERIC,
             ChaiPasswordRule.MinimumNumeric.getRuleType(),
-            ChaiPasswordRule.MinimumNumeric.getDefaultValue(),
-            false ),
+            ChaiPasswordRule.MinimumNumeric.getDefaultValue() ),
 
     MaximumNumeric(
             ChaiPasswordRule.MaximumNumeric,
             PwmSetting.PASSWORD_POLICY_MAXIMUM_NUMERIC,
             ChaiPasswordRule.MaximumNumeric.getRuleType(),
-            ChaiPasswordRule.MaximumNumeric.getDefaultValue(),
-            false ),
+            ChaiPasswordRule.MaximumNumeric.getDefaultValue() ),
 
     MinimumUnique(
             ChaiPasswordRule.MinimumUnique,
             PwmSetting.PASSWORD_POLICY_MINIMUM_UNIQUE,
             ChaiPasswordRule.MinimumUnique.getRuleType(),
-            ChaiPasswordRule.MinimumUnique.getDefaultValue(),
-            false ),
+            ChaiPasswordRule.MinimumUnique.getDefaultValue() ),
 
     MaximumUnique(
             ChaiPasswordRule.MaximumUnique,
             null,
             ChaiPasswordRule.MaximumUnique.getRuleType(),
-            ChaiPasswordRule.MaximumUnique.getDefaultValue(),
-            false ),
+            ChaiPasswordRule.MaximumUnique.getDefaultValue() ),
 
     AllowFirstCharNumeric(
             ChaiPasswordRule.AllowFirstCharNumeric,
             PwmSetting.PASSWORD_POLICY_ALLOW_FIRST_CHAR_NUMERIC,
             ChaiPasswordRule.AllowFirstCharNumeric.getRuleType(),
-            ChaiPasswordRule.AllowFirstCharNumeric.getDefaultValue(),
-            false ),
+            ChaiPasswordRule.AllowFirstCharNumeric.getDefaultValue() ),
 
     AllowLastCharNumeric(
             ChaiPasswordRule.AllowLastCharNumeric,
             PwmSetting.PASSWORD_POLICY_ALLOW_LAST_CHAR_NUMERIC,
             ChaiPasswordRule.AllowLastCharNumeric.getRuleType(),
-            ChaiPasswordRule.AllowLastCharNumeric.getDefaultValue(),
-            false ),
+            ChaiPasswordRule.AllowLastCharNumeric.getDefaultValue() ),
 
     AllowSpecial(
             ChaiPasswordRule.AllowSpecial,
             PwmSetting.PASSWORD_POLICY_ALLOW_SPECIAL,
             ChaiPasswordRule.AllowSpecial.getRuleType(),
-            ChaiPasswordRule.AllowSpecial.getDefaultValue(),
-            false ),
+            ChaiPasswordRule.AllowSpecial.getDefaultValue() ),
 
     MinimumSpecial(
             ChaiPasswordRule.MinimumSpecial,
             PwmSetting.PASSWORD_POLICY_MINIMUM_SPECIAL,
             ChaiPasswordRule.MinimumSpecial.getRuleType(),
-            ChaiPasswordRule.MinimumSpecial.getDefaultValue(),
-            false ),
+            ChaiPasswordRule.MinimumSpecial.getDefaultValue() ),
 
     MaximumSpecial(
             ChaiPasswordRule.MaximumSpecial,
             PwmSetting.PASSWORD_POLICY_MAXIMUM_SPECIAL,
             ChaiPasswordRule.MaximumSpecial.getRuleType(),
-            ChaiPasswordRule.MaximumSpecial.getDefaultValue(),
-            false ),
+            ChaiPasswordRule.MaximumSpecial.getDefaultValue() ),
 
     AllowFirstCharSpecial(
             ChaiPasswordRule.AllowFirstCharSpecial,
             PwmSetting.PASSWORD_POLICY_ALLOW_FIRST_CHAR_SPECIAL,
             ChaiPasswordRule.AllowFirstCharSpecial.getRuleType(),
-            ChaiPasswordRule.AllowFirstCharSpecial.getDefaultValue(),
-            false ),
+            ChaiPasswordRule.AllowFirstCharSpecial.getDefaultValue() ),
 
     AllowLastCharSpecial(
             ChaiPasswordRule.AllowLastCharSpecial,
             PwmSetting.PASSWORD_POLICY_ALLOW_LAST_CHAR_SPECIAL,
             ChaiPasswordRule.AllowLastCharSpecial.getRuleType(),
-            ChaiPasswordRule.AllowLastCharSpecial.getDefaultValue(),
-            false ),
+            ChaiPasswordRule.AllowLastCharSpecial.getDefaultValue() ),
 
     MaximumRepeat(
             ChaiPasswordRule.MaximumRepeat,
             PwmSetting.PASSWORD_POLICY_MAXIMUM_REPEAT,
             ChaiPasswordRule.MaximumRepeat.getRuleType(),
-            ChaiPasswordRule.MaximumRepeat.getDefaultValue(),
-            false ),
+            ChaiPasswordRule.MaximumRepeat.getDefaultValue() ),
 
     MaximumSequentialRepeat(
             ChaiPasswordRule.MaximumSequentialRepeat,
             PwmSetting.PASSWORD_POLICY_MAXIMUM_SEQUENTIAL_REPEAT,
             ChaiPasswordRule.MaximumSequentialRepeat.getRuleType(),
-            ChaiPasswordRule.MaximumSequentialRepeat.getDefaultValue(),
-            false ),
+            ChaiPasswordRule.MaximumSequentialRepeat.getDefaultValue() ),
 
     ChangeMessage(
             ChaiPasswordRule.ChangeMessage,
             PwmSetting.PASSWORD_POLICY_CHANGE_MESSAGE,
             ChaiPasswordRule.ChangeMessage.getRuleType(),
-            ChaiPasswordRule.ChangeMessage.getDefaultValue(),
-            false ),
+            ChaiPasswordRule.ChangeMessage.getDefaultValue() ),
 
     ExpirationInterval(
             ChaiPasswordRule.ExpirationInterval,
             null,
             ChaiPasswordRule.ExpirationInterval.getRuleType(),
-            ChaiPasswordRule.ExpirationInterval.getDefaultValue(),
-            false ),
+            ChaiPasswordRule.ExpirationInterval.getDefaultValue() ),
 
     MinimumLifetime(
             ChaiPasswordRule.MinimumLifetime,
             PwmSetting.PASSWORD_POLICY_MINIMUM_LIFETIME,
             ChaiPasswordRule.MinimumLifetime.getRuleType(),
-            ChaiPasswordRule.MinimumLifetime.getDefaultValue(),
-            false ),
+            ChaiPasswordRule.MinimumLifetime.getDefaultValue() ),
 
     CaseSensitive(
             ChaiPasswordRule.CaseSensitive,
             null,
             ChaiPasswordRule.CaseSensitive.getRuleType(),
             ChaiPasswordRule.CaseSensitive.getDefaultValue(),
-            true ),
+            Flag.positiveBooleanMerge ),
 
     EnforceAtLogin(
             ChaiPasswordRule.EnforceAtLogin,
             null,
             ChaiPasswordRule.EnforceAtLogin.getRuleType(),
-            ChaiPasswordRule.EnforceAtLogin.getDefaultValue(),
-            false ),
+            ChaiPasswordRule.EnforceAtLogin.getDefaultValue() ),
 
     ChallengeResponseEnabled(
             ChaiPasswordRule.ChallengeResponseEnabled,
             null,
             ChaiPasswordRule.ChallengeResponseEnabled.getRuleType(),
-            ChaiPasswordRule.ChallengeResponseEnabled.getDefaultValue(),
-            false ),
+            ChaiPasswordRule.ChallengeResponseEnabled.getDefaultValue() ),
 
     UniqueRequired(
             ChaiPasswordRule.UniqueRequired,
             null,
             ChaiPasswordRule.UniqueRequired.getRuleType(),
             ChaiPasswordRule.UniqueRequired.getDefaultValue(),
-            true ),
+            Flag.positiveBooleanMerge ),
 
     DisallowedValues(
             ChaiPasswordRule.DisallowedValues,
             PwmSetting.PASSWORD_POLICY_DISALLOWED_VALUES,
             ChaiPasswordRule.DisallowedValues.getRuleType(),
-            ChaiPasswordRule.DisallowedValues.getDefaultValue(),
-            false ),
+            ChaiPasswordRule.DisallowedValues.getDefaultValue() ),
 
     DisallowedAttributes(
             ChaiPasswordRule.DisallowedAttributes,
             PwmSetting.PASSWORD_POLICY_DISALLOWED_ATTRIBUTES,
             ChaiPasswordRule.DisallowedAttributes.getRuleType(),
-            ChaiPasswordRule.DisallowedAttributes.getDefaultValue(),
-            false ),
+            ChaiPasswordRule.DisallowedAttributes.getDefaultValue() ),
 
     DisallowCurrent(
             null,
             PwmSetting.PASSWORD_POLICY_DISALLOW_CURRENT,
             ChaiPasswordRule.RuleType.BOOLEAN,
             "false",
-            true ),
+            Flag.positiveBooleanMerge ),
 
     AllowUserChange(
             ChaiPasswordRule.AllowUserChange,
             null,
             ChaiPasswordRule.AllowUserChange.getRuleType(),
             ChaiPasswordRule.AllowUserChange.getDefaultValue(),
-            true ),
+            Flag.positiveBooleanMerge ),
 
     AllowAdminChange(
             ChaiPasswordRule.AllowAdminChange,
             null,
             ChaiPasswordRule.AllowAdminChange.getRuleType(),
             ChaiPasswordRule.AllowAdminChange.getDefaultValue(),
-            true ),
+            Flag.positiveBooleanMerge ),
 
     ADComplexityMaxViolations(
             ChaiPasswordRule.ADComplexityMaxViolation,
             PwmSetting.PASSWORD_POLICY_AD_COMPLEXITY_MAX_VIOLATIONS,
             ChaiPasswordRule.ADComplexityMaxViolation.getRuleType(),
-            ChaiPasswordRule.ADComplexityMaxViolation.getDefaultValue(),
-            false ),
+            ChaiPasswordRule.ADComplexityMaxViolation.getDefaultValue() ),
 
     AllowNonAlpha(
             null,
             PwmSetting.PASSWORD_POLICY_ALLOW_NON_ALPHA,
             ChaiPasswordRule.AllowNonAlpha.getRuleType(),
-            ChaiPasswordRule.AllowNonAlpha.getDefaultValue(),
-            false ),
+            ChaiPasswordRule.AllowNonAlpha.getDefaultValue() ),
 
     MinimumNonAlpha(
             null,
             PwmSetting.PASSWORD_POLICY_MINIMUM_NON_ALPHA,
             ChaiPasswordRule.RuleType.MIN,
-            "0",
-            false ),
+            "0" ),
 
     MaximumNonAlpha(
             null,
             PwmSetting.PASSWORD_POLICY_MAXIMUM_NON_ALPHA,
             ChaiPasswordRule.RuleType.MAX,
-            "0",
-            false ),
+            "0" ),
 
     // pwm specific rules
     // value will be imported indirectly from chai rule
@@ -304,86 +274,73 @@ public enum PwmPasswordRule
             null,
             PwmSetting.PASSWORD_POLICY_AD_COMPLEXITY_LEVEL,
             ChaiPasswordRule.RuleType.OTHER,
-            "NONE",
-            false ),
+            "NONE" ),
 
     MaximumOldChars(
             null,
             PwmSetting.PASSWORD_POLICY_MAXIMUM_OLD_PASSWORD_CHARS,
             ChaiPasswordRule.RuleType.NUMERIC,
-            "",
-            false ),
+            "" ),
 
     RegExMatch(
             null,
             PwmSetting.PASSWORD_POLICY_REGULAR_EXPRESSION_MATCH,
             ChaiPasswordRule.RuleType.OTHER,
-            "",
-            false ),
+            "" ),
 
     RegExNoMatch(
             null,
             PwmSetting.PASSWORD_POLICY_REGULAR_EXPRESSION_NOMATCH,
             ChaiPasswordRule.RuleType.OTHER,
-            "",
-            false
-    ),
+            "" ),
 
     MinimumAlpha(
             null,
             PwmSetting.PASSWORD_POLICY_MINIMUM_ALPHA,
             ChaiPasswordRule.RuleType.MIN,
-            "0",
-            false ),
+            "0" ),
 
     MaximumAlpha(
             null,
             PwmSetting.PASSWORD_POLICY_MAXIMUM_ALPHA,
             ChaiPasswordRule.RuleType.MAX,
-            "0",
-            false
-    ),
+            "0" ),
 
     EnableWordlist(
             null,
             PwmSetting.PASSWORD_POLICY_ENABLE_WORDLIST,
             ChaiPasswordRule.RuleType.BOOLEAN,
-            "true",
-            true ),
+            "false",
+            Flag.positiveBooleanMerge ),
 
     MinimumStrength(
             null,
             PwmSetting.PASSWORD_POLICY_MINIMUM_STRENGTH,
             ChaiPasswordRule.RuleType.MIN,
-            "0",
-            false ),
+            "0" ),
 
     MaximumConsecutive(
             null,
             PwmSetting.PASSWORD_POLICY_MAXIMUM_CONSECUTIVE,
             ChaiPasswordRule.RuleType.MIN,
-            "0",
-            false ),
+            "0" ),
 
     CharGroupsMinMatch(
             null,
             PwmSetting.PASSWORD_POLICY_CHAR_GROUPS_MIN_MATCH,
             ChaiPasswordRule.RuleType.MIN,
-            "0",
-            false ),
+            "0" ),
 
     CharGroupsValues(
             null,
             PwmSetting.PASSWORD_POLICY_CHAR_GROUPS,
             ChaiPasswordRule.RuleType.OTHER,
-            "",
-            false ),
+            "" ),
 
     AllowMacroInRegExSetting(
             AppProperty.ALLOW_MACRO_IN_REGEX_SETTING,
             ChaiPasswordRule.RuleType.BOOLEAN,
-            "true",
-            false ),;
+            "true" ),;
 
     private final ChaiPasswordRule chaiPasswordRule;
     private final PwmSetting pwmSetting;
@@ -392,12 +349,17 @@ public enum PwmPasswordRule
     private final String defaultValue;
     private final boolean positiveBooleanMerge;
 
+    private enum Flag
+    {
+        positiveBooleanMerge,
+    }
+
     PwmPasswordRule(
             final ChaiPasswordRule chaiPasswordRule,
             final PwmSetting pwmSetting,
             final ChaiPasswordRule.RuleType ruleType,
             final String defaultValue,
-            final boolean positiveBooleanMerge
+            final Flag... flags
     )
     {
         this.pwmSetting = pwmSetting;
@@ -405,14 +367,14 @@ public enum PwmPasswordRule
         this.appProperty = null;
         this.ruleType = ruleType;
         this.defaultValue = defaultValue;
-        this.positiveBooleanMerge = positiveBooleanMerge;
+        this.positiveBooleanMerge = JavaHelper.enumArrayContainsValue( flags, Flag.positiveBooleanMerge );
     }
 
     PwmPasswordRule(
             final AppProperty appProperty,
             final ChaiPasswordRule.RuleType ruleType,
             final String defaultValue,
-            final boolean positiveBooleanMerge
+            final Flag... flags
     )
     {
         this.pwmSetting = null;
@@ -420,7 +382,7 @@ public enum PwmPasswordRule
         this.appProperty = appProperty;
         this.ruleType = ruleType;
         this.defaultValue = defaultValue;
-        this.positiveBooleanMerge = positiveBooleanMerge;
+        this.positiveBooleanMerge = JavaHelper.enumArrayContainsValue( flags, Flag.positiveBooleanMerge );
     }
 
     public String getKey( )

+ 1 - 1
server/src/main/java/password/pwm/config/stored/StoredConfigXmlSerializer.java

@@ -918,7 +918,7 @@ public class StoredConfigXmlSerializer implements StoredConfigSerializer
                 final XmlElement settingElement = StoredConfigXmlSerializer.XmlOutputHandler.makeSettingXmlElement(
                         null,
                         key,
-                        new StringArrayValue( newValues ),
+                        StringArrayValue.create( newValues ),
                         XmlOutputProcessData.builder().storedValueEncoderMode( StoredValueEncoder.Mode.PLAIN ).pwmSecurityKey( pwmSecurityKey ).build() );
                 final Optional<XmlElement> settingsElement = xmlDocument.getRootElement().getChild( StoredConfigXmlConstants.XML_ELEMENT_SETTING );
                 settingsElement.ifPresent( ( s ) -> s.attachElement( settingElement ) );

+ 1 - 1
server/src/main/java/password/pwm/config/stored/StoredConfigZipJsonSerializer.java

@@ -104,7 +104,7 @@ public class StoredConfigZipJsonSerializer implements StoredConfigSerializer
                 }
                 else
                 {
-                    storedValue = syntax.getFactory().fromJson( serializedValue.getValueData() );
+                    storedValue = syntax.getFactory().fromJson( key.toPwmSetting(), serializedValue.getValueData() );
                 }
                 storedValueMap.put( key, storedValue );
             }

+ 0 - 3
server/src/main/java/password/pwm/config/stored/StoredConfigurationFactory.java

@@ -25,7 +25,6 @@ import lombok.Value;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmUnrecoverableException;
-import password.pwm.util.logging.PwmLogger;
 
 import java.io.IOException;
 import java.io.InputStream;
@@ -33,8 +32,6 @@ import java.io.OutputStream;
 
 public class StoredConfigurationFactory
 {
-    private static final PwmLogger LOGGER = PwmLogger.forClass( StoredConfigurationFactory.class );
-
     private static final StoredConfigSerializer SERIALIZER = new StoredConfigXmlSerializer();
 
     public static StoredConfiguration newConfig() throws PwmUnrecoverableException

+ 25 - 41
server/src/main/java/password/pwm/config/stored/StoredConfigurationUtil.java

@@ -41,20 +41,23 @@ import password.pwm.error.PwmOperationalException;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.util.PasswordData;
 import password.pwm.util.java.CollectionUtil;
+import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.MiscUtil;
 import password.pwm.util.java.PwmExceptionLoggingConsumer;
 import password.pwm.util.java.StringUtil;
-import password.pwm.util.java.TimeDuration;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.secure.BCrypt;
-import password.pwm.util.secure.HmacAlgorithm;
+import password.pwm.util.secure.PwmHashAlgorithm;
 import password.pwm.util.secure.PwmRandom;
-import password.pwm.util.secure.SecureEngine;
 
-import java.time.Instant;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+import java.security.DigestOutputStream;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.Iterator;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Locale;
@@ -185,15 +188,10 @@ public abstract class StoredConfigurationUtil
             return Stream.empty();
         };
 
-        final Instant startTime = Instant.now();
-        final List<String> errorStrings = CollectionUtil.iteratorToStream( storedConfiguration.keys() )
+        return CollectionUtil.iteratorToStream( storedConfiguration.keys() )
                 .filter( key -> key.isRecordType( StoredConfigKey.RecordType.SETTING ) )
                 .flatMap( validateSettingFunction )
-                .collect( Collectors.toList() );
-
-
-        LOGGER.trace( () -> "StoredConfiguration validator completed", TimeDuration.fromCurrent( startTime ) );
-        return Collections.unmodifiableList( errorStrings );
+                .collect( Collectors.toUnmodifiableList() );
     }
 
     public static boolean verifyPassword( final StoredConfiguration storedConfiguration, final String password )
@@ -252,8 +250,6 @@ public abstract class StoredConfigurationUtil
                 new PasswordValue( new PasswordData( PwmRandom.getInstance().alphaNumericString( 1024 ) ) ),
                 null
         );
-
-        LOGGER.debug( () -> "initialized new random security key" );
     }
 
     public static Map<String, String> makeDebugMap(
@@ -321,8 +317,6 @@ public abstract class StoredConfigurationUtil
             final StoredConfiguration modifiedConfiguration
     )
     {
-        final Instant startTime = Instant.now();
-
         final Predicate<StoredConfigKey> hashTester = key ->
         {
             final Optional<String> hash1 = originalConfiguration.readStoredValue( key ).map( StoredValue::valueHash );
@@ -330,16 +324,12 @@ public abstract class StoredConfigurationUtil
             return !hash1.equals( hash2 );
         };
 
-        final Set<StoredConfigKey> deltaReferences = Stream.concat(
+        return Stream.concat(
                 CollectionUtil.iteratorToStream( originalConfiguration.keys() ),
                 CollectionUtil.iteratorToStream( modifiedConfiguration.keys() ) )
                 .distinct()
                 .filter( hashTester )
                 .collect( Collectors.toUnmodifiableSet() );
-
-        LOGGER.trace( () -> "generated " + deltaReferences.size() + " changeLog items via compare", TimeDuration.fromCurrent( startTime ) );
-
-        return deltaReferences;
     }
 
     public static StoredValue getValueOrDefault(
@@ -437,7 +427,7 @@ public abstract class StoredConfigurationUtil
             final List<String> newProfileIDList = new ArrayList<>( existingProfiles );
             newProfileIDList.add( destinationID );
             final StoredConfigKey key = StoredConfigKey.forSetting( profileSetting, null, domainID );
-            final StoredValue value = new StringArrayValue( newProfileIDList );
+            final StoredValue value = StringArrayValue.create( newProfileIDList );
             modifier.writeSetting( key, value, userIdentity );
         }
 
@@ -452,7 +442,6 @@ public abstract class StoredConfigurationUtil
     )
             throws PwmUnrecoverableException
     {
-        final Instant startTime = Instant.now();
         final DomainID sourceID = DomainID.create( source );
         final DomainID destinationID = DomainID.create( destination );
 
@@ -491,38 +480,33 @@ public abstract class StoredConfigurationUtil
             final StoredConfigKey key = StoredConfigKey.forSetting( PwmSetting.DOMAIN_LIST, null, DomainID.systemId() );
             final List<String> domainList = new ArrayList<>( ValueTypeConverter.valueToStringArray( StoredConfigurationUtil.getValueOrDefault( oldStoredConfiguration, key ) ) );
             domainList.add( destination );
-            final StoredValue value = new StringArrayValue( domainList );
+            final StoredValue value = StringArrayValue.create( domainList );
             modifier.writeSetting( key, value, userIdentity );
         }
 
-        LOGGER.trace( () -> "copied " + modifier.modificationCount() + " domain settings from '" + source + "' to '" + destination + "' domain",
-                TimeDuration.fromCurrent( startTime ) );
-
         return modifier.newStoredConfiguration();
     }
 
     public static String valueHash( final StoredConfiguration storedConfiguration )
     {
-        final Instant startTime = Instant.now();
-        final StringBuilder sb = new StringBuilder();
-
-        CollectionUtil.iteratorToStream( storedConfiguration.keys() )
-                .map( storedConfiguration::readStoredValue )
-                .flatMap( Optional::stream )
-                .forEach( v -> sb.append( v.valueHash() ) );
-
-        final String output;
-        try
+        try ( DigestOutputStream digestOutputStream = new DigestOutputStream( OutputStream.nullOutputStream(), PwmHashAlgorithm.SHA512.newMessageDigest() ) )
         {
-            output = SecureEngine.hmac( HmacAlgorithm.HMAC_SHA_512, storedConfiguration.getKey(), sb.toString() );
+            final Iterator<StoredConfigKey> keyIterator = storedConfiguration.keys();
+            while ( keyIterator.hasNext() )
+            {
+                final Optional<StoredValue> value = storedConfiguration.readStoredValue( keyIterator.next() );
+                if ( value.isPresent() )
+                {
+                    digestOutputStream.write( value.get().valueHash().getBytes( StandardCharsets.UTF_8 ) );
+                }
+            }
+
+            return JavaHelper.binaryArrayToHex( digestOutputStream.getMessageDigest().digest() );
         }
-        catch ( final PwmUnrecoverableException e )
+        catch ( final IOException e )
         {
             throw new IllegalStateException( e );
         }
-
-        LOGGER.trace( () -> "calculated StoredConfiguration hash: " + output, TimeDuration.fromCurrent( startTime ) );
-        return output;
     }
 
     public static boolean isDefaultValue( final StoredConfiguration storedConfiguration, final StoredConfigKey key )

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

@@ -67,7 +67,7 @@ public class ActionValue extends AbstractValue implements StoredValue
     private static class ActionStoredValueFactory implements StoredValueFactory
     {
         @Override
-        public ActionValue fromJson( final String input )
+        public ActionValue fromJson( final PwmSetting pwmSetting, final String input )
         {
             return input == null
                     ? new ActionValue( Collections.emptyList() )

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

@@ -58,7 +58,7 @@ public class BooleanValue implements StoredValue
         return new StoredValueFactory()
         {
             @Override
-            public BooleanValue fromJson( final String value )
+            public BooleanValue fromJson( final PwmSetting pwmSetting, final String value )
             {
                 return BooleanValue.of( JsonFactory.get().deserialize( value, Boolean.class ) );
             }

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

@@ -78,7 +78,7 @@ public class ChallengeValue extends AbstractValue implements StoredValue
         return new StoredValueFactory()
         {
             @Override
-            public ChallengeValue fromJson( final String input )
+            public ChallengeValue fromJson( final PwmSetting pwmSetting, final String input )
             {
                 if ( input == null )
                 {

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

@@ -50,7 +50,7 @@ public class CustomLinkValue extends AbstractValue implements StoredValue
         return new StoredValueFactory()
         {
             @Override
-            public CustomLinkValue fromJson( final String input )
+            public CustomLinkValue fromJson( final PwmSetting pwmSetting, final String input )
             {
                 if ( input == null )
                 {

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

@@ -56,7 +56,7 @@ public class EmailValue extends AbstractValue implements StoredValue
         return new StoredValueFactory()
         {
             @Override
-            public EmailValue fromJson( final String input )
+            public EmailValue fromJson( final PwmSetting pwmSetting, final String input )
             {
                 if ( input == null )
                 {

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

@@ -171,7 +171,7 @@ public class FileValue extends AbstractValue implements StoredValue
             }
 
             @Override
-            public StoredValue fromJson( final String input )
+            public StoredValue fromJson( final PwmSetting pwmSetting, final String input )
             {
                 throw new IllegalStateException( "not implemented" );
             }

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

@@ -54,7 +54,7 @@ public class FormValue extends AbstractValue implements StoredValue
         return new StoredValueFactory()
         {
             @Override
-            public FormValue fromJson( final String input )
+            public FormValue fromJson( final PwmSetting pwmSetting, final String input )
             {
                 if ( input == null )
                 {

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

@@ -72,7 +72,7 @@ public class LocalizedStringArrayValue extends AbstractValue implements StoredVa
         return new StoredValueFactory()
         {
             @Override
-            public LocalizedStringArrayValue fromJson( final String input )
+            public LocalizedStringArrayValue fromJson( final PwmSetting pwmSetting, final String input )
             {
                 if ( input == null )
                 {

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

@@ -68,7 +68,7 @@ public class LocalizedStringValue extends AbstractValue implements StoredValue
         return new StoredValueFactory()
         {
             @Override
-            public LocalizedStringValue fromJson( final String input )
+            public LocalizedStringValue fromJson( final PwmSetting pwmSetting, final String input )
             {
                 if ( input == null )
                 {

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

@@ -76,7 +76,7 @@ public class NamedSecretValue implements StoredValue
         return new StoredValue.StoredValueFactory()
         {
             @Override
-            public NamedSecretValue fromJson( final String value )
+            public NamedSecretValue fromJson( final PwmSetting pwmSetting, final String value )
             {
                 try
                 {

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

@@ -50,7 +50,7 @@ public class NumericArrayValue extends AbstractValue implements StoredValue
         return new StoredValueFactory()
         {
             @Override
-            public NumericArrayValue fromJson( final String value )
+            public NumericArrayValue fromJson( final PwmSetting pwmSetting, final String value )
             {
                 final long[] longArray = JsonFactory.get().deserialize( value, long[].class );
                 final List<Long> list = Arrays.stream( longArray ).boxed().collect( Collectors.toList() );

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

@@ -47,7 +47,7 @@ public class NumericValue extends AbstractValue implements StoredValue
         return new StoredValueFactory()
         {
             @Override
-            public NumericValue fromJson( final String value )
+            public NumericValue fromJson( final PwmSetting pwmSetting, final String value )
             {
                 return new NumericValue( JsonFactory.get().deserialize( value, Long.class ) );
             }

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

@@ -50,7 +50,7 @@ public class OptionListValue extends AbstractValue implements StoredValue
         return new StoredValueFactory()
         {
             @Override
-            public OptionListValue fromJson( final String input )
+            public OptionListValue fromJson( final PwmSetting pwmSetting, final String input )
             {
                 if ( input == null )
                 {

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

@@ -66,7 +66,7 @@ public class PasswordValue implements StoredValue
         return new StoredValueFactory()
         {
             @Override
-            public PasswordValue fromJson( final String value )
+            public PasswordValue fromJson( final PwmSetting pwmSetting, final String value )
             {
                 final String strValue = JsonFactory.get().deserialize( value, String.class );
                 if ( strValue != null && !strValue.isEmpty() )

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

@@ -136,7 +136,7 @@ public class PrivateKeyValue extends AbstractValue
             }
 
             @Override
-            public X509CertificateValue fromJson( final String input )
+            public X509CertificateValue fromJson( final PwmSetting pwmSetting, final String input )
             {
                 return new X509CertificateValue( Collections.emptyList() );
             }

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

@@ -59,7 +59,7 @@ public class RemoteWebServiceValue extends AbstractValue implements StoredValue
         return new StoredValueFactory()
         {
             @Override
-            public RemoteWebServiceValue fromJson( final String input )
+            public RemoteWebServiceValue fromJson( final PwmSetting pwmSetting, final String input )
             {
                 if ( input == null )
                 {

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

@@ -46,7 +46,7 @@ public interface StoredValue extends Serializable
 
     interface StoredValueFactory
     {
-        StoredValue fromJson( String input );
+        StoredValue fromJson( PwmSetting pwmSetting, String input );
 
         StoredValue fromXmlElement( PwmSetting pwmSetting, XmlElement settingElement, PwmSecurityKey key )
                 throws PwmException;

+ 18 - 12
server/src/main/java/password/pwm/config/value/StringArrayValue.java

@@ -26,6 +26,7 @@ import password.pwm.config.PwmSetting;
 import password.pwm.config.PwmSettingFlag;
 import password.pwm.config.stored.StoredConfigXmlConstants;
 import password.pwm.config.stored.XmlOutputProcessData;
+import password.pwm.util.java.CollectionUtil;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.json.JsonFactory;
 import password.pwm.util.secure.PwmSecurityKey;
@@ -44,10 +45,15 @@ public class StringArrayValue extends AbstractValue implements StoredValue
 {
     private final List<String> values;
 
-    public StringArrayValue( final List<String> values )
+    private StringArrayValue( final PwmSetting pwmSetting, final List<String> values )
     {
-        final List<String> copiedValues = new ArrayList<>( values == null ? Collections.emptyList() : values );
-        copiedValues.removeAll( Collections.singleton( null ) );
+        final List<String> copiedValues = new ArrayList<>( CollectionUtil.stripNulls( values ) );
+
+        if ( pwmSetting != null && pwmSetting.getFlags().contains( PwmSettingFlag.Sorted ) )
+        {
+            Collections.sort( copiedValues );
+        }
+
         this.values = List.copyOf( copiedValues );
     }
 
@@ -56,15 +62,15 @@ public class StringArrayValue extends AbstractValue implements StoredValue
         return new StoredValueFactory()
         {
             @Override
-            public StringArrayValue fromJson( final String input )
+            public StringArrayValue fromJson( final PwmSetting pwmSetting, final String input )
             {
                 if ( StringUtil.isEmpty( input ) )
                 {
-                    return new StringArrayValue( Collections.emptyList() );
+                    return new StringArrayValue( pwmSetting, Collections.emptyList() );
                 }
                 else
                 {
-                    return new StringArrayValue( JsonFactory.get().deserializeStringList( input ) );
+                    return new StringArrayValue( pwmSetting, JsonFactory.get().deserializeStringList( input ) );
                 }
             }
 
@@ -76,16 +82,16 @@ public class StringArrayValue extends AbstractValue implements StoredValue
                         .flatMap( Optional::stream )
                         .collect( Collectors.toList() );
 
-                if ( pwmSetting != null && pwmSetting.getFlags().contains( PwmSettingFlag.Sorted ) )
-                {
-                    Collections.sort( values );
-                }
-
-                return new StringArrayValue( values );
+                return new StringArrayValue( pwmSetting, values );
             }
         };
     }
 
+    public static StringArrayValue create( final List<String> values )
+    {
+        return new StringArrayValue( null, values );
+    }
+
     @Override
     public List<XmlElement> toXmlValues( final String valueElementName, final XmlOutputProcessData xmlOutputProcessData )
     {

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

@@ -59,7 +59,7 @@ public class StringValue extends AbstractValue implements StoredValue
         return new StoredValueFactory()
         {
             @Override
-            public StringValue fromJson( final String input )
+            public StringValue fromJson( final PwmSetting pwmSetting, final String input )
             {
                 final String newValue = JsonFactory.get().deserialize( input, String.class );
                 return new StringValue( newValue );

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

@@ -63,7 +63,7 @@ public class UserPermissionValue extends AbstractValue implements StoredValue
         return new StoredValueFactory()
         {
             @Override
-            public UserPermissionValue fromJson( final String input )
+            public UserPermissionValue fromJson( final PwmSetting pwmSetting, final String input )
             {
                 if ( input == null )
                 {

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

@@ -42,7 +42,7 @@ public class ValueFactory
         try
         {
             final StoredValue.StoredValueFactory factory = setting.getSyntax().getFactory();
-            return factory.fromJson( input );
+            return factory.fromJson( setting, input );
         }
         catch ( final Exception e )
         {

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

@@ -124,7 +124,7 @@ public class VerificationMethodValue extends AbstractValue implements StoredValu
         return new StoredValueFactory()
         {
             @Override
-            public VerificationMethodValue fromJson( final String input )
+            public VerificationMethodValue fromJson( final PwmSetting pwmSetting, final String input )
             {
                 if ( input == null )
                 {

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

@@ -74,7 +74,7 @@ public class X509CertificateValue extends AbstractValue implements StoredValue
             }
 
             @Override
-            public X509CertificateValue fromJson( final String input )
+            public X509CertificateValue fromJson( final PwmSetting pwmSetting, final String input )
             {
                 return new X509CertificateValue( Collections.emptyList() );
             }

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

@@ -391,9 +391,9 @@ public class PwmRequest extends PwmHttpRequestWrapper
         this.getHttpServletRequest().setAttribute( name.toString(), value );
     }
 
-    public Serializable getAttribute( final PwmRequestAttribute name )
+    public Object getAttribute( final PwmRequestAttribute name )
     {
-        return ( Serializable ) this.getHttpServletRequest().getAttribute( name.toString() );
+        return this.getHttpServletRequest().getAttribute( name.toString() );
     }
 
     public PwmURL getURL()

+ 16 - 25
server/src/main/java/password/pwm/http/servlet/ClientApiServlet.java

@@ -51,6 +51,7 @@ import password.pwm.svc.stats.Statistic;
 import password.pwm.svc.stats.StatisticsService;
 import password.pwm.util.i18n.LocaleComparators;
 import password.pwm.util.i18n.LocaleHelper;
+import password.pwm.util.java.CollectionUtil;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.java.TimeDuration;
 import password.pwm.util.logging.PwmLogger;
@@ -65,14 +66,12 @@ import password.pwm.ws.server.rest.bean.PublicHealthData;
 import javax.servlet.ServletException;
 import javax.servlet.annotation.WebServlet;
 import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
 import java.io.IOException;
 import java.io.Serializable;
 import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
-import java.util.EnumSet;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Locale;
@@ -179,10 +178,8 @@ public class ClientApiServlet extends ControlledPwmServlet
         final AppData appData = makeAppData(
                 pwmRequest.getPwmDomain(),
                 pwmRequest,
-                pwmRequest.getHttpServletRequest(),
-                pwmRequest.getPwmResponse().getHttpServletResponse(),
-                pageUrl
-        );
+                pageUrl );
+
         final RestResultBean<AppData> restResultBean = RestResultBean.withData( appData, AppData.class );
         pwmRequest.outputJsonResult( restResultBean );
         return ProcessStatus.Halt;
@@ -290,26 +287,22 @@ public class ClientApiServlet extends ControlledPwmServlet
 
     private AppData makeAppData(
             final PwmDomain pwmDomain,
-            final PwmRequest pwmSession,
-            final HttpServletRequest request,
-            final HttpServletResponse response,
+            final PwmRequest pwmRequest,
             final String pageUrl
     )
             throws ChaiUnavailableException, PwmUnrecoverableException
     {
         final AppData appData = new AppData();
-        appData.PWM_GLOBAL = makeClientData( pwmDomain, pwmSession, request, response, pageUrl );
+        appData.PWM_GLOBAL = makeClientData( pwmDomain, pwmRequest, pageUrl );
         return appData;
     }
 
     private static Map<String, Object> makeClientData(
             final PwmDomain pwmDomain,
             final PwmRequest pwmRequest,
-            final HttpServletRequest request,
-            final HttpServletResponse response,
             final String pageUrl
     )
-            throws ChaiUnavailableException, PwmUnrecoverableException
+            throws PwmUnrecoverableException
     {
         final PwmSession pwmSession = pwmRequest.getPwmSession();
         final Locale userLocale = pwmSession.getSessionStateBean().getLocale();
@@ -359,12 +352,14 @@ public class ClientApiServlet extends ControlledPwmServlet
         settingMap.put( "runtimeNonce", pwmDomain.getPwmApplication().getRuntimeNonce() );
         settingMap.put( "applicationMode", pwmDomain.getApplicationMode() );
 
-        final String contextPath = pwmRequest.getBasePath();
-        settingMap.put( "url-context", contextPath );
-        settingMap.put( "url-logout", contextPath + PwmServletDefinition.Logout.servletUrl() );
-        settingMap.put( "url-command", contextPath + PwmServletDefinition.PublicCommand.servletUrl() );
-        settingMap.put( "url-resources", contextPath + "/public/resources" + pwmDomain.getResourceServletService().getResourceNonce() );
-        settingMap.put( "url-restservice", contextPath + "/public/rest" );
+        {
+            final String contextPath = pwmRequest.getBasePath();
+            settingMap.put( "url-context", contextPath );
+            settingMap.put( "url-logout", contextPath + PwmServletDefinition.Logout.servletUrl() );
+            settingMap.put( "url-command", contextPath + PwmServletDefinition.PublicCommand.servletUrl() );
+            settingMap.put( "url-resources", contextPath + "/public/resources" + pwmDomain.getResourceServletService().getResourceNonce() );
+            settingMap.put( "url-restservice", contextPath + "/public/rest" );
+        }
 
         if ( pwmRequest.isAuthenticated() )
         {
@@ -382,21 +377,17 @@ public class ClientApiServlet extends ControlledPwmServlet
                     final String expandedText = macroRequest.expandMacros( configuredGuideText );
                     settingMap.put( "passwordGuideText", expandedText );
                 }
-
             }
         }
 
-        settingMap.put( "epsTypes", EnumSet.allOf( EpsStatistic.class )
-                .stream()
+        settingMap.put( "epsTypes", CollectionUtil.enumStream( EpsStatistic.class )
                 .map( EpsStatistic::toString )
                 .collect( Collectors.toList() ) );
 
-        settingMap.put( "epsDurations", EnumSet.allOf( Statistic.EpsDuration.class )
-                .stream()
+        settingMap.put( "epsDurations", CollectionUtil.enumStream( Statistic.EpsDuration.class )
                 .map( Statistic.EpsDuration::toString )
                 .collect( Collectors.toList() ) );
 
-
         {
             final List<Locale> knownLocales = new ArrayList<>( pwmRequest.getAppConfig().getKnownLocales() );
             knownLocales.sort( LocaleComparators.localeComparator( ) );

+ 11 - 13
server/src/main/java/password/pwm/http/servlet/admin/AdminServlet.java

@@ -33,7 +33,6 @@ import password.pwm.config.PwmSetting;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmException;
-import password.pwm.error.PwmOperationalException;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.http.HttpContentType;
 import password.pwm.http.HttpMethod;
@@ -65,10 +64,10 @@ import password.pwm.util.java.ClosableIterator;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.MiscUtil;
 import password.pwm.util.java.PwmTimeUtil;
-import password.pwm.util.json.JsonProvider;
-import password.pwm.util.json.JsonFactory;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.java.TimeDuration;
+import password.pwm.util.json.JsonFactory;
+import password.pwm.util.json.JsonProvider;
 import password.pwm.util.logging.LocalDBLogger;
 import password.pwm.util.logging.LocalDBSearchQuery;
 import password.pwm.util.logging.LocalDBSearchResults;
@@ -558,20 +557,19 @@ public class AdminServlet extends ControlledPwmServlet
             userIdentity = userSearchEngine.resolveUsername( username, null, null, pwmRequest.getLabel() );
             final AdminBean adminBean = pwmRequest.getPwmDomain().getSessionStateService().getBean( pwmRequest, AdminBean.class );
             adminBean.setLastUserDebug( userIdentity );
+
+            final UserDebugDataBean userDebugData = UserDebugDataReader.readUserDebugData(
+                    pwmRequest.getPwmDomain(),
+                    pwmRequest.getLocale(),
+                    pwmRequest.getLabel(),
+                    userIdentity
+            );
+            pwmRequest.setAttribute( PwmRequestAttribute.UserDebugData, userDebugData );
         }
-        catch ( final PwmUnrecoverableException | PwmOperationalException e )
+        catch ( final PwmException e )
         {
             setLastError( pwmRequest, e.getErrorInformation() );
-            return;
         }
-
-        final UserDebugDataBean userDebugData = UserDebugDataReader.readUserDebugData(
-                pwmRequest.getPwmDomain(),
-                pwmRequest.getLocale(),
-                pwmRequest.getLabel(),
-                userIdentity
-        );
-        pwmRequest.setAttribute( PwmRequestAttribute.UserDebugData, userDebugData );
     }
 
     private void processThreadPageView( final PwmRequest pwmRequest ) throws IOException

+ 5 - 5
server/src/main/java/password/pwm/http/servlet/configguide/ConfigGuideForm.java

@@ -124,13 +124,13 @@ public class ConfigGuideForm
 
         // establish a default ldap profile
 
-        modifySetting( modifier, PwmSetting.LDAP_PROFILE_LIST, null, new StringArrayValue(
+        modifySetting( modifier, PwmSetting.LDAP_PROFILE_LIST, null, StringArrayValue.create(
                 Collections.singletonList( LDAP_PROFILE_NAME )
         ) );
 
         {
             final String newLdapURI = figureLdapUrlFromFormConfig( formData );
-            final StringArrayValue newValue = new StringArrayValue( Collections.singletonList( newLdapURI ) );
+            final StringArrayValue newValue = StringArrayValue.create( Collections.singletonList( newLdapURI ) );
             modifySetting( modifier, PwmSetting.LDAP_SERVER_URLS, LDAP_PROFILE_NAME, newValue );
         }
 
@@ -149,13 +149,13 @@ public class ConfigGuideForm
             modifySetting( modifier, PwmSetting.LDAP_PROXY_USER_PASSWORD, LDAP_PROFILE_NAME, passwordValue );
         }
 
-        modifySetting( modifier, PwmSetting.LDAP_CONTEXTLESS_ROOT, LDAP_PROFILE_NAME, new StringArrayValue(
+        modifySetting( modifier, PwmSetting.LDAP_CONTEXTLESS_ROOT, LDAP_PROFILE_NAME, StringArrayValue.create(
                 Collections.singletonList( formData.get( ConfigGuideFormField.PARAM_LDAP_CONTEXT ) )
         ) );
 
         {
             final String ldapContext = formData.get( ConfigGuideFormField.PARAM_LDAP_CONTEXT );
-            modifySetting( modifier, PwmSetting.LDAP_CONTEXTLESS_ROOT, LDAP_PROFILE_NAME, new StringArrayValue(
+            modifySetting( modifier, PwmSetting.LDAP_CONTEXTLESS_ROOT, LDAP_PROFILE_NAME, StringArrayValue.create(
                     Collections.singletonList( ldapContext )
             ) );
         }
@@ -219,7 +219,7 @@ public class ConfigGuideForm
         if ( formData.containsKey( ConfigGuideFormField.CHALLENGE_RESPONSE_DATA ) )
         {
             final String stringValue = formData.get( ConfigGuideFormField.CHALLENGE_RESPONSE_DATA );
-            final StoredValue challengeValue = ChallengeValue.factory().fromJson( stringValue );
+            final StoredValue challengeValue = ChallengeValue.factory().fromJson( PwmSetting.CHALLENGE_RANDOM_CHALLENGES, stringValue );
             modifySetting( modifier, PwmSetting.CHALLENGE_RANDOM_CHALLENGES, "default", challengeValue );
         }
 

+ 2 - 3
server/src/main/java/password/pwm/http/state/CryptoCookieBeanImpl.java

@@ -33,7 +33,6 @@ import password.pwm.util.java.TimeDuration;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.secure.PwmSecurityKey;
 
-import java.io.Serializable;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.Optional;
@@ -164,7 +163,7 @@ class CryptoCookieBeanImpl implements SessionBeanProvider
     }
 
     @Override
-    public <E extends PwmSessionBean> void clearSessionBean( final PwmRequest pwmRequest, final Class<E> userBeanClass ) throws PwmUnrecoverableException
+    public <E extends PwmSessionBean> void clearSessionBean( final PwmRequest pwmRequest, final Class<E> userBeanClass )
     {
         final Map<Class<? extends PwmSessionBean>, PwmSessionBean> sessionBeans = getRequestBeanMap( pwmRequest );
         sessionBeans.put( userBeanClass, null );
@@ -173,7 +172,7 @@ class CryptoCookieBeanImpl implements SessionBeanProvider
 
     private static Map<Class<? extends PwmSessionBean>, PwmSessionBean> getRequestBeanMap( final PwmRequest pwmRequest )
     {
-        Serializable sessionBeans = pwmRequest.getAttribute( PwmRequestAttribute.CookieBeanStorage );
+        Object sessionBeans = pwmRequest.getAttribute( PwmRequestAttribute.CookieBeanStorage );
         if ( sessionBeans == null )
         {
             sessionBeans = new HashMap<>();

+ 3 - 3
server/src/main/java/password/pwm/svc/AbstractPwmService.java

@@ -38,7 +38,6 @@ import java.util.List;
 import java.util.Objects;
 import java.util.Set;
 import java.util.concurrent.ScheduledExecutorService;
-import java.util.function.Supplier;
 
 public abstract class AbstractPwmService implements PwmService
 {
@@ -48,7 +47,7 @@ public abstract class AbstractPwmService implements PwmService
     private DomainID domainID;
     private SessionLabel sessionLabel;
 
-    private Supplier<ScheduledExecutorService> executorService;
+    private LazySupplier<ScheduledExecutorService> executorService;
 
 
     public final PwmService.STATUS status()
@@ -98,8 +97,9 @@ public abstract class AbstractPwmService implements PwmService
     public void shutdown()
     {
         this.status = STATUS.CLOSED;
-        if ( executorService != null )
+        if ( executorService != null && executorService.isSupplied() )
         {
+            executorService.get().shutdownNow();
             JavaHelper.closeAndWaitExecutor( executorService.get(), TimeDuration.SECONDS_10 );
         }
         shutdownImpl();

+ 9 - 7
server/src/main/java/password/pwm/svc/stats/StatisticsService.java

@@ -45,8 +45,6 @@ import java.math.BigDecimal;
 import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Collections;
-import java.util.EnumSet;
-import java.util.HashMap;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Locale;
@@ -79,9 +77,9 @@ public class StatisticsService extends AbstractPwmService implements PwmService
     private DailyKey initialDailyKey = DailyKey.forToday();
 
     private final StatisticsBundle statsCurrent = new StatisticsBundle();
+    private final Map<EpsKey, EventRateMeter> epsMeterMap;
     private StatisticsBundle statsDaily = new StatisticsBundle();
     private StatisticsBundle statsCummulative = new StatisticsBundle();
-    private Map<EpsKey, EventRateMeter> epsMeterMap = new HashMap<>();
 
 
     private final Map<String, StatisticsBundle> cachedStoredStats = new LinkedHashMap<>()
@@ -95,7 +93,11 @@ public class StatisticsService extends AbstractPwmService implements PwmService
 
     public StatisticsService( )
     {
-        EpsKey.allKeys().forEach( epsKey -> epsMeterMap.put( epsKey, new EventRateMeter( epsKey.getEpsDuration().getTimeDuration() ) ) );
+        epsMeterMap = EpsKey.allKeys().stream()
+                .map( key -> Map.entry( key, new EventRateMeter( key.getEpsDuration().getTimeDuration() ) ) )
+                .collect( Collectors.toUnmodifiableMap(
+                        Map.Entry::getKey,
+                        Map.Entry::getValue ) );
     }
 
     public void incrementValue( final Statistic statistic )
@@ -298,7 +300,7 @@ public class StatisticsService extends AbstractPwmService implements PwmService
 
     public Map<String, String> dailyStatisticsAsLabelValueMap()
     {
-        return Collections.unmodifiableMap( EnumSet.allOf( Statistic.class ).stream()
+        return Collections.unmodifiableMap( CollectionUtil.enumStream( Statistic.class )
                 .collect( CollectionUtil.collectorToLinkedMap(
                         statistic -> statistic.getLabel( PwmConstants.DEFAULT_LOCALE ),
                         statistic -> statsDaily.getStatistic( statistic )
@@ -332,7 +334,7 @@ public class StatisticsService extends AbstractPwmService implements PwmService
     {
         return Collections.emptyList();
     }
-    
+
     private class NightlyTask extends TimerTask
     {
         @Override
@@ -403,7 +405,7 @@ public class StatisticsService extends AbstractPwmService implements PwmService
             lineOutput.add( String.valueOf( loopKey.getYear() ) );
             lineOutput.add( String.valueOf( loopKey.getDay() ) );
 
-            lineOutput.addAll( EnumSet.allOf( Statistic.class ).stream()
+            lineOutput.addAll( CollectionUtil.enumStream( Statistic.class )
                     .map( bundle::getStatistic )
                     .collect( Collectors.toList() ) );
 

+ 26 - 20
server/src/main/java/password/pwm/svc/version/VersionCheckService.java

@@ -170,45 +170,51 @@ public class VersionCheckService extends AbstractPwmService
 
         final TimeDuration delayUntilNextExecution = TimeDuration.fromCurrent( this.nextScheduledCheck );
 
-        getExecutorService().schedule( this::doPeriodicCheck, delayUntilNextExecution.asMillis(), TimeUnit.MILLISECONDS );
+        getExecutorService().schedule( new PeriodicCheck(), delayUntilNextExecution.asMillis(), TimeUnit.MILLISECONDS );
 
         LOGGER.trace( getSessionLabel(), () -> "scheduled next check execution at " + StringUtil.toIsoDate( nextScheduledCheck )
                 + " in " + delayUntilNextExecution.asCompactString() );
     }
 
-    private void doPeriodicCheck()
+    private class PeriodicCheck implements Runnable
     {
-        if ( status() != PwmService.STATUS.OPEN )
+        @Override
+        public void run()
         {
-            return;
-        }
+            if ( status() != PwmService.STATUS.OPEN )
+            {
+                return;
+            }
 
-        try
-        {
-            processReturnedVersionBean( executeFetch() );
-        }
-        catch ( final PwmUnrecoverableException e )
-        {
-            cacheHolder.setVersionCheckInfoCache( VersionCheckInfoCache.builder()
-                    .lastError( e.getErrorInformation() )
-                    .lastCheckTimestamp( Instant.now() )
-                    .build() );
-        }
+            try
+            {
+                processReturnedVersionBean( executeFetch() );
+            }
+            catch ( final PwmUnrecoverableException e )
+            {
+                cacheHolder.setVersionCheckInfoCache( VersionCheckInfoCache.builder()
+                        .lastError( e.getErrorInformation() )
+                        .lastCheckTimestamp( Instant.now() )
+                        .build() );
+            }
 
-        scheduleNextCheck();
+            scheduleNextCheck();
+        }
     }
 
     private void processReturnedVersionBean( final PublishVersionBean publishVersionBean )
     {
-        final VersionNumber currentVersion = publishVersionBean.getVersions().get( PublishVersionBean.VersionKey.current );
+        final Instant startTime = Instant.now();
 
-        LOGGER.trace( getSessionLabel(), () -> "successfully fetched current version information from cloud service: "
-                + currentVersion );
+        final VersionNumber currentVersion = publishVersionBean.getVersions().get( PublishVersionBean.VersionKey.current );
 
         cacheHolder.setVersionCheckInfoCache( VersionCheckInfoCache.builder()
                 .currentVersion( currentVersion )
                 .lastCheckTimestamp( Instant.now() )
                 .build() );
+
+        LOGGER.trace( getSessionLabel(), () -> "successfully fetched current version information from cloud service: "
+                + currentVersion, TimeDuration.fromCurrent( startTime ) );
     }
 
     private PublishVersionBean executeFetch()

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

@@ -21,12 +21,12 @@
 package password.pwm.svc.wordlist;
 
 import lombok.Value;
+import password.pwm.util.java.CollectionUtil;
 import password.pwm.util.java.StatisticAverageBundle;
 import password.pwm.util.java.StatisticCounterBundle;
 
 import java.util.Collections;
 import java.util.EnumMap;
-import java.util.EnumSet;
 import java.util.Map;
 import java.util.TreeMap;
 import java.util.concurrent.atomic.LongAdder;
@@ -58,7 +58,7 @@ class WordlistStatistics
 
     WordlistStatistics()
     {
-        EnumSet.allOf( WordType.class ).forEach( wordType -> wordTypeHits.put( wordType, new LongAdder() ) );
+        CollectionUtil.enumStream( WordType.class ).forEach( wordType -> wordTypeHits.put( wordType, new LongAdder() ) );
     }
 
     Map<String, String> asDebugMap()

+ 3 - 3
server/src/main/java/password/pwm/util/PropertyConfigurationImporter.java

@@ -159,7 +159,7 @@ public class PropertyConfigurationImporter
         modifySetting( modifier, PwmSetting.LDAP_PROXY_USER_PASSWORD, LDAP_PROFILE,
                 new PasswordValue( PasswordData.forStringValue( inputMap.get( PropertyKey.ID_VAULT_PASSWORD.name( ) ) ) ) );
         modifySetting( modifier, PwmSetting.LDAP_CONTEXTLESS_ROOT, LDAP_PROFILE,
-                new StringArrayValue( Collections.singletonList( inputMap.get( PropertyKey.USER_CONTAINER.name( ) ) ) ) );
+                StringArrayValue.create( Collections.singletonList( inputMap.get( PropertyKey.USER_CONTAINER.name( ) ) ) ) );
 
         // oauth
         modifySetting( modifier, PwmSetting.OAUTH_ID_LOGIN_URL, null, new StringValue( makeOAuthBaseUrl( ) + "/grant" ) );
@@ -223,7 +223,7 @@ public class PropertyConfigurationImporter
 
     private StringArrayValue makeWhitelistUrl( )
     {
-        return new StringArrayValue( Collections.singletonList( "https://" + inputMap.get( PropertyKey.SSO_SERVER_HOST.name( ) )
+        return StringArrayValue.create( Collections.singletonList( "https://" + inputMap.get( PropertyKey.SSO_SERVER_HOST.name( ) )
                 + ":" + inputMap.getOrDefault( PropertyKey.SSO_SERVER_SSL_PORT.name( ), PropertyKey.SSO_SERVER_SSL_PORT.getDefaultValue() ) ) );
     }
 
@@ -255,7 +255,7 @@ public class PropertyConfigurationImporter
     {
         final String ldapUrl = "ldaps://" + inputMap.get( PropertyKey.ID_VAULT_HOST.name( ) )
                 + ":" + inputMap.getOrDefault( PropertyKey.ID_VAULT_LDAPS_PORT.name( ), PropertyKey.ID_VAULT_LDAPS_PORT.getDefaultValue() );
-        return new StringArrayValue( Collections.singletonList( ldapUrl ) );
+        return StringArrayValue.create( Collections.singletonList( ldapUrl ) );
     }
 
     private StoredValue makeAdminPermissions( )

+ 56 - 3
server/src/main/java/password/pwm/util/SampleDataGenerator.java

@@ -21,17 +21,31 @@
 package password.pwm.util;
 
 import com.novell.ldapchai.cr.Answer;
+import org.apache.log4j.Level;
+import org.apache.log4j.Logger;
+import password.pwm.PwmApplication;
+import password.pwm.PwmApplicationMode;
 import password.pwm.PwmConstants;
 import password.pwm.PwmDomain;
+import password.pwm.PwmEnvironment;
 import password.pwm.bean.DomainID;
 import password.pwm.bean.LoginInfoBean;
 import password.pwm.bean.ResponseInfoBean;
 import password.pwm.bean.UserIdentity;
+import password.pwm.config.AppConfig;
+import password.pwm.config.PwmSetting;
 import password.pwm.config.option.DataStorageMethod;
+import password.pwm.config.stored.StoredConfigKey;
+import password.pwm.config.stored.StoredConfiguration;
+import password.pwm.config.stored.StoredConfigurationFactory;
+import password.pwm.config.stored.StoredConfigurationModifier;
+import password.pwm.config.value.LocalizedStringValue;
+import password.pwm.config.value.StringValue;
 import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.svc.otp.OTPUserRecord;
 import password.pwm.user.UserInfo;
 import password.pwm.user.UserInfoBean;
-import password.pwm.svc.otp.OTPUserRecord;
+import password.pwm.util.logging.PwmLogLevel;
 import password.pwm.util.macro.MacroRequest;
 
 import java.time.Instant;
@@ -41,7 +55,10 @@ import java.util.Map;
 public class SampleDataGenerator
 {
     private static final DomainID SAMPLE_USER_DOMAIN = DomainID.create( "default" );
-    private static final String SAMPLE_USER_LDAP_PROFILE = "profile1";
+    private static final String SAMPLE_USER_LDAP_PROFILE = "default";
+
+    private static final UserIdentity SAMPLE_CONFIG_MODIFIER_IDENTITY = UserIdentity
+            .create( "cn=configModifier,ou=users,o=org", SAMPLE_USER_LDAP_PROFILE, SAMPLE_USER_DOMAIN );
 
     private static final Map<String, String> USER1_ATTRIBUTES = Map.ofEntries(
             Map.entry( "givenName", "First" ),
@@ -156,11 +173,47 @@ public class SampleDataGenerator
         loginInfoBean.setUserCurrentPassword( PasswordData.forStringValue( "PaSSw0rd" ) );
 
         return MacroRequest.builder()
-                .pwmApplication( null )
+                .pwmApplication( makeSamplePwmApp( ) )
                 .userInfo( userInfoBean )
                 .targetUserInfo( targetUserInfoBean )
                 .loginInfoBean( loginInfoBean )
                 .build();
 
     }
+
+    private static AppConfig makeConfig()
+            throws PwmUnrecoverableException
+    {
+        final StoredConfiguration storedConfiguration = StoredConfigurationFactory.newConfig();
+        final StoredConfigurationModifier modifier = StoredConfigurationModifier.newModifier( storedConfiguration );
+
+        modifier.writeSetting( StoredConfigKey.forSetting(
+                        PwmSetting.EVENTS_JAVA_STDOUT_LEVEL, SAMPLE_USER_LDAP_PROFILE, SAMPLE_USER_DOMAIN ),
+                new StringValue( PwmLogLevel.FATAL.toString() ), SAMPLE_CONFIG_MODIFIER_IDENTITY );
+        modifier.writeSetting( StoredConfigKey.forSetting(
+                        PwmSetting.LDAP_PROFILE_DISPLAY_NAME, SAMPLE_USER_LDAP_PROFILE, SAMPLE_USER_DOMAIN ),
+                new LocalizedStringValue( Map.of( "", "ProfileName" ) ), SAMPLE_CONFIG_MODIFIER_IDENTITY );
+
+        return new AppConfig( modifier.newStoredConfiguration() );
+    }
+
+    private static PwmApplication makeSamplePwmApp()
+            throws PwmUnrecoverableException
+    {
+        return makeSamplePwmApp( makeConfig() );
+    }
+
+    private static PwmApplication makeSamplePwmApp( final AppConfig appConfig )
+            throws PwmUnrecoverableException
+    {
+        Logger.getRootLogger().setLevel( Level.OFF );
+        final PwmEnvironment pwmEnvironment = PwmEnvironment.builder()
+                .config( appConfig )
+                .applicationPath( null )
+                .applicationMode( PwmApplicationMode.READ_ONLY )
+                .internalRuntimeInstance( true )
+                .build();
+
+        return PwmApplication.createPwmApplication( pwmEnvironment );
+    }
 }

+ 27 - 6
server/src/main/java/password/pwm/util/macro/MacroRequest.java

@@ -23,6 +23,7 @@ package password.pwm.util.macro;
 import lombok.Builder;
 import lombok.Value;
 import password.pwm.PwmApplication;
+import password.pwm.PwmConstants;
 import password.pwm.bean.LoginInfoBean;
 import password.pwm.bean.SessionLabel;
 import password.pwm.bean.UserIdentity;
@@ -44,10 +45,30 @@ public class MacroRequest
     private final LoginInfoBean loginInfoBean;
     private final MacroReplacer macroReplacer;
     private final UserInfo targetUserInfo;
+    private final Locale userLocale;
+
+    private MacroRequest(
+            final PwmApplication pwmApplication,
+            final SessionLabel sessionLabel,
+            final UserInfo userInfo,
+            final LoginInfoBean loginInfoBean,
+            final MacroReplacer macroReplacer,
+            final UserInfo targetUserInfo,
+            final Locale userLocale
+    )
+    {
+        this.pwmApplication = pwmApplication;
+        this.sessionLabel = sessionLabel;
+        this.userInfo = userInfo;
+        this.loginInfoBean = loginInfoBean;
+        this.macroReplacer = macroReplacer;
+        this.targetUserInfo = targetUserInfo;
+        this.userLocale = userLocale == null ? PwmConstants.DEFAULT_LOCALE : userLocale;
+    }
 
     public static MacroRequest forStatic()
     {
-        return new MacroRequest( null, null, null, null, null, null );
+        return new MacroRequest( null, null, null, null, null, null, null );
     }
 
     public static MacroRequest forUser(
@@ -85,7 +106,7 @@ public class MacroRequest
             final LoginInfoBean loginInfoBean
     )
     {
-        return new MacroRequest( pwmApplication, sessionLabel, userInfo, loginInfoBean, null, null );
+        return new MacroRequest( pwmApplication, sessionLabel, userInfo, loginInfoBean, null, null, null );
     }
 
     public static MacroRequest forUser(
@@ -96,7 +117,7 @@ public class MacroRequest
             final MacroReplacer macroReplacer
     )
     {
-        return new MacroRequest( pwmApplication, sessionLabel, userInfo, loginInfoBean, macroReplacer, null );
+        return new MacroRequest( pwmApplication, sessionLabel, userInfo, loginInfoBean, macroReplacer, null, null );
     }
 
     public static MacroRequest forUser(
@@ -108,7 +129,7 @@ public class MacroRequest
             throws PwmUnrecoverableException
     {
         final UserInfo userInfoBean = UserInfoFactory.newUserInfoUsingProxy( pwmApplication, sessionLabel, userIdentity, userLocale );
-        return new MacroRequest( pwmApplication, sessionLabel, userInfoBean, null, null, null );
+        return new MacroRequest( pwmApplication, sessionLabel, userInfoBean, null, null, null, userLocale );
     }
 
     public static MacroRequest forUser(
@@ -121,7 +142,7 @@ public class MacroRequest
             throws PwmUnrecoverableException
     {
         final UserInfo userInfoBean = UserInfoFactory.newUserInfoUsingProxy( pwmApplication, sessionLabel, userIdentity, userLocale );
-        return new MacroRequest( pwmApplication, sessionLabel, userInfoBean, null, macroReplacer, null );
+        return new MacroRequest( pwmApplication, sessionLabel, userInfoBean, null, macroReplacer, null, userLocale );
     }
 
     public static MacroRequest forNonUserSpecific(
@@ -129,7 +150,7 @@ public class MacroRequest
             final SessionLabel sessionLabel
     )
     {
-        return new MacroRequest( pwmApplication, sessionLabel, null, null, null, null );
+        return new MacroRequest( pwmApplication, sessionLabel, null, null, null, null, null );
     }
 
     public String expandMacros( final String input )

+ 107 - 33
server/src/main/java/password/pwm/util/macro/UserMacros.java

@@ -21,11 +21,13 @@
 package password.pwm.util.macro;
 
 import password.pwm.AppProperty;
+import password.pwm.PwmConstants;
 import password.pwm.PwmDomain;
 import password.pwm.bean.DomainID;
 import password.pwm.bean.LoginInfoBean;
 import password.pwm.bean.UserIdentity;
 import password.pwm.config.PwmSetting;
+import password.pwm.config.profile.LdapProfile;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.user.UserInfo;
 import password.pwm.util.java.StringUtil;
@@ -36,6 +38,7 @@ import java.time.Instant;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
+import java.util.Locale;
 import java.util.Optional;
 import java.util.Set;
 import java.util.regex.Pattern;
@@ -46,12 +49,14 @@ public class UserMacros
 
     static final List<Macro> USER_MACROS = List.of(
             new UserIDMacro(),
+            new DomainIdMacro(),
+            new UserLdapProfileIDMacro(),
+            new UserLdapProfileNameMacro(),
             new UserLdapMacro(),
             new UserPwExpirationTimeMacro(),
             new UserDaysUntilPwExpireMacro(),
             new UserEmailMacro(),
             new UserPasswordMacro(),
-            new UserLdapProfileMacro(),
             new OtpSetupTimeMacro(),
             new ResponseSetupTimeMacro(),
             new DefaultDomainEmailFromAddressMacro(),
@@ -272,7 +277,6 @@ public class UserMacros
 
     abstract static class AbstractUserIDMacro extends AbstractUserMacro
     {
-
         @Override
         public String replaceValue(
                 final String matchValue,
@@ -358,6 +362,107 @@ public class UserMacros
         }
     }
 
+    public static class UserLdapProfileIDMacro extends AbstractUserMacro
+    {
+        private static final Pattern PATTERN = Pattern.compile( "@User:LdapProfile:ID@" );
+
+        @Override
+        public Pattern getRegExPattern( )
+        {
+            return PATTERN;
+        }
+
+        @Override
+        public String replaceValue(
+                final String matchValue,
+                final MacroRequest macroRequest
+
+        )
+        {
+            final UserInfo userInfo = macroRequest.getUserInfo();
+
+            if ( userInfo != null )
+            {
+                final UserIdentity userIdentity = userInfo.getUserIdentity();
+                if ( userIdentity != null )
+                {
+                    return userIdentity.getLdapProfileID();
+                }
+            }
+
+            return "";
+        }
+    }
+
+    public static class DomainIdMacro extends AbstractUserMacro
+    {
+        private static final Pattern PATTERN = Pattern.compile( "@User:Domain:ID@" );
+
+        @Override
+        public Pattern getRegExPattern( )
+        {
+            return PATTERN;
+        }
+
+        @Override
+        public String replaceValue(
+                final String matchValue,
+                final MacroRequest macroRequest
+
+        )
+        {
+            final UserInfo userInfo = macroRequest.getUserInfo();
+
+            if ( userInfo != null )
+            {
+                final UserIdentity userIdentity = userInfo.getUserIdentity();
+                if ( userIdentity != null )
+                {
+                    return userIdentity.getDomainID().stringValue();
+                }
+            }
+
+            return "";
+        }
+    }
+
+    public static class UserLdapProfileNameMacro extends AbstractUserMacro
+    {
+        private static final Pattern PATTERN = Pattern.compile( "@User:LdapProfile:Name@" );
+
+        @Override
+        public Pattern getRegExPattern( )
+        {
+            return PATTERN;
+        }
+
+        @Override
+        public String replaceValue(
+                final String matchValue,
+                final MacroRequest macroRequest
+
+        )
+        {
+            final UserInfo userInfo = macroRequest.getUserInfo();
+
+            if ( userInfo != null )
+            {
+                final UserIdentity userIdentity = userInfo.getUserIdentity();
+                if ( userIdentity != null )
+                {
+                    final LdapProfile ldapProfile = userIdentity.getLdapProfile( macroRequest.getPwmApplication().getConfig() );
+                    if ( ldapProfile != null )
+                    {
+                        final Locale userLocale = macroRequest.getUserLocale() == null ? PwmConstants.DEFAULT_LOCALE : macroRequest.getUserLocale();
+                        return ldapProfile.getDisplayName( userLocale );
+                    }
+                }
+            }
+
+            return "";
+        }
+    }
+
     public static class TargetUserIDMacro extends AbstractUserIDMacro
     {
         private static final Pattern PATTERN = Pattern.compile( "@TargetUser:ID@" );
@@ -662,37 +767,6 @@ public class UserMacros
         }
     }
 
-    public static class UserLdapProfileMacro extends AbstractUserMacro
-    {
-        private static final Pattern PATTERN = Pattern.compile( "@User:LdapProfile@" );
-
-        @Override
-        public Pattern getRegExPattern( )
-        {
-            return PATTERN;
-        }
-
-        @Override
-        public String replaceValue(
-                final String matchValue,
-                final MacroRequest request
-        )
-        {
-            final UserInfo userInfo = request.getUserInfo();
-
-            if ( userInfo != null )
-            {
-                final UserIdentity userIdentity = userInfo.getUserIdentity();
-                if ( userIdentity != null )
-                {
-                    return userIdentity.getLdapProfileID();
-                }
-            }
-
-            return "";
-        }
-    }
-
     public static class OtpSetupTimeMacro extends AbstractUserMacro
     {
         private static final Pattern PATTERN = Pattern.compile( "@OtpSetupTime" + PATTERN_OPTIONAL_PARAMETER_MATCH + "@" );

+ 2 - 2
server/src/main/java/password/pwm/ws/server/rest/RestStatisticsServer.java

@@ -40,6 +40,7 @@ import password.pwm.svc.stats.StatisticType;
 import password.pwm.svc.stats.StatisticsBundle;
 import password.pwm.svc.stats.StatisticsClient;
 import password.pwm.svc.stats.StatisticsService;
+import password.pwm.util.java.CollectionUtil;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.MiscUtil;
 import password.pwm.util.java.StringUtil;
@@ -58,7 +59,6 @@ import java.time.ZoneOffset;
 import java.time.format.DateTimeFormatter;
 import java.util.ArrayList;
 import java.util.Collections;
-import java.util.EnumSet;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Locale;
@@ -174,7 +174,7 @@ public class RestStatisticsServer extends RestServlet
 
         private static List<StatValue> makeStatInfos( final StatisticsService statisticsManager, final String key )
         {
-            final Map<String, StatValue> output = EnumSet.allOf( Statistic.class ).stream()
+            final Map<String, StatValue> output = CollectionUtil.enumStream( Statistic.class )
                     .collect( Collectors.toMap(
                             Enum::name,
                             stat -> new StatValue( stat.name(), statisticsManager.getStatBundleForKey( key ).getStatistic( stat ) )

+ 19 - 87
server/src/main/resources/password/pwm/config/PwmSetting.xml

@@ -918,10 +918,7 @@
             <flag>MacroSupport</flag>
         </flags>
         <default>
-            <value>{"to":"@User:Email@","from":"Change Password Notice \u003c@DefaultEmailFromAddress@\u003e","subject":"Password Change Notification","bodyPlain":"You have changed
-                your password. If you did not initiate a password change please contact your help desk immediately.","bodyHtml":"\u003cb\u003eYou have changed your
-                password.\u003c/b\u003e If you have changed your password, then no action is required. If you did not initiate a password change please contact your help desk."}
-            </value>
+            <value>{"to":"@User:Email@","from":"Change Password Notice \u003c@DefaultEmailFromAddress@\u003e","subject":"Password Change Notification","bodyPlain":"You have changed your password. If you did not initiate a password change please contact your help desk immediately.","bodyHtml":"\u003cb\u003eYou have changed your password.\u003c/b\u003e If you have changed your password, then no action is required. If you did not initiate a password change please contact your help desk."}</value>
         </default>
     </setting>
     <setting hidden="false" key="email.changePassword.helpdesk" level="1">
@@ -929,11 +926,7 @@
             <flag>MacroSupport</flag>
         </flags>
         <default>
-            <value>{"to":"@User:Email@","from":"Change Password Notice \u003c@DefaultEmailFromAddress@\u003e","subject":"Password Change Notification","bodyPlain":"Your password
-                has been changed by the heldesk. You should set a new password immediately. If you did not initiate a password change please contact your
-                helpdesk.","bodyHtml":"\u003cb\u003eYour password has been changed by the helpdesk.\u003c/b\u003e You should set a new password immediately. If you did not initiate
-                a password change please contact your helpdesk."}
-            </value>
+            <value>{"to":"@User:Email@","from":"Change Password Notice \u003c@DefaultEmailFromAddress@\u003e","subject":"Password Change Notification","bodyPlain":"Your password has been changed by the heldesk. You should set a new password immediately. If you did not initiate a password change please contact your helpdesk.","bodyHtml":"\u003cb\u003eYour password has been changed by the helpdesk.\u003c/b\u003e You should set a new password immediately. If you did not initiate a password change please contact your helpdesk."}</value>
         </default>
     </setting>
     <setting hidden="false" key="email.updateProfile" level="1">
@@ -941,10 +934,7 @@
             <flag>MacroSupport</flag>
         </flags>
         <default>
-            <value>{"to":"@User:Email@","from":"Profile Update \u003c@DefaultEmailFromAddress@\u003e","subject":"Profile Update","bodyPlain":"Thank you for updating your profile
-                information, @LDAP:givenName@.","bodyHtml":"\u003cb\u003eThank you for updating your profile information,
-                \u003c/b\u003e\u003cb\u003e@LDAP:givenName@.\u003c/b\u003e"}
-            </value>
+            <value>{"to":"@User:Email@","from":"Profile Update \u003c@DefaultEmailFromAddress@\u003e","subject":"Profile Update","bodyPlain":"Thank you for updating your profile information, @LDAP:givenName@.","bodyHtml":"\u003cb\u003eThank you for updating your profile information, \u003c/b\u003e\u003cb\u003e@LDAP:givenName@.\u003c/b\u003e"}</value>
         </default>
     </setting>
     <setting hidden="false" key="email.updateProfile.token" level="1">
@@ -952,14 +942,7 @@
             <flag>MacroSupport</flag>
         </flags>
         <default>
-            <value>{"to":"@User:Email@","from":"Profile Update \u003c@DefaultEmailFromAddress@\u003e","subject":"Profile Update","bodyPlain":"Thank you for updating your profile
-                information. To continue with the update, please copy and paste the following code on the registration form:\n\n%TOKEN%\n\nIf you did not request to change your
-                profile, you do not need to take any action.","bodyHtml":"Thank you for updating your profile. To complete the update, please \u003ca
-                href\u003d\"@SiteURL@/public/newuser/%TOKEN%\"\u003eclick here\u003c/a\u003e to continue.\u003cbr /\u003e\u003cbr /\u003eIf for some reason this link does not work,
-                you can copy and paste the following code on the registration form:\u003cbr /\u003e\u003cbr
-                /\u003e\u003cb\u003e\u003cpre\u003e%TOKEN%\u003c/pre\u003e\u003c/b\u003e\u003cbr /\u003eIf you did not request to change your profile, you do not need to take any
-                action."}
-            </value>
+            <value>{"to":"@User:Email@","from":"Profile Update \u003c@DefaultEmailFromAddress@\u003e","subject":"Profile Update","bodyPlain":"Thank you for updating your profile information. To continue with the update, please copy and paste the following code on the registration form:\n\n%TOKEN%\n\nIf you did not request to change your profile, you do not need to take any action.","bodyHtml":"Thank you for updating your profile. To complete the update, please \u003ca href\u003d\"@SiteURL@/public/newuser/%TOKEN%\"\u003eclick here\u003c/a\u003e to continue.\u003cbr /\u003e\u003cbr /\u003eIf for some reason this link does not work, you can copy and paste the following code on the registration form:\u003cbr /\u003e\u003cbr /\u003e\u003cb\u003e\u003cpre\u003e%TOKEN%\u003c/pre\u003e\u003c/b\u003e\u003cbr /\u003eIf you did not request to change your profile, you do not need to take any action."}</value>
         </default>
     </setting>
     <setting hidden="false" key="email.newUser" level="1">
@@ -967,9 +950,7 @@
             <flag>MacroSupport</flag>
         </flags>
         <default>
-            <value>{"to":"@User:Email@","from":"New User Registration \u003c@DefaultEmailFromAddress@\u003e","subject":"Welcome","bodyPlain":"Thank you for registering your
-                account, @LDAP:givenName@.","bodyHtml":"\u003cb\u003eThank you for registering your account, \u003c/b\u003e\u003cb\u003e@LDAP:givenName@.\u003c/b\u003e"}
-            </value>
+            <value>{"to":"@User:Email@","from":"New User Registration \u003c@DefaultEmailFromAddress@\u003e","subject":"Welcome","bodyPlain":"Thank you for registering your account, @LDAP:givenName@.","bodyHtml":"\u003cb\u003eThank you for registering your account, \u003c/b\u003e\u003cb\u003e@LDAP:givenName@.\u003c/b\u003e"}</value>
         </default>
     </setting>
     <setting hidden="false" key="email.newUser.token" level="1">
@@ -977,14 +958,7 @@
             <flag>MacroSupport</flag>
         </flags>
         <default>
-            <value>{"to":"@User:Email@","from":"New User Registration \u003c@DefaultEmailFromAddress@\u003e","subject":"New User Verification","bodyPlain":"Thank you for requesting
-                a new account. To continue with your account registration, please copy and paste the following code on the registration form:\n\n%TOKEN%\n\nIf you did not request
-                to create a new account, you do not need to take any action.","bodyHtml":"Thank you for requesting a new account. To continue with your account registration, please
-                \u003ca href\u003d\"@SiteURL@/public/newuser/%TOKEN%\"\u003eclick here\u003c/a\u003e to continue.\u003cbr /\u003e\u003cbr /\u003eIf for some reason this link does
-                not work, you can copy and paste the following code on the registration form:\u003cbr /\u003e\u003cbr
-                /\u003e\u003cb\u003e\u003cpre\u003e%TOKEN%\u003c/pre\u003e\u003c/b\u003e\u003cbr /\u003eIf you did not request to create a new account, you do not need to take any
-                action."}
-            </value>
+            <value>{"to":"@User:Email@","from":"New User Registration \u003c@DefaultEmailFromAddress@\u003e","subject":"New User Verification","bodyPlain":"Thank you for requesting a new account. To continue with your account registration, please copy and paste the following code on the registration form:\n\n%TOKEN%\n\nIf you did not request to create a new account, you do not need to take any action.","bodyHtml":"Thank you for requesting a new account. To continue with your account registration, please \u003ca href\u003d\"@SiteURL@/public/newuser/%TOKEN%\"\u003eclick here\u003c/a\u003e to continue.\u003cbr /\u003e\u003cbr /\u003eIf for some reason this link does not work, you can copy and paste the following code on the registration form:\u003cbr /\u003e\u003cbr /\u003e\u003cb\u003e\u003cpre\u003e%TOKEN%\u003c/pre\u003e\u003c/b\u003e\u003cbr /\u003eIf you did not request to create a new account, you do not need to take any action."}</value>
         </default>
     </setting>
     <setting hidden="false" key="email.activation" level="1">
@@ -992,9 +966,7 @@
             <flag>MacroSupport</flag>
         </flags>
         <default>
-            <value>{"to":"@User:Email@","from":"Activation Notification \u003c@DefaultEmailFromAddress@\u003e","subject":"Account Activated","bodyPlain":"Thank you for activating
-                your account, @LDAP:givenName@.","bodyHtml":"\u003cb\u003eThank you for activating your account, @LDAP:givenName@.\u003c/b\u003e"}
-            </value>
+            <value>{"to":"@User:Email@","from":"Activation Notification \u003c@DefaultEmailFromAddress@\u003e","subject":"Account Activated","bodyPlain":"Thank you for activating your account, @LDAP:givenName@.","bodyHtml":"\u003cb\u003eThank you for activating your account, @LDAP:givenName@.\u003c/b\u003e"}</value>
         </default>
     </setting>
     <setting hidden="false" key="email.activation.token" level="1">
@@ -1002,14 +974,7 @@
             <flag>MacroSupport</flag>
         </flags>
         <default>
-            <value>{"to":"@User:Email@","from":"Activation Verification \u003c@DefaultEmailFromAddress@\u003e","subject":"Account Verification","bodyPlain":"Thank you for
-                requesting your account activation. To continue with your account activation, please copy and paste the following code onto the activation form:\n\n%TOKEN%\n\nIf
-                you did not request to create a new account, you do not need to take any action.","bodyHtml":"Thank you for requesting your account activation. To continue with
-                your account activation, please \u003ca href\u003d\"@SiteURL@/public/activate/%TOKEN%\"\u003eclick here\u003c/a\u003e to continue.\u003cbr /\u003e\u003cbr /\u003eIf
-                for some reason this link doesn\u0027t work, you can copy and paste the following code onto the activation form:\u003cbr /\u003e\u003cbr
-                /\u003e\u003cb\u003e\u003cpre\u003e%TOKEN%\u003c/pre\u003e\u003c/b\u003e\u003cbr /\u003eIf you did not request to create a new account, you do not need to take any
-                action."}
-            </value>
+            <value>{"to":"@User:Email@","from":"Activation Verification \u003c@DefaultEmailFromAddress@\u003e","subject":"Account Verification","bodyPlain":"Thank you for requesting your account activation. To continue with your account activation, please copy and paste the following code onto the activation form:\n\n%TOKEN%\n\nIf you did not request to create a new account, you do not need to take any action.","bodyHtml":"Thank you for requesting your account activation. To continue with your account activation, please \u003ca href\u003d\"@SiteURL@/public/activate/%TOKEN%\"\u003eclick here\u003c/a\u003e to continue.\u003cbr /\u003e\u003cbr /\u003eIf for some reason this link doesn\u0027t work, you can copy and paste the following code onto the activation form:\u003cbr /\u003e\u003cbr /\u003e\u003cb\u003e\u003cpre\u003e%TOKEN%\u003c/pre\u003e\u003c/b\u003e\u003cbr /\u003eIf you did not request to create a new account, you do not need to take any action."}</value>
         </default>
     </setting>
     <setting hidden="false" key="email.challenge.token" level="1">
@@ -1017,13 +982,7 @@
             <flag>MacroSupport</flag>
         </flags>
         <default>
-            <value>{"to":"@User:Email@","from":"Forgotten Password \u003c@DefaultEmailFromAddress@\u003e","subject":"Forgotten Password Verification","bodyPlain":"Thank you for
-                requesting a password reset. To continue with your password reset, please copy and paste the following code onto the password reset form:\n\n%TOKEN%\n\nIf you do
-                not wish to change your password at this time, you do not need to take any action.","bodyHtml":"Thank you for requesting a password reset. To continue with your
-                password reset, please \u003ca href\u003d\"@SiteURL@/public/forgottenpassword/%TOKEN%\"\u003eclick here\u003c/a\u003e to continue.\u003cbr /\u003e\u003cbr /\u003eIf
-                for some reason this link doesn\u0027t work, you can copy and paste the following code onto the password reset form:\u003cbr /\u003e\u003cbr /\u003e%TOKEN%\u003cbr
-                /\u003e\u003cbr /\u003eIf you do not wish to change your password at this time, you do not need to take any action."}
-            </value>
+            <value>{"to":"@User:Email@","from":"Forgotten Password \u003c@DefaultEmailFromAddress@\u003e","subject":"Forgotten Password Verification","bodyPlain":"Thank you for requesting a password reset. To continue with your password reset, please copy and paste the following code onto the password reset form:\n\n%TOKEN%\n\nIf you do not wish to change your password at this time, you do not need to take any action.","bodyHtml":"Thank you for requesting a password reset. To continue with your password reset, please \u003ca href\u003d\"@SiteURL@/public/forgottenpassword/%TOKEN%\"\u003eclick here\u003c/a\u003e to continue.\u003cbr /\u003e\u003cbr /\u003eIf for some reason this link doesn\u0027t work, you can copy and paste the following code onto the password reset form:\u003cbr /\u003e\u003cbr /\u003e%TOKEN%\u003cbr /\u003e\u003cbr /\u003eIf you do not wish to change your password at this time, you do not need to take any action."}</value>
         </default>
     </setting>
     <setting hidden="false" key="email.helpdesk.token" level="1">
@@ -1031,10 +990,7 @@
             <flag>MacroSupport</flag>
         </flags>
         <default>
-            <value>{"to":"@User:Email@","from":"Helpdesk \u003c@DefaultEmailFromAddress@\u003e","subject":"Helpdesk Verification","bodyPlain":"Your helpdesk has sent you a code to
-                verify your identity. Your verification code is:\n\n%TOKEN%","bodyHtml":"Your helpdesk has sent you a code to verify your identity. Your verification code is:
-                %TOKEN%."}
-            </value>
+            <value>{"to":"@User:Email@","from":"Helpdesk \u003c@DefaultEmailFromAddress@\u003e","subject":"Helpdesk Verification","bodyPlain":"Your helpdesk has sent you a code to verify your identity. Your verification code is:\n\n%TOKEN%","bodyHtml":"Your helpdesk has sent you a code to verify your identity. Your verification code is: %TOKEN%."}</value>
         </default>
     </setting>
     <setting hidden="false" key="email.guest" level="2">
@@ -1042,10 +998,7 @@
             <flag>MacroSupport</flag>
         </flags>
         <default>
-            <value>{"to":"@User:Email@","from":"Guest Registration Agent \u003c@DefaultEmailFromAddress@\u003e","subject":"Welcome","bodyPlain":"Your account has been
-                created.\n\nYour username is: @User:ID@\nYour password is: @User:Password@","bodyHtml":"\u003cb\u003eYour account has been created.\u003c/b\u003e\u003cp\u003eYour
-                username is:@User:ID@\u003cbr /\u003eYour password is: \u003cb\u003e@User:Password@\u003c/b\u003e\u003c/p\u003e"}
-            </value>
+            <value>{"to":"@User:Email@","from":"Guest Registration Agent \u003c@DefaultEmailFromAddress@\u003e","subject":"Welcome","bodyPlain":"Your account has been created.\n\nYour username is: @User:ID@\nYour password is: @User:Password@","bodyHtml":"\u003cb\u003eYour account has been created.\u003c/b\u003e\u003cp\u003eYour username is:@User:ID@\u003cbr /\u003eYour password is: \u003cb\u003e@User:Password@\u003c/b\u003e\u003c/p\u003e"}</value>
         </default>
     </setting>
     <setting hidden="false" key="email.updateguest" level="2">
@@ -1053,9 +1006,7 @@
             <flag>MacroSupport</flag>
         </flags>
         <default>
-            <value>{"to":"@User:Email@","from":"Guest Registration Agent \u003c@DefaultEmailFromAddress@\u003e","subject":"Account update notification","bodyPlain":"Your account
-                has been updated.","bodyHtml":"\u003cb\u003eYour account has been created.\u003c/b\u003e"}
-            </value>
+            <value>{"to":"@User:Email@","from":"Guest Registration Agent \u003c@DefaultEmailFromAddress@\u003e","subject":"Account update notification","bodyPlain":"Your account has been updated.","bodyHtml":"\u003cb\u003eYour account has been created.\u003c/b\u003e"}</value>
         </default>
     </setting>
     <setting hidden="false" key="email.sendpassword" level="2">
@@ -1063,11 +1014,7 @@
             <flag>MacroSupport</flag>
         </flags>
         <default>
-            <value>{"to":"@User:Email@","from":"Password Notifier \u003c@DefaultEmailFromAddress@\u003e","subject":"Password Information","bodyPlain":"Your new password
-                is:\n\n@User:Password@\n\nPlease change your password as soon as possible.","bodyHtml":"Thank you for requesting a password reset. Your new password is:\u003cbr
-                /\u003e\u003cbr /\u003e\u003cfont face\u003d\"\u0027Courier New\u0027\"\u003e\u003cb\u003e@User:Password@\u003cbr /\u003e\u003cbr
-                /\u003e\u003c/b\u003e\u003c/font\u003e"}
-            </value>
+            <value>{"to":"@User:Email@","from":"Password Notifier \u003c@DefaultEmailFromAddress@\u003e","subject":"Password Information","bodyPlain":"Your new password is:\n\n@User:Password@\n\nPlease change your password as soon as possible.","bodyHtml":"Thank you for requesting a password reset. Your new password is:\u003cbr /\u003e\u003cbr /\u003e\u003cfont face\u003d\"\u0027Courier New\u0027\"\u003e\u003cb\u003e@User:Password@\u003cbr /\u003e\u003cbr /\u003e\u003c/b\u003e\u003c/font\u003e"}</value>
         </default>
     </setting>
     <setting hidden="false" key="email.sendUsername" level="2">
@@ -1075,10 +1022,7 @@
             <flag>MacroSupport</flag>
         </flags>
         <default>
-            <value>{"to":"@User:Email@","from":"Username Notifier \u003c@DefaultEmailFromAddress@\u003e","subject":"Username Information","bodyPlain":"Your username
-                is:\n\n@User:ID@\n\n","bodyHtml":"Your username is:\u003cbr /\u003e\u003cbr /\u003e\u003cfont face\u003d\"\u0027Courier
-                New\u0027\"\u003e\u003cb\u003e@User:ID@\u003cbr /\u003e\u003cbr /\u003e\u003c/b\u003e\u003c/font\u003e"}
-            </value>
+            <value>{"to":"@User:Email@","from":"Username Notifier \u003c@DefaultEmailFromAddress@\u003e","subject":"Username Information","bodyPlain":"Your username is:\n\n@User:ID@\n\n","bodyHtml":"Your username is:\u003cbr /\u003e\u003cbr /\u003e\u003cfont face\u003d\"\u0027Courier New\u0027\"\u003e\u003cb\u003e@User:ID@\u003cbr /\u003e\u003cbr /\u003e\u003c/b\u003e\u003c/font\u003e"}</value>
         </default>
     </setting>
     <setting hidden="false" key="email.intruderNotice" level="1">
@@ -1086,11 +1030,7 @@
             <flag>MacroSupport</flag>
         </flags>
         <default>
-            <value>{"to":"@User:Email@","from":"Intruder Notifier \u003c@DefaultEmailFromAddress@\u003e","subject":"Password Information","bodyPlain":"Your account has been
-                temporarily disabled due to several incorrect login/password reset attempts. If this activity was not caused by you, please contact your
-                administrator.","bodyHtml":"Your account has been temporarily disabled due to several incorrect login/password reset attempts. If this activity was not caused by
-                you, please contact your administrator."}
-            </value>
+            <value>{"to":"@User:Email@","from":"Intruder Notifier \u003c@DefaultEmailFromAddress@\u003e","subject":"Password Information","bodyPlain":"Your account has been temporarily disabled due to several incorrect login/password reset attempts. If this activity was not caused by you, please contact your administrator.","bodyHtml":"Your account has been temporarily disabled due to several incorrect login/password reset attempts. If this activity was not caused by you, please contact your administrator."}</value>
         </default>
     </setting>
     <setting hidden="false" key="email.deleteAccount" level="1">
@@ -1098,9 +1038,7 @@
             <flag>MacroSupport</flag>
         </flags>
         <default>
-            <value>{"to":"@User:Email@","from":"Delete Account Notice \u003c@DefaultEmailFromAddress@\u003e","subject":"Account Deletion Notice","bodyPlain":"Your account has been
-                deleted at your request.","bodyHtml":""}
-            </value>
+            <value>{"to":"@User:Email@","from":"Delete Account Notice \u003c@DefaultEmailFromAddress@\u003e","subject":"Account Deletion Notice","bodyPlain":"Your account has been deleted at your request.","bodyHtml":""}</value>
         </default>
     </setting>
     <setting hidden="false" key="email.helpdesk.unlock" level="1">
@@ -1108,9 +1046,7 @@
             <flag>MacroSupport</flag>
         </flags>
         <default>
-            <value>{"to":"@User:Email@","from":"Unlock Account Notice \u003c@DefaultEmailFromAddress@\u003e","subject":"Account Unlock Notice","bodyPlain":"Your account has been
-                unlocked by the helpdesk.","bodyHtml":""}
-            </value>
+            <value>{"to":"@User:Email@","from":"Unlock Account Notice \u003c@DefaultEmailFromAddress@\u003e","subject":"Account Unlock Notice","bodyPlain":"Your account has been unlocked by the helpdesk.","bodyHtml":""}</value>
         </default>
     </setting>
     <setting hidden="false" key="email.unlock" level="1">
@@ -1118,9 +1054,7 @@
             <flag>MacroSupport</flag>
         </flags>
         <default>
-            <value>{"to":"@User:Email@","from":"Unlock Account Notice \u003c@DefaultEmailFromAddress@\u003e","subject":"Account Unlock Notice","bodyPlain":"Your account has been
-                unlocked.","bodyHtml":""}
-            </value>
+            <value>{"to":"@User:Email@","from":"Unlock Account Notice \u003c@DefaultEmailFromAddress@\u003e","subject":"Account Unlock Notice","bodyPlain":"Your account has been unlocked.","bodyHtml":""}</value>
         </default>
     </setting>
     <setting hidden="false" key="email.pwNotice" level="1">
@@ -1128,9 +1062,7 @@
             <flag>MacroSupport</flag>
         </flags>
         <default>
-            <value>{"to":"@User:Email@","from":"Password Expiration Notice \u003c@DefaultEmailFromAddress@\u003e","subject":"Password Expiration Notice","bodyPlain":"Your password
-                is about to expire. Your password will expire in @User:DaysUntilPwExpire@ days.","bodyHtml":""}
-            </value>
+            <value>{"to":"@User:Email@","from":"Password Expiration Notice \u003c@DefaultEmailFromAddress@\u003e","subject":"Password Expiration Notice","bodyPlain":"Your password is about to expire. Your password will expire in @User:DaysUntilPwExpire@ days.","bodyHtml":""}</value>
         </default>
     </setting>
     <setting hidden="false" key="email.smtp.advancedSettings" level="2">

+ 9 - 0
server/src/test/java/password/pwm/config/PwmSettingTest.java

@@ -155,6 +155,15 @@ public class PwmSettingTest
         }
     }
 
+    @Test
+    public void testFlagValues() throws PwmUnrecoverableException, PwmOperationalException
+    {
+        final Set<PwmSettingFlag> flags = PwmSetting.DOMAIN_LIST.getFlags();
+        Assert.assertEquals( 2, flags.size() );
+        Assert.assertTrue( flags.contains( PwmSettingFlag.Sorted ) );
+        Assert.assertTrue( flags.contains( PwmSettingFlag.ReloadEditorOnModify ) );
+    }
+
     @Test
     public void testProperties() throws PwmUnrecoverableException, PwmOperationalException
     {

+ 2 - 2
server/src/test/java/password/pwm/http/PwmURLTest.java

@@ -59,7 +59,7 @@ public class PwmURLTest
         {
             final StoredConfigurationModifier modifier = StoredConfigurationModifier.newModifier( StoredConfigurationFactory.newConfig() );
             final List<String> domainStrList = List.of( "aaaa", "bbbb", "cccc" );
-            final StoredValue storedValue = new StringArrayValue( domainStrList );
+            final StoredValue storedValue = StringArrayValue.create( domainStrList );
             modifier.writeSetting( StoredConfigKey.forSetting( PwmSetting.DOMAIN_LIST, null, DomainID.systemId() ), storedValue, null );
             appConfig = new AppConfig( modifier.newStoredConfiguration( ) );
         }
@@ -90,7 +90,7 @@ public class PwmURLTest
         {
             final StoredConfigurationModifier modifier = StoredConfigurationModifier.newModifier( StoredConfigurationFactory.newConfig() );
             final List<String> domainStrList = List.of( "aaaa", "bbbb", "cccc" );
-            final StoredValue storedValue = new StringArrayValue( domainStrList );
+            final StoredValue storedValue = StringArrayValue.create( domainStrList );
             modifier.writeSetting( StoredConfigKey.forSetting( PwmSetting.DOMAIN_LIST, null, DomainID.systemId() ), storedValue, null );
             appConfig = new AppConfig( modifier.newStoredConfiguration( ) );
         }

+ 1 - 1
server/src/test/java/password/pwm/http/client/PwmHttpClientTest.java

@@ -306,7 +306,7 @@ public class PwmHttpClientTest
             }
             modifier.writeSetting(
                     StoredConfigKey.forSetting( PwmSetting.APP_PROPERTY_OVERRIDES, null, DomainID.systemId() ),
-                    new StringArrayValue( array ), null );
+                    StringArrayValue.create( array ), null );
 
         }
         return new AppConfig( modifier.newStoredConfiguration() );

+ 9 - 2
server/src/test/java/password/pwm/util/macro/MacroTest.java

@@ -304,9 +304,16 @@ public class MacroTest
     @Test
     public void testUserLdapProfileMacro()
     {
+        final String goal = "UserLdapProfile default ProfileName test";
+        final String expanded = macroRequest.expandMacros( "UserLdapProfile @User:LdapProfile:ID@ @User:LdapProfile:Name@ test" );
+        Assert.assertEquals( goal, expanded );
+    }
 
-        final String goal = "UserLdapProfile profile1 test";
-        final String expanded = macroRequest.expandMacros( "UserLdapProfile @User:LdapProfile@ test" );
+    @Test
+    public void testDomainIdMacro()
+    {
+        final String goal = "UserDomain default test";
+        final String expanded = macroRequest.expandMacros( "UserDomain @User:Domain:ID@ test" );
         Assert.assertEquals( goal, expanded );
     }
 

+ 1 - 1
webapp/src/main/webapp/public/resources/themes/pwm/style.css

@@ -136,7 +136,7 @@ h3 {
 
 
 .btn-icon {
-    color: rgba(255,255,255,0.75)
+    opacity:0.75;
 }