فهرست منبع

Merge branch 'enh-ngconfig'

Jason Rivard 5 سال پیش
والد
کامیت
1c1e8cd594
100فایلهای تغییر یافته به همراه4003 افزوده شده و 4026 حذف شده
  1. 1 0
      server/pom.xml
  2. 18 2
      server/src/main/java/password/pwm/PwmAboutProperty.java
  3. 32 23
      server/src/main/java/password/pwm/PwmApplication.java
  4. 1 4
      server/src/main/java/password/pwm/PwmConstants.java
  5. 4 3
      server/src/main/java/password/pwm/PwmEnvironment.java
  6. 5 19
      server/src/main/java/password/pwm/bean/PrivateKeyCertificate.java
  7. 57 87
      server/src/main/java/password/pwm/config/Configuration.java
  8. 22 9
      server/src/main/java/password/pwm/config/PwmSetting.java
  9. 4 3
      server/src/main/java/password/pwm/config/PwmSettingCategory.java
  10. 1 0
      server/src/main/java/password/pwm/config/PwmSettingFlag.java
  11. 2 1
      server/src/main/java/password/pwm/config/PwmSettingProperty.java
  12. 1 1
      server/src/main/java/password/pwm/config/PwmSettingSyntax.java
  13. 19 24
      server/src/main/java/password/pwm/config/PwmSettingXml.java
  14. 2 2
      server/src/main/java/password/pwm/config/SettingUIFunction.java
  15. 3 5
      server/src/main/java/password/pwm/config/StoredValue.java
  16. 14 8
      server/src/main/java/password/pwm/config/function/AbstractUriCertImportFunction.java
  17. 12 6
      server/src/main/java/password/pwm/config/function/ActionCertImportFunction.java
  18. 4 4
      server/src/main/java/password/pwm/config/function/LdapCertImportFunction.java
  19. 6 4
      server/src/main/java/password/pwm/config/function/OAuthCertImportFunction.java
  20. 11 10
      server/src/main/java/password/pwm/config/function/RemoteWebServiceCertImportFunction.java
  21. 5 7
      server/src/main/java/password/pwm/config/function/SMSGatewayCertImportFunction.java
  22. 4 4
      server/src/main/java/password/pwm/config/function/SmtpCertImportFunction.java
  23. 5 5
      server/src/main/java/password/pwm/config/function/SyslogCertImportFunction.java
  24. 5 4
      server/src/main/java/password/pwm/config/function/UserMatchViewerFunction.java
  25. 7 4
      server/src/main/java/password/pwm/config/profile/PwmPasswordPolicy.java
  26. 0 150
      server/src/main/java/password/pwm/config/stored/ConfigChangeLogImpl.java
  27. 2 15
      server/src/main/java/password/pwm/config/stored/ConfigRestartRequirement.java
  28. 258 200
      server/src/main/java/password/pwm/config/stored/ConfigurationCleaner.java
  29. 2 2
      server/src/main/java/password/pwm/config/stored/ConfigurationProperty.java
  30. 134 59
      server/src/main/java/password/pwm/config/stored/ConfigurationReader.java
  31. 70 0
      server/src/main/java/password/pwm/config/stored/StoredConfigData.java
  32. 235 0
      server/src/main/java/password/pwm/config/stored/StoredConfigItemKey.java
  33. 0 67
      server/src/main/java/password/pwm/config/stored/StoredConfigReferenceBean.java
  34. 50 0
      server/src/main/java/password/pwm/config/stored/StoredConfigXmlConstants.java
  35. 18 51
      server/src/main/java/password/pwm/config/stored/StoredConfiguration.java
  36. 671 3
      server/src/main/java/password/pwm/config/stored/StoredConfigurationFactory.java
  37. 124 1418
      server/src/main/java/password/pwm/config/stored/StoredConfigurationImpl.java
  38. 291 0
      server/src/main/java/password/pwm/config/stored/StoredConfigurationModifier.java
  39. 348 37
      server/src/main/java/password/pwm/config/stored/StoredConfigurationUtil.java
  40. 11 15
      server/src/main/java/password/pwm/config/stored/XmlOutputProcessData.java
  41. 0 99
      server/src/main/java/password/pwm/config/stored/ng/NGStorageEngineImpl.java
  42. 0 202
      server/src/main/java/password/pwm/config/stored/ng/NGStoredConfiguration.java
  43. 0 253
      server/src/main/java/password/pwm/config/stored/ng/NGStoredConfigurationFactory.java
  44. 31 72
      server/src/main/java/password/pwm/config/value/AbstractValue.java
  45. 56 28
      server/src/main/java/password/pwm/config/value/ActionValue.java
  46. 13 13
      server/src/main/java/password/pwm/config/value/BooleanValue.java
  47. 12 6
      server/src/main/java/password/pwm/config/value/ChallengeValue.java
  48. 5 4
      server/src/main/java/password/pwm/config/value/CustomLinkValue.java
  49. 4 3
      server/src/main/java/password/pwm/config/value/EmailValue.java
  50. 50 101
      server/src/main/java/password/pwm/config/value/FileValue.java
  51. 7 14
      server/src/main/java/password/pwm/config/value/FormValue.java
  52. 4 3
      server/src/main/java/password/pwm/config/value/LocalizedStringArrayValue.java
  53. 7 5
      server/src/main/java/password/pwm/config/value/LocalizedStringValue.java
  54. 32 32
      server/src/main/java/password/pwm/config/value/NamedSecretValue.java
  55. 4 3
      server/src/main/java/password/pwm/config/value/NumericArrayValue.java
  56. 15 5
      server/src/main/java/password/pwm/config/value/NumericValue.java
  57. 4 3
      server/src/main/java/password/pwm/config/value/OptionListValue.java
  58. 54 48
      server/src/main/java/password/pwm/config/value/PasswordValue.java
  59. 34 19
      server/src/main/java/password/pwm/config/value/PrivateKeyValue.java
  60. 29 12
      server/src/main/java/password/pwm/config/value/RemoteWebServiceValue.java
  61. 268 0
      server/src/main/java/password/pwm/config/value/StoredValueEncoder.java
  62. 6 4
      server/src/main/java/password/pwm/config/value/StringArrayValue.java
  63. 7 3
      server/src/main/java/password/pwm/config/value/StringValue.java
  64. 13 9
      server/src/main/java/password/pwm/config/value/UserPermissionValue.java
  65. 3 3
      server/src/main/java/password/pwm/config/value/ValueFactory.java
  66. 19 11
      server/src/main/java/password/pwm/config/value/VerificationMethodValue.java
  67. 13 8
      server/src/main/java/password/pwm/config/value/X509CertificateValue.java
  68. 144 0
      server/src/main/java/password/pwm/config/value/data/ActionConfiguration.java
  69. 0 180
      server/src/main/java/password/pwm/config/value/data/ActionConfigurationOldVersion1.java
  70. 4 15
      server/src/main/java/password/pwm/config/value/data/ChallengeItemConfiguration.java
  71. 3 3
      server/src/main/java/password/pwm/config/value/data/CustomLinkConfiguration.java
  72. 14 27
      server/src/main/java/password/pwm/config/value/data/FormConfiguration.java
  73. 2 4
      server/src/main/java/password/pwm/config/value/data/NamedSecretData.java
  74. 4 4
      server/src/main/java/password/pwm/config/value/data/RemoteWebServiceConfiguration.java
  75. 2 4
      server/src/main/java/password/pwm/config/value/data/ShortcutItem.java
  76. 8 31
      server/src/main/java/password/pwm/config/value/data/UserPermission.java
  77. 29 0
      server/src/main/java/password/pwm/error/PwmInternalException.java
  78. 10 10
      server/src/main/java/password/pwm/health/CertificateChecker.java
  79. 138 153
      server/src/main/java/password/pwm/health/ConfigurationChecker.java
  80. 5 5
      server/src/main/java/password/pwm/health/HealthRecord.java
  81. 55 53
      server/src/main/java/password/pwm/http/ContextManager.java
  82. 4 44
      server/src/main/java/password/pwm/http/bean/ConfigManagerBean.java
  83. 5 4
      server/src/main/java/password/pwm/http/filter/ConfigAccessFilter.java
  84. 0 7
      server/src/main/java/password/pwm/http/servlet/ClientApiServlet.java
  85. 84 77
      server/src/main/java/password/pwm/http/servlet/configeditor/ConfigEditorServlet.java
  86. 81 0
      server/src/main/java/password/pwm/http/servlet/configeditor/ConfigEditorServletUtils.java
  87. 9 9
      server/src/main/java/password/pwm/http/servlet/configeditor/NavTreeHelper.java
  88. 15 10
      server/src/main/java/password/pwm/http/servlet/configguide/ConfigGuideForm.java
  89. 30 24
      server/src/main/java/password/pwm/http/servlet/configguide/ConfigGuideServlet.java
  90. 19 13
      server/src/main/java/password/pwm/http/servlet/configguide/ConfigGuideUtils.java
  91. 9 9
      server/src/main/java/password/pwm/http/servlet/configmanager/ConfigManagerCertificatesServlet.java
  92. 56 26
      server/src/main/java/password/pwm/http/servlet/configmanager/ConfigManagerLoginServlet.java
  93. 25 40
      server/src/main/java/password/pwm/http/servlet/configmanager/ConfigManagerServlet.java
  94. 42 21
      server/src/main/java/password/pwm/http/servlet/configmanager/DebugItemGenerator.java
  95. 3 3
      server/src/main/java/password/pwm/http/state/CryptoCookieBeanImpl.java
  96. 0 2
      server/src/main/java/password/pwm/i18n/Display.java
  97. 49 24
      server/src/main/java/password/pwm/i18n/PwmLocaleBundle.java
  98. 3 3
      server/src/main/java/password/pwm/ldap/LdapBrowser.java
  99. 1 1
      server/src/main/java/password/pwm/svc/node/NodeMachine.java
  100. 1 1
      server/src/main/java/password/pwm/svc/node/StoredNodeData.java

+ 1 - 0
server/pom.xml

@@ -52,6 +52,7 @@
                         </goals>
                         </goals>
                         <phase>test</phase>
                         <phase>test</phase>
                         <configuration>
                         <configuration>
+                            <trimStackTrace>false</trimStackTrace>
                             <skipTests>${skipTests}</skipTests>
                             <skipTests>${skipTests}</skipTests>
                             <excludes>
                             <excludes>
                                 <exclude>**/ExtendedTest*.java</exclude>
                                 <exclude>**/ExtendedTest*.java</exclude>

+ 18 - 2
server/src/main/java/password/pwm/PwmAboutProperty.java

@@ -28,8 +28,10 @@ import password.pwm.util.java.FileSystemUtility;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.logging.PwmLogger;
 
 
+import javax.net.ssl.SSLContext;
 import java.lang.management.ManagementFactory;
 import java.lang.management.ManagementFactory;
 import java.nio.charset.Charset;
 import java.nio.charset.Charset;
+import java.security.NoSuchAlgorithmException;
 import java.time.Instant;
 import java.time.Instant;
 import java.util.Collections;
 import java.util.Collections;
 import java.util.Date;
 import java.util.Date;
@@ -69,6 +71,7 @@ public enum PwmAboutProperty
     app_secureBlockAlgorithm( null, pwmApplication -> pwmApplication.getSecureService().getDefaultBlockAlgorithm().getLabel() ),
     app_secureBlockAlgorithm( null, pwmApplication -> pwmApplication.getSecureService().getDefaultBlockAlgorithm().getLabel() ),
     app_secureHashAlgorithm( null, pwmApplication -> pwmApplication.getSecureService().getDefaultHashAlgorithm().toString() ),
     app_secureHashAlgorithm( null, pwmApplication -> pwmApplication.getSecureService().getDefaultHashAlgorithm().toString() ),
     app_ldapProfileCount( null, pwmApplication -> Integer.toString( pwmApplication.getConfig().getLdapProfiles().size() ) ),
     app_ldapProfileCount( null, pwmApplication -> Integer.toString( pwmApplication.getConfig().getLdapProfiles().size() ) ),
+    app_ldapConnectionCount( null, pwmApplication -> Integer.toString( pwmApplication.getLdapConnectionService().connectionCount() ) ),
 
 
     build_Time( "Build Time", pwmApplication -> PwmConstants.BUILD_TIME ),
     build_Time( "Build Time", pwmApplication -> PwmConstants.BUILD_TIME ),
     build_Number( "Build Number", pwmApplication -> PwmConstants.BUILD_NUMBER ),
     build_Number( "Build Number", pwmApplication -> PwmConstants.BUILD_NUMBER ),
@@ -91,9 +94,10 @@ public enum PwmAboutProperty
     java_osName( "Operating System Name", pwmApplication -> System.getProperty( "os.name" ) ),
     java_osName( "Operating System Name", pwmApplication -> System.getProperty( "os.name" ) ),
     java_osVersion( "Operating System Version", pwmApplication -> System.getProperty( "os.version" ) ),
     java_osVersion( "Operating System Version", pwmApplication -> System.getProperty( "os.version" ) ),
     java_osArch( "Operating System Architecture", pwmApplication -> System.getProperty( "os.arch" ) ),
     java_osArch( "Operating System Architecture", pwmApplication -> System.getProperty( "os.arch" ) ),
-    java_randomAlgorithm( null, pwmApplication -> pwmApplication.getSecureService().pwmRandom().getAlgorithm() ),
-    java_defaultCharset( null, pwmApplication -> Charset.defaultCharset().name() ),
+    java_randomAlgorithm( "Random Algorithm", pwmApplication -> pwmApplication.getSecureService().pwmRandom().getAlgorithm() ),
+    java_defaultCharset( "Default Character Set", pwmApplication -> Charset.defaultCharset().name() ),
     java_appServerInfo( "Java AppServer Info", pwmApplication -> pwmApplication.getPwmEnvironment().getContextManager().getServerInfo() ),
     java_appServerInfo( "Java AppServer Info", pwmApplication -> pwmApplication.getPwmEnvironment().getContextManager().getServerInfo() ),
+    java_sslVersions( "Java SSL Versions", pwmApplication ->  readSslVersions() ),
 
 
     database_driverName( null,
     database_driverName( null,
             pwmApplication -> pwmApplication.getDatabaseService().getConnectionDebugProperties().get( DatabaseService.DatabaseAboutProperty.driverName ) ),
             pwmApplication -> pwmApplication.getDatabaseService().getConnectionDebugProperties().get( DatabaseService.DatabaseAboutProperty.driverName ) ),
@@ -188,4 +192,16 @@ public enum PwmAboutProperty
         }
         }
         return Collections.unmodifiableMap( outputProps );
         return Collections.unmodifiableMap( outputProps );
     }
     }
+
+    private static String readSslVersions()
+    {
+        try
+        {
+            return String.join( " ", SSLContext.getDefault().getSupportedSSLParameters().getProtocols() );
+        }
+        catch ( NoSuchAlgorithmException e )
+        {
+            return "";
+        }
+    }
 }
 }

+ 32 - 23
server/src/main/java/password/pwm/PwmApplication.java

@@ -28,6 +28,8 @@ import password.pwm.bean.SmsItemBean;
 import password.pwm.bean.UserIdentity;
 import password.pwm.bean.UserIdentity;
 import password.pwm.config.Configuration;
 import password.pwm.config.Configuration;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.PwmSetting;
+import password.pwm.config.stored.StoredConfiguration;
+import password.pwm.config.stored.StoredConfigurationUtil;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmException;
 import password.pwm.error.PwmException;
@@ -101,8 +103,8 @@ import java.util.List;
 import java.util.Locale;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Map;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.ExecutorService;
-
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Supplier;
 
 
 /**
 /**
  * A repository for objects common to the servlet context.  A singleton
  * A repository for objects common to the servlet context.  A singleton
@@ -305,34 +307,16 @@ public class PwmApplication
     {
     {
         final Instant startTime = Instant.now();
         final Instant startTime = Instant.now();
 
 
-        pwmEnvironment.getConfig().outputToLog();
-
-        // detect if config has been modified since previous startup
         try
         try
         {
         {
-            final String previousHash = readAppAttribute( AppAttribute.CONFIG_HASH, String.class );
-            final String currentHash = pwmEnvironment.getConfig().configurationHash();
-            if ( previousHash == null || !previousHash.equals( currentHash ) )
-            {
-                writeAppAttribute( AppAttribute.CONFIG_HASH, currentHash );
-                LOGGER.warn( "configuration checksum does not match previously seen checksum, configuration has been modified since last startup" );
-                if ( this.getAuditManager() != null )
-                {
-                    final String modifyMessage = "configuration was modified directly (not using ConfigEditor UI)";
-                    this.getAuditManager().submit( new AuditRecordFactory( this ).createUserAuditRecord(
-                            AuditEvent.MODIFY_CONFIGURATION,
-                            null,
-                            null,
-                            modifyMessage
-                    ) );
-                }
-            }
+            outputConfigurationToLog( this );
         }
         }
-        catch ( Exception e )
+        catch ( PwmException e )
         {
         {
-            LOGGER.debug( () -> "unable to detect if configuration has been modified since previous startup: " + e.getMessage() );
+            LOGGER.error( "error outputting log to debug: " + e.getMessage() );
         }
         }
 
 
+
         if ( this.getConfig() != null )
         if ( this.getConfig() != null )
         {
         {
             final Map<AppProperty, String> nonDefaultProperties = getConfig().readAllNonDefaultAppProperties();
             final Map<AppProperty, String> nonDefaultProperties = getConfig().readAllNonDefaultAppProperties();
@@ -511,6 +495,31 @@ public class PwmApplication
         }
         }
     }
     }
 
 
+    private static void outputConfigurationToLog( final PwmApplication pwmApplication )
+            throws PwmUnrecoverableException
+    {
+        if ( !LOGGER.isEnabled( PwmLogLevel.TRACE ) )
+        {
+            return;
+        }
+
+        final StoredConfiguration storedConfiguration = pwmApplication.getConfig().getStoredConfiguration();
+        final Map<String, String> debugStrings = StoredConfigurationUtil.makeDebugMap( storedConfiguration, storedConfiguration.modifiedItems(), PwmConstants.DEFAULT_LOCALE );
+        final List<Supplier<CharSequence>> outputStrings = new ArrayList<>();
+
+        for ( final Map.Entry<String, String> entry : debugStrings.entrySet() )
+        {
+            final String spacedValue = entry.getValue().replace( "\n", "\n   " );
+            final String output = " " + entry.getKey() + "\n   " + spacedValue + "\n";
+            outputStrings.add( () -> output );
+        }
+
+        LOGGER.trace( () -> "--begin current configuration output--" );
+        outputStrings.forEach( LOGGER::trace );
+        LOGGER.trace( () -> "--end current configuration output--" );
+    }
+
+
     public String getInstanceID( )
     public String getInstanceID( )
     {
     {
         return instanceID;
         return instanceID;

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

@@ -23,7 +23,6 @@ package password.pwm;
 import org.apache.commons.csv.CSVFormat;
 import org.apache.commons.csv.CSVFormat;
 import password.pwm.util.java.JsonUtil;
 import password.pwm.util.java.JsonUtil;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.java.StringUtil;
-import password.pwm.util.secure.PwmHashAlgorithm;
 
 
 import java.io.InputStream;
 import java.io.InputStream;
 import java.net.URL;
 import java.net.URL;
@@ -101,6 +100,7 @@ public abstract class PwmConstants
 
 
     public static final String LDAP_AD_PASSWORD_POLICY_CONTROL_ASN = "1.2.840.113556.1.4.2066";
     public static final String LDAP_AD_PASSWORD_POLICY_CONTROL_ASN = "1.2.840.113556.1.4.2066";
     public static final String PROFILE_ID_ALL = "all";
     public static final String PROFILE_ID_ALL = "all";
+    public static final String PROFILE_ID_DEFAULT = "default";
 
 
     public static final String TOKEN_KEY_PWD_CHG_DATE = "_lastPwdChange";
     public static final String TOKEN_KEY_PWD_CHG_DATE = "_lastPwdChange";
 
 
@@ -118,9 +118,6 @@ public abstract class PwmConstants
     public static final String REQUEST_ATTR_FORGOTTEN_PW_AVAIL_TOKEN_DEST_CACHE = "ForgottenPw-AvailableTokenDestCache";
     public static final String REQUEST_ATTR_FORGOTTEN_PW_AVAIL_TOKEN_DEST_CACHE = "ForgottenPw-AvailableTokenDestCache";
     public static final String REQUEST_ATTR_PWM_APPLICATION = "PwmApplication";
     public static final String REQUEST_ATTR_PWM_APPLICATION = "PwmApplication";
 
 
-    public static final PwmHashAlgorithm SETTING_CHECKSUM_HASH_METHOD = PwmHashAlgorithm.SHA256;
-
-
     public static final String LOG_REMOVED_VALUE_REPLACEMENT = readPwmConstantsBundle( "log.removedValue" );
     public static final String LOG_REMOVED_VALUE_REPLACEMENT = readPwmConstantsBundle( "log.removedValue" );
 
 
     public static final Collection<Locale> INCLUDED_LOCALES;
     public static final Collection<Locale> INCLUDED_LOCALES;

+ 4 - 3
server/src/main/java/password/pwm/PwmEnvironment.java

@@ -74,7 +74,8 @@ public class PwmEnvironment
         AppliancePort,
         AppliancePort,
         ApplianceHostnameFile,
         ApplianceHostnameFile,
         ApplianceTokenFile,
         ApplianceTokenFile,
-        InstanceID,;
+        InstanceID,
+        InitConsoleLogLevel,;
 
 
         public static ApplicationParameter forString( final String input )
         public static ApplicationParameter forString( final String input )
         {
         {
@@ -317,7 +318,7 @@ public class PwmEnvironment
             final String rawValue = readValueFromSystem( EnvironmentParameter.applicationParamFile, contextName );
             final String rawValue = readValueFromSystem( EnvironmentParameter.applicationParamFile, contextName );
             if ( rawValue != null )
             if ( rawValue != null )
             {
             {
-                return parseApplicationParamValueParameter( rawValue );
+                return readAppParametersFromPath( rawValue );
             }
             }
             return Collections.emptyMap();
             return Collections.emptyMap();
         }
         }
@@ -393,7 +394,7 @@ public class PwmEnvironment
             return returnFlags;
             return returnFlags;
         }
         }
 
 
-        public static Map<ApplicationParameter, String> parseApplicationParamValueParameter( final String input )
+        public static Map<ApplicationParameter, String> readAppParametersFromPath( final String input )
         {
         {
             if ( input == null )
             if ( input == null )
             {
             {

+ 5 - 19
server/src/main/java/password/pwm/bean/PrivateKeyCertificate.java

@@ -20,30 +20,16 @@
 
 
 package password.pwm.bean;
 package password.pwm.bean;
 
 
+import lombok.Value;
+
 import java.io.Serializable;
 import java.io.Serializable;
 import java.security.PrivateKey;
 import java.security.PrivateKey;
 import java.security.cert.X509Certificate;
 import java.security.cert.X509Certificate;
-import java.util.Collections;
 import java.util.List;
 import java.util.List;
 
 
+@Value
 public class PrivateKeyCertificate implements Serializable
 public class PrivateKeyCertificate implements Serializable
 {
 {
-    private final List<X509Certificate> certificates;
-    private final PrivateKey key;
-
-    public PrivateKeyCertificate( final List<X509Certificate> certificates, final PrivateKey key )
-    {
-        this.certificates = Collections.unmodifiableList( certificates );
-        this.key = key;
-    }
-
-    public List<X509Certificate> getCertificates( )
-    {
-        return Collections.unmodifiableList( certificates );
-    }
-
-    public PrivateKey getKey( )
-    {
-        return key;
-    }
+    private List<X509Certificate> certificates;
+    private PrivateKey key;
 }
 }

+ 57 - 87
server/src/main/java/password/pwm/config/Configuration.java

@@ -45,8 +45,8 @@ import password.pwm.config.profile.PwmPasswordPolicy;
 import password.pwm.config.profile.PwmPasswordRule;
 import password.pwm.config.profile.PwmPasswordRule;
 import password.pwm.config.profile.SetupOtpProfile;
 import password.pwm.config.profile.SetupOtpProfile;
 import password.pwm.config.profile.UpdateProfileProfile;
 import password.pwm.config.profile.UpdateProfileProfile;
-import password.pwm.config.stored.ConfigurationProperty;
-import password.pwm.config.stored.StoredConfigurationImpl;
+import password.pwm.config.stored.StoredConfigItemKey;
+import password.pwm.config.stored.StoredConfiguration;
 import password.pwm.config.stored.StoredConfigurationUtil;
 import password.pwm.config.stored.StoredConfigurationUtil;
 import password.pwm.config.value.BooleanValue;
 import password.pwm.config.value.BooleanValue;
 import password.pwm.config.value.CustomLinkValue;
 import password.pwm.config.value.CustomLinkValue;
@@ -70,6 +70,7 @@ import password.pwm.config.value.data.UserPermission;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.i18n.PwmLocaleBundle;
 import password.pwm.util.PasswordData;
 import password.pwm.util.PasswordData;
 import password.pwm.util.i18n.LocaleHelper;
 import password.pwm.util.i18n.LocaleHelper;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.java.StringUtil;
@@ -77,8 +78,8 @@ import password.pwm.util.logging.PwmLogLevel;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.secure.PwmRandom;
 import password.pwm.util.secure.PwmRandom;
 import password.pwm.util.secure.PwmSecurityKey;
 import password.pwm.util.secure.PwmSecurityKey;
+import password.pwm.util.secure.SecureService;
 
 
-import java.io.Serializable;
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.InvocationTargetException;
 import java.security.cert.X509Certificate;
 import java.security.cert.X509Certificate;
 import java.util.ArrayList;
 import java.util.ArrayList;
@@ -95,7 +96,6 @@ import java.util.Map;
 import java.util.Optional;
 import java.util.Optional;
 import java.util.Set;
 import java.util.Set;
 import java.util.TreeMap;
 import java.util.TreeMap;
-import java.util.function.Supplier;
 
 
 /**
 /**
  * @author Jason D. Rivard
  * @author Jason D. Rivard
@@ -104,13 +104,11 @@ public class Configuration implements SettingReader
 {
 {
     private static final PwmLogger LOGGER = PwmLogger.forClass( Configuration.class );
     private static final PwmLogger LOGGER = PwmLogger.forClass( Configuration.class );
 
 
-    private final StoredConfigurationImpl storedConfiguration;
+    private final StoredConfiguration storedConfiguration;
 
 
     private DataCache dataCache = new DataCache();
     private DataCache dataCache = new DataCache();
 
 
-    private String cashedConfigurationHash;
-
-    public Configuration( final StoredConfigurationImpl storedConfiguration )
+    public Configuration( final StoredConfiguration storedConfiguration )
     {
     {
         this.storedConfiguration = storedConfiguration;
         this.storedConfiguration = storedConfiguration;
     }
     }
@@ -125,28 +123,6 @@ public class Configuration implements SettingReader
         }
         }
     }
     }
 
 
-    public void outputToLog( )
-    {
-        if ( !LOGGER.isEnabled( PwmLogLevel.TRACE ) )
-        {
-            return;
-        }
-
-        final Map<String, String> debugStrings = storedConfiguration.getModifiedSettingDebugValues( PwmConstants.DEFAULT_LOCALE, true );
-        final List<Supplier<CharSequence>> outputStrings = new ArrayList<>();
-
-        for ( final Map.Entry<String, String> entry : debugStrings.entrySet() )
-        {
-            final String spacedValue = entry.getValue().replace( "\n", "\n   " );
-            final String output = " " + entry.getKey() + "\n   " + spacedValue + "\n";
-            outputStrings.add( () -> output );
-        }
-
-        LOGGER.trace( () -> "--begin current configuration output--" );
-        outputStrings.forEach( LOGGER::trace );
-        LOGGER.trace( () -> "--end current configuration output--" );
-    }
-
     public List<FormConfiguration> readSettingAsForm( final PwmSetting setting )
     public List<FormConfiguration> readSettingAsForm( final PwmSetting setting )
     {
     {
         final StoredValue value = readStoredValue( setting );
         final StoredValue value = readStoredValue( setting );
@@ -516,7 +492,7 @@ public class Configuration implements SettingReader
         }
         }
     }
     }
 
 
-    public Map<Locale, String> readLocalizedBundle( final String className, final String keyName )
+    public Map<Locale, String> readLocalizedBundle( final PwmLocaleBundle className, final String keyName )
     {
     {
         final String key = className + "-" + keyName;
         final String key = className + "-" + keyName;
         if ( dataCache.customText.containsKey( key ) )
         if ( dataCache.customText.containsKey( key ) )
@@ -643,7 +619,7 @@ public class Configuration implements SettingReader
 
 
         // set case sensitivity
         // set case sensitivity
         final String caseSensitivitySetting = JavaTypeConverter.valueToString( storedConfiguration.readSetting(
         final String caseSensitivitySetting = JavaTypeConverter.valueToString( storedConfiguration.readSetting(
-                PwmSetting.PASSWORD_POLICY_CASE_SENSITIVITY ) );
+                PwmSetting.PASSWORD_POLICY_CASE_SENSITIVITY, null ) );
         if ( !"read".equals( caseSensitivitySetting ) )
         if ( !"read".equals( caseSensitivitySetting ) )
         {
         {
             passwordPolicySettings.put( PwmPasswordRule.CaseSensitive.getKey(), caseSensitivitySetting );
             passwordPolicySettings.put( PwmPasswordRule.CaseSensitive.getKey(), caseSensitivitySetting );
@@ -674,7 +650,7 @@ public class Configuration implements SettingReader
 
 
     public boolean isDefaultValue( final PwmSetting pwmSetting )
     public boolean isDefaultValue( final PwmSetting pwmSetting )
     {
     {
-        return storedConfiguration.isDefaultValue( pwmSetting );
+        return storedConfiguration.isDefaultValue( pwmSetting, null );
     }
     }
 
 
     public Collection<Locale> localesForSetting( final PwmSetting setting )
     public Collection<Locale> localesForSetting( final PwmSetting setting )
@@ -705,11 +681,6 @@ public class Configuration implements SettingReader
         return returnCollection;
         return returnCollection;
     }
     }
 
 
-    public String readProperty( final ConfigurationProperty key )
-    {
-        return storedConfiguration.readConfigProperty( key );
-    }
-
     public boolean readSettingAsBoolean( final PwmSetting setting )
     public boolean readSettingAsBoolean( final PwmSetting setting )
     {
     {
         return JavaTypeConverter.valueToBoolean( readStoredValue( setting ) );
         return JavaTypeConverter.valueToBoolean( readStoredValue( setting ) );
@@ -717,7 +688,7 @@ public class Configuration implements SettingReader
 
 
     public Map<FileValue.FileInformation, FileValue.FileContent> readSettingAsFile( final PwmSetting setting )
     public Map<FileValue.FileInformation, FileValue.FileContent> readSettingAsFile( final PwmSetting setting )
     {
     {
-        final FileValue fileValue = ( FileValue ) storedConfiguration.readSetting( setting );
+        final FileValue fileValue = ( FileValue ) storedConfiguration.readSetting( setting, null );
         return ( Map ) fileValue.toNativeObject();
         return ( Map ) fileValue.toNativeObject();
     }
     }
 
 
@@ -748,48 +719,50 @@ public class Configuration implements SettingReader
         return ( PrivateKeyCertificate ) readStoredValue( setting ).toNativeObject();
         return ( PrivateKeyCertificate ) readStoredValue( setting ).toNativeObject();
     }
     }
 
 
-    public String getNotes( )
-    {
-        return storedConfiguration.readConfigProperty( ConfigurationProperty.NOTES );
-    }
-
     private PwmSecurityKey tempInstanceKey = null;
     private PwmSecurityKey tempInstanceKey = null;
 
 
     public PwmSecurityKey getSecurityKey( ) throws PwmUnrecoverableException
     public PwmSecurityKey getSecurityKey( ) throws PwmUnrecoverableException
     {
     {
-        final PasswordData configValue = readSettingAsPassword( PwmSetting.PWM_SECURITY_KEY );
-
-        if ( configValue == null || configValue.getStringValue().isEmpty() )
+        if ( dataCache.pwmSecurityKey == null )
         {
         {
-            final String errorMsg = "Security Key value is not configured,will generate temp value for use by runtime instance";
-            final ErrorInformation errorInfo = new ErrorInformation( PwmError.ERROR_INVALID_SECURITY_KEY, errorMsg );
-            LOGGER.warn( errorInfo.toDebugStr() );
-            if ( tempInstanceKey == null )
+            final PasswordData configValue = readSettingAsPassword( PwmSetting.PWM_SECURITY_KEY );
+
+            if ( configValue == null || configValue.getStringValue().isEmpty() )
             {
             {
-                tempInstanceKey = new PwmSecurityKey( PwmRandom.getInstance().alphaNumericString( 256 ) );
+                final String errorMsg = "Security Key value is not configured, will generate temp value for use by runtime instance";
+                final ErrorInformation errorInfo = new ErrorInformation( PwmError.ERROR_INVALID_SECURITY_KEY, errorMsg );
+                LOGGER.warn( errorInfo.toDebugStr() );
+                if ( tempInstanceKey == null )
+                {
+                    tempInstanceKey = new PwmSecurityKey( PwmRandom.getInstance().alphaNumericString( 1024 ) );
+                }
+                dataCache.pwmSecurityKey = tempInstanceKey;
             }
             }
-            return tempInstanceKey;
-        }
+            else
+            {
+                final int minSecurityKeyLength = Integer.parseInt( readAppProperty( AppProperty.SECURITY_CONFIG_MIN_SECURITY_KEY_LENGTH ) );
+                if ( configValue.getStringValue().length() < minSecurityKeyLength )
+                {
+                    final String errorMsg = "Security Key must be greater than 32 characters in length";
+                    final ErrorInformation errorInfo = new ErrorInformation( PwmError.ERROR_INVALID_SECURITY_KEY, errorMsg );
+                    throw new PwmUnrecoverableException( errorInfo );
+                }
 
 
-        final int minSecurityKeyLength = Integer.parseInt( readAppProperty( AppProperty.SECURITY_CONFIG_MIN_SECURITY_KEY_LENGTH ) );
-        if ( configValue.getStringValue().length() < minSecurityKeyLength )
-        {
-            final String errorMsg = "Security Key must be greater than 32 characters in length";
-            final ErrorInformation errorInfo = new ErrorInformation( PwmError.ERROR_INVALID_SECURITY_KEY, errorMsg );
-            throw new PwmUnrecoverableException( errorInfo );
+                try
+                {
+                    dataCache.pwmSecurityKey = new PwmSecurityKey( configValue.getStringValue() );
+                }
+                catch ( Exception e )
+                {
+                    final String errorMsg = "unexpected error generating Security Key crypto: " + e.getMessage();
+                    final ErrorInformation errorInfo = new ErrorInformation( PwmError.ERROR_INVALID_SECURITY_KEY, errorMsg );
+                    LOGGER.error( errorInfo.toDebugStr(), e );
+                    throw new PwmUnrecoverableException( errorInfo );
+                }
+            }
         }
         }
 
 
-        try
-        {
-            return new PwmSecurityKey( configValue.getStringValue() );
-        }
-        catch ( Exception e )
-        {
-            final String errorMsg = "unexpected error generating Security Key crypto: " + e.getMessage();
-            final ErrorInformation errorInfo = new ErrorInformation( PwmError.ERROR_INVALID_SECURITY_KEY, errorMsg );
-            LOGGER.error( errorInfo.toDebugStr(), e );
-            throw new PwmUnrecoverableException( errorInfo );
-        }
+        return dataCache.pwmSecurityKey;
     }
     }
 
 
     public List<DataStorageMethod> getResponseStorageLocations( final PwmSetting setting )
     public List<DataStorageMethod> getResponseStorageLocations( final PwmSetting setting )
@@ -1012,12 +985,12 @@ public class Configuration implements SettingReader
             return dataCache.settings.get( setting );
             return dataCache.settings.get( setting );
         }
         }
 
 
-        final StoredValue readValue = storedConfiguration.readSetting( setting );
+        final StoredValue readValue = storedConfiguration.readSetting( setting, null );
         dataCache.settings.put( setting, readValue );
         dataCache.settings.put( setting, readValue );
         return readValue;
         return readValue;
     }
     }
 
 
-    private static class DataCache implements Serializable
+    private static class DataCache
     {
     {
         private final Map<String, Map<Locale, PwmPasswordPolicy>> cachedPasswordPolicy = new LinkedHashMap<>();
         private final Map<String, Map<Locale, PwmPasswordPolicy>> cachedPasswordPolicy = new LinkedHashMap<>();
         private Map<Locale, String> localeFlagMap = null;
         private Map<Locale, String> localeFlagMap = null;
@@ -1025,6 +998,7 @@ public class Configuration implements SettingReader
         private final Map<String, Map<Locale, String>> customText = new LinkedHashMap<>();
         private final Map<String, Map<Locale, String>> customText = new LinkedHashMap<>();
         private final Map<ProfileDefinition, Map> profileCache = new LinkedHashMap<>();
         private final Map<ProfileDefinition, Map> profileCache = new LinkedHashMap<>();
         private Map<String, String> appPropertyOverrides = null;
         private Map<String, String> appPropertyOverrides = null;
+        private PwmSecurityKey pwmSecurityKey;
     }
     }
 
 
     public Map<AppProperty, String> readAllNonDefaultAppProperties( )
     public Map<AppProperty, String> readAllNonDefaultAppProperties( )
@@ -1125,11 +1099,9 @@ public class Configuration implements SettingReader
         return profileFactory.makeFromStoredConfiguration( storedConfiguration, profileID );
         return profileFactory.makeFromStoredConfiguration( storedConfiguration, profileID );
     }
     }
 
 
-    public StoredConfigurationImpl getStoredConfiguration( ) throws PwmUnrecoverableException
+    public StoredConfiguration getStoredConfiguration( )
     {
     {
-        final StoredConfigurationImpl copiedStoredConfiguration = StoredConfigurationImpl.copy( storedConfiguration );
-        copiedStoredConfiguration.lock();
-        return copiedStoredConfiguration;
+        return this.storedConfiguration;
     }
     }
 
 
     public boolean isDevDebugMode( )
     public boolean isDevDebugMode( )
@@ -1137,22 +1109,21 @@ public class Configuration implements SettingReader
         return Boolean.parseBoolean( readAppProperty( AppProperty.LOGGING_DEV_OUTPUT ) );
         return Boolean.parseBoolean( readAppProperty( AppProperty.LOGGING_DEV_OUTPUT ) );
     }
     }
 
 
-    public String configurationHash( )
+    public String configurationHash( final SecureService secureService )
             throws PwmUnrecoverableException
             throws PwmUnrecoverableException
     {
     {
-        if ( this.cashedConfigurationHash == null )
-        {
-            this.cashedConfigurationHash = storedConfiguration.settingChecksum();
-        }
-        return cashedConfigurationHash;
+        return storedConfiguration.valueHash();
     }
     }
 
 
     public Set<PwmSetting> nonDefaultSettings( )
     public Set<PwmSetting> nonDefaultSettings( )
     {
     {
-        final Set returnSet = new LinkedHashSet();
-        for ( final StoredConfigurationImpl.SettingValueRecord valueRecord : this.storedConfiguration.modifiedSettings() )
+        final Set<PwmSetting> returnSet = new LinkedHashSet<>();
+        for ( final StoredConfigItemKey key : this.storedConfiguration.modifiedItems() )
         {
         {
-            returnSet.add( valueRecord.getSetting() );
+            if ( key.getRecordType() == StoredConfigItemKey.RecordType.SETTING )
+            {
+                returnSet.add( key.toPwmSetting() );
+            }
         }
         }
         return returnSet;
         return returnSet;
     }
     }
@@ -1163,7 +1134,6 @@ public class Configuration implements SettingReader
         return mode == null
         return mode == null
                 ? CertificateMatchingMode.CA_ONLY
                 ? CertificateMatchingMode.CA_ONLY
                 : mode;
                 : mode;
-
     }
     }
 
 
     public Optional<PeopleSearchProfile> getPublicPeopleSearchProfile()
     public Optional<PeopleSearchProfile> getPublicPeopleSearchProfile()

+ 22 - 9
server/src/main/java/password/pwm/config/PwmSetting.java

@@ -38,10 +38,13 @@ import java.util.Collection;
 import java.util.Collections;
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.HashSet;
 import java.util.LinkedHashMap;
 import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.List;
 import java.util.Locale;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Map;
+import java.util.Optional;
 import java.util.Set;
 import java.util.Set;
+import java.util.TreeMap;
 import java.util.function.Supplier;
 import java.util.function.Supplier;
 import java.util.regex.Pattern;
 import java.util.regex.Pattern;
 import java.util.regex.PatternSyntaxException;
 import java.util.regex.PatternSyntaxException;
@@ -1467,6 +1470,16 @@ public enum PwmSetting
         private final Set<PwmSettingTemplate> settingTemplates;
         private final Set<PwmSettingTemplate> settingTemplates;
     }
     }
 
 
+    public static Set<PwmSetting> sortedByMenuLocation( final Locale locale )
+    {
+        final TreeMap<String, PwmSetting> treeMap = new TreeMap<>();
+        for ( final PwmSetting pwmSetting : PwmSetting.values() )
+        {
+            treeMap.put( pwmSetting.toMenuLocationDebug( null, locale ), pwmSetting );
+        }
+        return Collections.unmodifiableSet( new LinkedHashSet<>( treeMap.values() ) );
+    }
+
     private static TemplateSetAssociation associationForTempleSet(
     private static TemplateSetAssociation associationForTempleSet(
             final List<TemplateSetAssociation> associationSets,
             final List<TemplateSetAssociation> associationSets,
             final PwmSettingTemplateSet pwmSettingTemplate
             final PwmSettingTemplateSet pwmSettingTemplate
@@ -1529,10 +1542,10 @@ public enum PwmSetting
         {
         {
             final Map<String, String> returnList = new LinkedHashMap<>();
             final Map<String, String> returnList = new LinkedHashMap<>();
             final XmlElement settingElement = PwmSettingXml.readSettingXml( pwmSetting );
             final XmlElement settingElement = PwmSettingXml.readSettingXml( pwmSetting );
-            final XmlElement optionsElement = settingElement.getChild( PwmSettingXml.XML_ELEMENT_OPTIONS );
-            if ( optionsElement != null )
+            final Optional<XmlElement> optionsElement = settingElement.getChild( PwmSettingXml.XML_ELEMENT_OPTIONS );
+            if ( optionsElement.isPresent() )
             {
             {
-                final List<XmlElement> optionElements = optionsElement.getChildren( PwmSettingXml.XML_ELEMENT_OPTION );
+                final List<XmlElement> optionElements = optionsElement.get().getChildren( PwmSettingXml.XML_ELEMENT_OPTION );
                 if ( optionElements != null )
                 if ( optionElements != null )
                 {
                 {
                     for ( final XmlElement optionElement : optionElements )
                     for ( final XmlElement optionElement : optionElements )
@@ -1601,10 +1614,10 @@ public enum PwmSetting
         {
         {
             final Map<PwmSettingProperty, String> newProps = new LinkedHashMap<>();
             final Map<PwmSettingProperty, String> newProps = new LinkedHashMap<>();
             final XmlElement settingElement = PwmSettingXml.readSettingXml( pwmSetting );
             final XmlElement settingElement = PwmSettingXml.readSettingXml( pwmSetting );
-            final XmlElement propertiesElement = settingElement.getChild( PwmSettingXml.XML_ELEMENT_PROPERTIES );
-            if ( propertiesElement != null )
+            final Optional<XmlElement> propertiesElement = settingElement.getChild( PwmSettingXml.XML_ELEMENT_PROPERTIES );
+            if ( propertiesElement.isPresent() )
             {
             {
-                final List<XmlElement> propertyElements = propertiesElement.getChildren( PwmSettingXml.XML_ELEMENT_PROPERTY );
+                final List<XmlElement> propertyElements = propertiesElement.get().getChildren( PwmSettingXml.XML_ELEMENT_PROPERTY );
                 if ( propertyElements != null )
                 if ( propertyElements != null )
                 {
                 {
                     for ( final XmlElement propertyElement : propertyElements )
                     for ( final XmlElement propertyElement : propertyElements )
@@ -1678,12 +1691,12 @@ public enum PwmSetting
         private static Pattern readPattern( final PwmSetting pwmSetting )
         private static Pattern readPattern( final PwmSetting pwmSetting )
         {
         {
             final XmlElement settingNode = PwmSettingXml.readSettingXml( pwmSetting );
             final XmlElement settingNode = PwmSettingXml.readSettingXml( pwmSetting );
-            final XmlElement regexNode = settingNode.getChild( PwmSettingXml.XML_ELEMENT_REGEX );
-            if ( regexNode != null )
+            final Optional<XmlElement> regexNode = settingNode.getChild( PwmSettingXml.XML_ELEMENT_REGEX );
+            if ( regexNode.isPresent() )
             {
             {
                 try
                 try
                 {
                 {
-                    return Pattern.compile( regexNode.getText() );
+                    return Pattern.compile( regexNode.get().getText() );
                 }
                 }
                 catch ( final PatternSyntaxException e )
                 catch ( final PatternSyntaxException e )
                 {
                 {

+ 4 - 3
server/src/main/java/password/pwm/config/PwmSettingCategory.java

@@ -32,6 +32,7 @@ import java.util.Iterator;
 import java.util.List;
 import java.util.List;
 import java.util.Locale;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Map;
+import java.util.Optional;
 import java.util.TreeMap;
 import java.util.TreeMap;
 import java.util.function.Supplier;
 import java.util.function.Supplier;
 
 
@@ -322,10 +323,10 @@ public enum PwmSettingCategory
         while ( nextCategory != null )
         while ( nextCategory != null )
         {
         {
             final XmlElement categoryElement = PwmSettingXml.readCategoryXml( nextCategory );
             final XmlElement categoryElement = PwmSettingXml.readCategoryXml( nextCategory );
-            final XmlElement profileElement = categoryElement.getChild( "profile" );
-            if ( profileElement != null )
+            final Optional<XmlElement> profileElement = categoryElement.getChild( "profile" );
+            if ( profileElement.isPresent() )
             {
             {
-                final String settingKey = profileElement.getAttributeValue( "setting" );
+                final String settingKey = profileElement.get().getAttributeValue( "setting" );
                 if ( settingKey != null )
                 if ( settingKey != null )
                 {
                 {
                     return password.pwm.config.PwmSetting.forKey( settingKey );
                     return password.pwm.config.PwmSetting.forKey( settingKey );

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

@@ -54,4 +54,5 @@ public enum PwmSettingFlag
 
 
     WebService_NoBody,
     WebService_NoBody,
 
 
+    Deprecated,
 }
 }

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

@@ -22,7 +22,6 @@ package password.pwm.config;
 
 
 public enum PwmSettingProperty
 public enum PwmSettingProperty
 {
 {
-
     ModificationWarning,
     ModificationWarning,
 
 
     Minimum,
     Minimum,
@@ -36,4 +35,6 @@ public enum PwmSettingProperty
     Cert_ImportHandler,
     Cert_ImportHandler,
 
 
     MethodType,
     MethodType,
+
+    Restart_Requirements,
 }
 }

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

@@ -83,7 +83,7 @@ public enum PwmSettingSyntax
         this.storedValueImpl = storedValueImpl;
         this.storedValueImpl = storedValueImpl;
     }
     }
 
 
-    public StoredValue.StoredValueFactory getStoredValueImpl( )
+    public StoredValue.StoredValueFactory getFactory( )
     {
     {
         return storedValueImpl;
         return storedValueImpl;
     }
     }

+ 19 - 24
server/src/main/java/password/pwm/config/PwmSettingXml.java

@@ -22,6 +22,7 @@ package password.pwm.config;
 
 
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.JavaHelper;
+import password.pwm.util.java.LazySoftReference;
 import password.pwm.util.java.TimeDuration;
 import password.pwm.util.java.TimeDuration;
 import password.pwm.util.java.XmlDocument;
 import password.pwm.util.java.XmlDocument;
 import password.pwm.util.java.XmlElement;
 import password.pwm.util.java.XmlElement;
@@ -35,14 +36,13 @@ import javax.xml.validation.SchemaFactory;
 import javax.xml.validation.Validator;
 import javax.xml.validation.Validator;
 import java.io.IOException;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStream;
-import java.lang.ref.WeakReference;
 import java.time.Instant;
 import java.time.Instant;
 import java.util.Collections;
 import java.util.Collections;
 import java.util.LinkedHashSet;
 import java.util.LinkedHashSet;
 import java.util.Set;
 import java.util.Set;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.concurrent.atomic.AtomicInteger;
 
 
-class PwmSettingXml
+public class PwmSettingXml
 {
 {
     public static final String SETTING_XML_FILENAME = ( PwmSetting.class.getPackage().getName()
     public static final String SETTING_XML_FILENAME = ( PwmSetting.class.getPackage().getName()
             + "." + PwmSetting.class.getSimpleName() ).replace( ".", "/" ) + ".xml";
             + "." + PwmSetting.class.getSimpleName() ).replace( ".", "/" ) + ".xml";
@@ -68,31 +68,23 @@ class PwmSettingXml
 
 
     private static final PwmLogger LOGGER = PwmLogger.forClass( PwmSettingXml.class );
     private static final PwmLogger LOGGER = PwmLogger.forClass( PwmSettingXml.class );
 
 
-    private static WeakReference<XmlDocument> xmlDocCache = new WeakReference<>( null );
+    private static LazySoftReference<XmlDocument> xmlDocCache = new LazySoftReference<>( () -> readXml() );
     private static final AtomicInteger LOAD_COUNTER = new AtomicInteger( 0 );
     private static final AtomicInteger LOAD_COUNTER = new AtomicInteger( 0 );
 
 
     private static XmlDocument readXml( )
     private static XmlDocument readXml( )
     {
     {
-        final XmlDocument docRefCopy = xmlDocCache.get();
-        if ( docRefCopy == null )
+        try ( InputStream inputStream = PwmSetting.class.getClassLoader().getResourceAsStream( SETTING_XML_FILENAME ) )
         {
         {
-            try ( InputStream inputStream = PwmSetting.class.getClassLoader().getResourceAsStream( SETTING_XML_FILENAME ) )
-            {
-                final Instant startTime = Instant.now();
-                final XmlDocument newDoc = XmlFactory.getFactory().parseXml( inputStream );
-                final TimeDuration parseDuration = TimeDuration.fromCurrent( startTime );
-                LOGGER.trace( () -> "parsed PwmSettingXml in " + parseDuration.asCompactString() + ", loads=" + LOAD_COUNTER.getAndIncrement() );
-
-                xmlDocCache = new WeakReference<>( newDoc );
-
-                return newDoc;
-            }
-            catch ( final IOException | PwmUnrecoverableException e )
-            {
-                throw new IllegalStateException( "error parsing " + SETTING_XML_FILENAME + ": " + e.getMessage() );
-            }
+            final Instant startTime = Instant.now();
+            final XmlDocument newDoc = XmlFactory.getFactory().parseXml( inputStream );
+            final TimeDuration parseDuration = TimeDuration.fromCurrent( startTime );
+            LOGGER.trace( () -> "parsed PwmSettingXml in " + parseDuration.asCompactString() + ", loads=" + LOAD_COUNTER.getAndIncrement() );
+            return newDoc;
+        }
+        catch ( IOException | PwmUnrecoverableException e )
+        {
+            throw new IllegalStateException( "error parsing " + SETTING_XML_FILENAME + ": " + e.getMessage() );
         }
         }
-        return docRefCopy;
     }
     }
 
 
     private static void validateXmlSchema( )
     private static void validateXmlSchema( )
@@ -115,19 +107,22 @@ class PwmSettingXml
     static XmlElement readSettingXml( final PwmSetting setting )
     static XmlElement readSettingXml( final PwmSetting setting )
     {
     {
         final String expression = "/settings/setting[@key=\"" + setting.getKey() + "\"]";
         final String expression = "/settings/setting[@key=\"" + setting.getKey() + "\"]";
-        return readXml().evaluateXpathToElement( expression );
+        return xmlDocCache.get().evaluateXpathToElement( expression )
+                .orElseThrow( () -> new IllegalStateException( "PwmSetting.xml is missing setting for key '" + setting.getKey() + "'" ) );
     }
     }
 
 
     static XmlElement readCategoryXml( final PwmSettingCategory category )
     static XmlElement readCategoryXml( final PwmSettingCategory category )
     {
     {
         final String expression = "/settings/category[@key=\"" + category.toString() + "\"]";
         final String expression = "/settings/category[@key=\"" + category.toString() + "\"]";
-        return readXml().evaluateXpathToElement( expression );
+        return xmlDocCache.get().evaluateXpathToElement( expression )
+                .orElseThrow( () -> new IllegalStateException( "PwmSetting.xml is missing category for key '" + category.getKey() + "'" ) );
     }
     }
 
 
     static XmlElement readTemplateXml( final PwmSettingTemplate template )
     static XmlElement readTemplateXml( final PwmSettingTemplate template )
     {
     {
         final String expression = "/settings/template[@key=\"" + template.toString() + "\"]";
         final String expression = "/settings/template[@key=\"" + template.toString() + "\"]";
-        return readXml().evaluateXpathToElement( expression );
+        return xmlDocCache.get().evaluateXpathToElement( expression )
+                .orElseThrow( () -> new IllegalStateException( "PwmSetting.xml is missing template for key '" + template.toString() + "'" ) );
     }
     }
 
 
     static Set<PwmSettingTemplate> parseTemplateAttribute( final XmlElement element )
     static Set<PwmSettingTemplate> parseTemplateAttribute( final XmlElement element )

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

@@ -20,7 +20,7 @@
 
 
 package password.pwm.config;
 package password.pwm.config;
 
 
-import password.pwm.config.stored.StoredConfigurationImpl;
+import password.pwm.config.stored.StoredConfigurationModifier;
 import password.pwm.http.PwmRequest;
 import password.pwm.http.PwmRequest;
 
 
 import java.io.Serializable;
 import java.io.Serializable;
@@ -29,7 +29,7 @@ public interface SettingUIFunction
 {
 {
     Serializable provideFunction(
     Serializable provideFunction(
             PwmRequest pwmRequest,
             PwmRequest pwmRequest,
-            StoredConfigurationImpl storedConfiguration,
+            StoredConfigurationModifier modifier,
             PwmSetting setting,
             PwmSetting setting,
             String profile,
             String profile,
             String extraData
             String extraData

+ 3 - 5
server/src/main/java/password/pwm/config/StoredValue.java

@@ -20,8 +20,8 @@
 
 
 package password.pwm.config;
 package password.pwm.config;
 
 
+import password.pwm.config.stored.XmlOutputProcessData;
 import password.pwm.error.PwmException;
 import password.pwm.error.PwmException;
-import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.util.java.XmlElement;
 import password.pwm.util.java.XmlElement;
 import password.pwm.util.secure.PwmSecurityKey;
 import password.pwm.util.secure.PwmSecurityKey;
 
 
@@ -31,7 +31,7 @@ import java.util.Locale;
 
 
 public interface StoredValue extends Serializable
 public interface StoredValue extends Serializable
 {
 {
-    List<XmlElement> toXmlValues( String valueElementName, PwmSecurityKey pwmSecurityKey );
+    List<XmlElement> toXmlValues( String valueElementName, XmlOutputProcessData xmlOutputProcessData );
 
 
     Object toNativeObject( );
     Object toNativeObject( );
 
 
@@ -41,8 +41,6 @@ public interface StoredValue extends Serializable
 
 
     String toDebugString( Locale locale );
     String toDebugString( Locale locale );
 
 
-    boolean requiresStoredUpdate( );
-
     int currentSyntaxVersion( );
     int currentSyntaxVersion( );
 
 
     interface StoredValueFactory
     interface StoredValueFactory
@@ -53,5 +51,5 @@ public interface StoredValue extends Serializable
                 throws PwmException;
                 throws PwmException;
     }
     }
 
 
-    String valueHash( ) throws PwmUnrecoverableException;
+    String valueHash();
 }
 }

+ 14 - 8
server/src/main/java/password/pwm/config/function/AbstractUriCertImportFunction.java

@@ -24,7 +24,7 @@ import password.pwm.bean.UserIdentity;
 import password.pwm.config.Configuration;
 import password.pwm.config.Configuration;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.SettingUIFunction;
 import password.pwm.config.SettingUIFunction;
-import password.pwm.config.stored.StoredConfigurationImpl;
+import password.pwm.config.stored.StoredConfigurationModifier;
 import password.pwm.config.value.X509CertificateValue;
 import password.pwm.config.value.X509CertificateValue;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmError;
@@ -45,7 +45,7 @@ abstract class AbstractUriCertImportFunction implements SettingUIFunction
     @Override
     @Override
     public String provideFunction(
     public String provideFunction(
             final PwmRequest pwmRequest,
             final PwmRequest pwmRequest,
-            final StoredConfigurationImpl storedConfiguration,
+            final StoredConfigurationModifier modifier,
             final PwmSetting setting,
             final PwmSetting setting,
             final String profile,
             final String profile,
             final String extraData )
             final String extraData )
@@ -54,17 +54,17 @@ abstract class AbstractUriCertImportFunction implements SettingUIFunction
         final PwmSession pwmSession = pwmRequest.getPwmSession();
         final PwmSession pwmSession = pwmRequest.getPwmSession();
         final List<X509Certificate> certs;
         final List<X509Certificate> certs;
 
 
-        final String urlString = getUri( storedConfiguration, setting, profile, extraData );
+        final String urlString = getUri( modifier, setting, profile, extraData );
         try
         try
         {
         {
             final URI uri = URI.create( urlString );
             final URI uri = URI.create( urlString );
             if ( "https".equalsIgnoreCase( uri.getScheme() ) )
             if ( "https".equalsIgnoreCase( uri.getScheme() ) )
             {
             {
-                certs = X509Utils.readRemoteHttpCertificates( pwmRequest.getPwmApplication(), pwmSession.getLabel(), uri, new Configuration( storedConfiguration ) );
+                certs = X509Utils.readRemoteHttpCertificates( pwmRequest.getPwmApplication(), pwmSession.getLabel(), uri, new Configuration( modifier.newStoredConfiguration() ) );
             }
             }
             else
             else
             {
             {
-                final Configuration configuration = new Configuration( storedConfiguration );
+                final Configuration configuration = new Configuration( modifier.newStoredConfiguration() );
                 certs = X509Utils.readRemoteCertificates( URI.create( urlString ), configuration );
                 certs = X509Utils.readRemoteCertificates( URI.create( urlString ), configuration );
             }
             }
         }
         }
@@ -80,7 +80,7 @@ abstract class AbstractUriCertImportFunction implements SettingUIFunction
 
 
 
 
         final UserIdentity userIdentity = pwmSession.isAuthenticated() ? pwmSession.getUserInfo().getUserIdentity() : null;
         final UserIdentity userIdentity = pwmSession.isAuthenticated() ? pwmSession.getUserInfo().getUserIdentity() : null;
-        store( certs, storedConfiguration, setting, profile, extraData, userIdentity );
+        store( certs, modifier, setting, profile, extraData, userIdentity );
 
 
         final StringBuffer returnStr = new StringBuffer();
         final StringBuffer returnStr = new StringBuffer();
         for ( final X509Certificate loopCert : certs )
         for ( final X509Certificate loopCert : certs )
@@ -91,12 +91,18 @@ abstract class AbstractUriCertImportFunction implements SettingUIFunction
         return returnStr.toString();
         return returnStr.toString();
     }
     }
 
 
-    abstract String getUri( StoredConfigurationImpl storedConfiguration, PwmSetting pwmSetting, String profile, String extraData ) throws PwmOperationalException;
+    abstract String getUri(
+            StoredConfigurationModifier modifier,
+            PwmSetting pwmSetting,
+            String profile,
+            String extraData
+    )
+            throws PwmOperationalException, PwmUnrecoverableException;
 
 
 
 
     void store(
     void store(
             final List<X509Certificate> certs,
             final List<X509Certificate> certs,
-            final StoredConfigurationImpl storedConfiguration,
+            final StoredConfigurationModifier storedConfiguration,
             final PwmSetting pwmSetting,
             final PwmSetting pwmSetting,
             final String profile,
             final String profile,
             final String extraData,
             final String extraData,

+ 12 - 6
server/src/main/java/password/pwm/config/function/ActionCertImportFunction.java

@@ -23,7 +23,7 @@ package password.pwm.config.function;
 import com.google.gson.reflect.TypeToken;
 import com.google.gson.reflect.TypeToken;
 import password.pwm.bean.UserIdentity;
 import password.pwm.bean.UserIdentity;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.PwmSetting;
-import password.pwm.config.stored.StoredConfigurationImpl;
+import password.pwm.config.stored.StoredConfigurationModifier;
 import password.pwm.config.value.ActionValue;
 import password.pwm.config.value.ActionValue;
 import password.pwm.config.value.data.ActionConfiguration;
 import password.pwm.config.value.data.ActionConfiguration;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.ErrorInformation;
@@ -42,14 +42,20 @@ public class ActionCertImportFunction extends AbstractUriCertImportFunction
     private static final String KEY_ITERATION = "iteration";
     private static final String KEY_ITERATION = "iteration";
     private static final String KEY_WEB_ACTION_ITERATION = "webActionIter";
     private static final String KEY_WEB_ACTION_ITERATION = "webActionIter";
 
 
-            @Override
-    String getUri( final StoredConfigurationImpl storedConfiguration, final PwmSetting pwmSetting, final String profile, final String extraData ) throws PwmOperationalException
+    @Override
+    String getUri(
+            final StoredConfigurationModifier modifier,
+            final PwmSetting pwmSetting,
+            final String profile,
+            final String extraData
+    )
+            throws PwmOperationalException, PwmUnrecoverableException
     {
     {
         final Map<String, Integer> extraDataMap = JsonUtil.deserialize( extraData, new TypeToken<Map<String, Integer>>()
         final Map<String, Integer> extraDataMap = JsonUtil.deserialize( extraData, new TypeToken<Map<String, Integer>>()
         {
         {
         } );
         } );
 
 
-        final ActionValue actionValue = ( ActionValue ) storedConfiguration.readSetting( pwmSetting, profile );
+        final ActionValue actionValue = ( ActionValue ) modifier.newStoredConfiguration().readSetting( pwmSetting, profile );
         final ActionConfiguration action = ( actionValue.toNativeObject() ).get( extraDataMap.get( KEY_ITERATION ) );
         final ActionConfiguration action = ( actionValue.toNativeObject() ).get( extraDataMap.get( KEY_ITERATION ) );
         final ActionConfiguration.WebAction webAction = action.getWebActions().get( extraDataMap.get( KEY_WEB_ACTION_ITERATION ) );
         final ActionConfiguration.WebAction webAction = action.getWebActions().get( extraDataMap.get( KEY_WEB_ACTION_ITERATION ) );
 
 
@@ -79,7 +85,7 @@ public class ActionCertImportFunction extends AbstractUriCertImportFunction
 
 
     void store(
     void store(
             final List<X509Certificate> certs,
             final List<X509Certificate> certs,
-            final StoredConfigurationImpl storedConfiguration,
+            final StoredConfigurationModifier storedConfiguration,
             final PwmSetting pwmSetting,
             final PwmSetting pwmSetting,
             final String profile,
             final String profile,
             final String extraData,
             final String extraData,
@@ -91,7 +97,7 @@ public class ActionCertImportFunction extends AbstractUriCertImportFunction
         {
         {
         } );
         } );
 
 
-        final ActionValue actionValue = ( ActionValue ) storedConfiguration.readSetting( pwmSetting, profile );
+        final ActionValue actionValue = ( ActionValue ) storedConfiguration.newStoredConfiguration().readSetting( pwmSetting, profile );
         final List<ActionConfiguration> actionConfigurations = actionValue.toNativeObject();
         final List<ActionConfiguration> actionConfigurations = actionValue.toNativeObject();
         final ActionConfiguration action = actionConfigurations.get( extraDataMap.get( KEY_ITERATION ) );
         final ActionConfiguration action = actionConfigurations.get( extraDataMap.get( KEY_ITERATION ) );
         final ActionConfiguration.WebAction webAction = action.getWebActions().get( extraDataMap.get( KEY_WEB_ACTION_ITERATION ) );
         final ActionConfiguration.WebAction webAction = action.getWebActions().get( extraDataMap.get( KEY_WEB_ACTION_ITERATION ) );

+ 4 - 4
server/src/main/java/password/pwm/config/function/LdapCertImportFunction.java

@@ -24,7 +24,7 @@ import password.pwm.PwmApplication;
 import password.pwm.bean.UserIdentity;
 import password.pwm.bean.UserIdentity;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.SettingUIFunction;
 import password.pwm.config.SettingUIFunction;
-import password.pwm.config.stored.StoredConfigurationImpl;
+import password.pwm.config.stored.StoredConfigurationModifier;
 import password.pwm.config.value.StringArrayValue;
 import password.pwm.config.value.StringArrayValue;
 import password.pwm.config.value.X509CertificateValue;
 import password.pwm.config.value.X509CertificateValue;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.error.PwmUnrecoverableException;
@@ -43,7 +43,7 @@ public class LdapCertImportFunction implements SettingUIFunction
     @Override
     @Override
     public String provideFunction(
     public String provideFunction(
             final PwmRequest pwmRequest,
             final PwmRequest pwmRequest,
-            final StoredConfigurationImpl storedConfiguration,
+            final StoredConfigurationModifier modifier,
             final PwmSetting setting,
             final PwmSetting setting,
             final String profile,
             final String profile,
             final String extraData
             final String extraData
@@ -53,7 +53,7 @@ public class LdapCertImportFunction implements SettingUIFunction
         final PwmApplication pwmApplication = pwmRequest.getPwmApplication();
         final PwmApplication pwmApplication = pwmRequest.getPwmApplication();
         final PwmSession pwmSession = pwmRequest.getPwmSession();
         final PwmSession pwmSession = pwmRequest.getPwmSession();
 
 
-        final StringArrayValue ldapUrlsValue = ( StringArrayValue ) storedConfiguration.readSetting( PwmSetting.LDAP_SERVER_URLS, profile );
+        final StringArrayValue ldapUrlsValue = ( StringArrayValue ) modifier.newStoredConfiguration().readSetting( PwmSetting.LDAP_SERVER_URLS, profile );
         final Set<X509Certificate> resultCertificates = new LinkedHashSet<>();
         final Set<X509Certificate> resultCertificates = new LinkedHashSet<>();
         if ( ldapUrlsValue != null && ldapUrlsValue.toNativeObject() != null )
         if ( ldapUrlsValue != null && ldapUrlsValue.toNativeObject() != null )
         {
         {
@@ -62,7 +62,7 @@ public class LdapCertImportFunction implements SettingUIFunction
         }
         }
 
 
         final UserIdentity userIdentity = pwmSession.isAuthenticated() ? pwmSession.getUserInfo().getUserIdentity() : null;
         final UserIdentity userIdentity = pwmSession.isAuthenticated() ? pwmSession.getUserInfo().getUserIdentity() : null;
-        storedConfiguration.writeSetting( setting, profile, new X509CertificateValue( resultCertificates ), userIdentity );
+        modifier.writeSetting( setting, profile, new X509CertificateValue( resultCertificates ), userIdentity );
         return Message.getLocalizedMessage( pwmSession.getSessionStateBean().getLocale(), Message.Success_Unknown, pwmApplication.getConfig() );
         return Message.getLocalizedMessage( pwmSession.getSessionStateBean().getLocale(), Message.Success_Unknown, pwmApplication.getConfig() );
     }
     }
 
 

+ 6 - 4
server/src/main/java/password/pwm/config/function/OAuthCertImportFunction.java

@@ -22,10 +22,11 @@ package password.pwm.config.function;
 
 
 import password.pwm.PwmConstants;
 import password.pwm.PwmConstants;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.PwmSetting;
-import password.pwm.config.stored.StoredConfigurationImpl;
+import password.pwm.config.stored.StoredConfigurationModifier;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmOperationalException;
 import password.pwm.error.PwmOperationalException;
+import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.JavaHelper;
 
 
 import java.net.URI;
 import java.net.URI;
@@ -35,7 +36,8 @@ public class OAuthCertImportFunction extends AbstractUriCertImportFunction
 
 
 
 
     @Override
     @Override
-    String getUri( final StoredConfigurationImpl storedConfiguration, final PwmSetting pwmSetting, final String profile, final String extraData ) throws PwmOperationalException
+    String getUri( final StoredConfigurationModifier modifier, final PwmSetting pwmSetting, final String profile, final String extraData )
+            throws PwmOperationalException, PwmUnrecoverableException
     {
     {
 
 
         final String uriString;
         final String uriString;
@@ -44,12 +46,12 @@ public class OAuthCertImportFunction extends AbstractUriCertImportFunction
         switch ( pwmSetting )
         switch ( pwmSetting )
         {
         {
             case OAUTH_ID_CERTIFICATE:
             case OAUTH_ID_CERTIFICATE:
-                uriString = ( String ) storedConfiguration.readSetting( PwmSetting.OAUTH_ID_CODERESOLVE_URL ).toNativeObject();
+                uriString = ( String ) modifier.newStoredConfiguration().readSetting( PwmSetting.OAUTH_ID_CODERESOLVE_URL, null ).toNativeObject();
                 menuDebugLocation = PwmSetting.OAUTH_ID_CODERESOLVE_URL.toMenuLocationDebug( null, PwmConstants.DEFAULT_LOCALE );
                 menuDebugLocation = PwmSetting.OAUTH_ID_CODERESOLVE_URL.toMenuLocationDebug( null, PwmConstants.DEFAULT_LOCALE );
                 break;
                 break;
 
 
             case RECOVERY_OAUTH_ID_CERTIFICATE:
             case RECOVERY_OAUTH_ID_CERTIFICATE:
-                uriString = ( String ) storedConfiguration.readSetting( PwmSetting.RECOVERY_OAUTH_ID_CODERESOLVE_URL, profile ).toNativeObject();
+                uriString = ( String ) modifier.newStoredConfiguration().readSetting( PwmSetting.RECOVERY_OAUTH_ID_CODERESOLVE_URL, profile ).toNativeObject();
                 menuDebugLocation = PwmSetting.RECOVERY_OAUTH_ID_CERTIFICATE.toMenuLocationDebug( profile, PwmConstants.DEFAULT_LOCALE );
                 menuDebugLocation = PwmSetting.RECOVERY_OAUTH_ID_CERTIFICATE.toMenuLocationDebug( profile, PwmConstants.DEFAULT_LOCALE );
                 break;
                 break;
 
 

+ 11 - 10
server/src/main/java/password/pwm/config/function/RemoteWebServiceCertImportFunction.java

@@ -22,14 +22,13 @@ package password.pwm.config.function;
 
 
 import password.pwm.bean.UserIdentity;
 import password.pwm.bean.UserIdentity;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.PwmSetting;
-import password.pwm.config.stored.StoredConfigurationImpl;
+import password.pwm.config.stored.StoredConfigurationModifier;
 import password.pwm.config.value.RemoteWebServiceValue;
 import password.pwm.config.value.RemoteWebServiceValue;
 import password.pwm.config.value.data.RemoteWebServiceConfiguration;
 import password.pwm.config.value.data.RemoteWebServiceConfiguration;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmOperationalException;
 import password.pwm.error.PwmOperationalException;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.error.PwmUnrecoverableException;
-import password.pwm.util.java.JsonUtil;
 
 
 import java.net.URI;
 import java.net.URI;
 import java.security.cert.X509Certificate;
 import java.security.cert.X509Certificate;
@@ -40,9 +39,10 @@ public class RemoteWebServiceCertImportFunction extends AbstractUriCertImportFun
 {
 {
 
 
     @Override
     @Override
-    String getUri( final StoredConfigurationImpl storedConfiguration, final PwmSetting pwmSetting, final String profile, final String extraData ) throws PwmOperationalException
+    String getUri( final StoredConfigurationModifier modifier, final PwmSetting pwmSetting, final String profile, final String extraData )
+            throws PwmOperationalException, PwmUnrecoverableException
     {
     {
-        final RemoteWebServiceValue actionValue = ( RemoteWebServiceValue ) storedConfiguration.readSetting( pwmSetting, profile );
+        final RemoteWebServiceValue actionValue = ( RemoteWebServiceValue ) modifier.newStoredConfiguration().readSetting( pwmSetting, profile );
         final String serviceName = actionNameFromExtraData( extraData );
         final String serviceName = actionNameFromExtraData( extraData );
         final RemoteWebServiceConfiguration action = actionValue.forName( serviceName );
         final RemoteWebServiceConfiguration action = actionValue.forName( serviceName );
         final String uriString = action.getUrl();
         final String uriString = action.getUrl();
@@ -73,7 +73,7 @@ public class RemoteWebServiceCertImportFunction extends AbstractUriCertImportFun
 
 
     void store(
     void store(
             final List<X509Certificate> certs,
             final List<X509Certificate> certs,
-            final StoredConfigurationImpl storedConfiguration,
+            final StoredConfigurationModifier modifier,
             final PwmSetting pwmSetting,
             final PwmSetting pwmSetting,
             final String profile,
             final String profile,
             final String extraData,
             final String extraData,
@@ -81,24 +81,25 @@ public class RemoteWebServiceCertImportFunction extends AbstractUriCertImportFun
     )
     )
             throws PwmOperationalException, PwmUnrecoverableException
             throws PwmOperationalException, PwmUnrecoverableException
     {
     {
-        final RemoteWebServiceValue actionValue = ( RemoteWebServiceValue ) storedConfiguration.readSetting( pwmSetting, profile );
+        final RemoteWebServiceValue actionValue = ( RemoteWebServiceValue ) modifier.newStoredConfiguration().readSetting( pwmSetting, profile );
         final String actionName = actionNameFromExtraData( extraData );
         final String actionName = actionNameFromExtraData( extraData );
         final List<RemoteWebServiceConfiguration> newList = new ArrayList<>();
         final List<RemoteWebServiceConfiguration> newList = new ArrayList<>();
         for ( final RemoteWebServiceConfiguration loopConfiguration : actionValue.toNativeObject() )
         for ( final RemoteWebServiceConfiguration loopConfiguration : actionValue.toNativeObject() )
         {
         {
             if ( actionName.equals( loopConfiguration.getName() ) )
             if ( actionName.equals( loopConfiguration.getName() ) )
             {
             {
-                final RemoteWebServiceConfiguration newConfig = JsonUtil.cloneUsingJson( loopConfiguration, RemoteWebServiceConfiguration.class );
-                newConfig.setCertificates( certs );
+                final RemoteWebServiceConfiguration newConfig = loopConfiguration.toBuilder()
+                        .certificates( certs )
+                        .build();
                 newList.add( newConfig );
                 newList.add( newConfig );
             }
             }
             else
             else
             {
             {
-                newList.add( JsonUtil.cloneUsingJson( loopConfiguration, RemoteWebServiceConfiguration.class ) );
+                newList.add( loopConfiguration );
             }
             }
         }
         }
         final RemoteWebServiceValue newActionValue = new RemoteWebServiceValue( newList );
         final RemoteWebServiceValue newActionValue = new RemoteWebServiceValue( newList );
-        storedConfiguration.writeSetting( pwmSetting, profile, newActionValue, userIdentity );
+        modifier.writeSetting( pwmSetting, profile, newActionValue, userIdentity );
     }
     }
 
 
 }
 }

+ 5 - 7
server/src/main/java/password/pwm/config/function/SMSGatewayCertImportFunction.java

@@ -22,26 +22,24 @@ package password.pwm.config.function;
 
 
 import password.pwm.PwmConstants;
 import password.pwm.PwmConstants;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.PwmSetting;
-import password.pwm.config.stored.StoredConfigurationImpl;
+import password.pwm.config.stored.StoredConfigurationModifier;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmOperationalException;
 import password.pwm.error.PwmOperationalException;
+import password.pwm.error.PwmUnrecoverableException;
 
 
 import java.net.URI;
 import java.net.URI;
 
 
 public class SMSGatewayCertImportFunction extends AbstractUriCertImportFunction
 public class SMSGatewayCertImportFunction extends AbstractUriCertImportFunction
 {
 {
-
-
     @Override
     @Override
-    String getUri( final StoredConfigurationImpl storedConfiguration, final PwmSetting pwmSetting, final String profile, final String extraData ) throws PwmOperationalException
+    String getUri( final StoredConfigurationModifier modifier, final PwmSetting pwmSetting, final String profile, final String extraData )
+            throws PwmOperationalException, PwmUnrecoverableException
     {
     {
-
-
         final String uriString;
         final String uriString;
         final String menuDebugLocation;
         final String menuDebugLocation;
 
 
-        uriString = ( String ) storedConfiguration.readSetting( PwmSetting.SMS_GATEWAY_URL ).toNativeObject();
+        uriString = ( String ) modifier.newStoredConfiguration().readSetting( PwmSetting.SMS_GATEWAY_URL, null ).toNativeObject();
         menuDebugLocation = PwmSetting.SMS_GATEWAY_URL.toMenuLocationDebug( null, PwmConstants.DEFAULT_LOCALE );
         menuDebugLocation = PwmSetting.SMS_GATEWAY_URL.toMenuLocationDebug( null, PwmConstants.DEFAULT_LOCALE );
 
 
         if ( uriString.isEmpty() )
         if ( uriString.isEmpty() )

+ 4 - 4
server/src/main/java/password/pwm/config/function/SmtpCertImportFunction.java

@@ -24,7 +24,7 @@ import password.pwm.bean.UserIdentity;
 import password.pwm.config.Configuration;
 import password.pwm.config.Configuration;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.SettingUIFunction;
 import password.pwm.config.SettingUIFunction;
-import password.pwm.config.stored.StoredConfigurationImpl;
+import password.pwm.config.stored.StoredConfigurationModifier;
 import password.pwm.config.value.X509CertificateValue;
 import password.pwm.config.value.X509CertificateValue;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.http.PwmRequest;
 import password.pwm.http.PwmRequest;
@@ -41,7 +41,7 @@ public class SmtpCertImportFunction implements SettingUIFunction
     @Override
     @Override
     public String provideFunction(
     public String provideFunction(
             final PwmRequest pwmRequest,
             final PwmRequest pwmRequest,
-            final StoredConfigurationImpl storedConfiguration,
+            final StoredConfigurationModifier modifier,
             final PwmSetting setting,
             final PwmSetting setting,
             final String profile,
             final String profile,
             final String extraData
             final String extraData
@@ -50,12 +50,12 @@ public class SmtpCertImportFunction implements SettingUIFunction
     {
     {
         final PwmSession pwmSession = pwmRequest.getPwmSession();
         final PwmSession pwmSession = pwmRequest.getPwmSession();
 
 
-        final Configuration configuration = new Configuration( storedConfiguration );
+        final Configuration configuration = new Configuration( modifier.newStoredConfiguration() );
         final List<X509Certificate> certs = EmailServerUtil.readCertificates( configuration, profile );
         final List<X509Certificate> certs = EmailServerUtil.readCertificates( configuration, profile );
         if ( !JavaHelper.isEmpty( certs ) )
         if ( !JavaHelper.isEmpty( certs ) )
         {
         {
             final UserIdentity userIdentity = pwmSession.isAuthenticated() ? pwmSession.getUserInfo().getUserIdentity() : null;
             final UserIdentity userIdentity = pwmSession.isAuthenticated() ? pwmSession.getUserInfo().getUserIdentity() : null;
-            storedConfiguration.writeSetting( PwmSetting.EMAIL_SERVER_CERTS, profile, new X509CertificateValue( certs ), userIdentity );
+            modifier.writeSetting( PwmSetting.EMAIL_SERVER_CERTS, profile, new X509CertificateValue( certs ), userIdentity );
         }
         }
 
 
         return Message.getLocalizedMessage( pwmSession.getSessionStateBean().getLocale(), Message.Success_Unknown, pwmRequest.getConfig() );
         return Message.getLocalizedMessage( pwmSession.getSessionStateBean().getLocale(), Message.Success_Unknown, pwmRequest.getConfig() );

+ 5 - 5
server/src/main/java/password/pwm/config/function/SyslogCertImportFunction.java

@@ -25,7 +25,7 @@ import password.pwm.bean.UserIdentity;
 import password.pwm.config.Configuration;
 import password.pwm.config.Configuration;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.SettingUIFunction;
 import password.pwm.config.SettingUIFunction;
-import password.pwm.config.stored.StoredConfigurationImpl;
+import password.pwm.config.stored.StoredConfigurationModifier;
 import password.pwm.config.value.X509CertificateValue;
 import password.pwm.config.value.X509CertificateValue;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmError;
@@ -49,7 +49,7 @@ public class SyslogCertImportFunction implements SettingUIFunction
     @Override
     @Override
     public String provideFunction(
     public String provideFunction(
             final PwmRequest pwmRequest,
             final PwmRequest pwmRequest,
-            final StoredConfigurationImpl storedConfiguration,
+            final StoredConfigurationModifier modifier,
             final PwmSetting setting,
             final PwmSetting setting,
             final String profile,
             final String profile,
             final String extraData )
             final String extraData )
@@ -62,7 +62,7 @@ public class SyslogCertImportFunction implements SettingUIFunction
 
 
         final Set<X509Certificate> resultCertificates = new LinkedHashSet<>();
         final Set<X509Certificate> resultCertificates = new LinkedHashSet<>();
 
 
-        final List<String> syslogConfigStrs = ( List<String> ) storedConfiguration.readSetting( PwmSetting.AUDIT_SYSLOG_SERVERS ).toNativeObject();
+        final List<String> syslogConfigStrs = ( List<String> ) modifier.newStoredConfiguration().readSetting( PwmSetting.AUDIT_SYSLOG_SERVERS, null ).toNativeObject();
         if ( syslogConfigStrs != null && !syslogConfigStrs.isEmpty() )
         if ( syslogConfigStrs != null && !syslogConfigStrs.isEmpty() )
         {
         {
             for ( String entry : syslogConfigStrs )
             for ( String entry : syslogConfigStrs )
@@ -77,7 +77,7 @@ public class SyslogCertImportFunction implements SettingUIFunction
                             final List<X509Certificate> certs = X509Utils.readRemoteCertificates(
                             final List<X509Certificate> certs = X509Utils.readRemoteCertificates(
                                     syslogConfig.getHost(),
                                     syslogConfig.getHost(),
                                     syslogConfig.getPort(),
                                     syslogConfig.getPort(),
-                                    new Configuration( storedConfiguration )
+                                    new Configuration( modifier.newStoredConfiguration() )
                             );
                             );
                             if ( certs != null )
                             if ( certs != null )
                             {
                             {
@@ -98,7 +98,7 @@ public class SyslogCertImportFunction implements SettingUIFunction
         if ( !error )
         if ( !error )
         {
         {
             final UserIdentity userIdentity = pwmSession.isAuthenticated() ? pwmSession.getUserInfo().getUserIdentity() : null;
             final UserIdentity userIdentity = pwmSession.isAuthenticated() ? pwmSession.getUserInfo().getUserIdentity() : null;
-            storedConfiguration.writeSetting( setting, new X509CertificateValue( resultCertificates ), userIdentity );
+            modifier.writeSetting( setting, null, new X509CertificateValue( resultCertificates ), userIdentity );
             return Message.getLocalizedMessage( pwmSession.getSessionStateBean().getLocale(), Message.Success_Unknown, pwmApplication.getConfig() );
             return Message.getLocalizedMessage( pwmSession.getSessionStateBean().getLocale(), Message.Success_Unknown, pwmApplication.getConfig() );
         }
         }
         else
         else

+ 5 - 4
server/src/main/java/password/pwm/config/function/UserMatchViewerFunction.java

@@ -31,7 +31,8 @@ import password.pwm.bean.UserIdentity;
 import password.pwm.config.Configuration;
 import password.pwm.config.Configuration;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.SettingUIFunction;
 import password.pwm.config.SettingUIFunction;
-import password.pwm.config.stored.StoredConfigurationImpl;
+import password.pwm.config.stored.StoredConfiguration;
+import password.pwm.config.stored.StoredConfigurationModifier;
 import password.pwm.config.value.data.UserPermission;
 import password.pwm.config.value.data.UserPermission;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmError;
@@ -58,7 +59,7 @@ public class UserMatchViewerFunction implements SettingUIFunction
     @Override
     @Override
     public Serializable provideFunction(
     public Serializable provideFunction(
             final PwmRequest pwmRequest,
             final PwmRequest pwmRequest,
-            final StoredConfigurationImpl storedConfiguration,
+            final StoredConfigurationModifier storedConfiguration,
             final PwmSetting setting,
             final PwmSetting setting,
             final String profile,
             final String profile,
             final String extraData )
             final String extraData )
@@ -68,7 +69,7 @@ public class UserMatchViewerFunction implements SettingUIFunction
 
 
         final Instant startSearchTime = Instant.now();
         final Instant startSearchTime = Instant.now();
         final int maxResultSize = Integer.parseInt( pwmApplication.getConfig().readAppProperty( AppProperty.CONFIG_EDITOR_QUERY_FILTER_TEST_LIMIT ) );
         final int maxResultSize = Integer.parseInt( pwmApplication.getConfig().readAppProperty( AppProperty.CONFIG_EDITOR_QUERY_FILTER_TEST_LIMIT ) );
-        final Collection<UserIdentity> users = discoverMatchingUsers( pwmApplication, maxResultSize, storedConfiguration, setting, profile );
+        final Collection<UserIdentity> users = discoverMatchingUsers( pwmApplication, maxResultSize, storedConfiguration.newStoredConfiguration(), setting, profile );
         final TimeDuration searchDuration = TimeDuration.fromCurrent( startSearchTime );
         final TimeDuration searchDuration = TimeDuration.fromCurrent( startSearchTime );
 
 
         final UserMatchViewerResults userMatchViewerResults = new UserMatchViewerResults();
         final UserMatchViewerResults userMatchViewerResults = new UserMatchViewerResults();
@@ -88,7 +89,7 @@ public class UserMatchViewerFunction implements SettingUIFunction
     public Collection<UserIdentity> discoverMatchingUsers(
     public Collection<UserIdentity> discoverMatchingUsers(
             final PwmApplication pwmApplication,
             final PwmApplication pwmApplication,
             final int maxResultSize,
             final int maxResultSize,
-            final StoredConfigurationImpl storedConfiguration,
+            final StoredConfiguration storedConfiguration,
             final PwmSetting setting,
             final PwmSetting setting,
             final String profile
             final String profile
     )
     )

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

@@ -59,7 +59,7 @@ public class PwmPasswordPolicy implements Profile, Serializable
 
 
     private static final PwmPasswordPolicy DEFAULT_POLICY;
     private static final PwmPasswordPolicy DEFAULT_POLICY;
 
 
-    private final Map<String, String> policyMap = new HashMap<>();
+    private final Map<String, String> policyMap;
 
 
     private final transient ChaiPasswordPolicy chaiPasswordPolicy;
     private final transient ChaiPasswordPolicy chaiPasswordPolicy;
 
 
@@ -130,19 +130,20 @@ public class PwmPasswordPolicy implements Profile, Serializable
             final PolicyMetaData policyMetaData
             final PolicyMetaData policyMetaData
     )
     )
     {
     {
+        final Map<String, String> effectivePolicyMap = new HashMap<>();
         if ( policyMap != null )
         if ( policyMap != null )
         {
         {
-            this.policyMap.putAll( policyMap );
+            effectivePolicyMap.putAll( policyMap );
         }
         }
         if ( chaiPasswordPolicy != null )
         if ( chaiPasswordPolicy != null )
         {
         {
             if ( Boolean.parseBoolean( chaiPasswordPolicy.getValue( ChaiPasswordRule.ADComplexity ) ) )
             if ( Boolean.parseBoolean( chaiPasswordPolicy.getValue( ChaiPasswordRule.ADComplexity ) ) )
             {
             {
-                this.policyMap.put( PwmPasswordRule.ADComplexityLevel.getKey(), ADPolicyComplexity.AD2003.toString() );
+                effectivePolicyMap.put( PwmPasswordRule.ADComplexityLevel.getKey(), ADPolicyComplexity.AD2003.toString() );
             }
             }
             else if ( Boolean.parseBoolean( chaiPasswordPolicy.getValue( ChaiPasswordRule.ADComplexity2008 ) ) )
             else if ( Boolean.parseBoolean( chaiPasswordPolicy.getValue( ChaiPasswordRule.ADComplexity2008 ) ) )
             {
             {
-                this.policyMap.put( PwmPasswordRule.ADComplexityLevel.getKey(), ADPolicyComplexity.AD2008.toString() );
+                effectivePolicyMap.put( PwmPasswordRule.ADComplexityLevel.getKey(), ADPolicyComplexity.AD2008.toString() );
             }
             }
         }
         }
         this.chaiPasswordPolicy = chaiPasswordPolicy;
         this.chaiPasswordPolicy = chaiPasswordPolicy;
@@ -152,6 +153,8 @@ public class PwmPasswordPolicy implements Profile, Serializable
             this.userPermissions = policyMetaData.getUserPermissions();
             this.userPermissions = policyMetaData.getUserPermissions();
             this.profileID = policyMetaData.getProfileID();
             this.profileID = policyMetaData.getProfileID();
         }
         }
+
+        this.policyMap = Collections.unmodifiableMap( effectivePolicyMap );
     }
     }
 
 
     @Override
     @Override

+ 0 - 150
server/src/main/java/password/pwm/config/stored/ConfigChangeLogImpl.java

@@ -1,150 +0,0 @@
-/*
- * Password Management Servlets (PWM)
- * http://www.pwm-project.org
- *
- * Copyright (c) 2006-2009 Novell, Inc.
- * Copyright (c) 2009-2019 The PWM Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package password.pwm.config.stored;
-
-import password.pwm.config.PwmSetting;
-import password.pwm.config.StoredValue;
-import password.pwm.util.java.StringUtil;
-
-import java.io.Serializable;
-import java.util.Collection;
-import java.util.LinkedHashMap;
-import java.util.Locale;
-import java.util.Map;
-import java.util.TreeMap;
-
-public class ConfigChangeLogImpl implements Serializable, ConfigChangeLog
-{
-    private final Map<StoredConfigReference, StoredValue> changeLog = new LinkedHashMap<>();
-    private final Map<StoredConfigReference, StoredValue> originalValue = new LinkedHashMap<>();
-    private final transient StorageEngine storedConfiguration;
-
-    public ConfigChangeLogImpl( final StorageEngine storageEngine )
-    {
-        this.storedConfiguration = storageEngine;
-    }
-
-    @Override
-    public boolean isModified( )
-    {
-        return !changeLog.isEmpty();
-    }
-
-    @Override
-    public String changeLogAsDebugString( final Locale locale, final boolean asHtml )
-    {
-        final Map<String, String> outputMap = new TreeMap<>();
-
-        for ( final StoredConfigReference configReference : changeLog.keySet() )
-        {
-            switch ( configReference.getRecordType() )
-            {
-                case SETTING:
-                {
-                    final PwmSetting pwmSetting = PwmSetting.forKey( configReference.getRecordID() );
-                    final StoredValue currentValue = storedConfiguration.read( configReference );
-                    final String keyName = pwmSetting.toMenuLocationDebug( configReference.getProfileID(), locale );
-                    final String debugValue = currentValue.toDebugString( locale );
-                    outputMap.put( keyName, debugValue );
-                }
-                break;
-
-                /*
-                case LOCALE_BUNDLE: {
-                    final String SEPARATOR = LocaleHelper.getLocalizedMessage(locale, Config.Display_SettingNavigationSeparator, null);
-                    final String key = (String) configReference.recordID;
-                    final String bundleName = key.split("!")[0];
-                    final String keys = key.split("!")[1];
-                    final Map<String,String> currentValue = readLocaleBundleMap(bundleName,keys);
-                    final String debugValue = JsonUtil.serializeMap(currentValue, JsonUtil.Flag.PrettyPrint);
-                    outputMap.put("LocaleBundle" + SEPARATOR + bundleName + " " + keys,debugValue);
-                }
-                break;
-                */
-
-                default:
-                    //continue
-                    break;
-            }
-        }
-        final StringBuilder output = new StringBuilder();
-        if ( outputMap.isEmpty() )
-        {
-            output.append( "No setting changes." );
-        }
-        else
-        {
-            for ( final Map.Entry<String, String> entry : outputMap.entrySet() )
-            {
-                final String keyName = entry.getKey();
-                final String value = entry.getValue();
-                if ( asHtml )
-                {
-                    output.append( "<div class=\"changeLogKey\">" );
-                    output.append( keyName );
-                    output.append( "</div><div class=\"changeLogValue\">" );
-                    output.append( StringUtil.escapeHtml( value ) );
-                    output.append( "</div>" );
-                }
-                else
-                {
-                    output.append( keyName );
-                    output.append( "\n" );
-                    output.append( " Value: " );
-                    output.append( value );
-                    output.append( "\n" );
-                }
-            }
-        }
-        return output.toString();
-    }
-
-    @Override
-    public void updateChangeLog( final StoredConfigReference reference, final StoredValue newValue )
-    {
-        changeLog.put( reference, newValue );
-        originalValue.put( reference, null );
-    }
-
-    @Override
-    public void updateChangeLog( final StoredConfigReference reference, final StoredValue currentValue, final StoredValue newValue )
-    {
-        if ( originalValue.containsKey( reference ) )
-        {
-            if ( newValue.equals( originalValue.get( reference ) ) )
-            {
-                originalValue.remove( reference );
-                changeLog.remove( reference );
-            }
-        }
-        else
-        {
-            originalValue.put( reference, currentValue );
-            changeLog.put( reference, newValue );
-        }
-    }
-
-    @Override
-    public Collection<StoredConfigReference> changedValues( )
-    {
-        return changeLog.keySet();
-    }
-}

+ 2 - 15
server/src/main/java/password/pwm/config/stored/StoredConfigReference.java → server/src/main/java/password/pwm/config/stored/ConfigRestartRequirement.java

@@ -20,20 +20,7 @@
 
 
 package password.pwm.config.stored;
 package password.pwm.config.stored;
 
 
-import java.io.Serializable;
-
-public interface StoredConfigReference extends Serializable, Comparable
+public enum ConfigRestartRequirement
 {
 {
-    RecordType getRecordType( );
-
-    String getRecordID( );
-
-    String getProfileID( );
-
-    enum RecordType
-    {
-        SETTING,
-        LOCALE_BUNDLE,
-        PROPERTY,
-    }
+    Application
 }
 }

+ 258 - 200
server/src/main/java/password/pwm/config/stored/ConfigurationCleaner.java

@@ -22,106 +22,130 @@ package password.pwm.config.stored;
 
 
 import password.pwm.PwmConstants;
 import password.pwm.PwmConstants;
 import password.pwm.bean.UserIdentity;
 import password.pwm.bean.UserIdentity;
+import password.pwm.config.Configuration;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.StoredValue;
 import password.pwm.config.StoredValue;
 import password.pwm.config.option.ADPolicyComplexity;
 import password.pwm.config.option.ADPolicyComplexity;
+import password.pwm.config.option.RecoveryMinLifetimeOption;
 import password.pwm.config.option.WebServiceUsage;
 import password.pwm.config.option.WebServiceUsage;
 import password.pwm.config.value.OptionListValue;
 import password.pwm.config.value.OptionListValue;
+import password.pwm.config.value.StoredValueEncoder;
 import password.pwm.config.value.StringArrayValue;
 import password.pwm.config.value.StringArrayValue;
 import password.pwm.config.value.StringValue;
 import password.pwm.config.value.StringValue;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.util.java.JavaHelper;
+import password.pwm.util.java.PwmExceptionLoggingConsumer;
+import password.pwm.util.java.StringUtil;
 import password.pwm.util.java.XmlDocument;
 import password.pwm.util.java.XmlDocument;
 import password.pwm.util.java.XmlElement;
 import password.pwm.util.java.XmlElement;
+import password.pwm.util.java.XmlFactory;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.logging.PwmLogger;
+import password.pwm.util.secure.PwmSecurityKey;
 
 
 import java.util.ArrayList;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.Collections;
 import java.util.LinkedHashSet;
 import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.List;
+import java.util.Optional;
 import java.util.Set;
 import java.util.Set;
 
 
 class ConfigurationCleaner
 class ConfigurationCleaner
 {
 {
     private static final PwmLogger LOGGER = PwmLogger.forClass( ConfigurationCleaner.class );
     private static final PwmLogger LOGGER = PwmLogger.forClass( ConfigurationCleaner.class );
-    private static final String NEW_PROFILE_NAME = "default";
 
 
-    private final StoredConfigurationImpl storedConfiguration;
-    private final XmlDocument document;
 
 
-    ConfigurationCleaner(
-            final StoredConfigurationImpl storedConfiguration, final XmlDocument document
-    )
-    {
-        this.storedConfiguration = storedConfiguration;
-        this.document = document;
-    }
+    private static final List<PwmExceptionLoggingConsumer<XmlDocument>> XML_PRE_PROCESSORS = Collections.unmodifiableList( Arrays.asList(
+            new MigratePreValueXmlElements(),
+            new MigrateOldPropertyFormat(),
+            new AppPropertyOverrideMigration(),
+            new ProfileNonProfiledSettings(),
+            new MigrateDeprecatedProperties(),
+            new UpdatePropertiesWithoutType()
+    ) );
 
 
+    private static final List<PwmExceptionLoggingConsumer<StoredConfigurationModifier>> STORED_CONFIG_POST_PROCESSORS = Collections.unmodifiableList( Arrays.asList(
+            new UpdateDeprecatedAdComplexitySettings(),
+            new UpdateDeprecatedMinPwdLifetimeSetting(),
+            new UpdateDeprecatedPublicHealthSetting()
+    ) );
 
 
-    static void cleanup(
-            final StoredConfigurationImpl storedConfiguration, final XmlDocument document
-    )
-            throws PwmUnrecoverableException
-    {
-        new ConfigurationCleaner( storedConfiguration, document ).cleanupImpl();
-    }
 
 
-    static void updateMandatoryElements(
-            final StoredConfigurationImpl storedConfiguration,
+    static void preProcessXml(
             final XmlDocument document
             final XmlDocument document
     )
     )
     {
     {
-        new ConfigurationCleaner( storedConfiguration, document ).updateMandatoryElementsImpl();
+        XML_PRE_PROCESSORS.forEach( ( c ) -> PwmExceptionLoggingConsumer.wrapConsumer( c ).accept( document ) );
     }
     }
 
 
 
 
-    private void cleanupImpl(
+    static void postProcessStoredConfig(
+            final StoredConfigurationModifier storedConfiguration
     )
     )
-            throws PwmUnrecoverableException
     {
     {
-        updateProperitiesWithoutType( );
-        updateMandatoryElementsImpl();
-        profilizeNonProfiledSettings( );
-        stripOrphanedProfileSettings( );
-        migrateAppProperties( );
-        updateDeprecatedSettings( );
-        migrateDeprecatedProperties( );
+        STORED_CONFIG_POST_PROCESSORS.forEach( aClass -> PwmExceptionLoggingConsumer.wrapConsumer( aClass ).accept( storedConfiguration ) );
     }
     }
 
 
-
-    private void updateMandatoryElementsImpl( )
+    private static class MigratePreValueXmlElements implements PwmExceptionLoggingConsumer<XmlDocument>
     {
     {
-        final XmlElement rootElement = document.getRootElement();
-        rootElement.setComment( Collections.singletonList( generateCommentText() ) );
+        @Override
+        public void accept( final XmlDocument xmlDocument )
+        {
+            if ( readDocVersion( xmlDocument ) >= 4 )
+            {
+                return;
+            }
 
 
-        rootElement.setAttribute( "pwmVersion", PwmConstants.BUILD_VERSION );
-        rootElement.setAttribute( "pwmBuild", PwmConstants.BUILD_NUMBER );
-        rootElement.setAttribute( "xmlVersion", StoredConfigurationImpl.XML_FORMAT_VERSION );
+            final List<XmlElement> settingElements = xmlDocument.evaluateXpathToElements( "//"
+                    + StoredConfigXmlConstants.XML_ELEMENT_SETTING );
+            for ( final XmlElement settingElement : settingElements )
+            {
+                final Optional<XmlElement> valueElement = settingElement.getChild( StoredConfigXmlConstants.XML_ELEMENT_VALUE );
+                final Optional<XmlElement> defaultElement = settingElement.getChild( StoredConfigXmlConstants.XML_ELEMENT_DEFAULT );
+                if ( valueElement.isPresent() && defaultElement.isPresent() )
+                {
+                    final String textValue = settingElement.getTextTrim();
+                    if ( !StringUtil.isEmpty( textValue ) )
+                    {
+                        final XmlElement newValueElement = XmlFactory.getFactory().newElement( StoredConfigXmlConstants.XML_ELEMENT_VALUE );
+                        newValueElement.addText( textValue );
+                        settingElement.addContent( newValueElement );
+                        final String key = settingElement.getAttributeValue( StoredConfigXmlConstants.XML_ATTRIBUTE_KEY );
+                        LOGGER.info( () -> "migrating pre-xml 'value' tag format to use value element for key: " + key );
+                    }
+                }
+            }
+        }
+    }
 
 
-        // migrate old properties
+    private static class MigrateOldPropertyFormat implements PwmExceptionLoggingConsumer<XmlDocument>
+    {
+        @Override
+        public void accept( final XmlDocument xmlDocument )
         {
         {
-
             // read correct (new) //properties[@type="config"]
             // read correct (new) //properties[@type="config"]
-            final String configPropertiesXpath = "//" + StoredConfigurationImpl.XML_ELEMENT_PROPERTIES
-                    + "[@" + StoredConfigurationImpl.XML_ATTRIBUTE_TYPE + "=\"" + StoredConfigurationImpl.XML_ATTRIBUTE_VALUE_CONFIG + "\"]";
-            final XmlElement configPropertiesElement = document.evaluateXpathToElement( configPropertiesXpath );
+            final String configPropertiesXpath = "//" + StoredConfigXmlConstants.XML_ELEMENT_PROPERTIES
+                    + "[@" + StoredConfigXmlConstants.XML_ATTRIBUTE_TYPE + "=\"" + StoredConfigXmlConstants.XML_ATTRIBUTE_VALUE_CONFIG + "\"]";
+            final Optional<XmlElement> configPropertiesElement = xmlDocument.evaluateXpathToElement( configPropertiesXpath );
 
 
             // read list of old //properties[not (@type)]/property
             // read list of old //properties[not (@type)]/property
-            final String nonAttributedPropertyXpath = "//" + StoredConfigurationImpl.XML_ELEMENT_PROPERTIES
-                    + "[not (@" + StoredConfigurationImpl.XML_ATTRIBUTE_TYPE + ")]/" + StoredConfigurationImpl.XML_ELEMENT_PROPERTY;
-            final List<XmlElement> nonAttributedProperties = document.evaluateXpathToElements( nonAttributedPropertyXpath );
+            final String nonAttributedPropertyXpath = "//" + StoredConfigXmlConstants.XML_ELEMENT_PROPERTIES
+                    + "[not (@" + StoredConfigXmlConstants.XML_ATTRIBUTE_TYPE + ")]/" + StoredConfigXmlConstants.XML_ELEMENT_PROPERTY;
+            final List<XmlElement> nonAttributedProperties = xmlDocument.evaluateXpathToElements( nonAttributedPropertyXpath );
 
 
-            if ( configPropertiesElement != null && nonAttributedProperties != null )
+            if ( configPropertiesElement.isPresent() && nonAttributedProperties != null )
             {
             {
                 for ( final XmlElement element : nonAttributedProperties )
                 for ( final XmlElement element : nonAttributedProperties )
                 {
                 {
                     element.detach();
                     element.detach();
-                    configPropertiesElement.addContent( element );
+                    configPropertiesElement.get().addContent( element );
                 }
                 }
             }
             }
 
 
             // remove old //properties[not (@type] element
             // remove old //properties[not (@type] element
-            final String oldPropertiesXpath = "//" + StoredConfigurationImpl.XML_ELEMENT_PROPERTIES + "[not (@" + StoredConfigurationImpl.XML_ATTRIBUTE_TYPE + ")]";
-            final List<XmlElement> oldPropertiesElements = document.evaluateXpathToElements( oldPropertiesXpath );
+            final String oldPropertiesXpath = "//" + StoredConfigXmlConstants.XML_ELEMENT_PROPERTIES
+                    + "[not (@" + StoredConfigXmlConstants.XML_ATTRIBUTE_TYPE + ")]";
+            final List<XmlElement> oldPropertiesElements = xmlDocument.evaluateXpathToElements( oldPropertiesXpath );
             if ( oldPropertiesElements != null )
             if ( oldPropertiesElements != null )
             {
             {
                 for ( final XmlElement element : oldPropertiesElements )
                 for ( final XmlElement element : oldPropertiesElements )
@@ -132,226 +156,260 @@ class ConfigurationCleaner
         }
         }
     }
     }
 
 
-    private String generateCommentText( )
+    static class ProfileNonProfiledSettings implements PwmExceptionLoggingConsumer<XmlDocument>
     {
     {
-        final StringBuilder commentText = new StringBuilder();
-        commentText.append( "\t\t" ).append( " " ).append( "\n" );
-        commentText.append( "\t\t" ).append( "This configuration file has been auto-generated by the " ).append( PwmConstants.PWM_APP_NAME )
-                .append( " password self service application." ).append( "\n" );
-        commentText.append( "\t\t" ).append( "" ).append( "\n" );
-        commentText.append( "\t\t" ).append( "WARNING: This configuration file contains sensitive security information, please handle with care!" ).append( "\n" );
-        commentText.append( "\t\t" ).append( "" ).append( "\n" );
-        commentText.append( "\t\t" ).append( "WARNING: If a server is currently running using this configuration file, it will be restarted" ).append( "\n" );
-        commentText.append( "\t\t" ).append( "         and the configuration updated immediately when it is modified." ).append( "\n" );
-        commentText.append( "\t\t" ).append( "" ).append( "\n" );
-        commentText.append( "\t\t" ).append( "NOTICE: This file is encoded as UTF-8.  Do not save or edit this file with an editor that does not" ).append( "\n" );
-        commentText.append( "\t\t" ).append( "        support UTF-8 encoding." ).append( "\n" );
-        commentText.append( "\t\t" ).append( "" ).append( "\n" );
-        commentText.append( "\t\t" ).append( "If unable to edit using the application ConfigurationEditor web UI, the following options are available." ).append( "\n" );
-        commentText.append( "\t\t" ).append( "   or 1. Edit this file directly by hand." ).append( "\n" );
-        commentText.append( "\t\t" ).append( "   or 2. Remove restrictions of the configuration by setting the property 'configIsEditable' to 'true' in this file.  This will " )
-                .append( "\n" );
-        commentText.append( "\t\t" ).append( "         allow access to the ConfigurationEditor web UI without having to authenticate to an LDAP server first." ).append( "\n" );
-        commentText.append( "\t\t" ).append( "   or 3. Remove restrictions of the configuration by using the the command line utility. " ).append( "\n" );
-        commentText.append( "\t\t" ).append( "" ).append( "\n" );
-        return commentText.toString();
-    }
-
-
-    private void profilizeNonProfiledSettings()
-            throws PwmUnrecoverableException
-    {
-        for ( final PwmSetting setting : PwmSetting.values() )
+        @Override
+        public void accept( final XmlDocument xmlDocument )
         {
         {
-            if ( setting.getCategory().hasProfiles() )
+            final StoredConfigurationFactory.XmlInputDocumentReader reader = new StoredConfigurationFactory.XmlInputDocumentReader( xmlDocument );
+            for ( final PwmSetting setting : PwmSetting.values() )
             {
             {
-
-                final XmlElement settingElement = storedConfiguration.getXmlHelper().xpathForSetting( setting, null );
-                if ( settingElement != null )
+                if ( setting.getCategory().hasProfiles() )
                 {
                 {
-                    settingElement.detach();
-
-                    final PwmSetting profileSetting = setting.getCategory().getProfileSetting();
-                    final List<String> profileStringDefinitions = new ArrayList<>();
+                    reader.xpathForSetting( setting, null ).ifPresent( existingSettingElement ->
                     {
                     {
-                        final StringArrayValue profileDefinitions = ( StringArrayValue ) storedConfiguration.readSetting( profileSetting );
-                        if ( profileDefinitions != null )
+                        final List<String> profileStringDefinitions = new ArrayList<>();
                         {
                         {
-                            if ( profileDefinitions.toNativeObject() != null )
+                            final List<String> configuredProfiles = reader.profilesForSetting( setting );
+                            if ( !JavaHelper.isEmpty( configuredProfiles ) )
                             {
                             {
-                                profileStringDefinitions.addAll( profileDefinitions.toNativeObject() );
+                                profileStringDefinitions.addAll( configuredProfiles );
                             }
                             }
                         }
                         }
-                    }
 
 
-                    if ( profileStringDefinitions.isEmpty() )
-                    {
-                        profileStringDefinitions.add( NEW_PROFILE_NAME );
-                    }
-
-                    final UserIdentity userIdentity = settingElement.getAttributeValue( StoredConfigurationImpl.XML_ATTRIBUTE_MODIFY_USER ) != null
-                            ? UserIdentity.fromDelimitedKey( settingElement.getAttributeValue(  StoredConfigurationImpl.XML_ATTRIBUTE_MODIFY_USER ) )
-                            : null;
+                        if ( profileStringDefinitions.isEmpty() )
+                        {
+                            profileStringDefinitions.add( PwmConstants.PROFILE_ID_DEFAULT );
+                        }
 
 
-                    for ( final String destProfile : profileStringDefinitions )
-                    {
-                        LOGGER.info( () -> "moving setting " + setting.getKey() + " without profile attribute to profile \"" + destProfile + "\"." );
+                        for ( final String destProfile : profileStringDefinitions )
                         {
                         {
-                            storedConfiguration.writeSetting( profileSetting, new StringArrayValue( profileStringDefinitions ), userIdentity );
+                            LOGGER.info( () -> "moving setting " + setting.getKey() + " without profile attribute to profile \"" + destProfile + "\"." );
+                            {
+                                //existingSettingElement.detach();
+                                final XmlElement newSettingElement = existingSettingElement.copy();
+                                newSettingElement.setAttribute( StoredConfigXmlConstants.XML_ATTRIBUTE_PROFILE, destProfile );
+
+                                final XmlElement settingsElement = reader.xpathForSettings();
+                                settingsElement.addContent( newSettingElement );
+                            }
                         }
                         }
-                    }
+                    } );
                 }
                 }
             }
             }
         }
         }
     }
     }
 
 
-    private void migrateDeprecatedProperties(
-    )
-            throws PwmUnrecoverableException
+    private static class MigrateDeprecatedProperties implements PwmExceptionLoggingConsumer<XmlDocument>
     {
     {
+        @Override
+        public void accept( final XmlDocument xmlDocument ) throws PwmUnrecoverableException
         {
         {
-            final String xpathString = "//property[@key=\"" + ConfigurationProperty.LDAP_TEMPLATE.getKey() + "\"]";
-            final List<XmlElement> propertyElement = document.evaluateXpathToElements( xpathString );
-            if ( propertyElement != null && !propertyElement.isEmpty() )
             {
             {
-                final String value = propertyElement.get( 0 ).getText();
-                storedConfiguration.writeSetting( PwmSetting.TEMPLATE_LDAP, new StringValue( value ), null );
-                propertyElement.get( 0 ).detach();
+                final String xpathString = "//property[@key=\"" + ConfigurationProperty.LDAP_TEMPLATE.getKey() + "\"]";
+                final List<XmlElement> propertyElement = xmlDocument.evaluateXpathToElements( xpathString );
+                if ( propertyElement != null && !propertyElement.isEmpty() )
+                {
+                    final String value = propertyElement.get( 0 ).getText();
+                    propertyElement.get( 0 ).detach();
+                    attachStringSettingElement( xmlDocument, PwmSetting.TEMPLATE_LDAP, value );
+
+                }
             }
             }
-        }
-        {
-            final String xpathString = "//property[@key=\"" + ConfigurationProperty.NOTES.getKey() + "\"]";
-            final List<XmlElement> propertyElement = document.evaluateXpathToElements( xpathString );
-            if ( propertyElement != null && !propertyElement.isEmpty() )
             {
             {
-                final String value = propertyElement.get( 0 ).getText();
-                storedConfiguration.writeSetting( PwmSetting.NOTES, new StringValue( value ), null );
-                propertyElement.get( 0 ).detach();
+                final String xpathString = "//property[@key=\"" + ConfigurationProperty.NOTES.getKey() + "\"]";
+                final List<XmlElement> propertyElement = xmlDocument.evaluateXpathToElements( xpathString );
+                if ( propertyElement != null && !propertyElement.isEmpty() )
+                {
+                    final String value = propertyElement.get( 0 ).getText();
+                    propertyElement.get( 0 ).detach();
+                    attachStringSettingElement( xmlDocument, PwmSetting.NOTES, value );
+                }
             }
             }
         }
         }
+
+        private static void attachStringSettingElement(
+                final XmlDocument xmlDocument,
+                final PwmSetting pwmSetting,
+                final String stringValue
+        )
+                throws PwmUnrecoverableException
+        {
+            final StoredConfigurationFactory.XmlInputDocumentReader inputDocumentReader = new StoredConfigurationFactory.XmlInputDocumentReader( xmlDocument );
+
+            final PwmSecurityKey pwmSecurityKey = inputDocumentReader.getKey();
+
+            final XmlElement settingElement = StoredConfigurationFactory.XmlOutputHandler.makeSettingXmlElement(
+                    null,
+                    pwmSetting,
+                    null,
+                    new StringValue( stringValue ),
+                    XmlOutputProcessData.builder().storedValueEncoderMode( StoredValueEncoder.Mode.PLAIN ).pwmSecurityKey( pwmSecurityKey ).build() );
+            final Optional<XmlElement> settingsElement = xmlDocument.getRootElement().getChild( StoredConfigXmlConstants.XML_ELEMENT_SETTING );
+            settingsElement.ifPresent( xmlElement -> xmlElement.addContent( settingElement ) );
+        }
     }
     }
 
 
-    private void updateProperitiesWithoutType()
+    private static class UpdatePropertiesWithoutType implements PwmExceptionLoggingConsumer<XmlDocument>
     {
     {
-        final String xpathString = "//properties[not(@type)]";
-        final List<XmlElement> propertiesElements = document.evaluateXpathToElements( xpathString );
-        for ( final XmlElement propertiesElement : propertiesElements )
+        @Override
+        public void accept( final XmlDocument xmlDocument )
         {
         {
-            propertiesElement.setAttribute( StoredConfigurationImpl.XML_ATTRIBUTE_TYPE, StoredConfigurationImpl.XML_ATTRIBUTE_VALUE_CONFIG );
+            final String xpathString = "//properties[not(@type)]";
+            final List<XmlElement> propertiesElements = xmlDocument.evaluateXpathToElements( xpathString );
+            for ( final XmlElement propertiesElement : propertiesElements )
+            {
+                propertiesElement.setAttribute( StoredConfigXmlConstants.XML_ATTRIBUTE_TYPE, StoredConfigXmlConstants.XML_ATTRIBUTE_VALUE_CONFIG );
+            }
         }
         }
     }
     }
 
 
-    private void stripOrphanedProfileSettings()
+    private static class AppPropertyOverrideMigration implements PwmExceptionLoggingConsumer<XmlDocument>
     {
     {
-        for ( final PwmSetting setting : PwmSetting.values() )
+        @Override
+        public void accept( final XmlDocument xmlDocument ) throws PwmUnrecoverableException
         {
         {
-            if ( setting.getCategory().hasProfiles() )
+            final StoredConfigurationFactory.XmlInputDocumentReader documentReader = new StoredConfigurationFactory.XmlInputDocumentReader( xmlDocument );
+            final List<XmlElement> appPropertiesElements = documentReader.xpathForAppProperties();
+            for ( final XmlElement element : appPropertiesElements )
             {
             {
-                final List<String> validProfiles = storedConfiguration.profilesForSetting( setting );
-                final String xpathString = "//setting[@key=\"" + setting.getKey() + "\"]";
-                final List<XmlElement> settingElements =  document.evaluateXpathToElements( xpathString );
-                for ( final XmlElement settingElement : settingElements )
+                final List<XmlElement> properties = element.getChildren();
+                for ( final XmlElement property : properties )
                 {
                 {
-                    final String profileID = settingElement.getAttributeValue( StoredConfigurationImpl.XML_ATTRIBUTE_PROFILE );
-                    if ( profileID != null )
+                    final String key = property.getAttributeValue( "key" );
+                    final String value = property.getText();
+                    if ( key != null && !key.isEmpty() && value != null && !value.isEmpty() )
                     {
                     {
-                        if ( !validProfiles.contains( profileID ) )
+                        LOGGER.info( () -> "migrating app-property config element '" + key + "' to setting " + PwmSetting.APP_PROPERTY_OVERRIDES.getKey() );
+                        final String newValue = key + "=" + value;
+
+                        final List<String> existingValues = new ArrayList<>();
                         {
                         {
-                            LOGGER.info( () -> "removing setting " + setting.getKey() + " with profile \"" + profileID + "\", profile is not a valid profile" );
-                            settingElement.detach();
+                            final Optional<StoredConfigData.ValueAndMetaCarrier> valueAndMetaTuple =  documentReader.readSetting( PwmSetting.APP_PROPERTY_OVERRIDES, null );
+                            valueAndMetaTuple.ifPresent( ( t ) -> existingValues.addAll( ( List<String> ) t.getValue().toNativeObject() ) );
                         }
                         }
+                        existingValues.add( newValue );
+                        rewriteAppPropertySettingElement( xmlDocument, existingValues );
                     }
                     }
                 }
                 }
+                element.detach();
+            }
+        }
+
+        private static void rewriteAppPropertySettingElement( final XmlDocument xmlDocument, final List<String> newValues )
+                throws PwmUnrecoverableException
+        {
+            final StoredConfigurationFactory.XmlInputDocumentReader inputDocumentReader = new StoredConfigurationFactory.XmlInputDocumentReader( xmlDocument );
+
+            {
+                final Optional<XmlElement> existingAppPropertySetting = inputDocumentReader.xpathForSetting( PwmSetting.APP_PROPERTY_OVERRIDES, null );
+                existingAppPropertySetting.ifPresent( XmlElement::detach );
             }
             }
+
+            final PwmSecurityKey pwmSecurityKey = inputDocumentReader.getKey();
+
+            final XmlElement settingElement = StoredConfigurationFactory.XmlOutputHandler.makeSettingXmlElement(
+                    null,
+                    PwmSetting.APP_PROPERTY_OVERRIDES,
+                    null,
+                    new StringArrayValue( 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.addContent( settingElement ) );
         }
         }
     }
     }
 
 
-    private void migrateAppProperties(
-    )
-            throws PwmUnrecoverableException
+    private static class UpdateDeprecatedAdComplexitySettings implements PwmExceptionLoggingConsumer<StoredConfigurationModifier>
     {
     {
-        final List<XmlElement> appPropertiesElements = storedConfiguration.getXmlHelper().xpathForAppProperties();
-        for ( final XmlElement element : appPropertiesElements )
+        @Override
+        public void accept( final StoredConfigurationModifier modifier )
+                throws PwmUnrecoverableException
         {
         {
-            final List<XmlElement> properties = element.getChildren();
-            for ( final XmlElement property : properties )
+            final StoredConfiguration oldConfig = modifier.newStoredConfiguration();
+            final Configuration configuration = new Configuration( oldConfig );
+            for ( final String profileID : configuration.getPasswordProfileIDs() )
             {
             {
-                final String key = property.getAttributeValue( "key" );
-                final String value = property.getText();
-                if ( key != null && !key.isEmpty() && value != null && !value.isEmpty() )
+                if ( !oldConfig.isDefaultValue( PwmSetting.PASSWORD_POLICY_AD_COMPLEXITY, profileID ) )
                 {
                 {
-                    LOGGER.info( () -> "migrating app-property config element '" + key + "' to setting " + PwmSetting.APP_PROPERTY_OVERRIDES.getKey() );
-                    final String newValue = key + "=" + value;
-                    List<String> existingValues = ( List<String> ) storedConfiguration.readSetting( PwmSetting.APP_PROPERTY_OVERRIDES ).toNativeObject();
-                    if ( existingValues == null )
+                    final boolean ad2003Enabled = ( boolean ) oldConfig.readSetting( PwmSetting.PASSWORD_POLICY_AD_COMPLEXITY, profileID ).toNativeObject();
+                    final StoredValue value;
+                    if ( ad2003Enabled )
+                    {
+                        value = new StringValue( ADPolicyComplexity.AD2003.toString() );
+                    }
+                    else
                     {
                     {
-                        existingValues = new ArrayList<>();
+                        value = new StringValue( ADPolicyComplexity.NONE.toString() );
                     }
                     }
-                    existingValues = new ArrayList<>( existingValues );
-                    existingValues.add( newValue );
-                    storedConfiguration.writeSetting( PwmSetting.APP_PROPERTY_OVERRIDES, new StringArrayValue( existingValues ), null );
+                    LOGGER.info( () -> "converting deprecated non-default setting " + PwmSetting.PASSWORD_POLICY_AD_COMPLEXITY.getKey() + "/" + profileID
+                            + " to replacement setting " + PwmSetting.PASSWORD_POLICY_AD_COMPLEXITY_LEVEL + ", value=" + value.toNativeObject().toString() );
+                    final Optional<ValueMetaData> valueMetaData = oldConfig.readMetaData(
+                            StoredConfigItemKey.fromSetting( PwmSetting.PASSWORD_POLICY_AD_COMPLEXITY, profileID ) );
+                    final UserIdentity userIdentity = valueMetaData.map( ValueMetaData::getUserIdentity ).orElse( null );
+                    modifier.writeSetting( PwmSetting.PASSWORD_POLICY_AD_COMPLEXITY_LEVEL, profileID, value, userIdentity );
                 }
                 }
             }
             }
-            element.detach();
         }
         }
     }
     }
 
 
-    private void updateDeprecatedSettings( ) throws PwmUnrecoverableException
+    private static class UpdateDeprecatedMinPwdLifetimeSetting implements PwmExceptionLoggingConsumer<StoredConfigurationModifier>
     {
     {
-        final UserIdentity actor = new UserIdentity( "UpgradeProcessor", null );
-        for ( final String profileID : storedConfiguration.profilesForSetting( PwmSetting.PASSWORD_POLICY_AD_COMPLEXITY ) )
+        @Override
+        public void accept( final StoredConfigurationModifier modifier )
+                throws PwmUnrecoverableException
         {
         {
-            if ( !storedConfiguration.isDefaultValue( PwmSetting.PASSWORD_POLICY_AD_COMPLEXITY, profileID ) )
+            final StoredConfiguration oldConfig = modifier.newStoredConfiguration();
+            for ( final String profileID : oldConfig.profilesForSetting( PwmSetting.RECOVERY_ENFORCE_MINIMUM_PASSWORD_LIFETIME ) )
             {
             {
-                final boolean ad2003Enabled = ( boolean ) storedConfiguration.readSetting( PwmSetting.PASSWORD_POLICY_AD_COMPLEXITY, profileID ).toNativeObject();
-                final StoredValue value;
-                if ( ad2003Enabled )
+                if ( !oldConfig.isDefaultValue( PwmSetting.RECOVERY_ENFORCE_MINIMUM_PASSWORD_LIFETIME, profileID ) )
                 {
                 {
-                    value = new StringValue( ADPolicyComplexity.AD2003.toString() );
-                }
-                else
-                {
-                    value = new StringValue( ADPolicyComplexity.NONE.toString() );
+                    final boolean enforceEnabled = ( boolean ) oldConfig.readSetting( PwmSetting.RECOVERY_ENFORCE_MINIMUM_PASSWORD_LIFETIME, profileID ).toNativeObject();
+                    final StoredValue value = enforceEnabled
+                            ? new StringValue( RecoveryMinLifetimeOption.NONE.name() )
+                            : new StringValue( RecoveryMinLifetimeOption.ALLOW.name() );
+                    final ValueMetaData existingData = oldConfig.readSettingMetadata( PwmSetting.RECOVERY_ENFORCE_MINIMUM_PASSWORD_LIFETIME, profileID );
+                    final UserIdentity newActor = existingData != null && existingData.getUserIdentity() != null
+                            ? existingData.getUserIdentity()
+                            : null;
+                    LOGGER.info( () -> "converting deprecated non-default setting "
+                            + PwmSetting.RECOVERY_ENFORCE_MINIMUM_PASSWORD_LIFETIME.toMenuLocationDebug( profileID, PwmConstants.DEFAULT_LOCALE ) + "/" + profileID
+                            + " to replacement setting " + PwmSetting.RECOVERY_MINIMUM_PASSWORD_LIFETIME_OPTIONS.toMenuLocationDebug( profileID, PwmConstants.DEFAULT_LOCALE )
+                            + ", value=" + value.toNativeObject().toString() );
+                    modifier.writeSetting( PwmSetting.RECOVERY_MINIMUM_PASSWORD_LIFETIME_OPTIONS, profileID, value, newActor );
                 }
                 }
-                LOGGER.warn( "converting deprecated non-default setting " + PwmSetting.PASSWORD_POLICY_AD_COMPLEXITY.getKey() + "/" + profileID
-                        + " to replacement setting " + PwmSetting.PASSWORD_POLICY_AD_COMPLEXITY_LEVEL + ", value=" + value.toNativeObject().toString() );
-                storedConfiguration.writeSetting( PwmSetting.PASSWORD_POLICY_AD_COMPLEXITY_LEVEL, profileID, value, actor );
-                storedConfiguration.resetSetting( PwmSetting.PASSWORD_POLICY_AD_COMPLEXITY, profileID, actor );
             }
             }
         }
         }
+    }
 
 
-        for ( final String profileID : storedConfiguration.profilesForSetting( PwmSetting.RECOVERY_ENFORCE_MINIMUM_PASSWORD_LIFETIME ) )
+    private static class UpdateDeprecatedPublicHealthSetting implements PwmExceptionLoggingConsumer<StoredConfigurationModifier>
+    {
+        @Override
+        public void accept( final StoredConfigurationModifier modifier )
+                throws PwmUnrecoverableException
         {
         {
-            if ( !storedConfiguration.isDefaultValue( PwmSetting.RECOVERY_ENFORCE_MINIMUM_PASSWORD_LIFETIME, profileID ) )
+            final StoredConfiguration oldConfig = modifier.newStoredConfiguration();
+            if ( !oldConfig.isDefaultValue( PwmSetting.PUBLIC_HEALTH_STATS_WEBSERVICES, null ) )
             {
             {
-                final boolean enforceEnabled = ( boolean ) storedConfiguration.readSetting( PwmSetting.RECOVERY_ENFORCE_MINIMUM_PASSWORD_LIFETIME, profileID ).toNativeObject();
-                final StoredValue value = enforceEnabled
-                        ? new StringValue( "NONE" )
-                        : new StringValue( "ALLOW" );
-                final ValueMetaData existingData = storedConfiguration.readSettingMetadata( PwmSetting.RECOVERY_ENFORCE_MINIMUM_PASSWORD_LIFETIME, profileID );
-                LOGGER.warn( "converting deprecated non-default setting "
-                        + PwmSetting.RECOVERY_ENFORCE_MINIMUM_PASSWORD_LIFETIME.toMenuLocationDebug( profileID, PwmConstants.DEFAULT_LOCALE ) + "/" + profileID
-                        + " to replacement setting " + PwmSetting.RECOVERY_MINIMUM_PASSWORD_LIFETIME_OPTIONS.toMenuLocationDebug( profileID, PwmConstants.DEFAULT_LOCALE )
-                        + ", value=" + value.toNativeObject().toString() );
-                final UserIdentity newActor = existingData != null && existingData.getUserIdentity() != null
-                        ? existingData.getUserIdentity()
-                        : actor;
-                storedConfiguration.writeSetting( PwmSetting.RECOVERY_MINIMUM_PASSWORD_LIFETIME_OPTIONS, profileID, value, newActor );
-                storedConfiguration.resetSetting( PwmSetting.RECOVERY_ENFORCE_MINIMUM_PASSWORD_LIFETIME, profileID, actor );
+                LOGGER.info( () -> "converting deprecated non-default setting "
+                        + PwmSetting.PUBLIC_HEALTH_STATS_WEBSERVICES.toMenuLocationDebug( null, PwmConstants.DEFAULT_LOCALE )
+                        + " to replacement setting " + PwmSetting.WEBSERVICES_PUBLIC_ENABLE.toMenuLocationDebug( null, PwmConstants.DEFAULT_LOCALE ) );
+                final Set<String> existingValues = ( Set<String> ) oldConfig.readSetting( PwmSetting.WEBSERVICES_PUBLIC_ENABLE, null ).toNativeObject();
+                final Set<String> newValues = new LinkedHashSet<>( existingValues );
+                newValues.add( WebServiceUsage.Health.name() );
+                newValues.add( WebServiceUsage.Statistics.name() );
+
+                final Optional<ValueMetaData> valueMetaData = oldConfig.readMetaData(
+                        StoredConfigItemKey.fromSetting( PwmSetting.PUBLIC_HEALTH_STATS_WEBSERVICES, null ) );
+                final UserIdentity userIdentity = valueMetaData.map( ValueMetaData::getUserIdentity ).orElse( null );
+
+                modifier.writeSetting( PwmSetting.WEBSERVICES_PUBLIC_ENABLE, null, new OptionListValue( newValues ), userIdentity );
             }
             }
         }
         }
+    }
 
 
-        if ( !storedConfiguration.isDefaultValue( PwmSetting.PUBLIC_HEALTH_STATS_WEBSERVICES ) )
-        {
-            LOGGER.warn( "converting deprecated non-default setting "
-                    + PwmSetting.PUBLIC_HEALTH_STATS_WEBSERVICES.toMenuLocationDebug( null, PwmConstants.DEFAULT_LOCALE )
-                    + " to replacement setting " + PwmSetting.WEBSERVICES_PUBLIC_ENABLE.toMenuLocationDebug( null, PwmConstants.DEFAULT_LOCALE ) );
-            final Set<String> existingValues = (Set<String>) storedConfiguration.readSetting( PwmSetting.WEBSERVICES_PUBLIC_ENABLE ).toNativeObject();
-            final Set<String> newValues = new LinkedHashSet<>( existingValues );
-            newValues.add( WebServiceUsage.Health.name() );
-            newValues.add( WebServiceUsage.Statistics.name() );
-            storedConfiguration.writeSetting( PwmSetting.WEBSERVICES_PUBLIC_ENABLE, null, new OptionListValue( newValues ), actor );
-            storedConfiguration.resetSetting( PwmSetting.PUBLIC_HEALTH_STATS_WEBSERVICES, null, actor );
-        }
+    private static int readDocVersion( final XmlDocument xmlDocument )
+    {
+        final String xmlVersionStr = xmlDocument.getRootElement().getAttributeValue( StoredConfigXmlConstants.XML_ATTRIBUTE_XML_VERSION );
+        return JavaHelper.silentParseInt( xmlVersionStr, 0 );
     }
     }
 }
 }

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

@@ -27,8 +27,8 @@ public enum ConfigurationProperty
     LDAP_TEMPLATE( "configTemplate" ),
     LDAP_TEMPLATE( "configTemplate" ),
     NOTES( "notes" ),
     NOTES( "notes" ),
     PASSWORD_HASH( "configPasswordHash" ),
     PASSWORD_HASH( "configPasswordHash" ),
-    CONFIG_ON_START( "saveConfigOnStart" ),
-    MODIFIFICATION_TIMESTAMP( "modificationTimestamp" ),
+    STORE_PLAINTEXT_VALUES( "storePlaintextValues" ),
+    MODIFICATION_TIMESTAMP( "modificationTimestamp" ),
     IMPORT_LDAP_CERTIFICATES( "importLdapCertificates" ),;
     IMPORT_LDAP_CERTIFICATES( "importLdapCertificates" ),;
 
 
     private final String key;
     private final String key;

+ 134 - 59
server/src/main/java/password/pwm/config/stored/ConfigurationReader.java

@@ -25,13 +25,17 @@ import password.pwm.PwmApplication;
 import password.pwm.PwmApplicationMode;
 import password.pwm.PwmApplicationMode;
 import password.pwm.PwmConstants;
 import password.pwm.PwmConstants;
 import password.pwm.bean.SessionLabel;
 import password.pwm.bean.SessionLabel;
+import password.pwm.bean.UserIdentity;
 import password.pwm.config.Configuration;
 import password.pwm.config.Configuration;
+import password.pwm.config.StoredValue;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmOperationalException;
 import password.pwm.error.PwmOperationalException;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.svc.event.AuditEvent;
+import password.pwm.svc.event.AuditRecordFactory;
 import password.pwm.util.java.FileSystemUtility;
 import password.pwm.util.java.FileSystemUtility;
-import password.pwm.util.java.JsonUtil;
+import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.java.TimeDuration;
 import password.pwm.util.java.TimeDuration;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.logging.PwmLogger;
@@ -45,6 +49,8 @@ import java.nio.file.Files;
 import java.nio.file.StandardCopyOption;
 import java.nio.file.StandardCopyOption;
 import java.time.Instant;
 import java.time.Instant;
 import java.util.List;
 import java.util.List;
+import java.util.Optional;
+import java.util.Set;
 
 
 /**
 /**
  * Read the PWM configuration.
  * Read the PWM configuration.
@@ -58,9 +64,10 @@ public class ConfigurationReader
     private final File configFile;
     private final File configFile;
     private final String configFileChecksum;
     private final String configFileChecksum;
     private Configuration configuration;
     private Configuration configuration;
-    private StoredConfigurationImpl storedConfiguration;
+    private StoredConfiguration storedConfiguration;
     private ErrorInformation configFileError;
     private ErrorInformation configFileError;
 
 
+
     private PwmApplicationMode configMode = PwmApplicationMode.NEW;
     private PwmApplicationMode configMode = PwmApplicationMode.NEW;
 
 
     private volatile boolean saveInProgress;
     private volatile boolean saveInProgress;
@@ -83,7 +90,7 @@ public class ConfigurationReader
 
 
         if ( storedConfiguration == null )
         if ( storedConfiguration == null )
         {
         {
-            this.storedConfiguration = StoredConfigurationImpl.newStoredConfiguration();
+            this.storedConfiguration = StoredConfigurationFactory.newConfig();
         }
         }
 
 
         LOGGER.debug( () -> "configuration mode: " + configMode );
         LOGGER.debug( () -> "configuration mode: " + configMode );
@@ -94,7 +101,7 @@ public class ConfigurationReader
         return configMode;
         return configMode;
     }
     }
 
 
-    public StoredConfigurationImpl getStoredConfiguration( )
+    public StoredConfiguration getStoredConfiguration( )
     {
     {
         return storedConfiguration;
         return storedConfiguration;
     }
     }
@@ -103,19 +110,15 @@ public class ConfigurationReader
     {
     {
         if ( configuration == null )
         if ( configuration == null )
         {
         {
-            final StoredConfigurationImpl newStoredConfig = this.storedConfiguration == null
-                    ? StoredConfigurationImpl.newStoredConfiguration()
+            final StoredConfiguration newStoredConfig = this.storedConfiguration == null
+                    ? StoredConfigurationFactory.newConfig()
                     : this.storedConfiguration;
                     : this.storedConfiguration;
             configuration = new Configuration( newStoredConfig );
             configuration = new Configuration( newStoredConfig );
-            if ( storedConfiguration != null )
-            {
-                storedConfiguration.lock();
-            }
         }
         }
         return configuration;
         return configuration;
     }
     }
 
 
-    private StoredConfigurationImpl readStoredConfig( ) throws PwmUnrecoverableException
+    private StoredConfiguration readStoredConfig( ) throws PwmUnrecoverableException
     {
     {
         LOGGER.debug( () -> "loading configuration file: " + configFile );
         LOGGER.debug( () -> "loading configuration file: " + configFile );
 
 
@@ -126,6 +129,28 @@ public class ConfigurationReader
         }
         }
 
 
         final Instant startTime = Instant.now();
         final Instant startTime = Instant.now();
+
+        /*
+        try
+        {
+            final InputStream theFileData = Files.newInputStream( configFile.toPath() );
+            final StoredConfiguration storedConfiguration = StoredConfigurationFactory.fromXml( theFileData );
+
+            System.out.println( TimeDuration.compactFromCurrent( startTime ) );
+
+
+            //final ByteArrayOutputStream baos = new ByteArrayOutputStream();
+            final FileOutputStream fos = new FileOutputStream( new File( "/tmp/NEWCFG" ) );
+            StoredConfigurationFactory.toXml( storedConfiguration, fos );
+
+            //System.out.println( new String( baos.toByteArray(), "UTF-8" )  );
+        }
+        catch ( Exception e )
+        {
+            e.printStackTrace(  );
+        }
+        */
+
         final InputStream theFileData;
         final InputStream theFileData;
         try
         try
         {
         {
@@ -143,11 +168,10 @@ public class ConfigurationReader
             throw new PwmUnrecoverableException( errorInformation );
             throw new PwmUnrecoverableException( errorInformation );
         }
         }
 
 
-        final StoredConfigurationImpl storedConfiguration;
+        final StoredConfiguration storedConfiguration;
         try
         try
         {
         {
-            storedConfiguration = StoredConfigurationImpl.fromXml( theFileData );
-            //restoredConfiguration = (new NGStoredConfigurationFactory()).fromXml(theFileData);
+            storedConfiguration = StoredConfigurationFactory.fromXml( theFileData );
         }
         }
         catch ( PwmUnrecoverableException e )
         catch ( PwmUnrecoverableException e )
         {
         {
@@ -162,8 +186,8 @@ public class ConfigurationReader
             throw new PwmUnrecoverableException( errorInformation );
             throw new PwmUnrecoverableException( errorInformation );
         }
         }
 
 
-        final List<String> validationErrorMsgs = storedConfiguration.validateValues();
-        if ( validationErrorMsgs != null && !validationErrorMsgs.isEmpty() )
+        final List<String> validationErrorMsgs = StoredConfigurationUtil.validateValues( storedConfiguration );
+        if ( !JavaHelper.isEmpty( validationErrorMsgs ) )
         {
         {
             final String errorMsg = "value error in config file, please investigate: " + validationErrorMsgs.get( 0 );
             final String errorMsg = "value error in config file, please investigate: " + validationErrorMsgs.get( 0 );
             final ErrorInformation errorInformation = new ErrorInformation( PwmError.CONFIG_FORMAT_ERROR, null, new String[]
             final ErrorInformation errorInformation = new ErrorInformation( PwmError.CONFIG_FORMAT_ERROR, null, new String[]
@@ -175,8 +199,8 @@ public class ConfigurationReader
             throw new PwmUnrecoverableException( errorInformation );
             throw new PwmUnrecoverableException( errorInformation );
         }
         }
 
 
-        final String configIsEditable = storedConfiguration.readConfigProperty( ConfigurationProperty.CONFIG_IS_EDITABLE );
-        if ( PwmConstants.TRIAL_MODE || ( configIsEditable != null && "true".equalsIgnoreCase( configIsEditable ) ) )
+        final Optional<String> configIsEditable = storedConfiguration.readConfigProperty( ConfigurationProperty.CONFIG_IS_EDITABLE );
+        if ( PwmConstants.TRIAL_MODE || ( configIsEditable.isPresent() && "true".equalsIgnoreCase( configIsEditable.get() ) ) )
         {
         {
             this.configMode = PwmApplicationMode.CONFIGURATION;
             this.configMode = PwmApplicationMode.CONFIGURATION;
         }
         }
@@ -189,11 +213,13 @@ public class ConfigurationReader
         final TimeDuration timeDuration = TimeDuration.fromCurrent( startTime );
         final TimeDuration timeDuration = TimeDuration.fromCurrent( startTime );
         LOGGER.debug( () -> "configuration reading/parsing of " + fileSize + " complete in " + timeDuration.asLongString() );
         LOGGER.debug( () -> "configuration reading/parsing of " + fileSize + " complete in " + timeDuration.asLongString() );
 
 
+
+
         return storedConfiguration;
         return storedConfiguration;
     }
     }
 
 
     public void saveConfiguration(
     public void saveConfiguration(
-            final StoredConfigurationImpl storedConfiguration,
+            final StoredConfiguration storedConfiguration,
             final PwmApplication pwmApplication,
             final PwmApplication pwmApplication,
             final SessionLabel sessionLabel
             final SessionLabel sessionLabel
     )
     )
@@ -216,18 +242,21 @@ public class ConfigurationReader
 
 
         {
         {
             // increment the config epoch
             // increment the config epoch
-            String epochStrValue = storedConfiguration.readConfigProperty( ConfigurationProperty.CONFIG_EPOCH );
+            String newEpochStrValue = "0";
             try
             try
             {
             {
-                final BigInteger epochValue = epochStrValue == null || epochStrValue.length() < 0 ? BigInteger.ZERO : new BigInteger( epochStrValue );
-                epochStrValue = epochValue.add( BigInteger.ONE ).toString();
+                final Optional<String> storedEpochStrValue = storedConfiguration.readConfigProperty( ConfigurationProperty.CONFIG_EPOCH );
+                final BigInteger epochValue = storedEpochStrValue.map( BigInteger::new ).orElse( BigInteger.ZERO );
+                newEpochStrValue = epochValue.add( BigInteger.ONE ).toString();
             }
             }
             catch ( Exception e )
             catch ( Exception e )
             {
             {
                 LOGGER.error( sessionLabel, "error trying to parse previous config epoch property: " + e.getMessage() );
                 LOGGER.error( sessionLabel, "error trying to parse previous config epoch property: " + e.getMessage() );
-                epochStrValue = "0";
             }
             }
-            storedConfiguration.writeConfigProperty( ConfigurationProperty.CONFIG_EPOCH, epochStrValue );
+
+            final StoredConfigurationModifier modifier = StoredConfigurationModifier.newModifier( storedConfiguration );
+            modifier.writeConfigProperty( ConfigurationProperty.CONFIG_EPOCH, newEpochStrValue );
+            this.storedConfiguration = modifier.newStoredConfiguration();
         }
         }
 
 
         if ( backupDirectory != null && !backupDirectory.exists() )
         if ( backupDirectory != null && !backupDirectory.exists() )
@@ -239,51 +268,97 @@ public class ConfigurationReader
             }
             }
         }
         }
 
 
-        try
+        if ( pwmApplication != null && pwmApplication.getAuditManager() != null )
         {
         {
-            final File tempWriteFile = new File( configFile.getAbsoluteFile() + ".new" );
-            LOGGER.info( sessionLabel, () -> "beginning write to configuration file " + tempWriteFile );
-            saveInProgress = true;
-
-            try ( FileOutputStream fileOutputStream = new FileOutputStream( tempWriteFile, false ) )
-            {
-                storedConfiguration.toXml( fileOutputStream );
-            }
+            auditModifiedSettings( pwmApplication, storedConfiguration, sessionLabel );
+        }
 
 
-            LOGGER.info( () -> "saved configuration " + JsonUtil.serialize( storedConfiguration.toJsonDebugObject() ) );
-            if ( pwmApplication != null )
-            {
-                final String actualChecksum = storedConfiguration.settingChecksum();
-                pwmApplication.writeAppAttribute( PwmApplication.AppAttribute.CONFIG_HASH, actualChecksum );
-            }
+        try
+        {
+            outputConfigurationFile( storedConfiguration, pwmApplication, sessionLabel, backupRotations, backupDirectory );
+        }
+        finally
+        {
+            saveInProgress = false;
+        }
+    }
 
 
-            LOGGER.trace( () -> "renaming file " + tempWriteFile.getAbsolutePath() + " to " + configFile.getAbsolutePath() );
-            try
-            {
-                Files.move( tempWriteFile.toPath(), configFile.toPath(), StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE );
-            }
-            catch ( Exception e )
-            {
-                final String errorMsg = "unable to rename temporary save file from " + tempWriteFile.getAbsolutePath()
-                        + " to " + configFile.getAbsolutePath() + "; error: " + e.getMessage();
-                throw new PwmUnrecoverableException( new ErrorInformation( PwmError.ERROR_INTERNAL, errorMsg ) );
-            }
+    private static void auditModifiedSettings( final PwmApplication pwmApplication, final StoredConfiguration newConfig, final SessionLabel sessionLabel )
+            throws PwmUnrecoverableException
+    {
+        final Set<StoredConfigItemKey> changedKeys = StoredConfigurationUtil.changedValues( newConfig, pwmApplication.getConfig().getStoredConfiguration() );
 
 
-            if ( backupDirectory != null )
+        for ( final StoredConfigItemKey key : changedKeys )
+        {
+            if ( key.getRecordType() == StoredConfigItemKey.RecordType.SETTING
+                    || key.getRecordType() == StoredConfigItemKey.RecordType.LOCALE_BUNDLE )
             {
             {
-                final String configFileName = configFile.getName();
-                final String backupFilePath = backupDirectory.getAbsolutePath() + File.separatorChar + configFileName + "-backup";
-                final File backupFile = new File( backupFilePath );
-                FileSystemUtility.rotateBackups( backupFile, backupRotations );
-                try ( FileOutputStream fileOutputStream = new FileOutputStream( backupFile, false ) )
+                final Optional<StoredValue> storedValue = newConfig.readStoredValue( key );
+                if ( storedValue.isPresent() )
                 {
                 {
-                    storedConfiguration.toXml( fileOutputStream );
+                    final Optional<ValueMetaData> valueMetaData = newConfig.readMetaData( key );
+                    final UserIdentity userIdentity = valueMetaData.map( ValueMetaData::getUserIdentity ).orElse( null );
+                    final String modifyMessage = "configuration record '" + key.getLabel( PwmConstants.DEFAULT_LOCALE )
+                            + "' has been modified, new value: " + storedValue.get().toDebugString( PwmConstants.DEFAULT_LOCALE );
+                    pwmApplication.getAuditManager().submit( new AuditRecordFactory( pwmApplication ).createUserAuditRecord(
+                            AuditEvent.MODIFY_CONFIGURATION,
+                            userIdentity,
+                            sessionLabel,
+                            modifyMessage
+                    ) );
                 }
                 }
             }
             }
         }
         }
-        finally
+    }
+
+    private void outputConfigurationFile(
+            final StoredConfiguration storedConfiguration,
+            final PwmApplication pwmApplication,
+            final SessionLabel sessionLabel,
+            final int backupRotations,
+            final File backupDirectory
+    )
+            throws IOException, PwmUnrecoverableException
+    {
+        final Instant saveFileStartTime = Instant.now();
+        final File tempWriteFile = new File( configFile.getAbsoluteFile() + ".new" );
+        LOGGER.info( sessionLabel, () -> "beginning write to configuration file " + tempWriteFile );
+        saveInProgress = true;
+
+        try ( FileOutputStream fileOutputStream = new FileOutputStream( tempWriteFile, false ) )
         {
         {
-            saveInProgress = false;
+            StoredConfigurationFactory.toXml( storedConfiguration, fileOutputStream );
+        }
+
+        LOGGER.info( () -> "saved configuration in " + TimeDuration.compactFromCurrent( saveFileStartTime ) );
+        if ( pwmApplication != null )
+        {
+            final String actualChecksum = storedConfiguration.valueHash();
+            pwmApplication.writeAppAttribute( PwmApplication.AppAttribute.CONFIG_HASH, actualChecksum );
+        }
+
+        LOGGER.trace( () -> "renaming file " + tempWriteFile.getAbsolutePath() + " to " + configFile.getAbsolutePath() );
+        try
+        {
+            Files.move( tempWriteFile.toPath(), configFile.toPath(), StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE );
+        }
+        catch ( Exception e )
+        {
+            final String errorMsg = "unable to rename temporary save file from " + tempWriteFile.getAbsolutePath()
+                    + " to " + configFile.getAbsolutePath() + "; error: " + e.getMessage();
+            throw new PwmUnrecoverableException( new ErrorInformation( PwmError.ERROR_INTERNAL, errorMsg ) );
+        }
+
+        if ( backupDirectory != null )
+        {
+            final String configFileName = configFile.getName();
+            final String backupFilePath = backupDirectory.getAbsolutePath() + File.separatorChar + configFileName + "-backup";
+            final File backupFile = new File( backupFilePath );
+            FileSystemUtility.rotateBackups( backupFile, backupRotations );
+            try ( FileOutputStream fileOutputStream = new FileOutputStream( backupFile, false ) )
+            {
+                StoredConfigurationFactory.toXml( storedConfiguration, fileOutputStream );
+            }
         }
         }
     }
     }
 
 

+ 70 - 0
server/src/main/java/password/pwm/config/stored/StoredConfigData.java

@@ -0,0 +1,70 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2019 The PWM Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package password.pwm.config.stored;
+
+import lombok.Builder;
+import lombok.Singular;
+import lombok.Value;
+import password.pwm.config.StoredValue;
+
+import java.time.Instant;
+import java.util.Collection;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+@Value
+@Builder( toBuilder = true )
+class StoredConfigData
+{
+    @Builder.Default
+    private String createTime = "";
+
+    @Builder.Default
+    private Instant modifyTime = Instant.now();
+
+    @Singular
+    private Map<StoredConfigItemKey, StoredValue> storedValues;
+
+    @Singular
+    private Map<StoredConfigItemKey, ValueMetaData> metaDatas;
+
+    @Value
+    static class ValueAndMetaCarrier
+    {
+        private final StoredConfigItemKey key;
+        private final StoredValue value;
+        private final ValueMetaData metaData;
+    }
+
+    static Map<StoredConfigItemKey, ValueMetaData> carrierAsMetaDataMap( final Collection<ValueAndMetaCarrier> input )
+    {
+        return input.stream()
+                .filter( ( t ) -> t.getKey() != null && t.getMetaData() != null )
+                .collect( Collectors.toMap( StoredConfigData.ValueAndMetaCarrier::getKey, StoredConfigData.ValueAndMetaCarrier::getMetaData ) );
+    }
+
+    static Map<StoredConfigItemKey, StoredValue> carrierAsStoredValueMap( final Collection<ValueAndMetaCarrier> input )
+    {
+        return input.stream()
+                .filter( ( t ) -> t.getKey() != null && t.getValue() != null )
+                .collect( Collectors.toMap( StoredConfigData.ValueAndMetaCarrier::getKey, StoredConfigData.ValueAndMetaCarrier::getValue ) );
+    }
+}

+ 235 - 0
server/src/main/java/password/pwm/config/stored/StoredConfigItemKey.java

@@ -0,0 +1,235 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2019 The PWM Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package password.pwm.config.stored;
+
+import password.pwm.PwmConstants;
+import password.pwm.config.PwmSetting;
+import password.pwm.i18n.Config;
+import password.pwm.i18n.PwmLocaleBundle;
+import password.pwm.util.i18n.LocaleHelper;
+import password.pwm.util.java.JavaHelper;
+import password.pwm.util.java.StringUtil;
+
+import java.io.Serializable;
+import java.util.Locale;
+import java.util.Objects;
+
+public class StoredConfigItemKey implements Serializable, Comparable
+{
+    public enum RecordType
+    {
+        SETTING( "Setting" ),
+        LOCALE_BUNDLE ( "Localization" ),
+        PROPERTY ( "Property" ),;
+
+        private final String label;
+
+        RecordType( final String label )
+        {
+            this.label = label;
+        }
+
+        public String getLabel()
+        {
+            return label;
+        }
+    }
+
+    private final RecordType recordType;
+    private final String recordID;
+    private final String profileID;
+
+    private StoredConfigItemKey( final RecordType recordType, final String recordID, final String profileID )
+    {
+        Objects.requireNonNull( recordType, "recordType can not be null" );
+        Objects.requireNonNull( recordID, "recordID can not be null" );
+
+        this.recordType = recordType;
+        this.recordID = recordID;
+        this.profileID = profileID;
+    }
+
+    public RecordType getRecordType()
+    {
+        return recordType;
+    }
+
+    public String getRecordID()
+    {
+        return recordID;
+    }
+
+    public String getProfileID()
+    {
+        return profileID;
+    }
+
+    static StoredConfigItemKey fromSetting( final PwmSetting pwmSetting, final String profileID )
+    {
+        return new StoredConfigItemKey( RecordType.SETTING, pwmSetting.getKey(), profileID );
+    }
+
+    static StoredConfigItemKey fromLocaleBundle( final PwmLocaleBundle localeBundle, final String key )
+    {
+        return new StoredConfigItemKey( RecordType.LOCALE_BUNDLE, localeBundle.getKey(), key );
+    }
+
+    static StoredConfigItemKey fromConfigurationProperty( final ConfigurationProperty configurationProperty )
+    {
+        return new StoredConfigItemKey( RecordType.PROPERTY, configurationProperty.getKey(), null );
+    }
+
+    public boolean isValid()
+    {
+        try
+        {
+            validate();
+            return true;
+        }
+        catch ( final IllegalStateException e )
+        {
+            /* ignore */
+        }
+        return false;
+    }
+
+    public void validate()
+    {
+        switch ( recordType )
+        {
+            case SETTING:
+            {
+                final PwmSetting pwmSetting = this.toPwmSetting();
+                final boolean hasProfileID = !StringUtil.isEmpty( profileID );
+                if ( pwmSetting.getCategory().hasProfiles() && !hasProfileID )
+                {
+                    throw new IllegalStateException( "profileID is required for setting " + pwmSetting.getKey() );
+                }
+                else if ( !pwmSetting.getCategory().hasProfiles() && hasProfileID )
+                {
+                    throw new IllegalStateException( "profileID is not required for setting " + pwmSetting.getKey() );
+                }
+            }
+            break;
+
+            case LOCALE_BUNDLE:
+            {
+                Objects.requireNonNull( profileID, "profileID is required when recordType is LOCALE_BUNDLE" );
+                final PwmLocaleBundle pwmLocaleBundle = toLocaleBundle();
+                if ( !pwmLocaleBundle.getDisplayKeys().contains( profileID ) )
+                {
+                    throw new IllegalStateException( "key '" + profileID + "' is unrecognized for locale bundle " + pwmLocaleBundle.name() );
+                }
+            }
+            break;
+
+            case PROPERTY:
+                break;
+
+            default:
+                JavaHelper.unhandledSwitchStatement( recordType );
+        }
+    }
+
+    public String getLabel( final Locale locale )
+    {
+        final String separator = LocaleHelper.getLocalizedMessage( locale, Config.Display_SettingNavigationSeparator, null );
+
+        switch ( recordType )
+        {
+            case SETTING:
+                return recordType.getLabel() + separator + toPwmSetting().toMenuLocationDebug( profileID, locale );
+
+            case PROPERTY:
+                return recordType.getLabel() + separator + this.getRecordID();
+
+            case LOCALE_BUNDLE:
+                return recordType.getLabel()
+                        + separator
+                        + this.getRecordID()
+                        + separator
+                        + this.getProfileID();
+
+            default:
+                JavaHelper.unhandledSwitchStatement( recordType );
+        }
+
+        throw new IllegalStateException(  );
+    }
+
+    public PwmSetting toPwmSetting()
+    {
+        if ( getRecordType() != RecordType.SETTING )
+        {
+            throw new IllegalStateException( "attempt to read pwmSetting key for non-setting ConfigItemKey" );
+        }
+
+        return PwmSetting.forKey( this.recordID );
+    }
+
+    public PwmLocaleBundle toLocaleBundle()
+    {
+        if ( getRecordType() != RecordType.LOCALE_BUNDLE )
+        {
+            throw new IllegalStateException( "attempt to read PwmLocaleBundle key for non-locale ConfigItemKey" );
+        }
+
+        return PwmLocaleBundle.forKey( this.recordID )
+                .orElseThrow( () -> new IllegalStateException( "unexpected key value for locale bundle: " + this.recordID ) );
+    }
+
+    public ConfigurationProperty toConfigurationProperty()
+    {
+        if ( getRecordType() != RecordType.PROPERTY )
+        {
+            throw new IllegalStateException( "attempt to read ConfigurationProperty key for non-config property ConfigItemKey" );
+        }
+
+        return ConfigurationProperty.valueOf( recordID );
+    }
+
+    @Override
+    public int hashCode()
+    {
+        return toString().hashCode();
+    }
+
+    @Override
+    public boolean equals( final Object anotherObject )
+    {
+        return anotherObject != null
+                && anotherObject instanceof StoredConfigItemKey
+                && toString().equals( anotherObject.toString() );
+    }
+
+    @Override
+    public String toString()
+    {
+        return getLabel( PwmConstants.DEFAULT_LOCALE );
+       // return getRecordType().name() + "-" + this.getRecordID() + "-" + this.getProfileID();
+    }
+
+    @Override
+    public int compareTo( final Object o )
+    {
+        return getLabel( PwmConstants.DEFAULT_LOCALE ).compareTo( o.toString() );
+    }
+}

+ 0 - 67
server/src/main/java/password/pwm/config/stored/StoredConfigReferenceBean.java

@@ -1,67 +0,0 @@
-/*
- * Password Management Servlets (PWM)
- * http://www.pwm-project.org
- *
- * Copyright (c) 2006-2009 Novell, Inc.
- * Copyright (c) 2009-2019 The PWM Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package password.pwm.config.stored;
-
-import lombok.Value;
-
-import java.io.Serializable;
-
-@Value
-public class StoredConfigReferenceBean implements StoredConfigReference, Serializable, Comparable
-{
-    private RecordType recordType;
-    private String recordID;
-    private String profileID;
-
-    public StoredConfigReferenceBean( final RecordType type, final String recordID, final String profileID )
-    {
-        if ( type == null )
-        {
-            throw new NullPointerException( "recordType can not be null" );
-        }
-
-        if ( recordID == null )
-        {
-            throw new NullPointerException( "recordID can not be null" );
-        }
-
-        this.recordType = type;
-        this.recordID = recordID;
-        this.profileID = profileID;
-    }
-
-
-    @Override
-    public String toString( )
-    {
-        return this.getRecordType().toString()
-                + "-"
-                + ( this.getProfileID() == null ? "" : this.getProfileID() )
-                + "-"
-                + this.getRecordID();
-    }
-
-    @Override
-    public int compareTo( final Object o )
-    {
-        return toString().compareTo( o.toString() );
-    }
-}

+ 50 - 0
server/src/main/java/password/pwm/config/stored/StoredConfigXmlConstants.java

@@ -0,0 +1,50 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2019 The PWM Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package password.pwm.config.stored;
+
+public class StoredConfigXmlConstants
+{
+    public static final String XML_ATTRIBUTE_TYPE = "type";
+    public static final String XML_ELEMENT_ROOT = "PwmConfiguration";
+    public static final String XML_ELEMENT_PROPERTIES = "properties";
+    public static final String XML_ELEMENT_PROPERTY = "property";
+    public static final String XML_ELEMENT_SETTINGS = "settings";
+    public static final String XML_ELEMENT_SETTING = "setting";
+    public static final String XML_ELEMENT_DEFAULT = "default";
+    public static final String XML_ELEMENT_LOCALEBUNDLE = "localeBundle";
+    public static final String XML_ELEMENT_LABEL = "label";
+    public static final String XML_ELEMENT_VALUE = "value";
+
+    public static final String XML_ATTRIBUTE_KEY = "key";
+    public static final String XML_ATTRIBUTE_SYNTAX = "syntax";
+    public static final String XML_ATTRIBUTE_PROFILE = "profile";
+    public static final String XML_ATTRIBUTE_VALUE_APP = "app";
+    public static final String XML_ATTRIBUTE_VALUE_CONFIG = "config";
+    public static final String XML_ATTRIBUTE_CREATE_TIME = "createTime";
+    public static final String XML_ATTRIBUTE_MODIFY_TIME = "modifyTime";
+    public static final String XML_ATTRIBUTE_MODIFY_USER = "modifyUser";
+    public static final String XML_ATTRIBUTE_SYNTAX_VERSION = "syntaxVersion";
+    public static final String XML_ATTRIBUTE_BUNDLE = "bundle";
+    public static final String XML_ATTRIBUTE_XML_VERSION = "xmlVersion";
+    public static final String XML_ATTRRIBUTE_PWM_BUILD = "pwmBuild";
+    public static final String XML_ATTRIBUTE_PWM_VERSION = "pwmVersion";
+    public static final String XML_ATTRIBUTE_LOCALE = "locale";
+}

+ 18 - 51
server/src/main/java/password/pwm/config/stored/StoredConfiguration.java

@@ -20,81 +20,48 @@
 
 
 package password.pwm.config.stored;
 package password.pwm.config.stored;
 
 
-import password.pwm.bean.UserIdentity;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.PwmSetting;
-import password.pwm.config.PwmSettingCategory;
+import password.pwm.config.PwmSettingTemplateSet;
 import password.pwm.config.StoredValue;
 import password.pwm.config.StoredValue;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.i18n.PwmLocaleBundle;
 import password.pwm.util.secure.PwmSecurityKey;
 import password.pwm.util.secure.PwmSecurityKey;
 
 
 import java.time.Instant;
 import java.time.Instant;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
 
 
 public interface StoredConfiguration
 public interface StoredConfiguration
 {
 {
-    String XML_ELEMENT_ROOT = "PwmConfiguration";
-    String XML_ELEMENT_PROPERTIES = "properties";
-    String XML_ELEMENT_PROPERTY = "property";
-    String XML_ELEMENT_SETTINGS = "settings";
-    String XML_ELEMENT_SETTING = "setting";
-    String XML_ELEMENT_DEFAULT = "default";
-    String XML_ELEMENT_LOCALEBUNDLE = "localeBundle";
-
-    String XML_ATTRIBUTE_TYPE = "type";
-    String XML_ATTRIBUTE_KEY = "key";
-    String XML_ATTRIBUTE_SYNTAX = "syntax";
-    String XML_ATTRIBUTE_PROFILE = "profile";
-    String XML_ATTRIBUTE_VALUE_APP = "app";
-    String XML_ATTRIBUTE_VALUE_CONFIG = "config";
-    String XML_ATTRIBUTE_CREATE_TIME = "createTime";
-    String XML_ATTRIBUTE_MODIFY_TIME = "modifyTime";
-    String XML_ATTRIBUTE_MODIFY_USER = "modifyUser";
-    String XML_ATTRIBUTE_MODIFY_USER_PROFILE = "modifyUserProfile";
-    String XML_ATTRIBUTE_SYNTAX_VERSION = "syntaxVersion";
-    String XML_ATTRIBUTE_BUNDLE = "bundle";
-
-
     PwmSecurityKey getKey( ) throws PwmUnrecoverableException;
     PwmSecurityKey getKey( ) throws PwmUnrecoverableException;
 
 
-    Instant modifyTime( );
+    String createTime();
 
 
-    boolean isLocked( );
+    Instant modifyTime( );
 
 
-    String readConfigProperty( ConfigurationProperty propertyName );
+    Optional<String> readConfigProperty( ConfigurationProperty propertyName );
 
 
-    void writeConfigProperty(
-            ConfigurationProperty propertyName,
-            String value
-    );
+    PwmSettingTemplateSet getTemplateSet();
 
 
-    void lock( );
+    List<String> profilesForSetting( PwmSetting pwmSetting );
 
 
     ValueMetaData readSettingMetadata( PwmSetting setting, String profileID );
     ValueMetaData readSettingMetadata( PwmSetting setting, String profileID );
 
 
-    void resetSetting( PwmSetting setting, String profileID, UserIdentity userIdentity );
+    Map<String, String> readLocaleBundleMap( PwmLocaleBundle bundleName, String keyName );
 
 
-    boolean isDefaultValue( PwmSetting setting );
+    StoredValue readSetting( PwmSetting setting, String profileID );
 
 
     boolean isDefaultValue( PwmSetting setting, String profileID );
     boolean isDefaultValue( PwmSetting setting, String profileID );
 
 
-    StoredValue readSetting( PwmSetting setting );
-
-    StoredValue readSetting( PwmSetting setting, String profileID );
-
-    void copyProfileID( PwmSettingCategory category, String sourceID, String destinationID, UserIdentity userIdentity )
-            throws PwmUnrecoverableException;
+    String valueHash();
 
 
-    void writeSetting(
-            PwmSetting setting,
-            StoredValue value,
-            UserIdentity userIdentity
-    ) throws PwmUnrecoverableException;
+    Set<StoredConfigItemKey> modifiedItems();
 
 
-    void writeSetting(
-            PwmSetting setting,
-            String profileID,
-            StoredValue value,
-            UserIdentity userIdentity
-    ) throws PwmUnrecoverableException;
+    Optional<ValueMetaData> readMetaData( StoredConfigItemKey storedConfigItemKey );
 
 
+    Optional<StoredValue> readStoredValue( StoredConfigItemKey storedConfigItemKey );
 
 
+    StoredConfiguration copy();
 }
 }

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

@@ -20,14 +20,682 @@
 
 
 package password.pwm.config.stored;
 package password.pwm.config.stored;
 
 
+import lombok.Builder;
+import lombok.Value;
+import password.pwm.PwmConstants;
+import password.pwm.bean.UserIdentity;
+import password.pwm.config.PwmSetting;
+import password.pwm.config.PwmSettingFlag;
+import password.pwm.config.PwmSettingSyntax;
+import password.pwm.config.PwmSettingTemplate;
+import password.pwm.config.PwmSettingTemplateSet;
+import password.pwm.config.StoredValue;
+import password.pwm.config.value.LocalizedStringValue;
+import password.pwm.config.value.StoredValueEncoder;
+import password.pwm.config.value.StringValue;
+import password.pwm.config.value.ValueFactory;
+import password.pwm.error.PwmException;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.i18n.PwmLocaleBundle;
+import password.pwm.util.java.JavaHelper;
+import password.pwm.util.java.LazySupplier;
+import password.pwm.util.java.StringUtil;
+import password.pwm.util.java.XmlDocument;
+import password.pwm.util.java.XmlElement;
+import password.pwm.util.java.XmlFactory;
+import password.pwm.util.logging.PwmLogger;
+import password.pwm.util.macro.MacroMachine;
+import password.pwm.util.secure.PwmSecurityKey;
 
 
+import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStream;
 import java.io.OutputStream;
 import java.io.OutputStream;
+import java.time.Instant;
+import java.time.format.DateTimeParseException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.ResourceBundle;
+import java.util.Set;
 
 
-interface StoredConfigurationFactory
+public class StoredConfigurationFactory
 {
 {
-    StoredConfiguration fromXml( InputStream inputStream ) throws PwmUnrecoverableException;
+    private static final PwmLogger LOGGER = PwmLogger.forClass( StoredConfigurationFactory.class );
 
 
-    void toXml( OutputStream outputStream );
+    private static final String XML_FORMAT_VERSION = "5";
+
+
+    public static StoredConfiguration newConfig() throws PwmUnrecoverableException
+    {
+        final StoredConfiguration storedConfiguration = new StoredConfigurationImpl(  );
+        final StoredConfigurationModifier modifier = StoredConfigurationModifier.newModifier( storedConfiguration );
+
+        StoredConfigurationUtil.initNewRandomSecurityKey( modifier );
+        modifier.writeConfigProperty(
+                ConfigurationProperty.CONFIG_IS_EDITABLE, Boolean.toString( true ) );
+        modifier.writeConfigProperty(
+                ConfigurationProperty.CONFIG_EPOCH, String.valueOf( 0 ) );
+
+
+        return modifier.newStoredConfiguration();
+    }
+
+    public static StoredConfigurationModifier newModifiableConfig() throws PwmUnrecoverableException
+    {
+        final StoredConfiguration storedConfiguration = new StoredConfigurationImpl(  );
+        final StoredConfigurationModifier modifier = StoredConfigurationModifier.newModifier( storedConfiguration );
+
+        StoredConfigurationUtil.initNewRandomSecurityKey( modifier );
+        modifier.writeConfigProperty(
+                ConfigurationProperty.CONFIG_IS_EDITABLE, Boolean.toString( true ) );
+        modifier.writeConfigProperty(
+                ConfigurationProperty.CONFIG_EPOCH, String.valueOf( 0 ) );
+
+
+        return modifier;
+    }
+
+    public static StoredConfiguration fromXml( final InputStream inputStream )
+            throws PwmUnrecoverableException
+    {
+        final XmlFactory xmlFactory = XmlFactory.getFactory();
+        final XmlDocument xmlDocument = xmlFactory.parseXml( inputStream );
+        ConfigurationCleaner.preProcessXml( xmlDocument );
+
+        final XmlInputDocumentReader xmlInputDocumentReader = new XmlInputDocumentReader( xmlDocument );
+        final StoredConfigData storedConfigData = xmlInputDocumentReader.getStoredConfigData();
+        final StoredConfigurationModifier modifier = StoredConfigurationModifier.newModifier(  new StoredConfigurationImpl( storedConfigData ) );
+        ConfigurationCleaner.postProcessStoredConfig( modifier );
+
+        return modifier.newStoredConfiguration();
+    }
+
+    public static void toXml(
+            final StoredConfiguration storedConfiguration,
+            final OutputStream outputStream
+    )
+            throws PwmUnrecoverableException, IOException
+    {
+        toXml( storedConfiguration, outputStream, OutputSettings.builder().build() );
+    }
+
+    public static void toXml(
+            final StoredConfiguration storedConfiguration,
+            final OutputStream outputStream,
+            final OutputSettings outputSettings
+    )
+            throws PwmUnrecoverableException, IOException
+    {
+        final XmlFactory xmlFactory = XmlFactory.getFactory();
+        final XmlDocument xmlDocument = xmlFactory.newDocument( StoredConfigXmlConstants.XML_ELEMENT_ROOT );
+
+        XmlOutputHandler.makeXmlOutput( storedConfiguration, xmlDocument.getRootElement(), outputSettings );
+
+        xmlFactory.outputDocument( xmlDocument, outputStream );
+    }
+
+    static class XmlInputDocumentReader
+    {
+        private final XmlDocument document;
+
+        XmlInputDocumentReader( final XmlDocument document )
+        {
+            this.document = document;
+        }
+
+        StoredConfigData getStoredConfigData()
+        {
+            final String createTime = readCreateTime();
+            final Instant modifyTime = readModifyTime();
+
+            final List<StoredConfigData.ValueAndMetaCarrier> values = new ArrayList<>();
+            values.addAll( readProperties() );
+            values.addAll( readSettings() );
+            values.addAll( readLocaleBundles() );
+            return StoredConfigData.builder()
+                    .createTime( createTime )
+                    .modifyTime( modifyTime )
+                    .metaDatas( StoredConfigData.carrierAsMetaDataMap( values ) )
+                    .storedValues( StoredConfigData.carrierAsStoredValueMap( values ) )
+                    .build();
+        }
+
+        private Collection<StoredConfigData.ValueAndMetaCarrier> readProperties()
+        {
+            final List<StoredConfigData.ValueAndMetaCarrier> valueAndMetaWrapper = new ArrayList<>();
+            for ( final ConfigurationProperty configurationProperty : ConfigurationProperty.values() )
+            {
+                final Optional<XmlElement> propertyElement = xpathForConfigProperty( configurationProperty );
+                if ( propertyElement.isPresent() && !StringUtil.isEmpty( propertyElement.get().getText() ) )
+                {
+                    final StoredConfigItemKey key = StoredConfigItemKey.fromConfigurationProperty( configurationProperty );
+                    final StoredValue storedValue = new StringValue( propertyElement.get().getText() );
+                    final ValueMetaData metaData = readMetaDataFromXmlElement( key, propertyElement.get() ).orElse( null );
+                    valueAndMetaWrapper.add( new StoredConfigData.ValueAndMetaCarrier( key, storedValue, metaData ) );
+                }
+            }
+            return valueAndMetaWrapper;
+        }
+
+        private Collection<StoredConfigData.ValueAndMetaCarrier> readSettings()
+        {
+            final List<StoredConfigData.ValueAndMetaCarrier> returnList = new ArrayList<>();
+            for ( final PwmSetting pwmSetting : PwmSetting.values() )
+            {
+                if ( !pwmSetting.getCategory().hasProfiles() )
+                {
+                    readSetting( pwmSetting, null ).ifPresent( returnList::add );
+                }
+            }
+
+            for ( final PwmSetting pwmSetting : PwmSetting.values() )
+            {
+                if ( pwmSetting.getCategory().hasProfiles() )
+                {
+                    final List<String> profileIDs = profilesForSetting( pwmSetting );
+                    for ( final String profileID : profileIDs )
+                    {
+                        readSetting( pwmSetting, profileID ).ifPresent( returnList::add );
+                    }
+                }
+            }
+            return returnList;
+        }
+
+        Optional<StoredConfigData.ValueAndMetaCarrier> readSetting( final PwmSetting setting, final String profileID )
+        {
+            final Optional<XmlElement> settingElement = xpathForSetting( setting, profileID );
+
+            if ( !settingElement.isPresent() )
+            {
+                return Optional.empty();
+            }
+
+            if ( settingElement.get().getChild( StoredConfigXmlConstants.XML_ELEMENT_DEFAULT ).isPresent() )
+            {
+                return Optional.empty();
+            }
+
+            try
+            {
+                final StoredValue storedValue = ValueFactory.fromXmlValues( setting, settingElement.get(), getKey() );
+                final StoredConfigItemKey key = StoredConfigItemKey.fromSetting( setting, profileID );
+                final ValueMetaData metaData = readMetaDataFromXmlElement( key, settingElement.get() ).orElse( null );
+                return Optional.of( new StoredConfigData.ValueAndMetaCarrier( key, storedValue, metaData ) );
+            }
+            catch ( final PwmException e )
+            {
+                final String errorMsg = "unexpected error reading setting '" + setting.getKey() + "' profile '" + profileID + "', error: " + e.getMessage();
+                throw new IllegalStateException( errorMsg );
+            }
+        }
+
+        List<String> profilesForSetting( final PwmSetting pwmSetting )
+        {
+            if ( !pwmSetting.getCategory().hasProfiles() && pwmSetting.getSyntax() != PwmSettingSyntax.PROFILE )
+            {
+                return Collections.emptyList();
+            }
+
+            final PwmSetting profileSetting;
+            if ( pwmSetting.getSyntax() == PwmSettingSyntax.PROFILE )
+            {
+                profileSetting = pwmSetting;
+            }
+            else
+            {
+                profileSetting = pwmSetting.getCategory().getProfileSetting();
+            }
+
+            final StoredValue effectiveValue;
+            {
+                final Optional<StoredConfigData.ValueAndMetaCarrier> configuredValue = readSetting( profileSetting, null );
+                if ( configuredValue.isPresent() )
+                {
+                    effectiveValue = configuredValue.get().getValue();
+                }
+                else
+                {
+                    effectiveValue = profileSetting.getDefaultValue( templateSetSupplier.get() );
+                }
+            }
+
+            final List<String> settingValues = ( List<String> ) effectiveValue.toNativeObject();
+            final LinkedList<String> profiles = new LinkedList<>( settingValues );
+            profiles.removeIf( StringUtil::isEmpty );
+            return Collections.unmodifiableList( profiles );
+        }
+
+
+        public PwmSecurityKey getKey() throws PwmUnrecoverableException
+        {
+            final XmlElement rootElement = document.getRootElement();
+            final String createTimeString = rootElement.getAttributeValue( StoredConfigXmlConstants.XML_ATTRIBUTE_CREATE_TIME );
+            return new PwmSecurityKey( createTimeString + "StoredConfiguration" );
+        }
+
+        String readCreateTime()
+        {
+            final XmlElement rootElement = document.getRootElement();
+            final String createTimeString = rootElement.getAttributeValue( StoredConfigXmlConstants.XML_ATTRIBUTE_CREATE_TIME );
+            if ( StringUtil.isEmpty( createTimeString ) )
+            {
+                throw new IllegalStateException( "missing createTime timestamp" );
+            }
+            else
+            {
+                return createTimeString;
+            }
+        }
+
+        Instant readModifyTime()
+        {
+            final XmlElement rootElement = document.getRootElement();
+            final String modifyTimeString = rootElement.getAttributeValue( StoredConfigXmlConstants.XML_ATTRIBUTE_MODIFY_TIME );
+            if ( !StringUtil.isEmpty( modifyTimeString ) )
+            {
+                try
+                {
+                    return JavaHelper.parseIsoToInstant( modifyTimeString );
+                }
+                catch ( final Exception e )
+                {
+                    LOGGER.error( "error parsing root last modified timestamp: " + e.getMessage() );
+                }
+            }
+
+            return null;
+        }
+
+        private final LazySupplier<PwmSettingTemplateSet> templateSetSupplier = new LazySupplier<>( () ->
+        {
+            final Set<PwmSettingTemplate> templates = new HashSet<>();
+            templates.add( readTemplateValue( PwmSetting.TEMPLATE_LDAP ) );
+            templates.add( readTemplateValue( PwmSetting.TEMPLATE_STORAGE ) );
+            templates.add( readTemplateValue( PwmSetting.DB_VENDOR_TEMPLATE ) );
+            return new PwmSettingTemplateSet( templates );
+        } );
+
+        private PwmSettingTemplate readTemplateValue( final PwmSetting pwmSetting )
+        {
+            final Optional<XmlElement> settingElement = xpathForSetting( pwmSetting, null );
+            if ( settingElement.isPresent() )
+            {
+                try
+                {
+                    final String strValue = ( String ) ValueFactory.fromXmlValues( pwmSetting, settingElement.get(), null ).toNativeObject();
+                    return JavaHelper.readEnumFromString( PwmSettingTemplate.class, null, strValue );
+                }
+                catch ( final IllegalStateException e )
+                {
+                    LOGGER.error( "error reading template", e );
+                }
+            }
+            return null;
+        }
+
+        private Collection<StoredConfigData.ValueAndMetaCarrier> readLocaleBundles()
+        {
+            final List<StoredConfigData.ValueAndMetaCarrier> returnWrapper = new ArrayList<>();
+
+            for ( final XmlElement localeBundleElement : xpathForLocaleBundles() )
+            {
+                final String bundleName = localeBundleElement.getAttributeValue( StoredConfigXmlConstants.XML_ATTRIBUTE_BUNDLE );
+                final Optional<PwmLocaleBundle> pwmLocaleBundle = PwmLocaleBundle.forKey( bundleName );
+                pwmLocaleBundle.ifPresent( ( bundle ) ->
+                {
+                    final String key = localeBundleElement.getAttributeValue( StoredConfigXmlConstants.XML_ATTRIBUTE_KEY );
+                    if ( bundle.getDisplayKeys().contains( key ) )
+                    {
+                        final Map<String, String> bundleMap = new LinkedHashMap<>();
+                        for ( final XmlElement valueElement : localeBundleElement.getChildren( StoredConfigXmlConstants.XML_ELEMENT_VALUE ) )
+                        {
+                            final String localeStrValue = valueElement.getAttributeValue( StoredConfigXmlConstants.XML_ATTRIBUTE_LOCALE );
+                            bundleMap.put( localeStrValue == null ? "" : localeStrValue, valueElement.getText() );
+                        }
+                        if ( !bundleMap.isEmpty() )
+                        {
+                            final StoredConfigItemKey storedConfigItemKey = StoredConfigItemKey.fromLocaleBundle( pwmLocaleBundle.get(), key );
+                            final StoredValue storedValue = new LocalizedStringValue( bundleMap );
+                            final ValueMetaData metaData = readMetaDataFromXmlElement( storedConfigItemKey, localeBundleElement ).orElse( null );
+                            returnWrapper.add( new StoredConfigData.ValueAndMetaCarrier( storedConfigItemKey, storedValue, metaData ) );
+                        }
+                    }
+                } );
+            }
+            return Collections.unmodifiableList( returnWrapper );
+        }
+
+        private Optional<ValueMetaData> readMetaDataFromXmlElement( final StoredConfigItemKey key, final XmlElement xmlElement )
+        {
+            Instant instant = null;
+            {
+                final String modifyTimeValue = xmlElement.getAttributeValue( StoredConfigXmlConstants.XML_ATTRIBUTE_MODIFY_TIME );
+                if ( !StringUtil.isEmpty( modifyTimeValue ) )
+                {
+                    try
+                    {
+                        instant = JavaHelper.parseIsoToInstant( modifyTimeValue );
+                    }
+                    catch ( final DateTimeParseException e )
+                    {
+                        e.printStackTrace();
+                    }
+                }
+            }
+
+            UserIdentity userIdentity = null;
+            {
+                final String modifyUserValue = xmlElement.getAttributeValue( StoredConfigXmlConstants.XML_ATTRIBUTE_MODIFY_USER );
+                if ( !StringUtil.isEmpty( modifyUserValue ) )
+                {
+                    try
+                    {
+                        userIdentity = UserIdentity.fromDelimitedKey( modifyUserValue );
+                    }
+                    catch ( final DateTimeParseException | PwmUnrecoverableException e )
+                    {
+                        LOGGER.trace( () -> "unable to parse userIdentity metadata for key " + key.toString() );
+                    }
+                }
+            }
+
+            if ( instant != null || userIdentity != null )
+            {
+                return Optional.of( new ValueMetaData( instant, userIdentity ) );
+            }
+
+            return Optional.empty();
+        }
+
+        List<XmlElement> xpathForLocaleBundles()
+        {
+            final String xpathString = "//localeBundle";
+            return document.evaluateXpathToElements( xpathString );
+        }
+
+        XmlElement xpathForSettings()
+        {
+            return document.getRootElement().getChild( StoredConfigXmlConstants.XML_ELEMENT_SETTINGS )
+                    .orElseThrow( () -> new IllegalStateException( "configuration xml document missing 'settings' element" ) );
+        }
+
+        Optional<XmlElement> xpathForSetting( final PwmSetting setting, final String profileID )
+        {
+            final String xpathString;
+            if ( profileID == null || profileID.length() < 1 )
+            {
+                xpathString = "//" + StoredConfigXmlConstants.XML_ELEMENT_SETTING + "[@" + StoredConfigXmlConstants.XML_ATTRIBUTE_KEY
+                        + "=\"" + setting.getKey()
+                        + "\"][(not (@" + StoredConfigXmlConstants.XML_ATTRIBUTE_PROFILE + ")) or @"
+                        + StoredConfigXmlConstants.XML_ATTRIBUTE_PROFILE + "=\"\"]";
+            }
+            else
+            {
+                xpathString = "//" + StoredConfigXmlConstants.XML_ELEMENT_SETTING + "[@" + StoredConfigXmlConstants.XML_ATTRIBUTE_KEY
+                        + "=\"" + setting.getKey()
+                        + "\"][@" + StoredConfigXmlConstants.XML_ATTRIBUTE_PROFILE + "=\"" + profileID + "\"]";
+            }
+
+            return document.evaluateXpathToElement( xpathString );
+        }
+
+        Optional<XmlElement> xpathForConfigProperty( final ConfigurationProperty configProperty )
+        {
+            final String xpathString = "//" + StoredConfigXmlConstants.XML_ELEMENT_PROPERTIES + "[@" + StoredConfigXmlConstants.XML_ATTRIBUTE_TYPE
+                    + "=\"" + StoredConfigXmlConstants.XML_ATTRIBUTE_VALUE_CONFIG + "\"]/"
+                    + StoredConfigXmlConstants.XML_ELEMENT_PROPERTY + "[@" + StoredConfigXmlConstants.XML_ATTRIBUTE_KEY + "=\"" + configProperty.getKey() + "\"]";
+            return document.evaluateXpathToElement( xpathString );
+        }
+
+        List<XmlElement> xpathForAppProperties( )
+        {
+            final String xpathString = "//" + StoredConfigXmlConstants.XML_ELEMENT_PROPERTIES
+                    + "[@" + StoredConfigXmlConstants.XML_ATTRIBUTE_TYPE + "=\"" + StoredConfigXmlConstants.XML_ATTRIBUTE_VALUE_APP + "\"]";
+            return document.evaluateXpathToElements( xpathString );
+        }
+    }
+
+
+    static class XmlOutputHandler
+    {
+        static void makeXmlOutput( final StoredConfiguration storedConfiguration, final XmlElement rootElement, final OutputSettings outputSettings )
+                throws PwmUnrecoverableException
+        {
+            decorateRootElement( rootElement, storedConfiguration );
+
+            rootElement.addContent( makePropertiesElement( storedConfiguration ) );
+
+            rootElement.addContent( makeSettingsXmlElement( storedConfiguration, outputSettings ) );
+
+            rootElement.addContent( XmlOutputHandler.makeLocaleBundleXmlElements( storedConfiguration ) );
+        }
+
+        static void decorateRootElement( final XmlElement rootElement, final StoredConfiguration storedConfiguration )
+        {
+            rootElement.setComment( Collections.singletonList( generateCommentText() ) );
+            rootElement.setAttribute( StoredConfigXmlConstants.XML_ATTRIBUTE_PWM_VERSION, PwmConstants.BUILD_VERSION );
+            rootElement.setAttribute( StoredConfigXmlConstants.XML_ATTRRIBUTE_PWM_BUILD, PwmConstants.BUILD_NUMBER );
+            rootElement.setAttribute( StoredConfigXmlConstants.XML_ATTRIBUTE_XML_VERSION, XML_FORMAT_VERSION );
+
+            rootElement.setAttribute( StoredConfigXmlConstants.XML_ATTRIBUTE_CREATE_TIME, storedConfiguration.createTime() );
+            rootElement.setAttribute( StoredConfigXmlConstants.XML_ATTRIBUTE_MODIFY_TIME, JavaHelper.toIsoDate( storedConfiguration.modifyTime() ) );
+        }
+
+        static XmlElement makeSettingsXmlElement(
+                final StoredConfiguration storedConfiguration,
+                final OutputSettings outputSettings
+        )
+                throws PwmUnrecoverableException
+        {
+            final PwmSecurityKey pwmSecurityKey = storedConfiguration.getKey();
+
+            final XmlFactory xmlFactory = XmlFactory.getFactory();
+            final XmlElement settingsElement = xmlFactory.newElement( StoredConfigXmlConstants.XML_ELEMENT_SETTINGS );
+
+            final XmlOutputProcessData xmlOutputProcessData = XmlOutputProcessData.builder()
+                    .pwmSecurityKey( pwmSecurityKey )
+                    .storedValueEncoderMode( figureEncoderMode( storedConfiguration, outputSettings ) )
+                    .build();
+
+            for ( final PwmSetting pwmSetting : PwmSetting.sortedByMenuLocation( PwmConstants.DEFAULT_LOCALE ) )
+            {
+                if ( !pwmSetting.getFlags().contains( PwmSettingFlag.Deprecated ) )
+                {
+                    if ( pwmSetting.getCategory().hasProfiles() )
+                    {
+                        for ( final String profileID : storedConfiguration.profilesForSetting( pwmSetting ) )
+                        {
+                            final StoredValue storedValue = storedConfiguration.readSetting( pwmSetting, profileID );
+                            final XmlElement settingElement = makeSettingXmlElement( storedConfiguration, pwmSetting, profileID, storedValue, xmlOutputProcessData );
+                            decorateElementWithMetaData( storedConfiguration, StoredConfigItemKey.fromSetting( pwmSetting, profileID ), settingElement );
+                            settingsElement.addContent( settingElement );
+                        }
+                    }
+                    else
+                    {
+                        final StoredValue storedValue = storedConfiguration.readSetting( pwmSetting, null );
+                        final XmlElement settingElement = makeSettingXmlElement( storedConfiguration, pwmSetting, null, storedValue, xmlOutputProcessData );
+                        decorateElementWithMetaData( storedConfiguration, StoredConfigItemKey.fromSetting( pwmSetting, null ), settingElement );
+                        settingsElement.addContent( settingElement );
+                    }
+                }
+            }
+
+            return settingsElement;
+        }
+
+        static StoredValueEncoder.Mode figureEncoderMode(
+                final StoredConfiguration storedConfiguration,
+                final OutputSettings outputSettings
+        )
+        {
+            if ( outputSettings == null || outputSettings.getMode() == null )
+            {
+                return StoredValueEncoder.Mode.ENCODED;
+            }
+
+            if ( outputSettings.getMode() == OutputSettings.SecureOutputMode.STRIPPED )
+            {
+                return StoredValueEncoder.Mode.STRIPPED;
+            }
+
+            final Optional<String> strValue = storedConfiguration.readConfigProperty( ConfigurationProperty.STORE_PLAINTEXT_VALUES );
+            if ( strValue.isPresent() && Boolean.parseBoolean( strValue.get() ) )
+            {
+                return StoredValueEncoder.Mode.PLAIN;
+            }
+
+            return StoredValueEncoder.Mode.ENCODED;
+        }
+
+
+        static XmlElement makeSettingXmlElement(
+                final StoredConfiguration storedConfiguration,
+                final PwmSetting pwmSetting,
+                final String profileID,
+                final StoredValue storedValue,
+                final XmlOutputProcessData xmlOutputProcessData
+        )
+        {
+            final XmlFactory xmlFactory = XmlFactory.getFactory();
+
+            final XmlElement settingElement = xmlFactory.newElement( StoredConfigXmlConstants.XML_ELEMENT_SETTING );
+            settingElement.setAttribute( StoredConfigXmlConstants.XML_ATTRIBUTE_KEY, pwmSetting.getKey() );
+
+            if ( !StringUtil.isEmpty( profileID ) )
+            {
+                settingElement.setAttribute( StoredConfigXmlConstants.XML_ATTRIBUTE_PROFILE, profileID );
+            }
+
+            settingElement.setAttribute( StoredConfigXmlConstants.XML_ATTRIBUTE_SYNTAX, pwmSetting.getSyntax().name() );
+
+            {
+                final XmlElement labelElement = xmlFactory.newElement( StoredConfigXmlConstants.XML_ELEMENT_LABEL );
+                labelElement.addText( pwmSetting.toMenuLocationDebug( profileID, PwmConstants.DEFAULT_LOCALE ) );
+                settingElement.addContent( labelElement );
+            }
+
+            final List<XmlElement> valueElements = new ArrayList<>(  );
+            if ( storedConfiguration != null && storedConfiguration.isDefaultValue( pwmSetting, profileID ) )
+            {
+                final XmlElement defaultValue = xmlFactory.newElement( StoredConfigXmlConstants.XML_ELEMENT_DEFAULT );
+                valueElements.add( defaultValue );
+            }
+            else
+            {
+                valueElements.addAll( storedValue.toXmlValues( StoredConfigXmlConstants.XML_ELEMENT_VALUE, xmlOutputProcessData ) );
+            }
+
+            settingElement.setAttribute( StoredConfigXmlConstants.XML_ATTRIBUTE_SYNTAX_VERSION, String.valueOf( storedValue.currentSyntaxVersion() ) );
+            settingElement.addContent( valueElements );
+            return settingElement;
+        }
+
+        private static void decorateElementWithMetaData(
+                final StoredConfiguration storedConfiguration,
+                final StoredConfigItemKey key,
+                final XmlElement xmlElement
+        )
+        {
+            final Optional<ValueMetaData> valueMetaData = ( ( StoredConfiguration ) storedConfiguration ).readMetaData( key );
+
+            if ( valueMetaData.isPresent() )
+            {
+                if ( valueMetaData.get().getUserIdentity() != null )
+                {
+                    xmlElement.setAttribute( StoredConfigXmlConstants.XML_ATTRIBUTE_MODIFY_USER, valueMetaData.get().getUserIdentity().toDelimitedKey() );
+                }
+
+                if ( valueMetaData.get().getModifyDate() != null )
+                {
+                    xmlElement.setAttribute( StoredConfigXmlConstants.XML_ATTRIBUTE_MODIFY_TIME, JavaHelper.toIsoDate( valueMetaData.get().getModifyDate() ) );
+                }
+            }
+        }
+
+        private static XmlElement makePropertiesElement( final StoredConfiguration storedConfiguration )
+        {
+            final XmlFactory xmlFactory = XmlFactory.getFactory();
+            final XmlElement propertiesElement = xmlFactory.newElement( StoredConfigXmlConstants.XML_ELEMENT_PROPERTIES );
+            propertiesElement.setAttribute( StoredConfigXmlConstants.XML_ATTRIBUTE_TYPE, StoredConfigXmlConstants.XML_ATTRIBUTE_VALUE_CONFIG );
+
+            for ( final ConfigurationProperty configurationProperty : ConfigurationProperty.values() )
+            {
+                storedConfiguration.readConfigProperty( configurationProperty ).ifPresent( s ->
+                        {
+                            final XmlElement propertyElement = xmlFactory.newElement( StoredConfigXmlConstants.XML_ELEMENT_PROPERTY );
+                            propertyElement.setAttribute( StoredConfigXmlConstants.XML_ATTRIBUTE_KEY, configurationProperty.getKey() );
+                            propertyElement.addText( s );
+                            decorateElementWithMetaData( storedConfiguration, StoredConfigItemKey.fromConfigurationProperty( configurationProperty ), propertyElement );
+                            propertiesElement.addContent( propertyElement );
+                        }
+                );
+            }
+
+            return propertiesElement;
+        }
+
+        private static List<XmlElement> makeLocaleBundleXmlElements( final StoredConfiguration storedConfiguration )
+        {
+            final XmlFactory xmlFactory = XmlFactory.getFactory();
+            final List<XmlElement> returnList = new ArrayList<>();
+            for ( final PwmLocaleBundle pwmLocaleBundle : PwmLocaleBundle.values() )
+            {
+                for ( final String key : pwmLocaleBundle.getDisplayKeys() )
+                {
+                    final Map<String, String> localeBundle = storedConfiguration.readLocaleBundleMap( pwmLocaleBundle, key );
+                    if ( !JavaHelper.isEmpty( localeBundle ) )
+                    {
+                        final XmlElement localeBundleElement = xmlFactory.newElement( StoredConfigXmlConstants.XML_ELEMENT_LOCALEBUNDLE );
+                        localeBundleElement.setAttribute( StoredConfigXmlConstants.XML_ATTRIBUTE_BUNDLE, pwmLocaleBundle.getKey() );
+                        localeBundleElement.setAttribute( StoredConfigXmlConstants.XML_ATTRIBUTE_KEY, key );
+
+                        final Map<String, String> localeBundleMap = storedConfiguration.readLocaleBundleMap( pwmLocaleBundle, key );
+                        for ( final Map.Entry<String, String> entry : localeBundleMap.entrySet() )
+                        {
+                            final XmlElement valueElement = xmlFactory.newElement( StoredConfigXmlConstants.XML_ELEMENT_VALUE );
+                            if ( !StringUtil.isEmpty( entry.getKey() ) )
+                            {
+                                valueElement.setAttribute( StoredConfigXmlConstants.XML_ATTRIBUTE_LOCALE, entry.getKey() );
+                            }
+                            valueElement.addText( entry.getValue() );
+                            localeBundleElement.addContent( valueElement );
+                        }
+
+                        decorateElementWithMetaData( storedConfiguration, StoredConfigItemKey.fromLocaleBundle( pwmLocaleBundle, key ), localeBundleElement );
+                        returnList.add( localeBundleElement );
+                    }
+                }
+            }
+            return Collections.unmodifiableList( returnList );
+        }
+
+        private static String generateCommentText()
+        {
+            final String resourceText = ResourceBundle.getBundle( StoredConfigurationFactory.class.getName() ).getString( "configCommentText" );
+            return MacroMachine.forStatic().expandMacros( resourceText );
+        }
+    }
+
+    @Value
+    @Builder
+    public static class OutputSettings
+    {
+        @Builder.Default
+        private SecureOutputMode mode = SecureOutputMode.NORMAL;
+
+        public enum SecureOutputMode
+        {
+            NORMAL,
+            STRIPPED,
+        }
+    }
 }
 }
+

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

@@ -20,429 +20,163 @@
 
 
 package password.pwm.config.stored;
 package password.pwm.config.stored;
 
 
-import password.pwm.AppProperty;
-import password.pwm.PwmConstants;
-import password.pwm.bean.UserIdentity;
-import password.pwm.config.Configuration;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.PwmSetting;
-import password.pwm.config.PwmSettingCategory;
-import password.pwm.config.PwmSettingSyntax;
 import password.pwm.config.PwmSettingTemplate;
 import password.pwm.config.PwmSettingTemplate;
 import password.pwm.config.PwmSettingTemplateSet;
 import password.pwm.config.PwmSettingTemplateSet;
 import password.pwm.config.StoredValue;
 import password.pwm.config.StoredValue;
-import password.pwm.config.value.NamedSecretValue;
-import password.pwm.config.value.PasswordValue;
-import password.pwm.config.value.PrivateKeyValue;
-import password.pwm.config.value.StringArrayValue;
-import password.pwm.config.value.ValueFactory;
-import password.pwm.error.ErrorInformation;
-import password.pwm.error.PwmError;
-import password.pwm.error.PwmException;
-import password.pwm.error.PwmOperationalException;
+import password.pwm.config.value.LocalizedStringValue;
+import password.pwm.config.value.StringValue;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.error.PwmUnrecoverableException;
-import password.pwm.i18n.Config;
 import password.pwm.i18n.PwmLocaleBundle;
 import password.pwm.i18n.PwmLocaleBundle;
-import password.pwm.util.PasswordData;
-import password.pwm.util.i18n.LocaleHelper;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.JavaHelper;
-import password.pwm.util.java.JsonUtil;
-import password.pwm.util.java.StringUtil;
-import password.pwm.util.java.TimeDuration;
-import password.pwm.util.java.XmlDocument;
-import password.pwm.util.java.XmlElement;
-import password.pwm.util.java.XmlFactory;
+import password.pwm.util.java.LazySupplier;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.logging.PwmLogger;
-import password.pwm.util.secure.BCrypt;
-import password.pwm.util.secure.PwmRandom;
+import password.pwm.util.secure.HmacAlgorithm;
 import password.pwm.util.secure.PwmSecurityKey;
 import password.pwm.util.secure.PwmSecurityKey;
 import password.pwm.util.secure.SecureEngine;
 import password.pwm.util.secure.SecureEngine;
 
 
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.io.Serializable;
 import java.time.Instant;
 import java.time.Instant;
 import java.util.ArrayList;
 import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collection;
 import java.util.Collections;
 import java.util.Collections;
-import java.util.HashMap;
 import java.util.HashSet;
 import java.util.HashSet;
-import java.util.Iterator;
-import java.util.LinkedHashMap;
-import java.util.LinkedHashSet;
-import java.util.LinkedList;
 import java.util.List;
 import java.util.List;
-import java.util.Locale;
 import java.util.Map;
 import java.util.Map;
-import java.util.Queue;
-import java.util.ResourceBundle;
+import java.util.Objects;
+import java.util.Optional;
 import java.util.Set;
 import java.util.Set;
-import java.util.SortedSet;
 import java.util.TreeMap;
 import java.util.TreeMap;
-import java.util.TreeSet;
-import java.util.concurrent.locks.ReentrantReadWriteLock;
-import java.util.stream.Collectors;
+import java.util.function.Supplier;
 
 
 /**
 /**
+ * Immutable in-memory configuration.
+ *
  * @author Jason D. Rivard
  * @author Jason D. Rivard
  */
  */
-@SuppressWarnings( "all" ) // this class will be replaced by NGStoredConfiguration
 public class StoredConfigurationImpl implements StoredConfiguration
 public class StoredConfigurationImpl implements StoredConfiguration
 {
 {
+    private final String createTime;
+    private final Instant modifyTime;
+    private final Map<StoredConfigItemKey, StoredValue> storedValues;
+    private final Map<StoredConfigItemKey, ValueMetaData> metaValues;
+    private final PwmSettingTemplateSet templateSet;
 
 
-    private static final PwmLogger LOGGER = PwmLogger.forClass( StoredConfigurationImpl.class );
-    static final String XML_FORMAT_VERSION = "4";
-
-    private XmlDocument document = XmlFactory.getFactory().newDocument( XML_ELEMENT_ROOT );
-    private ChangeLog changeLog = new ChangeLog();
-
-    private boolean locked;
-    private final boolean setting_writeLabels = true;
-    private final ReentrantReadWriteLock domModifyLock = new ReentrantReadWriteLock();
+    private final transient Supplier<String> valueHashSupplier = new LazySupplier<>( this::valueHashImpl );
 
 
-    private final XmlHelper xmlHelper = new XmlHelper();
+    private static final PwmLogger LOGGER = PwmLogger.forClass( StoredConfigurationImpl.class );
 
 
-    public static StoredConfigurationImpl newStoredConfiguration( ) throws PwmUnrecoverableException
+    StoredConfigurationImpl( final StoredConfigData storedConfigData )
     {
     {
-        return new StoredConfigurationImpl();
-    }
+        this.createTime = storedConfigData.getCreateTime();
+        this.modifyTime = storedConfigData.getModifyTime();
+        this.metaValues = Collections.unmodifiableMap(  new TreeMap<>( storedConfigData.getMetaDatas() ) );
+        this.templateSet = readTemplateSet( storedConfigData.getStoredValues() );
 
 
-    public static StoredConfigurationImpl copy( final StoredConfigurationImpl input ) throws PwmUnrecoverableException
-    {
-        final StoredConfigurationImpl copy = new StoredConfigurationImpl();
-        copy.document = input.document.copy();
-        return copy;
+        final Map<StoredConfigItemKey, StoredValue> tempMap = new TreeMap<>( storedConfigData.getStoredValues() );
+        removeAllDefaultValues( tempMap, templateSet );
+        this.storedValues = Collections.unmodifiableMap( tempMap );
     }
     }
 
 
-    public static StoredConfigurationImpl fromXml( final InputStream xmlData )
-            throws PwmUnrecoverableException
+    StoredConfigurationImpl()
     {
     {
-        final Instant startTime = Instant.now();
-        //validateXmlSchema(xmlData);
-
-        final XmlDocument inputDocument = XmlFactory.getFactory().parseXml( xmlData );
-        final StoredConfigurationImpl newConfiguration = StoredConfigurationImpl.newStoredConfiguration();
-
-        try
-        {
-            newConfiguration.document = inputDocument;
-            newConfiguration.createTime(); // verify create time;
-            ConfigurationCleaner.cleanup( newConfiguration, newConfiguration.document );
-        }
-        catch ( Exception e )
-        {
-            final String errorMsg = "error reading configuration file format, error=" + e.getMessage();
-            final ErrorInformation errorInfo = new ErrorInformation( PwmError.CONFIG_FORMAT_ERROR, null, new String[] { errorMsg } );
-            throw new PwmUnrecoverableException( errorInfo );
-        }
-
-        checkIfXmlRequiresUpdate( newConfiguration );
-        LOGGER.debug( () -> "successfully loaded configuration (" + TimeDuration.compactFromCurrent( startTime ) + ")" );
-        return newConfiguration;
+        this.createTime = JavaHelper.toIsoDate( Instant.now() );
+        this.modifyTime = Instant.now();
+        this.storedValues = Collections.emptyMap();
+        this.metaValues = Collections.emptyMap();
+        this.templateSet = readTemplateSet( Collections.emptyMap() );
     }
     }
 
 
-    /**
-     * Loop through all settings to see if setting value has flag {@link StoredValue#requiresStoredUpdate()} set to true.
-     * If so, then call {@link #writeSetting(PwmSetting, StoredValue, password.pwm.bean.UserIdentity)} or {@link #writeSetting(PwmSetting, String, StoredValue, password.pwm.bean.UserIdentity)}
-     * for that value so that the xml dom can be updated.
-     *
-     * @param storedConfiguration stored configuration to check
-     */
-    private static void checkIfXmlRequiresUpdate( final StoredConfigurationImpl storedConfiguration ) throws PwmUnrecoverableException
+    private static void removeAllDefaultValues( final Map<StoredConfigItemKey, StoredValue> valueMap, final PwmSettingTemplateSet pwmSettingTemplateSet )
     {
     {
-        for ( final PwmSetting setting : PwmSetting.values() )
-        {
-            if ( setting.getSyntax() != PwmSettingSyntax.PROFILE && !setting.getCategory().hasProfiles() )
-            {
-                final StoredValue value = storedConfiguration.readSetting( setting );
-                if ( value.requiresStoredUpdate() )
-                {
-                    storedConfiguration.writeSetting( setting, value, null );
-                }
-            }
-        }
-
-        for ( final PwmSettingCategory category : PwmSettingCategory.values() )
+        valueMap.entrySet().removeIf( entry ->
         {
         {
-            if ( category.hasProfiles() )
+            final StoredConfigItemKey key = entry.getKey();
+            if ( key.getRecordType() == StoredConfigItemKey.RecordType.SETTING )
             {
             {
-                for ( final String profileID : storedConfiguration.profilesForSetting( category.getProfileSetting() ) )
-                {
-                    for ( final PwmSetting profileSetting : category.getSettings() )
-                    {
-                        final StoredValue value = storedConfiguration.readSetting( profileSetting, profileID );
-                        if ( value.requiresStoredUpdate() )
-                        {
-                            storedConfiguration.writeSetting( profileSetting, profileID, value, null );
-                        }
-                    }
-                }
+                final StoredValue loopValue = entry.getValue();
+                final StoredValue defaultValue = key.toPwmSetting().getDefaultValue( pwmSettingTemplateSet );
+                return Objects.equals( loopValue.valueHash(), defaultValue.valueHash() );
             }
             }
-        }
+            return false;
+        } );
     }
     }
 
 
-    public void resetAllPasswordValues( final String comment )
-            throws PwmUnrecoverableException
-    {
-        for ( final Iterator<SettingValueRecord> settingValueRecordIterator = new StoredValueIterator( false ); settingValueRecordIterator.hasNext(); )
-        {
-            final SettingValueRecord settingValueRecord = settingValueRecordIterator.next();
-            if ( settingValueRecord.getSetting().getSyntax() == PwmSettingSyntax.PASSWORD )
-            {
-                final ValueMetaData valueMetaData = this.readSettingMetadata( settingValueRecord.getSetting(), settingValueRecord.getProfile() );
-                final PasswordValue passwordValue = new PasswordValue( new PasswordData( PwmConstants.LOG_REMOVED_VALUE_REPLACEMENT ) );
-                this.writeSetting( settingValueRecord.getSetting(), settingValueRecord.getProfile(), passwordValue, valueMetaData.getUserIdentity() );
-                if ( comment != null && !comment.isEmpty() )
-                {
-                    final XmlElement settingElement = xmlHelper.xpathForSetting( settingValueRecord.getSetting(), settingValueRecord.getProfile() );
-                    if ( settingElement != null )
-                    {
-                        settingElement.setComment( Collections.singletonList( comment ) );
-                    }
-                }
-            }
-        }
 
 
-        final String pwdHash = this.readConfigProperty( ConfigurationProperty.PASSWORD_HASH );
-        if ( pwdHash != null && !pwdHash.isEmpty() )
-        {
-            this.writeConfigProperty( ConfigurationProperty.PASSWORD_HASH, comment );
-        }
-    }
 
 
-    public StoredConfigurationImpl( ) throws PwmUnrecoverableException
+    @Override
+    public StoredConfiguration copy()
     {
     {
-        try
-        {
-            ConfigurationCleaner.cleanup( this, document );
-            final String createTime = JavaHelper.toIsoDate( Instant.now() );
-            document.getRootElement().setAttribute( XML_ATTRIBUTE_CREATE_TIME, createTime );
-        }
-        catch ( Exception e )
-        {
-            e.printStackTrace(  );
-            throw new IllegalStateException( e );
-        }
+        return new StoredConfigurationImpl( asStoredConfigData() );
     }
     }
 
 
-
-    @Override
-    public String readConfigProperty( final ConfigurationProperty propertyName )
+    StoredConfigData asStoredConfigData()
     {
     {
-        final XmlElement propertyElement = xmlHelper.xpathForConfigProperty( propertyName );
-        return propertyElement == null ? null : propertyElement.getText();
+        return new StoredConfigData( createTime, modifyTime, storedValues, metaValues );
     }
     }
 
 
     @Override
     @Override
-    public void writeConfigProperty(
-            final ConfigurationProperty propertyName,
-            final String value
-    )
+    public Optional<String> readConfigProperty( final ConfigurationProperty propertyName )
     {
     {
-        domModifyLock.writeLock().lock();
-        try
-        {
-
-            // remove existing element
-            {
-                final XmlElement propertyElement  = xmlHelper.xpathForConfigProperty( propertyName );
-                if ( propertyElement != null )
-                {
-                    propertyElement.detach();
-                }
-            }
-
-            // add new property
-            final XmlElement propertyElement = xmlHelper.getXmlFactory().newElement( XML_ELEMENT_PROPERTY );
-            propertyElement.setAttribute( XML_ATTRIBUTE_KEY, propertyName.getKey() );
-            propertyElement.addText( value );
-
-            if ( null == xmlHelper.xpathForConfigProperties() )
-            {
-                final XmlElement configProperties = xmlHelper.getXmlFactory().newElement( XML_ELEMENT_PROPERTIES );
-                configProperties.setAttribute( XML_ATTRIBUTE_TYPE, XML_ATTRIBUTE_VALUE_CONFIG );
-                document.getRootElement().addContent( configProperties );
-            }
-
-            final XmlElement propertiesElement = xmlHelper.xpathForConfigProperties();
-            propertyElement.setAttribute( XML_ATTRIBUTE_MODIFY_TIME, JavaHelper.toIsoDate( Instant.now() ) );
-            propertiesElement.setAttribute( XML_ATTRIBUTE_MODIFY_TIME, JavaHelper.toIsoDate( Instant.now() ) );
-            propertiesElement.addContent( propertyElement );
-        }
-        finally
+        final StoredConfigItemKey key = StoredConfigItemKey.fromConfigurationProperty( propertyName );
+        final StoredValue storedValue = storedValues.get( key );
+        if ( storedValue != null )
         {
         {
-            domModifyLock.writeLock().unlock();
+            return Optional.of( ( ( StringValue ) storedValue ).toNativeObject() );
         }
         }
+        return Optional.empty();
     }
     }
 
 
-    public void lock( )
+    public boolean isDefaultValue( final PwmSetting setting, final String profileID )
     {
     {
-        locked = true;
+        final StoredConfigItemKey key = StoredConfigItemKey.fromSetting( setting, profileID );
+        return !storedValues.containsKey( key );
     }
     }
 
 
-    public Map<String, String> readLocaleBundleMap( final String bundleName, final String keyName )
+    @Override
+    public Map<String, String> readLocaleBundleMap( final PwmLocaleBundle pwmLocaleBundle, final String keyName )
     {
     {
-        domModifyLock.readLock().lock();
-        try
-        {
-            final XmlElement localeBundleElement = xmlHelper.xpathForLocaleBundleSetting( bundleName, keyName );
-
-            if ( localeBundleElement != null )
-            {
-                final Map<String, String> bundleMap = new LinkedHashMap<>();
-                for ( final XmlElement valueElement : localeBundleElement.getChildren( "value" ) )
-                {
-                    final String localeStrValue = valueElement.getAttributeValue( "locale" );
-                    bundleMap.put( localeStrValue == null ? "" : localeStrValue, valueElement.getText() );
-                }
-                if ( !bundleMap.isEmpty() )
-                {
-                    return bundleMap;
-                }
-            }
-        }
-        finally
+        final StoredConfigItemKey key = StoredConfigItemKey.fromLocaleBundle( pwmLocaleBundle, keyName );
+        final StoredValue value = storedValues.get( key );
+        if ( value != null )
         {
         {
-            domModifyLock.readLock().unlock();
+            return ( ( LocalizedStringValue ) value ).toNativeObject();
         }
         }
         return Collections.emptyMap();
         return Collections.emptyMap();
     }
     }
 
 
-    public Map<String, Object> toOutputMap( final Locale locale )
-    {
-        final List<Map<String, String>> settingData = new ArrayList<>();
-        for ( final StoredConfigurationImpl.SettingValueRecord settingValueRecord : this.modifiedSettings() )
-        {
-            final Map<String, String> recordMap = new HashMap<>();
-            recordMap.put( "label", settingValueRecord.getSetting().getLabel( locale ) );
-            if ( settingValueRecord.getProfile() != null )
-            {
-                recordMap.put( "profile", settingValueRecord.getProfile() );
-            }
-            if ( settingValueRecord.getStoredValue() != null )
-            {
-                recordMap.put( "value", settingValueRecord.getStoredValue().toDebugString( locale ) );
-            }
-            final ValueMetaData settingMetaData = readSettingMetadata( settingValueRecord.getSetting(), settingValueRecord.getProfile() );
-            if ( settingMetaData != null )
-            {
-                if ( settingMetaData.getModifyDate() != null )
-                {
-                    recordMap.put( "modifyTime", JavaHelper.toIsoDate( settingMetaData.getModifyDate() ) );
-                }
-                if ( settingMetaData.getUserIdentity() != null )
-                {
-                    recordMap.put( "modifyUser", settingMetaData.getUserIdentity().toDisplayString() );
-                }
-            }
-            settingData.add( recordMap );
-        }
-
-        final HashMap<String, Object> outputObj = new HashMap<>();
-        outputObj.put( "settings", settingData );
-        outputObj.put( "template", this.getTemplateSet().toString() );
-
-        return Collections.unmodifiableMap( outputObj );
-    }
-
-    public void resetLocaleBundleMap( final String bundleName, final String keyName )
-    {
-        preModifyActions();
-        domModifyLock.writeLock().lock();
-        try
-        {
-            final XmlElement oldBundleElements = xmlHelper.xpathForLocaleBundleSetting( bundleName, keyName );
-            if ( oldBundleElements != null )
-            {
-                oldBundleElements.detach();
-            }
-        }
-        finally
-        {
-            domModifyLock.writeLock().unlock();
-        }
-    }
-
-    public void resetSetting( final PwmSetting setting, final String profileID, final UserIdentity userIdentity )
-    {
-        changeLog.updateChangeLog( setting, profileID, defaultValue( setting, this.getTemplateSet() ) );
-        domModifyLock.writeLock().lock();
-        try
-        {
-            preModifyActions();
-            final XmlElement settingElement = createOrGetSettingElement( setting, profileID );
-            settingElement.removeContent();
-            settingElement.addContent( xmlHelper.getXmlFactory().newElement( XML_ELEMENT_DEFAULT ) );
-            updateMetaData( settingElement, userIdentity );
-        }
-        finally
-        {
-            domModifyLock.writeLock().unlock();
-        }
-    }
-
-    public boolean isDefaultValue( final PwmSetting setting )
-    {
-        return isDefaultValue( setting, null );
-    }
-
-    public boolean isDefaultValue( final PwmSetting setting, final String profileID )
-    {
-        domModifyLock.readLock().lock();
-        try
-        {
-            final StoredValue currentValue = readSetting( setting, profileID );
-            if ( setting.getSyntax() == PwmSettingSyntax.PASSWORD )
-            {
-                return currentValue == null || currentValue.toNativeObject() == null;
-            }
-            final StoredValue defaultValue = defaultValue( setting, this.getTemplateSet() );
-            final String currentJsonValue = JsonUtil.serialize( ( Serializable ) currentValue.toNativeObject() );
-            final String defaultJsonValue = JsonUtil.serialize( ( Serializable ) defaultValue.toNativeObject() );
-            return defaultJsonValue.equalsIgnoreCase( currentJsonValue );
-        }
-        finally
-        {
-            domModifyLock.readLock().unlock();
-        }
-    }
-
-    private static StoredValue defaultValue( final PwmSetting pwmSetting, final PwmSettingTemplateSet template )
+    @Override
+    public PwmSettingTemplateSet getTemplateSet()
     {
     {
-        return pwmSetting.getDefaultValue( template );
+        return templateSet;
     }
     }
 
 
-    public PwmSettingTemplateSet getTemplateSet( )
+    private static PwmSettingTemplateSet readTemplateSet( final Map<StoredConfigItemKey, StoredValue> valueMap )
     {
     {
         final Set<PwmSettingTemplate> templates = new HashSet<>();
         final Set<PwmSettingTemplate> templates = new HashSet<>();
-        templates.add( readTemplateValue( PwmSetting.TEMPLATE_LDAP ) );
-        templates.add( readTemplateValue( PwmSetting.TEMPLATE_STORAGE ) );
-        templates.add( readTemplateValue( PwmSetting.DB_VENDOR_TEMPLATE ) );
+        readTemplateValue( valueMap, PwmSetting.TEMPLATE_LDAP ).ifPresent( templates::add );
+        readTemplateValue( valueMap, PwmSetting.TEMPLATE_STORAGE ).ifPresent( templates::add );
+        readTemplateValue( valueMap, PwmSetting.DB_VENDOR_TEMPLATE ).ifPresent( templates::add );
         return new PwmSettingTemplateSet( templates );
         return new PwmSettingTemplateSet( templates );
     }
     }
 
 
-    private PwmSettingTemplate readTemplateValue( final PwmSetting pwmSetting )
+    private static Optional<PwmSettingTemplate> readTemplateValue( final Map<StoredConfigItemKey, StoredValue> valueMap, final PwmSetting pwmSetting )
     {
     {
-        final XmlElement settingElement = xmlHelper.xpathForSetting( pwmSetting, null );
-        if ( settingElement != null )
+        final StoredConfigItemKey key = StoredConfigItemKey.fromSetting( pwmSetting, null );
+        final StoredValue storedValue = valueMap.get( key );
+
+        if ( storedValue != null )
         {
         {
             try
             try
             {
             {
-                final String strValue = ( String ) ValueFactory.fromXmlValues( pwmSetting, settingElement, null ).toNativeObject();
-                return JavaHelper.readEnumFromString( PwmSettingTemplate.class, null, strValue );
+                final String strValue = ( String ) storedValue.toNativeObject();
+                return Optional.ofNullable( JavaHelper.readEnumFromString( PwmSettingTemplate.class, null, strValue ) );
             }
             }
-            catch ( IllegalStateException e )
+            catch ( final IllegalStateException e )
             {
             {
                 LOGGER.error( "error reading template", e );
                 LOGGER.error( "error reading template", e );
             }
             }
         }
         }
-        return null;
-    }
 
 
-    public void setTemplate( final PwmSettingTemplate template )
-    {
-        writeConfigProperty( ConfigurationProperty.LDAP_TEMPLATE, template.toString() );
+        return Optional.empty();
     }
     }
 
 
     public String toString( final PwmSetting setting, final String profileID )
     public String toString( final PwmSetting setting, final String profileID )
@@ -451,1130 +185,102 @@ public class StoredConfigurationImpl implements StoredConfiguration
         return setting.getKey() + "=" + storedValue.toDebugString( null );
         return setting.getKey() + "=" + storedValue.toDebugString( null );
     }
     }
 
 
-    public Map<String, String> getModifiedSettingDebugValues( final Locale locale, final boolean prettyPrint )
-    {
-        final Map<String, String> returnObj = new TreeMap<>();
-        for ( final SettingValueRecord record : this.modifiedSettings() )
-        {
-            final String label = record.getSetting().toMenuLocationDebug( record.getProfile(), locale );
-            final String value = record.getStoredValue().toDebugString( locale );
-            returnObj.put( label, value );
-        }
-        return returnObj;
-    }
-
-    public List<SettingValueRecord> modifiedSettings( )
-    {
-        final List<SettingValueRecord> returnObj = new ArrayList<>();
-        domModifyLock.readLock().lock();
-        try
-        {
-            for ( final PwmSetting setting : PwmSetting.values() )
-            {
-                if ( setting.getSyntax() != PwmSettingSyntax.PROFILE && !setting.getCategory().hasProfiles() )
-                {
-                    if ( !isDefaultValue( setting, null ) )
-                    {
-                        final StoredValue value = readSetting( setting );
-                        if ( value != null )
-                        {
-                            returnObj.add( new SettingValueRecord( setting, null, value ) );
-                        }
-                    }
-                }
-            }
-
-            for ( final PwmSettingCategory category : PwmSettingCategory.values() )
-            {
-                if ( category.hasProfiles() )
-                {
-                    for ( final String profileID : this.profilesForSetting( category.getProfileSetting() ) )
-                    {
-                        for ( final PwmSetting profileSetting : category.getSettings() )
-                        {
-                            if ( !isDefaultValue( profileSetting, profileID ) )
-                            {
-                                final StoredValue value = readSetting( profileSetting, profileID );
-                                if ( value != null )
-                                {
-                                    returnObj.add( new SettingValueRecord( profileSetting, profileID, value ) );
-
-                                }
-                            }
-                        }
-                    }
-                }
-            }
-
-            return returnObj;
-        }
-        finally
-        {
-            domModifyLock.readLock().unlock();
-        }
-    }
-
-    public Serializable toJsonDebugObject( )
-    {
-        domModifyLock.readLock().lock();
-        try
-        {
-            final TreeMap<String, Object> outputObject = new TreeMap<>();
-
-            for ( final PwmSetting setting : PwmSetting.values() )
-            {
-                if ( setting.getSyntax() != PwmSettingSyntax.PROFILE && !setting.getCategory().hasProfiles() )
-                {
-                    if ( !isDefaultValue( setting, null ) )
-                    {
-                        final StoredValue value = readSetting( setting );
-                        outputObject.put( setting.getKey(), value.toDebugJsonObject( null ) );
-                    }
-                }
-            }
-
-            for ( final PwmSettingCategory category : PwmSettingCategory.values() )
-            {
-                if ( category.hasProfiles() )
-                {
-                    final TreeMap<String, Object> profiles = new TreeMap<>();
-                    for ( final String profileID : this.profilesForSetting( category.getProfileSetting() ) )
-                    {
-                        final TreeMap<String, Object> profileObject = new TreeMap<>();
-                        for ( final PwmSetting profileSetting : category.getSettings() )
-                        {
-                            if ( !isDefaultValue( profileSetting, profileID ) )
-                            {
-                                final StoredValue value = readSetting( profileSetting, profileID );
-                                profileObject.put( profileSetting.getKey(), value.toDebugJsonObject( null ) );
-                            }
-                        }
-                        profiles.put( profileID, profileObject );
-                    }
-                    outputObject.put( category.getProfileSetting().getKey(), profiles );
-                }
-            }
-
-            return outputObject;
-        }
-        finally
-        {
-            domModifyLock.readLock().unlock();
-        }
-    }
-
-    public void toXml( final OutputStream outputStream )
-            throws IOException, PwmUnrecoverableException
+    public Set<StoredConfigItemKey> modifiedItems()
     {
     {
-        ConfigurationCleaner.updateMandatoryElements( this, document );
-        XmlFactory.getFactory().outputDocument( document, outputStream );
+        return Collections.unmodifiableSet( storedValues.keySet() );
     }
     }
 
 
+    @Override
     public List<String> profilesForSetting( final PwmSetting pwmSetting )
     public List<String> profilesForSetting( final PwmSetting pwmSetting )
     {
     {
-        return StoredConfigurationUtil.profilesForSetting( pwmSetting, this );
-    }
-
-    public List<String> validateValues( )
-    {
-        final long startTime = System.currentTimeMillis();
-        final List<String> errorStrings = new ArrayList<>();
-
-        for ( final PwmSetting loopSetting : PwmSetting.values() )
+        final List<String> returnObj = new ArrayList<>();
+        for ( final StoredConfigItemKey storedConfigItemKey : storedValues.keySet() )
         {
         {
-
-            if ( loopSetting.getCategory().hasProfiles() )
+            if ( storedConfigItemKey.getRecordType() == StoredConfigItemKey.RecordType.SETTING
+                    && Objects.equals( storedConfigItemKey.getRecordID(), pwmSetting.getKey() ) )
             {
             {
-                for ( final String profile : profilesForSetting( loopSetting ) )
-                {
-                    final StoredValue loopValue = readSetting( loopSetting, profile );
-
-                    try
-                    {
-                        final List<String> errors = loopValue.validateValue( loopSetting );
-                        for ( final String loopError : errors )
-                        {
-                            errorStrings.add( loopSetting.toMenuLocationDebug( profile, PwmConstants.DEFAULT_LOCALE ) + " - " + loopError );
-                        }
-                    }
-                    catch ( Exception e )
-                    {
-                        LOGGER.error( "unexpected error during validate value for " + loopSetting.toMenuLocationDebug( profile, PwmConstants.DEFAULT_LOCALE ) + ", error: " + e.getMessage(), e );
-                    }
-                }
-            }
-            else
-            {
-                final StoredValue loopValue = readSetting( loopSetting );
-
-                try
-                {
-                    final List<String> errors = loopValue.validateValue( loopSetting );
-                    for ( final String loopError : errors )
-                    {
-                        errorStrings.add( loopSetting.toMenuLocationDebug( null, PwmConstants.DEFAULT_LOCALE ) + " - " + loopError );
-                    }
-                }
-                catch ( Exception e )
-                {
-                    LOGGER.error( "unexpected error during validate value for " + loopSetting.toMenuLocationDebug( null, PwmConstants.DEFAULT_LOCALE ) + ", error: " + e.getMessage(), e );
-                }
+                returnObj.add( storedConfigItemKey.getProfileID() );
             }
             }
         }
         }
-
-        LOGGER.trace( () -> "StoredConfiguration validator completed in " + TimeDuration.fromCurrent( startTime ).asCompactString() );
-        return errorStrings;
+        return Collections.unmodifiableList( returnObj );
     }
     }
 
 
     public ValueMetaData readSettingMetadata( final PwmSetting setting, final String profileID )
     public ValueMetaData readSettingMetadata( final PwmSetting setting, final String profileID )
     {
     {
-        final XmlElement settingElement = xmlHelper.xpathForSetting( setting, profileID );
-
-        if ( settingElement == null )
-        {
-            return null;
-        }
-
-        Instant modifyDate = null;
-        try
-        {
-            if ( settingElement.getAttributeValue( XML_ATTRIBUTE_MODIFY_TIME ) != null )
-            {
-                modifyDate = JavaHelper.parseIsoToInstant(
-                        settingElement.getAttributeValue( XML_ATTRIBUTE_MODIFY_TIME ) );
-            }
-        }
-        catch ( Exception e )
-        {
-            LOGGER.error( "can't read modifyDate for setting " + setting.getKey() + ", profile " + profileID + ", error: " + e.getMessage() );
-        }
-
-        UserIdentity userIdentity = null;
-        try
-        {
-            if ( settingElement.getAttributeValue( XML_ATTRIBUTE_MODIFY_USER ) != null )
-            {
-                userIdentity = UserIdentity.fromDelimitedKey(
-                        settingElement.getAttributeValue( XML_ATTRIBUTE_MODIFY_USER ) );
-            }
-        }
-        catch ( Exception e )
-        {
-            LOGGER.error( "can't read userIdentity for setting " + setting.getKey() + ", profile " + profileID + ", error: " + e.getMessage() );
-        }
-
-        return new ValueMetaData( modifyDate, userIdentity );
+        final StoredConfigItemKey key = StoredConfigItemKey.fromSetting( setting, profileID );
+        return metaValues.get( key );
     }
     }
 
 
-    public List<ConfigRecordID> search( final String searchTerm, final Locale locale )
+    public StoredValue readSetting( final PwmSetting setting, final String profileID )
     {
     {
-        if ( searchTerm == null )
+        final StoredConfigItemKey key = StoredConfigItemKey.fromSetting( setting, profileID );
+        final StoredValue storedValue = storedValues.get( key );
+        if ( storedValue == null )
         {
         {
-            return Collections.emptyList();
+            return setting.getDefaultValue( getTemplateSet() );
         }
         }
 
 
-        final SortedSet<ConfigRecordID> matches = new TreeSet<>(
-                allSettingConfigRecordIDs()
-                        .parallelStream()
-                        .filter( s -> matchSetting( s, searchTerm, locale ) )
-                        .collect( Collectors.toList() )
-        );
-
-        return new ArrayList<>( matches );
+        return storedValue;
     }
     }
 
 
-    private boolean matchSetting(
-            final ConfigRecordID configRecordID,
-            final String searchTerm,
-            final Locale locale
-    )
+    public String valueHash()
     {
     {
-
-        final PwmSetting pwmSetting = ( PwmSetting ) configRecordID.getRecordID();
-        final StoredValue value = readSetting( pwmSetting, configRecordID.getProfileID() );
-
-        return StringUtil.whitespaceSplit( searchTerm )
-                .parallelStream()
-                .allMatch( s -> matchSetting( pwmSetting, value, s, locale ) );
+        return valueHashSupplier.get();
     }
     }
 
 
-    public boolean matchSetting( final PwmSetting setting, final StoredValue value, final String searchTerm, final Locale locale )
+    private String valueHashImpl()
     {
     {
-        if ( setting.isHidden() || setting.getCategory().isHidden() )
-        {
-            return false;
-        }
+        final Set<StoredConfigItemKey> modifiedSettings = modifiedItems();
+        final StringBuilder sb = new StringBuilder();
 
 
-        if ( searchTerm == null || searchTerm.isEmpty() )
+        for ( final StoredConfigItemKey storedConfigItemKey : modifiedSettings )
         {
         {
-            return false;
+            final StoredValue storedValue = storedValues.get( storedConfigItemKey );
+            sb.append( storedValue.valueHash() );
         }
         }
 
 
-        final String lowerSearchTerm = searchTerm.toLowerCase();
-
-        {
-            final String key = setting.getKey();
-            if ( key.toLowerCase().contains( lowerSearchTerm ) )
-            {
-                return true;
-            }
-        }
-        {
-            final String label = setting.getLabel( locale );
-            if ( label.toLowerCase().contains( lowerSearchTerm ) )
-            {
-                return true;
-            }
-        }
+        try
         {
         {
-            final String descr = setting.getDescription( locale );
-            if ( descr.toLowerCase().contains( lowerSearchTerm ) )
-            {
-                return true;
-            }
+            return SecureEngine.hmac( HmacAlgorithm.HMAC_SHA_512, getKey(), sb.toString() );
         }
         }
+        catch ( final PwmUnrecoverableException e )
         {
         {
-            final String menuLocationString = setting.toMenuLocationDebug( null, locale );
-            if ( menuLocationString.toLowerCase().contains( lowerSearchTerm ) )
-            {
-                return true;
-            }
+            throw new IllegalStateException( e );
         }
         }
+    }
 
 
-        if ( setting.isConfidential() )
-        {
-            return false;
-        }
-        {
-            final String valueDebug = value.toDebugString( locale );
-            if ( valueDebug != null && valueDebug.toLowerCase().contains( lowerSearchTerm ) )
-            {
-                return true;
-            }
-        }
-        if ( PwmSettingSyntax.SELECT == setting.getSyntax()
-                || PwmSettingSyntax.OPTIONLIST == setting.getSyntax()
-                || PwmSettingSyntax.VERIFICATION_METHOD == setting.getSyntax()
-        )
+    private PwmSecurityKey cachedKey;
+
+    public PwmSecurityKey getKey() throws PwmUnrecoverableException
+    {
+        if ( cachedKey == null )
         {
         {
-            for ( final String key : setting.getOptions().keySet() )
-            {
-                if ( key.toLowerCase().contains( lowerSearchTerm ) )
-                {
-                    return true;
-                }
-                final String optionValue = setting.getOptions().get( key );
-                if ( optionValue != null && optionValue.toLowerCase().contains( lowerSearchTerm ) )
-                {
-                    return true;
-                }
-            }
+            cachedKey = new PwmSecurityKey( createTime + "StoredConfiguration" );
         }
         }
-        return false;
+        return cachedKey;
     }
     }
 
 
-
-    public StoredValue readSetting( final PwmSetting setting )
+    @Override
+    public Instant modifyTime()
     {
     {
-        return readSetting( setting, null );
+        return modifyTime;
     }
     }
 
 
-    public StoredValue readSetting( final PwmSetting setting, final String profileID )
+    @Override
+    public String createTime()
     {
     {
-        if ( profileID == null && setting.getCategory().hasProfiles() )
-        {
-            final IllegalArgumentException e = new IllegalArgumentException( "reading of setting " + setting.getKey() + " requires a non-null profileID" );
-            LOGGER.error( "error", e );
-            throw e;
-        }
-        if ( profileID != null && !setting.getCategory().hasProfiles() )
-        {
-            throw new IllegalStateException( "cannot read setting key " + setting.getKey() + " with non-null profileID" );
-        }
-        domModifyLock.readLock().lock();
-        try
-        {
-            final XmlElement settingElement = xmlHelper.xpathForSetting( setting, profileID );
-
-            if ( settingElement == null )
-            {
-                return defaultValue( setting, getTemplateSet() );
-            }
-
-            if ( settingElement.getChild( XML_ELEMENT_DEFAULT ) != null )
-            {
-                return defaultValue( setting, getTemplateSet() );
-            }
-
-            try
-            {
-                return ValueFactory.fromXmlValues( setting, settingElement, getKey() );
-            }
-            catch ( PwmException e )
-            {
-                final String errorMsg = "unexpected error reading setting '" + setting.getKey() + "' profile '" + profileID + "', error: " + e.getMessage();
-                throw new IllegalStateException( errorMsg );
-            }
-        }
-        finally
-        {
-            domModifyLock.readLock().unlock();
-        }
+        return createTime;
     }
     }
 
 
-    public void writeLocaleBundleMap( final String bundleName, final String keyName, final Map<String, String> localeMap )
+    @Override
+    public Optional<ValueMetaData> readMetaData( final StoredConfigItemKey storedConfigItemKey )
     {
     {
-        ResourceBundle theBundle = null;
-        for ( final PwmLocaleBundle bundle : PwmLocaleBundle.values() )
-        {
-            if ( bundle.getTheClass().getName().equals( bundleName ) )
-            {
-                theBundle = ResourceBundle.getBundle( bundleName );
-            }
-        }
-
-        if ( theBundle == null )
-        {
-            LOGGER.info( () -> "ignoring unknown locale bundle for bundle=" + bundleName + ", key=" + keyName );
-            return;
-        }
-
-        if ( theBundle.getString( keyName ) == null )
-        {
-            LOGGER.info( () -> "ignoring unknown key for bundle=" + bundleName + ", key=" + keyName );
-            return;
-        }
-
-
-        resetLocaleBundleMap( bundleName, keyName );
-        if ( localeMap == null || localeMap.isEmpty() )
-        {
-            LOGGER.info( () -> "cleared locale bundle map for bundle=" + bundleName + ", key=" + keyName );
-            return;
-        }
-
-        preModifyActions();
-        changeLog.updateChangeLog( bundleName, keyName, localeMap );
-        try
-        {
-            domModifyLock.writeLock().lock();
-            final XmlElement localeBundleElement = xmlHelper.getXmlFactory().newElement( "localeBundle" );
-            localeBundleElement.setAttribute( "bundle", bundleName );
-            localeBundleElement.setAttribute( "key", keyName );
-            for ( final Map.Entry<String, String> entry : localeMap.entrySet() )
-            {
-                final String locale = entry.getKey();
-                final String value = entry.getValue();
-                final XmlElement valueElement = xmlHelper.getXmlFactory().newElement( "value" );
-                if ( locale != null && locale.length() > 0 )
-                {
-                    valueElement.setAttribute( "locale", locale );
-                }
-                valueElement.addText( value );
-                localeBundleElement.addContent( valueElement );
-            }
-            localeBundleElement.setAttribute( XML_ATTRIBUTE_MODIFY_TIME, JavaHelper.toIsoDate( Instant.now() ) );
-            document.getRootElement().addContent( localeBundleElement );
-        }
-        finally
-        {
-            domModifyLock.writeLock().unlock();
-        }
-    }
-
-
-    public void copyProfileID( final PwmSettingCategory category, final String sourceID, final String destinationID, final UserIdentity userIdentity )
-            throws PwmUnrecoverableException
-    {
-
-        if ( !category.hasProfiles() )
-        {
-            throw PwmUnrecoverableException.newException( PwmError.ERROR_INVALID_CONFIG, "can not copy profile ID for category " + category + ", category does not have profiles" );
-        }
-        final List<String> existingProfiles = this.profilesForSetting( category.getProfileSetting() );
-        if ( !existingProfiles.contains( sourceID ) )
-        {
-            throw PwmUnrecoverableException.newException( PwmError.ERROR_INVALID_CONFIG, "can not copy profile ID for category, source profileID '" + sourceID + "' does not exist" );
-        }
-        if ( existingProfiles.contains( destinationID ) )
-        {
-            throw PwmUnrecoverableException.newException( PwmError.ERROR_INVALID_CONFIG, "can not copy profile ID for category, destination profileID '" + destinationID + "' already exists" );
-        }
-
-        {
-            final Collection<PwmSettingCategory> interestedCategories = PwmSettingCategory.associatedProfileCategories( category );
-            for ( final PwmSettingCategory interestedCategory : interestedCategories )
-            {
-                for ( final PwmSetting pwmSetting : interestedCategory.getSettings() )
-                {
-                    if ( !isDefaultValue( pwmSetting, sourceID ) )
-                    {
-                        final StoredValue value = readSetting( pwmSetting, sourceID );
-                        writeSetting( pwmSetting, destinationID, value, userIdentity );
-                    }
-                }
-            }
-        }
-
-        final List<String> newProfileIDList = new ArrayList<>();
-        newProfileIDList.addAll( existingProfiles );
-        newProfileIDList.add( destinationID );
-        writeSetting( category.getProfileSetting(), new StringArrayValue( newProfileIDList ), userIdentity );
-    }
-
-
-    public void writeSetting(
-            final PwmSetting setting,
-            final StoredValue value,
-            final UserIdentity userIdentity
-    ) throws PwmUnrecoverableException
-    {
-        writeSetting( setting, null, value, userIdentity );
-    }
-
-    public void writeSetting(
-            final PwmSetting setting,
-            final String profileID,
-            final StoredValue value,
-            final UserIdentity userIdentity
-    ) throws PwmUnrecoverableException
-    {
-        if ( profileID == null && setting.getCategory().hasProfiles() )
-        {
-            throw new IllegalArgumentException( "writing of setting " + setting.getKey() + " requires a non-null profileID" );
-        }
-        if ( profileID != null && !setting.getCategory().hasProfiles() )
-        {
-            throw new IllegalArgumentException( "cannot specify profile for non-profile setting" );
-        }
-
-        preModifyActions();
-        changeLog.updateChangeLog( setting, profileID, value );
-        domModifyLock.writeLock().lock();
-        try
-        {
-            final XmlElement settingElement = createOrGetSettingElement( setting, profileID );
-            settingElement.removeContent();
-            settingElement.setAttribute( XML_ATTRIBUTE_SYNTAX, setting.getSyntax().toString() );
-            settingElement.setAttribute( XML_ATTRIBUTE_SYNTAX_VERSION, Integer.toString( value.currentSyntaxVersion() ) );
-
-            if ( setting_writeLabels )
-            {
-                {
-                    final XmlElement existingLabel = settingElement.getChild( "label" );
-                    if ( existingLabel != null )
-                    {
-                        existingLabel.detach();
-                    }
-                }
-
-                {
-                    final XmlElement newLabelElement = xmlHelper.getXmlFactory().newElement( "label" );
-                    newLabelElement.addText( setting.getLabel( PwmConstants.DEFAULT_LOCALE ) );
-                    settingElement.addContent( newLabelElement );
-                }
-            }
-
-            if ( setting.getSyntax() == PwmSettingSyntax.PASSWORD )
-            {
-                final List<String> commentLines = Arrays.asList(
-                        "Note: This value is encrypted and can not be edited directly.",
-                        "Please use the Configuration Manager GUI to modify this value."
-                );
-                settingElement.setComment( commentLines );
-
-                final List<XmlElement> valueElements = ( ( PasswordValue ) value ).toXmlValues( "value", getKey() );
-                settingElement.addContent( valueElements );
-            }
-            else if ( setting.getSyntax() == PwmSettingSyntax.PRIVATE_KEY )
-            {
-                final List<XmlElement> valueElements = ( ( PrivateKeyValue ) value ).toXmlValues( "value", getKey() );
-                settingElement.addContent( valueElements );
-            }
-            else if ( setting.getSyntax() == PwmSettingSyntax.NAMED_SECRET )
-            {
-                final List<XmlElement> valueElements = ( ( NamedSecretValue ) value ).toXmlValues( "value", getKey() );
-                settingElement.addContent( valueElements );
-            }
-            else
-            {
-                settingElement.addContent( value.toXmlValues( "value", getKey() ) );
-            }
-
-
-            updateMetaData( settingElement, userIdentity );
-        }
-        finally
-        {
-            domModifyLock.writeLock().unlock();
-        }
-    }
-
-    public String settingChecksum( )
-            throws PwmUnrecoverableException
-    {
-        final Instant startTime = Instant.now();
-
-        final List<SettingValueRecord> modifiedSettings = modifiedSettings();
-        final StringBuilder sb = new StringBuilder();
-        sb.append( "PwmSettingsChecksum" );
-        for ( final SettingValueRecord settingValueRecord : modifiedSettings )
-        {
-            final StoredValue storedValue = settingValueRecord.getStoredValue();
-            sb.append( storedValue.valueHash() );
-        }
-
-
-        final String result = SecureEngine.hash( sb.toString(), PwmConstants.SETTING_CHECKSUM_HASH_METHOD );
-        LOGGER.trace( () -> "computed setting checksum in " + TimeDuration.fromCurrent( startTime ).asCompactString() );
-        return result;
-    }
-
-
-    private void preModifyActions( )
-    {
-        if ( locked )
-        {
-            throw new UnsupportedOperationException( "StoredConfiguration is locked and cannot be modified" );
-        }
-        document.getRootElement().setAttribute( XML_ATTRIBUTE_MODIFY_TIME, JavaHelper.toIsoDate( Instant.now() ) );
-    }
-
-    public void setPassword( final String password )
-            throws PwmOperationalException
-    {
-        if ( password == null || password.isEmpty() )
-        {
-            throw new PwmOperationalException( new ErrorInformation( PwmError.CONFIG_FORMAT_ERROR, null, new String[] { "can not set blank password" } ) );
-        }
-        final String trimmedPassword = password.trim();
-        if ( trimmedPassword.length() < 1 )
-        {
-            throw new PwmOperationalException( new ErrorInformation( PwmError.CONFIG_FORMAT_ERROR, null, new String[] { "can not set blank password" } ) );
-        }
-
-
-        final String passwordHash = BCrypt.hashPassword( password );
-        this.writeConfigProperty( ConfigurationProperty.PASSWORD_HASH, passwordHash );
-    }
-
-    public boolean verifyPassword( final String password, final Configuration configuration )
-    {
-        if ( !hasPassword() )
-        {
-            return false;
-        }
-        final String passwordHash = this.readConfigProperty( ConfigurationProperty.PASSWORD_HASH );
-        return BCrypt.testAnswer( password, passwordHash, configuration );
-    }
-
-    public boolean hasPassword( )
-    {
-        final String passwordHash = this.readConfigProperty( ConfigurationProperty.PASSWORD_HASH );
-        return passwordHash != null && passwordHash.length() > 0;
-    }
-
-    class XmlHelper
-    {
-        private XmlFactory getXmlFactory()
-        {
-            return XmlFactory.getFactory();
-        }
-
-        private XmlElement xpathForLocaleBundleSetting( final String bundleName, final String keyName )
-        {
-            final String xpathString = "//localeBundle[@bundle=\"" + bundleName + "\"][@key=\"" + keyName + "\"]";
-            return document.evaluateXpathToElement( xpathString );
-        }
-
-        XmlElement xpathForSetting( final PwmSetting setting, final String profileID )
-        {
-            final String xpathString;
-            if ( profileID == null || profileID.length() < 1 )
-            {
-                xpathString = "//setting[@key=\"" + setting.getKey() + "\"][(not (@profile)) or @profile=\"\"]";
-            }
-            else
-            {
-                xpathString = "//setting[@key=\"" + setting.getKey() + "\"][@profile=\"" + profileID + "\"]";
-            }
-
-            return document.evaluateXpathToElement( xpathString );
-        }
-
-        private XmlElement xpathForAppProperty( final AppProperty appProperty )
-        {
-            final String xpathString = "//" + XML_ELEMENT_PROPERTIES + "[@" + XML_ATTRIBUTE_TYPE + "=\"" + XML_ATTRIBUTE_VALUE_APP + "\"]/"
-                    + XML_ELEMENT_PROPERTY + "[@" + XML_ATTRIBUTE_KEY + "=\"" + appProperty.getKey() + "\"]";
-            return document.evaluateXpathToElement( xpathString );
-        }
-
-        List<XmlElement> xpathForAppProperties( )
-        {
-            final String xpathString = "//" + XML_ELEMENT_PROPERTIES + "[@" + XML_ATTRIBUTE_TYPE + "=\"" + XML_ATTRIBUTE_VALUE_APP + "\"]";
-            return document.evaluateXpathToElements( xpathString );
-        }
-
-        private XmlElement xpathForConfigProperty( final ConfigurationProperty configProperty )
-        {
-            final String xpathString = "//" + XML_ELEMENT_PROPERTIES + "[@" + XML_ATTRIBUTE_TYPE + "=\"" + XML_ATTRIBUTE_VALUE_CONFIG + "\"]/"
-                    + XML_ELEMENT_PROPERTY + "[@" + XML_ATTRIBUTE_KEY + "=\"" + configProperty.getKey() + "\"]";
-            return document.evaluateXpathToElement( xpathString );
-        }
-
-        private XmlElement xpathForConfigProperties( )
-        {
-            final String xpathString = "//" + XML_ELEMENT_PROPERTIES + "[@" + XML_ATTRIBUTE_TYPE + "=\"" + XML_ATTRIBUTE_VALUE_CONFIG + "\"]";
-            return document.evaluateXpathToElement( xpathString );
-        }
-    }
-
-
-    public static class ConfigRecordID implements Serializable, Comparable
-    {
-        private RecordType recordType;
-        private Object recordID;
-        private String profileID;
-
-        public enum RecordType
-        {
-            SETTING,
-            LOCALE_BUNDLE,
-        }
-
-        public ConfigRecordID(
-                final RecordType recordType,
-                final Object recordID,
-                final String profileID
-        )
-        {
-            this.recordType = recordType;
-            this.recordID = recordID;
-            this.profileID = profileID;
-        }
-
-
-        public RecordType getRecordType( )
-        {
-            return recordType;
-        }
-
-        public Object getRecordID( )
-        {
-            return recordID;
-        }
-
-        public String getProfileID( )
-        {
-            return profileID;
-        }
-
-        @Override
-        public boolean equals( final Object o )
-        {
-            return o != null
-                    && o instanceof ConfigRecordID
-                    && toString().equals( o.toString() );
-
-        }
-
-        @Override
-        public int hashCode( )
-        {
-            return toString().hashCode();
-        }
-
-        @Override
-        public String toString( )
-        {
-            return this.getRecordType().toString()
-                    + "-"
-                    + ( this.getProfileID() == null ? "" : this.getProfileID() )
-                    + "-"
-                    + this.getRecordID();
-        }
-
-        @Override
-        public int compareTo( final Object o )
-        {
-            return toString().compareTo( o.toString() );
-        }
-    }
-
-
-    public String changeLogAsDebugString( final Locale locale, final boolean asHtml )
-    {
-        return changeLog.changeLogAsDebugString( locale, asHtml );
-    }
-
-    private PwmSecurityKey cachedKey;
-
-    public PwmSecurityKey getKey( ) throws PwmUnrecoverableException
-    {
-        if ( cachedKey == null )
-        {
-            cachedKey = new PwmSecurityKey( createTime() + "StoredConfiguration" );
-        }
-        return cachedKey;
-    }
-
-    public boolean isModified( )
-    {
-        return changeLog.isModified();
-    }
-
-    private class ChangeLog
-    {
-        /* values contain the _original_ toJson version of the value. */
-        private Map<ConfigRecordID, String> changeLog = new LinkedHashMap<>();
-
-        public boolean isModified( )
-        {
-            return !changeLog.isEmpty();
-        }
-
-        public String changeLogAsDebugString( final Locale locale, final boolean asHtml )
-        {
-            final Map<String, String> outputMap = new TreeMap<>();
-            final String SEPARATOR = LocaleHelper.getLocalizedMessage( locale, Config.Display_SettingNavigationSeparator, null );
-
-            for ( final ConfigRecordID configRecordID : changeLog.keySet() )
-            {
-                switch ( configRecordID.recordType )
-                {
-                    case SETTING:
-                    {
-                        final StoredValue currentValue = readSetting( ( PwmSetting ) configRecordID.recordID, configRecordID.profileID );
-                        final PwmSetting pwmSetting = ( PwmSetting ) configRecordID.recordID;
-                        final String keyName = pwmSetting.toMenuLocationDebug( configRecordID.getProfileID(), locale );
-                        final String debugValue = currentValue.toDebugString( locale );
-                        outputMap.put( keyName, debugValue );
-                    }
-                    break;
-
-                    case LOCALE_BUNDLE:
-                    {
-                        final String key = ( String ) configRecordID.recordID;
-                        final String bundleName = key.split( "!" )[ 0 ];
-                        final String keys = key.split( "!" )[ 1 ];
-                        final Map<String, String> currentValue = readLocaleBundleMap( bundleName, keys );
-                        final String debugValue = JsonUtil.serializeMap( currentValue, JsonUtil.Flag.PrettyPrint );
-                        outputMap.put( "LocaleBundle" + SEPARATOR + bundleName + " " + keys, debugValue );
-                    }
-                    break;
-
-                    default:
-                        // continue processing
-                        break;
-                }
-            }
-            final StringBuilder output = new StringBuilder();
-            if ( outputMap.isEmpty() )
-            {
-                output.append( "No setting changes." );
-            }
-            else
-            {
-                for ( final Map.Entry<String, String> entry : outputMap.entrySet() )
-                {
-                    final String keyName = entry.getKey();
-                    final String value = entry.getValue();
-                    if ( asHtml )
-                    {
-                        output.append( "<div class=\"changeLogKey\">" );
-                        output.append( keyName );
-                        output.append( "</div><div class=\"changeLogValue\">" );
-                        output.append( StringUtil.escapeHtml( value ) );
-                        output.append( "</div>" );
-                    }
-                    else
-                    {
-                        output.append( keyName );
-                        output.append( "\n" );
-                        output.append( " Value: " );
-                        output.append( value );
-                        output.append( "\n" );
-                    }
-                }
-            }
-            return output.toString();
-        }
-
-        public void updateChangeLog( final String bundleName, final String keyName, final Map<String, String> localeValueMap )
-        {
-            final String key = bundleName + "!" + keyName;
-            final Map<String, String> currentValue = readLocaleBundleMap( bundleName, keyName );
-            final String currentJsonValue = JsonUtil.serializeMap( currentValue );
-            final String newJsonValue = JsonUtil.serializeMap( localeValueMap );
-            final ConfigRecordID configRecordID = new ConfigRecordID( ConfigRecordID.RecordType.LOCALE_BUNDLE, key, null );
-            updateChangeLog( configRecordID, currentJsonValue, newJsonValue );
-        }
-
-        public void updateChangeLog( final PwmSetting setting, final String profileID, final StoredValue newValue )
-        {
-            final StoredValue currentValue = readSetting( setting, profileID );
-            final String currentJsonValue = JsonUtil.serialize( currentValue );
-            final String newJsonValue = JsonUtil.serialize( newValue );
-            final ConfigRecordID configRecordID = new ConfigRecordID( ConfigRecordID.RecordType.SETTING, setting, profileID );
-            updateChangeLog( configRecordID, currentJsonValue, newJsonValue );
-        }
-
-        public void updateChangeLog( final ConfigRecordID configRecordID, final String currentValueString, final String newValueString )
-        {
-            if ( changeLog.containsKey( configRecordID ) )
-            {
-                final String currentRecord = changeLog.get( configRecordID );
-
-                if ( currentRecord == null && newValueString == null )
-                {
-                    changeLog.remove( configRecordID );
-                }
-                else if ( currentRecord != null && currentRecord.equals( newValueString ) )
-                {
-                    changeLog.remove( configRecordID );
-                }
-            }
-            else
-            {
-                changeLog.put( configRecordID, currentValueString );
-            }
-        }
-    }
-
-    public static void validateXmlSchema( final String xmlDocument )
-            throws PwmUnrecoverableException
-    {
-        return;
-                /*
-        try {
-            final InputStream xsdInputStream = PwmSetting.class.getClassLoader().getResourceAsStream("password/pwm/config/StoredConfiguration.xsd");
-            final SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
-            final Schema schema = factory.newSchema(new StreamSource(xsdInputStream));
-            Validator validator = schema.newValidator();
-            validator.validate(new StreamSource(new StringReader(xmlDocument)));
-        } catch (Exception e) {
-            final String errorMsg = "error while validating setting file schema definition: " + e.getMessage();
-            throw new PwmUnrecoverableException(new ErrorInformation(PwmError.CONFIG_FORMAT_ERROR,errorMsg));
-        }
-        */
-    }
-
-    private void updateMetaData( final XmlElement settingElement, final UserIdentity userIdentity )
-    {
-        final XmlElement settingsElement = document.getRootElement().getChild( XML_ELEMENT_SETTINGS );
-        settingElement.setAttribute( XML_ATTRIBUTE_MODIFY_TIME, JavaHelper.toIsoDate( Instant.now() ) );
-        settingsElement.setAttribute( XML_ATTRIBUTE_MODIFY_TIME, JavaHelper.toIsoDate( Instant.now() ) );
-        settingElement.removeAttribute( XML_ATTRIBUTE_MODIFY_USER );
-        settingsElement.removeAttribute( XML_ATTRIBUTE_MODIFY_USER );
-        if ( userIdentity != null )
-        {
-            settingElement.setAttribute( XML_ATTRIBUTE_MODIFY_USER, userIdentity.toDelimitedKey() );
-            settingsElement.setAttribute( XML_ATTRIBUTE_MODIFY_USER, userIdentity.toDelimitedKey() );
-        }
-    }
-
-    private XmlElement createOrGetSettingElement(
-            final PwmSetting setting,
-            final String profileID
-    )
-    {
-        final XmlElement existingSettingElement = xmlHelper.xpathForSetting( setting, profileID );
-        if ( existingSettingElement != null )
-        {
-            return existingSettingElement;
-        }
-
-        final XmlElement settingElement = xmlHelper.getXmlFactory().newElement( XML_ELEMENT_SETTING );
-        settingElement.setAttribute( XML_ATTRIBUTE_KEY, setting.getKey() );
-        settingElement.setAttribute( XML_ATTRIBUTE_SYNTAX, setting.getSyntax().toString() );
-        if ( profileID != null && profileID.length() > 0 )
-        {
-            settingElement.setAttribute( XML_ATTRIBUTE_PROFILE, profileID );
-        }
-
-        XmlElement settingsElement = document.getRootElement().getChild( XML_ELEMENT_SETTINGS );
-        if ( settingsElement == null )
-        {
-            settingsElement = xmlHelper.getXmlFactory().newElement( XML_ELEMENT_SETTINGS );
-            document.getRootElement().addContent( settingsElement );
-        }
-        settingsElement.addContent( settingElement );
-
-        return settingElement;
-    }
-
-    public static class SettingValueRecord implements Serializable
-    {
-        private PwmSetting setting;
-        private String profile;
-        private StoredValue storedValue;
-
-        public SettingValueRecord(
-                final PwmSetting setting,
-                final String profile,
-                final StoredValue storedValue
-        )
-        {
-            this.setting = setting;
-            this.profile = profile;
-            this.storedValue = storedValue;
-        }
-
-        public PwmSetting getSetting( )
-        {
-            return setting;
-        }
-
-        public String getProfile( )
-        {
-            return profile;
-        }
-
-        public StoredValue getStoredValue( )
-        {
-            return storedValue;
-        }
-    }
-
-    class StoredValueIterator implements Iterator<StoredConfigurationImpl.SettingValueRecord>
-    {
-
-        private Queue<SettingValueRecord> settingQueue = new LinkedList<>();
-
-        StoredValueIterator( final boolean includeDefaults )
-        {
-            for ( final PwmSetting setting : PwmSetting.values() )
-            {
-                if ( setting.getSyntax() != PwmSettingSyntax.PROFILE && !setting.getCategory().hasProfiles() )
-                {
-                    if ( includeDefaults || !isDefaultValue( setting ) )
-                    {
-                        final SettingValueRecord settingValueRecord = new SettingValueRecord( setting, null, null );
-                        settingQueue.add( settingValueRecord );
-                    }
-                }
-            }
-
-            for ( final PwmSettingCategory category : PwmSettingCategory.values() )
-            {
-                if ( category.hasProfiles() )
-                {
-                    for ( final String profileID : profilesForSetting( category.getProfileSetting() ) )
-                    {
-                        for ( final PwmSetting setting : category.getSettings() )
-                        {
-                            if ( includeDefaults || !isDefaultValue( setting, profileID ) )
-                            {
-                                final SettingValueRecord settingValueRecord = new SettingValueRecord( setting, profileID, null );
-                                settingQueue.add( settingValueRecord );
-                            }
-                        }
-                    }
-                }
-            }
-        }
-
-
-        @Override
-        public boolean hasNext( )
-        {
-            return !settingQueue.isEmpty();
-        }
-
-        @Override
-        public SettingValueRecord next( )
-        {
-            final StoredConfigurationImpl.SettingValueRecord settingValueRecord = settingQueue.poll();
-            return new SettingValueRecord(
-                    settingValueRecord.getSetting(),
-                    settingValueRecord.getProfile(),
-                    readSetting( settingValueRecord.getSetting(), settingValueRecord.getProfile() )
-            );
-        }
-
-        @Override
-        public void remove( )
-        {
-
-        }
-    }
-
-    private String createTime( )
-    {
-        final XmlElement rootElement = document.getRootElement();
-        final String createTimeString = rootElement.getAttributeValue( XML_ATTRIBUTE_CREATE_TIME );
-        if ( createTimeString == null || createTimeString.isEmpty() )
-        {
-            throw new IllegalStateException( "missing createTime timestamp" );
-        }
-        return createTimeString;
-    }
-
-    @Override
-    public Instant modifyTime( )
-    {
-        final XmlElement rootElement = document.getRootElement();
-        final String modifyTimeString = rootElement.getAttributeValue( XML_ATTRIBUTE_MODIFY_TIME );
-        if ( modifyTimeString != null )
-        {
-            try
-            {
-                return JavaHelper.parseIsoToInstant( modifyTimeString );
-            }
-            catch ( Exception e )
-            {
-                LOGGER.error( "error parsing root last modified timestamp: " + e.getMessage() );
-            }
-        }
-        return null;
-    }
-
-    public void initNewRandomSecurityKey( )
-            throws PwmUnrecoverableException
-    {
-        if ( !isDefaultValue( PwmSetting.PWM_SECURITY_KEY ) )
-        {
-            return;
-        }
-
-        writeSetting(
-                PwmSetting.PWM_SECURITY_KEY,
-                new PasswordValue( new PasswordData( PwmRandom.getInstance().alphaNumericString( 1024 ) ) ),
-                null
-        );
-
-        LOGGER.debug( () -> "initialized new random security key" );
-    }
-
+        return Optional.ofNullable( metaValues.get( storedConfigItemKey ) );
+    }
 
 
     @Override
     @Override
-    public boolean isLocked( )
-    {
-        return locked;
-    }
-
-    private List<ConfigRecordID> allSettingConfigRecordIDs( )
-    {
-        final LinkedHashSet<ConfigRecordID> loopResults = new LinkedHashSet<>();
-        for ( final PwmSetting loopSetting : PwmSetting.values() )
-        {
-            if ( loopSetting.getCategory().hasProfiles() )
-            {
-                for ( final String profile : profilesForSetting( loopSetting ) )
-                {
-                    loopResults.add( new ConfigRecordID( ConfigRecordID.RecordType.SETTING, loopSetting, profile ) );
-                }
-            }
-            else
-            {
-                loopResults.add( new ConfigRecordID( ConfigRecordID.RecordType.SETTING, loopSetting, null ) );
-            }
-        }
-        return new ArrayList<>( loopResults );
-    }
-
-    XmlHelper getXmlHelper()
+    public Optional<StoredValue> readStoredValue( final StoredConfigItemKey storedConfigItemKey )
     {
     {
-        return xmlHelper;
+        return Optional.ofNullable( storedValues.get( storedConfigItemKey ) );
     }
     }
 }
 }

+ 291 - 0
server/src/main/java/password/pwm/config/stored/StoredConfigurationModifier.java

@@ -0,0 +1,291 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2019 The PWM Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package password.pwm.config.stored;
+
+import password.pwm.bean.UserIdentity;
+import password.pwm.config.PwmSetting;
+import password.pwm.config.PwmSettingCategory;
+import password.pwm.config.StoredValue;
+import password.pwm.config.value.LocalizedStringValue;
+import password.pwm.config.value.StringArrayValue;
+import password.pwm.config.value.StringValue;
+import password.pwm.error.ErrorInformation;
+import password.pwm.error.PwmError;
+import password.pwm.error.PwmOperationalException;
+import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.i18n.PwmLocaleBundle;
+import password.pwm.util.java.StringUtil;
+import password.pwm.util.secure.BCrypt;
+
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.atomic.AtomicReference;
+
+public class StoredConfigurationModifier
+{
+    private final AtomicReference<StoredConfigData> ref = new AtomicReference<>( );
+
+    private StoredConfigurationModifier( final StoredConfiguration storedConfiguration )
+    {
+        this.ref.set( ( ( StoredConfigurationImpl ) storedConfiguration ).asStoredConfigData() );
+    }
+
+    public static StoredConfigurationModifier newModifier( final StoredConfiguration storedConfiguration )
+    {
+        return new StoredConfigurationModifier( storedConfiguration );
+    }
+
+    public StoredConfiguration newStoredConfiguration()
+    {
+        return new StoredConfigurationImpl( ref.get() );
+    }
+
+    public void writeSetting(
+            final PwmSetting setting,
+            final String profileID,
+            final StoredValue value,
+            final UserIdentity userIdentity
+    )
+            throws PwmUnrecoverableException
+    {
+        Objects.requireNonNull( setting );
+        Objects.requireNonNull( value );
+
+        update( ( storedConfigData ) ->
+        {
+            if ( StringUtil.isEmpty( profileID ) && setting.getCategory().hasProfiles() )
+            {
+                throw new IllegalArgumentException( "writing of setting " + setting.getKey() + " requires a non-null profileID" );
+            }
+            if ( !StringUtil.isEmpty( profileID ) && !setting.getCategory().hasProfiles() )
+            {
+                throw new IllegalArgumentException( "cannot specify profile for non-profile setting" );
+            }
+
+            final StoredConfigItemKey key = StoredConfigItemKey.fromSetting( setting, profileID );
+
+
+            return storedConfigData.toBuilder()
+                    .storedValue( key, value )
+                    .metaData( key, new ValueMetaData( Instant.now(), userIdentity ) )
+                    .build();
+        } );
+    }
+
+    public void writeConfigProperty(
+            final ConfigurationProperty propertyName,
+            final String value
+    )
+            throws PwmUnrecoverableException
+    {
+        update( ( storedConfigData ) ->
+        {
+            final StoredConfigItemKey key = StoredConfigItemKey.fromConfigurationProperty( propertyName );
+            final Map<StoredConfigItemKey, StoredValue> existingStoredValues = new HashMap<>( storedConfigData.getStoredValues() );
+
+            if ( StringUtil.isEmpty( value ) )
+            {
+                existingStoredValues.remove( key );
+            }
+            else
+            {
+                final StoredValue storedValue = new StringValue( value );
+                existingStoredValues.put( key, storedValue );
+            }
+
+            return storedConfigData.toBuilder()
+                    .clearStoredValues()
+                    .storedValues( existingStoredValues )
+                    .build();
+        } );
+    }
+
+    public void resetLocaleBundleMap( final PwmLocaleBundle pwmLocaleBundle, final String keyName )
+            throws PwmUnrecoverableException
+    {
+        update( ( storedConfigData ) ->
+        {
+            final Map<StoredConfigItemKey, StoredValue> existingStoredValues = new HashMap<>( storedConfigData.getStoredValues() );
+
+            final StoredConfigItemKey key = StoredConfigItemKey.fromLocaleBundle( pwmLocaleBundle, keyName );
+            existingStoredValues.remove( key );
+
+            return storedConfigData.toBuilder()
+                    .clearStoredValues()
+                    .storedValues( existingStoredValues )
+                    .build();
+        } );
+    }
+
+    public void resetSetting( final PwmSetting setting, final String profileID, final UserIdentity userIdentity )
+            throws PwmUnrecoverableException
+    {
+        update( ( storedConfigData ) ->
+        {
+            final Map<StoredConfigItemKey, StoredValue> existingStoredValues = new HashMap<>( storedConfigData.getStoredValues() );
+
+            final StoredConfigItemKey key = StoredConfigItemKey.fromSetting( setting, profileID );
+            existingStoredValues.remove( key );
+
+            return storedConfigData.toBuilder()
+                    .clearStoredValues()
+                    .storedValues( existingStoredValues )
+                    .metaData( key, new ValueMetaData( Instant.now(), userIdentity ) )
+                    .build();
+        } );
+    }
+
+    public void writeLocaleBundleMap(
+            final PwmLocaleBundle pwmLocaleBundle,
+            final String keyName,
+            final Map<String, String> localeMap
+    )
+            throws PwmUnrecoverableException
+    {
+        update( ( storedConfigData ) ->
+        {
+            final StoredConfigItemKey key = StoredConfigItemKey.fromLocaleBundle( pwmLocaleBundle, keyName );
+            final StoredValue value = new LocalizedStringValue( localeMap );
+
+            return storedConfigData.toBuilder()
+                    .storedValue( key, value )
+                    .build();
+        } );
+    }
+
+    public void copyProfileID(
+            final PwmSettingCategory category,
+            final String sourceID,
+            final String destinationID,
+            final UserIdentity userIdentity
+    )
+            throws PwmUnrecoverableException
+    {
+
+        if ( !category.hasProfiles() )
+        {
+            throw PwmUnrecoverableException.newException(
+                    PwmError.ERROR_INVALID_CONFIG, "can not copy profile ID for category " + category + ", category does not have profiles" );
+        }
+
+        update( ( storedConfigData ) ->
+        {
+            final StoredConfiguration oldStoredConfiguration = new StoredConfigurationImpl( storedConfigData );
+
+            final List<String> existingProfiles = oldStoredConfiguration.profilesForSetting( category.getProfileSetting() );
+            if ( !existingProfiles.contains( sourceID ) )
+            {
+                throw PwmUnrecoverableException.newException(
+                        PwmError.ERROR_INVALID_CONFIG, "can not copy profile ID for category, source profileID '" + sourceID + "' does not exist" );
+            }
+
+            if ( existingProfiles.contains( destinationID ) )
+            {
+                throw PwmUnrecoverableException.newException(
+                        PwmError.ERROR_INVALID_CONFIG, "can not copy profile ID for category, destination profileID '" + destinationID + "' already exists" );
+            }
+
+            final Collection<PwmSettingCategory> interestedCategories = PwmSettingCategory.associatedProfileCategories( category );
+            for ( final PwmSettingCategory interestedCategory : interestedCategories )
+            {
+                for ( final PwmSetting pwmSetting : interestedCategory.getSettings() )
+                {
+                    if ( !oldStoredConfiguration.isDefaultValue( pwmSetting, sourceID ) )
+                    {
+                        final StoredValue value = oldStoredConfiguration.readSetting( pwmSetting, sourceID );
+                        writeSetting( pwmSetting, destinationID, value, userIdentity );
+                    }
+                }
+            }
+            final List<String> newProfileIDList = new ArrayList<>( existingProfiles );
+            newProfileIDList.add( destinationID );
+
+            final StoredValue value = new StringArrayValue( newProfileIDList );
+            final StoredConfigItemKey key = StoredConfigItemKey.fromSetting( category.getProfileSetting(), null );
+            final ValueMetaData valueMetaData = new ValueMetaData( Instant.now(), userIdentity );
+
+            return storedConfigData.toBuilder()
+                    .storedValue( key, value )
+                    .metaData( key, valueMetaData )
+                    .build();
+
+        } );
+    }
+
+    public void setPassword( final String password )
+            throws PwmOperationalException, PwmUnrecoverableException
+    {
+        if ( password == null || password.isEmpty() )
+        {
+            throw new PwmOperationalException( new ErrorInformation( PwmError.CONFIG_FORMAT_ERROR, null, new String[]
+                    {
+                            "can not set blank password",
+                    }
+            ) );
+        }
+        final String trimmedPassword = password.trim();
+        if ( trimmedPassword.length() < 1 )
+        {
+            throw new PwmOperationalException( new ErrorInformation( PwmError.CONFIG_FORMAT_ERROR, null, new String[]
+                    {
+                            "can not set blank password",
+                    }
+            ) );
+        }
+
+
+        final String passwordHash = BCrypt.hashPassword( password );
+        this.writeConfigProperty( ConfigurationProperty.PASSWORD_HASH, passwordHash );
+    }
+
+    private void update( final FunctionWithException<StoredConfigData> function ) throws PwmUnrecoverableException
+    {
+        try
+        {
+            ref.updateAndGet( storedConfigData ->
+            {
+                try
+                {
+                    return function.applyThrows( storedConfigData );
+                }
+                catch ( final PwmUnrecoverableException e )
+                {
+                    throw new RuntimeException( e );
+                }
+            } );
+        }
+        catch ( final RuntimeException e )
+        {
+            throw ( PwmUnrecoverableException ) e.getCause();
+        }
+        ref.updateAndGet( storedConfigData -> storedConfigData.toBuilder().modifyTime( Instant.now() ).build() );
+    }
+
+    interface FunctionWithException<T>
+    {
+        T applyThrows( T value ) throws PwmUnrecoverableException;
+    }
+}

+ 348 - 37
server/src/main/java/password/pwm/config/stored/StoredConfigurationUtil.java

@@ -20,24 +20,49 @@
 
 
 package password.pwm.config.stored;
 package password.pwm.config.stored;
 
 
+import password.pwm.PwmConstants;
+import password.pwm.bean.UserIdentity;
+import password.pwm.config.Configuration;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.PwmSettingCategory;
 import password.pwm.config.PwmSettingCategory;
 import password.pwm.config.PwmSettingSyntax;
 import password.pwm.config.PwmSettingSyntax;
 import password.pwm.config.StoredValue;
 import password.pwm.config.StoredValue;
+import password.pwm.config.value.PasswordValue;
+import password.pwm.error.ErrorInformation;
+import password.pwm.error.PwmError;
+import password.pwm.error.PwmOperationalException;
+import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.util.PasswordData;
 import password.pwm.util.java.StringUtil;
 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.PwmRandom;
 
 
-import java.io.Serializable;
+import java.time.Instant;
 import java.util.ArrayList;
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.Collections;
+import java.util.HashSet;
 import java.util.LinkedList;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.List;
+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.TreeMap;
+import java.util.TreeSet;
+import java.util.stream.Collectors;
 
 
 public abstract class StoredConfigurationUtil
 public abstract class StoredConfigurationUtil
 {
 {
+    private static final PwmLogger LOGGER = PwmLogger.forClass( StoredConfigurationUtil.class );
+
     public static List<String> profilesForSetting
     public static List<String> profilesForSetting
-            ( final PwmSetting pwmSetting,
-              final StoredConfiguration storedConfiguration
+            (
+                    final PwmSetting pwmSetting,
+                    final StoredConfiguration storedConfiguration
             )
             )
     {
     {
         if ( !pwmSetting.getCategory().hasProfiles() && pwmSetting.getSyntax() != PwmSettingSyntax.PROFILE )
         if ( !pwmSetting.getCategory().hasProfiles() && pwmSetting.getSyntax() != PwmSettingSyntax.PROFILE )
@@ -88,7 +113,7 @@ public abstract class StoredConfigurationUtil
             profileSetting = pwmSetting.getCategory().getProfileSetting();
             profileSetting = pwmSetting.getCategory().getProfileSetting();
         }
         }
 
 
-        final Object nativeObject = storedConfiguration.readSetting( profileSetting ).toNativeObject();
+        final Object nativeObject = storedConfiguration.readSetting( profileSetting, null ).toNativeObject();
         final List<String> settingValues = ( List<String> ) nativeObject;
         final List<String> settingValues = ( List<String> ) nativeObject;
         final LinkedList<String> profiles = new LinkedList<>( settingValues );
         final LinkedList<String> profiles = new LinkedList<>( settingValues );
         profiles.removeIf( profile -> StringUtil.isEmpty( profile ) );
         profiles.removeIf( profile -> StringUtil.isEmpty( profile ) );
@@ -96,66 +121,352 @@ public abstract class StoredConfigurationUtil
 
 
     }
     }
 
 
+    public static String changeLogAsDebugString(
+            final StoredConfiguration storedConfiguration,
+            final Set<StoredConfigItemKey> configChangeLog,
+            final Locale locale
+    )
+            throws PwmUnrecoverableException
+    {
+
+        final Map<String, String> outputMap = StoredConfigurationUtil.makeDebugMap( storedConfiguration, configChangeLog, locale );
+        final StringBuilder output = new StringBuilder();
+        if ( outputMap.isEmpty() )
+        {
+            output.append( "No setting changes." );
+        }
+        else
+        {
+            for ( final Map.Entry<String, String> entry : outputMap.entrySet() )
+            {
+                final String keyName = entry.getKey();
+                final String value = entry.getValue();
+                output.append( keyName );
+                output.append( "\n" );
+                output.append( " Value: " );
+                output.append( value );
+                output.append( "\n" );
+            }
+        }
+        return output.toString();
+
+    }
 
 
-    public static List<StoredConfigReference> modifiedSettings( final StoredConfiguration storedConfiguration )
+    public static StoredConfiguration copyConfigAndBlankAllPasswords( final StoredConfiguration input )
+            throws PwmUnrecoverableException
     {
     {
-        final List<StoredConfigReference> returnObj = new ArrayList<>();
+        final StoredConfigurationModifier modifier = StoredConfigurationModifier.newModifier( input );
 
 
-        for ( final PwmSetting setting : PwmSetting.values() )
+        for ( final StoredConfigItemKey storedConfigItemKey : input.modifiedItems() )
         {
         {
-            if ( setting.getSyntax() != PwmSettingSyntax.PROFILE && !setting.getCategory().hasProfiles() )
+            if ( storedConfigItemKey.getRecordType() == StoredConfigItemKey.RecordType.SETTING )
             {
             {
-                if ( !storedConfiguration.isDefaultValue( setting, null ) )
+                final PwmSetting pwmSetting = storedConfigItemKey.toPwmSetting();
+                if ( pwmSetting.getSyntax() == PwmSettingSyntax.PASSWORD )
                 {
                 {
-                    final StoredConfigReference storedConfigReference = new StoredConfigReferenceBean(
-                            StoredConfigReference.RecordType.SETTING,
-                            setting.getKey(),
-                            null
-                    );
-                    returnObj.add( storedConfigReference );
+                    final ValueMetaData valueMetaData = input.readSettingMetadata( pwmSetting, storedConfigItemKey.getProfileID() );
+                    final UserIdentity userIdentity = valueMetaData == null ? null : valueMetaData.getUserIdentity();
+                    final PasswordValue passwordValue = new PasswordValue( new PasswordData( PwmConstants.LOG_REMOVED_VALUE_REPLACEMENT ) );
+                    modifier.writeSetting( pwmSetting, storedConfigItemKey.getProfileID(), passwordValue, userIdentity );
                 }
                 }
             }
             }
         }
         }
 
 
-        for ( final PwmSettingCategory category : PwmSettingCategory.values() )
+
+        final Optional<String> pwdHash = input.readConfigProperty( ConfigurationProperty.PASSWORD_HASH );
+        if ( pwdHash.isPresent() )
         {
         {
-            if ( category.hasProfiles() )
+            modifier.writeConfigProperty( ConfigurationProperty.PASSWORD_HASH, PwmConstants.LOG_REMOVED_VALUE_REPLACEMENT );
+        }
+
+        return modifier.newStoredConfiguration();
+    }
+
+    public static List<String> validateValues( final StoredConfiguration storedConfiguration )
+    {
+        final Instant startTime = Instant.now();
+        final List<String> errorStrings = new ArrayList<>();
+
+        for ( final StoredConfigItemKey storedConfigItemKey : storedConfiguration.modifiedItems() )
+        {
+            if ( storedConfigItemKey.getRecordType() == StoredConfigItemKey.RecordType.SETTING )
             {
             {
-                for ( final String profileID : profilesForSetting( category.getProfileSetting(), storedConfiguration ) )
+                final PwmSetting pwmSetting = storedConfigItemKey.toPwmSetting();
+                final String profileID = storedConfigItemKey.getProfileID();
+                final StoredValue loopValue = storedConfiguration.readSetting( pwmSetting, profileID );
+
+                try
                 {
                 {
-                    for ( final PwmSetting setting : category.getSettings() )
+                    final List<String> errors = loopValue.validateValue( pwmSetting );
+                    for ( final String loopError : errors )
                     {
                     {
-                        if ( !storedConfiguration.isDefaultValue( setting, profileID ) )
-                        {
-                            final StoredConfigReference storedConfigReference = new StoredConfigReferenceBean(
-                                    StoredConfigReference.RecordType.SETTING,
-                                    setting.getKey(),
-                                    profileID
-                            );
-                            returnObj.add( storedConfigReference );
-                        }
+                        errorStrings.add( pwmSetting.toMenuLocationDebug( storedConfigItemKey.getProfileID(), PwmConstants.DEFAULT_LOCALE ) + " - " + loopError );
+                    }
+                }
+                catch ( Exception e )
+                {
+                    LOGGER.error( "unexpected error during validate value for "
+                            + pwmSetting.toMenuLocationDebug( profileID, PwmConstants.DEFAULT_LOCALE ) + ", error: "
+                            + e.getMessage(), e );
+                }
+            }
+        }
+
+        LOGGER.trace( () -> "StoredConfiguration validator completed in " + TimeDuration.compactFromCurrent( startTime ) );
+        return errorStrings;
+    }
+
+    public static Set<StoredConfigItemKey> search( final StoredConfiguration storedConfiguration, final String searchTerm, final Locale locale )
+    {
+        return new SettingSearchMachine( storedConfiguration, searchTerm, locale ).search();
+    }
+
+    public static boolean matchSetting(
+            final StoredConfiguration storedConfiguration,
+            final PwmSetting setting,
+            final StoredValue storedValue,
+            final String term,
+            final Locale defaultLocale )
+    {
+        return new SettingSearchMachine( storedConfiguration, term, defaultLocale ).matchSetting( setting, storedValue, term );
+    }
+
+    private static class SettingSearchMachine
+    {
+        private final StoredConfiguration storedConfiguration;
+        private final String searchTerm;
+        private final Locale locale;
+
+        private SettingSearchMachine( final StoredConfiguration storedConfiguration, final String searchTerm, final Locale locale )
+        {
+            this.storedConfiguration = storedConfiguration;
+            this.searchTerm = searchTerm;
+            this.locale = locale;
+        }
+
+        public Set<StoredConfigItemKey> search()
+        {
+            if ( StringUtil.isEmpty( searchTerm ) )
+            {
+                return Collections.emptySet();
+            }
+
+            return allPossibleSettingKeysForConfiguration( storedConfiguration )
+                    .parallelStream()
+                    .filter( s -> s.getRecordType() == StoredConfigItemKey.RecordType.SETTING )
+                    .filter( this::matchSetting )
+                    .collect( Collectors.toCollection( TreeSet::new ) );
+        }
+
+        private boolean matchSetting(
+                final StoredConfigItemKey storedConfigItemKey
+        )
+        {
+            final PwmSetting pwmSetting = storedConfigItemKey.toPwmSetting();
+            final StoredValue value = storedConfiguration.readSetting( pwmSetting, storedConfigItemKey.getProfileID() );
+
+            return StringUtil.whitespaceSplit( searchTerm )
+                    .parallelStream()
+                    .allMatch( s -> matchSetting( pwmSetting, value, s ) );
+        }
+
+        private boolean matchSetting( final PwmSetting setting, final StoredValue value, final String searchTerm )
+        {
+            if ( setting.isHidden() || setting.getCategory().isHidden() )
+            {
+                return false;
+            }
+
+            if ( searchTerm == null || searchTerm.isEmpty() )
+            {
+                return false;
+            }
+
+            final String lowerSearchTerm = searchTerm.toLowerCase();
+
+            {
+                final String key = setting.getKey();
+                if ( key.toLowerCase().contains( lowerSearchTerm ) )
+                {
+                    return true;
+                }
+            }
+            {
+                final String label = setting.getLabel( locale );
+                if ( label.toLowerCase().contains( lowerSearchTerm ) )
+                {
+                    return true;
+                }
+            }
+            {
+                final String descr = setting.getDescription( locale );
+                if ( descr.toLowerCase().contains( lowerSearchTerm ) )
+                {
+                    return true;
+                }
+            }
+            {
+                final String menuLocationString = setting.toMenuLocationDebug( null, locale );
+                if ( menuLocationString.toLowerCase().contains( lowerSearchTerm ) )
+                {
+                    return true;
+                }
+            }
+
+            if ( setting.isConfidential() )
+            {
+                return false;
+            }
+            {
+                final String valueDebug = value.toDebugString( locale );
+                if ( valueDebug != null && valueDebug.toLowerCase().contains( lowerSearchTerm ) )
+                {
+                    return true;
+                }
+            }
+            if ( PwmSettingSyntax.SELECT == setting.getSyntax()
+                    || PwmSettingSyntax.OPTIONLIST == setting.getSyntax()
+                    || PwmSettingSyntax.VERIFICATION_METHOD == setting.getSyntax()
+            )
+            {
+                for ( final String key : setting.getOptions().keySet() )
+                {
+                    if ( key.toLowerCase().contains( lowerSearchTerm ) )
+                    {
+                        return true;
+                    }
+                    final String optionValue = setting.getOptions().get( key );
+                    if ( optionValue != null && optionValue.toLowerCase().contains( lowerSearchTerm ) )
+                    {
+                        return true;
                     }
                     }
                 }
                 }
             }
             }
+            return false;
         }
         }
+    }
 
 
-        return returnObj;
+    public static boolean verifyPassword( final StoredConfiguration storedConfiguration, final String password )
+    {
+        if ( !hasPassword( storedConfiguration ) )
+        {
+            return false;
+        }
+        final Optional<String> passwordHash = storedConfiguration.readConfigProperty( ConfigurationProperty.PASSWORD_HASH );
+        return passwordHash.isPresent() && BCrypt.testAnswer( password, passwordHash.get(), new Configuration( storedConfiguration ) );
     }
     }
 
 
-    public static Serializable toJsonDebugObject( final StoredConfiguration storedConfiguration )
+    public static boolean hasPassword( final StoredConfiguration storedConfiguration )
     {
     {
-        final TreeMap<String, Object> outputObject = new TreeMap<>();
+        final Optional<String> passwordHash = storedConfiguration.readConfigProperty( ConfigurationProperty.PASSWORD_HASH );
+        return passwordHash.isPresent();
+    }
 
 
-        for ( final StoredConfigReference storedConfigReference : modifiedSettings( storedConfiguration ) )
+    public static void setPassword( final StoredConfigurationModifier storedConfiguration, final String password )
+            throws PwmOperationalException, PwmUnrecoverableException
+    {
+        if ( StringUtil.isEmpty( password ) )
+        {
+            throw new PwmOperationalException( new ErrorInformation( PwmError.CONFIG_FORMAT_ERROR, null, new String[]
+                    {
+                            "can not set blank password",
+                    }
+            ) );
+        }
+        final String trimmedPassword = password.trim();
+        if ( trimmedPassword.length() < 1 )
         {
         {
-            final PwmSetting setting = PwmSetting.forKey( storedConfigReference.getRecordID() );
-            if ( setting != null )
+            throw new PwmOperationalException( new ErrorInformation( PwmError.CONFIG_FORMAT_ERROR, null, new String[]
+                    {
+                            "can not set blank password",
+                    }
+            ) );
+        }
+
+
+        final String passwordHash = BCrypt.hashPassword( password );
+        storedConfiguration.writeConfigProperty( ConfigurationProperty.PASSWORD_HASH, passwordHash );
+    }
+
+    public static void initNewRandomSecurityKey( final StoredConfigurationModifier modifier )
+            throws PwmUnrecoverableException
+    {
+        if ( !modifier.newStoredConfiguration().isDefaultValue( PwmSetting.PWM_SECURITY_KEY, null ) )
+        {
+            return;
+        }
+
+        modifier.writeSetting(
+                PwmSetting.PWM_SECURITY_KEY, null,
+                new PasswordValue( new PasswordData( PwmRandom.getInstance().alphaNumericString( 1024 ) ) ),
+                null
+        );
+
+        LOGGER.debug( () -> "initialized new random security key" );
+    }
+
+
+    public static Map<String, String> makeDebugMap(
+            final StoredConfiguration storedConfiguration,
+            final Collection<StoredConfigItemKey> interestedItems,
+            final Locale locale
+    )
+    {
+        final Map<String, String> outputMap = interestedItems.stream()
+                .filter( ( key ) -> key.getRecordType() != StoredConfigItemKey.RecordType.PROPERTY )
+                .filter( ( key ) -> storedConfiguration.readStoredValue( key ).isPresent() )
+                .collect( Collectors.toMap(
+                        key -> key.getLabel( locale ),
+                        key -> storedConfiguration.readStoredValue( key ).get().toDebugString( locale ) ) );
+
+        return Collections.unmodifiableMap( new TreeMap<>( outputMap ) );
+    }
+
+    public static Set<StoredConfigItemKey> allPossibleSettingKeysForConfiguration(
+            final StoredConfiguration storedConfiguration
+    )
+    {
+        final Set<StoredConfigItemKey> loopResults = new HashSet<>();
+        for ( final PwmSetting loopSetting : PwmSetting.values() )
+        {
+            if ( loopSetting.getCategory().hasProfiles() )
             {
             {
-                final StoredValue value = storedConfiguration.readSetting( setting, storedConfigReference.getProfileID() );
-                outputObject.put( setting.getKey(), value.toDebugJsonObject( null ) );
+                for ( final String profile : storedConfiguration.profilesForSetting( loopSetting ) )
+                {
+                    loopResults.add( StoredConfigItemKey.fromSetting( loopSetting, profile ) );
+                }
+            }
+            else
+            {
+                loopResults.add( StoredConfigItemKey.fromSetting( loopSetting, null ) );
             }
             }
         }
         }
-        return outputObject;
+        return Collections.unmodifiableSet( loopResults );
     }
     }
 
 
+    public static Set<StoredConfigItemKey> changedValues (
+            final StoredConfiguration originalConfiguration,
+            final StoredConfiguration modifiedConfiguration
+    )
+    {
+        final Instant startTime = Instant.now();
+
+        final Set<StoredConfigItemKey> interestedReferences = new HashSet<>();
+        interestedReferences.addAll( originalConfiguration.modifiedItems() );
+        interestedReferences.addAll( modifiedConfiguration.modifiedItems() );
+
+        final Set<StoredConfigItemKey> deltaReferences = interestedReferences
+                .parallelStream()
+                .filter( reference ->
+                        {
+                            final Optional<String> hash1 = originalConfiguration.readStoredValue( reference ).map( StoredValue::valueHash );
+                            final Optional<String> hash2 = modifiedConfiguration.readStoredValue( reference ).map( StoredValue::valueHash );
+                            return hash1.isPresent() && hash2.isPresent() && !Objects.equals( hash1.get(), hash2.get() );
+                        }
+                ).collect( Collectors.toSet() );
+
+        LOGGER.trace( () -> "generated changeLog items via compare in " + TimeDuration.compactFromCurrent( startTime ) );
+
+        return Collections.unmodifiableSet( deltaReferences );
+    }
 }
 }

+ 11 - 15
server/src/main/java/password/pwm/config/stored/ConfigChangeLog.java → server/src/main/java/password/pwm/config/stored/XmlOutputProcessData.java

@@ -20,20 +20,16 @@
 
 
 package password.pwm.config.stored;
 package password.pwm.config.stored;
 
 
-import password.pwm.config.StoredValue;
-
-import java.util.Collection;
-import java.util.Locale;
-
-public interface ConfigChangeLog
+import lombok.Builder;
+import lombok.Value;
+import password.pwm.config.value.StoredValueEncoder;
+import password.pwm.util.secure.PwmSecurityKey;
+
+@Value
+@Builder
+public class XmlOutputProcessData
 {
 {
-    boolean isModified( );
-
-    String changeLogAsDebugString( Locale locale, boolean asHtml );
-
-    void updateChangeLog( StoredConfigReference reference, StoredValue newValue );
-
-    void updateChangeLog( StoredConfigReference reference, StoredValue currentValue, StoredValue newValue );
-
-    Collection<StoredConfigReference> changedValues( );
+    @Builder.Default
+    private StoredValueEncoder.Mode storedValueEncoderMode = StoredValueEncoder.Mode.ENCODED;
+    private PwmSecurityKey pwmSecurityKey;
 }
 }

+ 0 - 99
server/src/main/java/password/pwm/config/stored/ng/NGStorageEngineImpl.java

@@ -1,99 +0,0 @@
-/*
- * Password Management Servlets (PWM)
- * http://www.pwm-project.org
- *
- * Copyright (c) 2006-2009 Novell, Inc.
- * Copyright (c) 2009-2019 The PWM Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package password.pwm.config.stored.ng;
-
-import password.pwm.bean.UserIdentity;
-import password.pwm.config.StoredValue;
-import password.pwm.config.stored.StoredConfigReference;
-import password.pwm.config.stored.ValueMetaData;
-
-import java.time.Instant;
-import java.util.HashMap;
-import java.util.Map;
-
-class NGStorageEngineImpl
-{
-    private final Map<StoredConfigReference, StoredValue> storedValues = new HashMap<>();
-    private final Map<StoredConfigReference, ValueMetaData> metaValues = new HashMap<>();
-
-    NGStorageEngineImpl()
-    {
-    }
-
-    NGStorageEngineImpl(
-            final Map<StoredConfigReference, StoredValue> storedValues,
-            final Map<StoredConfigReference, ValueMetaData> metaValues
-    )
-    {
-        this.storedValues.putAll( storedValues );
-        this.metaValues.putAll( metaValues );
-    }
-
-    StoredValue read( final StoredConfigReference storedConfigReference )
-    {
-        return storedValues.get( storedConfigReference );
-    }
-
-    ValueMetaData readMetaData( final StoredConfigReference storedConfigReference )
-    {
-        return metaValues.get( storedConfigReference );
-    }
-
-    void writeMetaData( final StoredConfigReference storedConfigReference, final ValueMetaData valueMetaData )
-    {
-        metaValues.put( storedConfigReference, valueMetaData );
-    }
-
-    void write( final StoredConfigReference reference, final StoredValue value, final UserIdentity userIdentity )
-    {
-        if ( reference != null )
-        {
-            if ( value != null )
-            {
-                storedValues.put( reference, value );
-            }
-
-            updateUserIdentity( reference, userIdentity );
-        }
-    }
-
-    void reset( final StoredConfigReference reference, final UserIdentity userIdentity )
-    {
-        if ( reference != null )
-        {
-            storedValues.remove( reference );
-            updateUserIdentity( reference, userIdentity );
-        }
-    }
-
-    private void updateUserIdentity(
-            final StoredConfigReference reference,
-            final UserIdentity userIdentity
-    )
-    {
-        metaValues.put(
-                reference,
-                ValueMetaData.builder().modifyDate( Instant.now() )
-                        .userIdentity( userIdentity )
-                        .build() );
-
-    }
-}

+ 0 - 202
server/src/main/java/password/pwm/config/stored/ng/NGStoredConfiguration.java

@@ -1,202 +0,0 @@
-/*
- * Password Management Servlets (PWM)
- * http://www.pwm-project.org
- *
- * Copyright (c) 2006-2009 Novell, Inc.
- * Copyright (c) 2009-2019 The PWM Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package password.pwm.config.stored.ng;
-
-import password.pwm.bean.UserIdentity;
-import password.pwm.config.PwmSetting;
-import password.pwm.config.PwmSettingCategory;
-import password.pwm.config.StoredValue;
-import password.pwm.config.stored.ConfigurationProperty;
-import password.pwm.config.stored.StoredConfigReference;
-import password.pwm.config.stored.StoredConfigReferenceBean;
-import password.pwm.config.stored.StoredConfiguration;
-import password.pwm.config.stored.ValueMetaData;
-import password.pwm.config.value.StringValue;
-import password.pwm.error.PwmUnrecoverableException;
-import password.pwm.util.java.JavaHelper;
-import password.pwm.util.logging.PwmLogger;
-import password.pwm.util.secure.PwmSecurityKey;
-
-import java.time.Instant;
-
-public class NGStoredConfiguration implements StoredConfiguration
-{
-    private static final PwmLogger LOGGER = PwmLogger.forClass( NGStoredConfiguration.class );
-    private final PwmSecurityKey configurationSecurityKey;
-    private final NGStorageEngineImpl engine;
-    private boolean readOnly = false;
-
-    NGStoredConfiguration(
-            final NGStorageEngineImpl storageEngine,
-            final PwmSecurityKey pwmSecurityKey )
-    {
-        engine = storageEngine;
-        configurationSecurityKey = pwmSecurityKey;
-    }
-
-    public String readConfigProperty( final ConfigurationProperty configurationProperty )
-    {
-        final StoredConfigReference storedConfigReference = new StoredConfigReferenceBean(
-                StoredConfigReference.RecordType.PROPERTY,
-                configurationProperty.getKey(),
-                null
-        );
-        final StoredValue storedValue = engine.read( storedConfigReference );
-        if ( storedValue == null | !( storedValue instanceof StringValue ) )
-        {
-            return null;
-        }
-        return ( String ) storedValue.toNativeObject();
-    }
-
-    public void writeConfigProperty(
-            final ConfigurationProperty configurationProperty,
-            final String value )
-    {
-        final StoredConfigReference storedConfigReference = new StoredConfigReferenceBean(
-                StoredConfigReference.RecordType.PROPERTY,
-                configurationProperty.getKey(),
-                null
-        );
-        final StoredValue storedValue = new StringValue( value );
-        engine.write( storedConfigReference, storedValue, null  );
-    }
-
-    public void resetSetting( final PwmSetting setting, final String profileID, final UserIdentity userIdentity )
-    {
-        final StoredConfigReference storedConfigReference = new StoredConfigReferenceBean(
-                StoredConfigReference.RecordType.SETTING,
-                setting.getKey(),
-                profileID
-        );
-        engine.reset( storedConfigReference, userIdentity );
-    }
-
-    public boolean isDefaultValue( final PwmSetting setting )
-    {
-        return isDefaultValue( setting, null );
-    }
-
-    public boolean isDefaultValue( final PwmSetting setting, final String profileID )
-    {
-        final StoredConfigReference storedConfigReference = new StoredConfigReferenceBean(
-                StoredConfigReference.RecordType.SETTING,
-                setting.getKey(),
-                profileID
-        );
-        final StoredValue value = engine.read( storedConfigReference );
-        return value == null;
-    }
-
-    public StoredValue readSetting( final PwmSetting setting )
-    {
-        return readSetting( setting, null );
-    }
-
-    public StoredValue readSetting( final PwmSetting setting, final String profileID )
-    {
-        final StoredConfigReference storedConfigReference = new StoredConfigReferenceBean(
-                StoredConfigReference.RecordType.SETTING,
-                setting.getKey(),
-                profileID
-        );
-        return engine.read( storedConfigReference );
-    }
-
-    public void copyProfileID( final PwmSettingCategory category, final String sourceID, final String destinationID, final UserIdentity userIdentity )
-            throws PwmUnrecoverableException
-    {
-        //@todo
-        throw new IllegalStateException( "not implemented" );
-    }
-
-    public void writeSetting(
-            final PwmSetting setting,
-            final StoredValue value,
-            final UserIdentity userIdentity
-    ) throws PwmUnrecoverableException
-    {
-        writeSetting( setting, null, value, userIdentity );
-    }
-
-    public void writeSetting(
-            final PwmSetting setting,
-            final String profileID,
-            final StoredValue value,
-            final UserIdentity userIdentity
-    )
-            throws PwmUnrecoverableException
-    {
-        final StoredConfigReference storedConfigReference = new StoredConfigReferenceBean(
-                StoredConfigReference.RecordType.SETTING,
-                setting.getKey(),
-                profileID
-        );
-        engine.write( storedConfigReference, value, userIdentity );
-    }
-
-    @Override
-    public PwmSecurityKey getKey( ) throws PwmUnrecoverableException
-    {
-        return configurationSecurityKey;
-    }
-
-    @Override
-    public boolean isLocked( )
-    {
-        return readOnly;
-    }
-
-    @Override
-    public void lock( )
-    {
-        readOnly = true;
-    }
-
-    @Override
-    public ValueMetaData readSettingMetadata( final PwmSetting setting, final String profileID )
-    {
-        final StoredConfigReference storedConfigReference = new StoredConfigReferenceBean(
-                StoredConfigReference.RecordType.SETTING,
-                setting.getKey(),
-                profileID
-        );
-        return engine.readMetaData( storedConfigReference );
-    }
-
-    public Instant modifyTime( )
-    {
-        final String modifyTimeString = readConfigProperty( ConfigurationProperty.MODIFIFICATION_TIMESTAMP );
-        if ( modifyTimeString != null )
-        {
-            try
-            {
-                return JavaHelper.parseIsoToInstant( ( modifyTimeString ) );
-            }
-            catch ( Exception e )
-            {
-                LOGGER.error( "error parsing last modified timestamp property: " + e.getMessage() );
-            }
-        }
-        return null;
-    }
-
-}

+ 0 - 253
server/src/main/java/password/pwm/config/stored/ng/NGStoredConfigurationFactory.java

@@ -1,253 +0,0 @@
-/*
- * Password Management Servlets (PWM)
- * http://www.pwm-project.org
- *
- * Copyright (c) 2006-2009 Novell, Inc.
- * Copyright (c) 2009-2019 The PWM Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package password.pwm.config.stored.ng;
-
-import password.pwm.bean.UserIdentity;
-import password.pwm.config.PwmSetting;
-import password.pwm.config.StoredValue;
-import password.pwm.config.stored.StoredConfigReference;
-import password.pwm.config.stored.StoredConfigReferenceBean;
-import password.pwm.config.stored.StoredConfiguration;
-import password.pwm.config.stored.ValueMetaData;
-import password.pwm.config.value.StringValue;
-import password.pwm.config.value.ValueFactory;
-import password.pwm.error.PwmUnrecoverableException;
-import password.pwm.util.java.JavaHelper;
-import password.pwm.util.java.XmlDocument;
-import password.pwm.util.java.XmlElement;
-import password.pwm.util.java.XmlFactory;
-import password.pwm.util.logging.PwmLogger;
-import password.pwm.util.secure.PwmSecurityKey;
-
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.time.Instant;
-
-
-public class NGStoredConfigurationFactory
-{
-    private static final PwmLogger LOGGER = PwmLogger.forClass( NGStoredConfigurationFactory.class );
-
-    //@Override
-    public NGStoredConfiguration fromXml( final InputStream inputStream ) throws PwmUnrecoverableException
-    {
-        return XmlEngine.fromXmlImpl( inputStream );
-    }
-
-    //@Override
-    public void toXml( final OutputStream outputStream )
-    {
-    }
-
-    private static class XmlEngine
-    {
-        static NGStoredConfiguration fromXmlImpl( final InputStream inputStream )
-                throws PwmUnrecoverableException
-        {
-            final NGStorageEngineImpl storageEngine = new NGStorageEngineImpl();
-
-            final XmlDocument inputDocument = XmlFactory.getFactory().parseXml( inputStream );
-            final XmlElement rootElement = inputDocument.getRootElement();
-
-            final PwmSecurityKey pwmSecurityKey = readSecurityKey( rootElement );
-
-            final XmlElement settingsElement = rootElement.getChild( StoredConfiguration.XML_ELEMENT_SETTINGS );
-
-            for ( final XmlElement loopElement : settingsElement.getChildren() )
-            {
-                if ( StoredConfiguration.XML_ELEMENT_PROPERTIES.equals( loopElement.getName() ) )
-                {
-                    for ( final XmlElement propertyElement : loopElement.getChildren( StoredConfiguration.XML_ELEMENT_PROPERTY ) )
-                    {
-                        readInterestingElement( propertyElement, pwmSecurityKey, storageEngine );
-                    }
-                }
-                else if ( StoredConfiguration.XML_ELEMENT_SETTING.equals( loopElement.getName() ) )
-                {
-                    readInterestingElement( loopElement, pwmSecurityKey, storageEngine );
-                }
-            }
-            return new NGStoredConfiguration( storageEngine, pwmSecurityKey );
-        }
-
-        static void readInterestingElement(
-                final XmlElement loopElement,
-                final PwmSecurityKey pwmSecurityKey,
-                final NGStorageEngineImpl engine
-        )
-        {
-            final StoredConfigReference reference = referenceForElement( loopElement );
-            if ( reference != null )
-            {
-                switch ( reference.getRecordType() )
-                {
-                    case SETTING:
-                    {
-                        final StoredValue storedValue = readSettingValue( reference, loopElement, pwmSecurityKey );
-                        if ( storedValue != null )
-                        {
-                            engine.write( reference, storedValue, null );
-                        }
-                    }
-                    break;
-
-                    case PROPERTY:
-                    {
-                        final StoredValue storedValue = readPropertyValue( reference, loopElement );
-                        if ( storedValue != null )
-                        {
-                            engine.write( reference, storedValue, null );
-                        }
-                    }
-                    break;
-
-                    default:
-                        throw new IllegalArgumentException( "unimplemented setting recordtype in reader" );
-                }
-                engine.writeMetaData( reference, readValueMetaData( loopElement ) );
-            }
-        }
-
-        static PwmSecurityKey readSecurityKey( final XmlElement rootElement )
-                throws PwmUnrecoverableException
-        {
-            final String createTime = rootElement.getAttributeValue( StoredConfiguration.XML_ATTRIBUTE_CREATE_TIME );
-            return new PwmSecurityKey( createTime + "StoredConfiguration" );
-        }
-
-        static StoredValue readSettingValue(
-                final StoredConfigReference storedConfigReference,
-                final XmlElement settingElement,
-                final PwmSecurityKey pwmSecurityKey
-        )
-        {
-            final String key = storedConfigReference.getRecordID();
-            final PwmSetting pwmSetting = PwmSetting.forKey( key );
-
-            if ( pwmSetting == null )
-            {
-                LOGGER.debug( () -> "ignoring setting for unknown key: " + key );
-            }
-            else
-            {
-                LOGGER.trace( () -> "parsing setting key=" + key + ", profile=" + storedConfigReference.getProfileID() );
-                final XmlElement defaultElement = settingElement.getChild( StoredConfiguration.XML_ELEMENT_DEFAULT );
-                if ( defaultElement != null )
-                {
-                    return null;
-                }
-
-                {
-                    try
-                    {
-                        return ValueFactory.fromXmlValues( pwmSetting, settingElement, pwmSecurityKey );
-                    }
-                    catch ( IllegalStateException e )
-                    {
-                        LOGGER.error( "error parsing configuration setting " + storedConfigReference + ", error: " + e.getMessage() );
-                    }
-                }
-            }
-            return null;
-        }
-
-        static StoredValue readPropertyValue(
-                final StoredConfigReference storedConfigReference,
-                final XmlElement settingElement
-        )
-        {
-            final String key = storedConfigReference.getRecordID();
-
-            LOGGER.trace( () -> "parsing property key=" + key + ", profile=" + storedConfigReference.getProfileID() );
-            if ( settingElement.getChild( StoredConfiguration.XML_ELEMENT_DEFAULT ) != null )
-            {
-                return new StringValue( settingElement.getText() );
-            }
-            return null;
-        }
-
-        static StoredConfigReference referenceForElement( final XmlElement settingElement )
-        {
-            final String key = settingElement.getAttributeValue( StoredConfiguration.XML_ATTRIBUTE_KEY );
-            final String profileID = readProfileID( settingElement );
-            final StoredConfigReference.RecordType recordType;
-            switch ( settingElement.getName() )
-            {
-                case StoredConfiguration.XML_ELEMENT_SETTING:
-                    recordType = StoredConfigReference.RecordType.SETTING;
-                    break;
-
-                case StoredConfiguration.XML_ELEMENT_PROPERTY:
-                    recordType = StoredConfigReference.RecordType.PROPERTY;
-                    break;
-
-                case StoredConfiguration.XML_ELEMENT_LOCALEBUNDLE:
-                    recordType = StoredConfigReference.RecordType.LOCALE_BUNDLE;
-                    break;
-
-                default:
-                    LOGGER.warn( "unrecognized xml element " + settingElement.getName() + " in configuration" );
-                    return null;
-            }
-
-
-            return new StoredConfigReferenceBean(
-                    recordType,
-                    key,
-                    profileID
-            );
-        }
-
-        static String readProfileID( final XmlElement settingElement )
-        {
-            final String profileIDStr = settingElement.getAttributeValue( StoredConfiguration.XML_ATTRIBUTE_PROFILE );
-            return profileIDStr != null && !profileIDStr.isEmpty() ? profileIDStr : null;
-        }
-
-        static ValueMetaData readValueMetaData( final XmlElement element )
-        {
-            final String modifyDateStr = element.getAttributeValue( StoredConfiguration.XML_ATTRIBUTE_MODIFY_TIME );
-            Instant modifyDate = null;
-            try
-            {
-                modifyDate = modifyDateStr == null || modifyDateStr.isEmpty()
-                        ? null
-                        : JavaHelper.parseIsoToInstant( modifyDateStr );
-            }
-            catch ( Exception e )
-            {
-                LOGGER.warn( "error parsing stored date: " + e.getMessage() );
-            }
-            final String modifyUser = element.getAttributeValue( StoredConfiguration.XML_ATTRIBUTE_MODIFY_USER );
-            final String modifyUserProfile = element.getAttributeValue( StoredConfiguration.XML_ATTRIBUTE_MODIFY_USER_PROFILE );
-            final UserIdentity userIdentity;
-            userIdentity = modifyUser != null
-                    ? new UserIdentity( modifyUser, modifyUserProfile )
-                    : null;
-
-            return ValueMetaData.builder()
-                    .modifyDate( modifyDate )
-                    .userIdentity( userIdentity )
-                    .build();
-        }
-    }
-
-}

+ 31 - 72
server/src/main/java/password/pwm/config/value/AbstractValue.java

@@ -20,27 +20,31 @@
 
 
 package password.pwm.config.value;
 package password.pwm.config.value;
 
 
-import lombok.Value;
 import password.pwm.PwmConstants;
 import password.pwm.PwmConstants;
 import password.pwm.config.StoredValue;
 import password.pwm.config.StoredValue;
-import password.pwm.error.ErrorInformation;
-import password.pwm.error.PwmError;
-import password.pwm.error.PwmOperationalException;
+import password.pwm.config.stored.StoredConfigXmlConstants;
+import password.pwm.config.stored.XmlOutputProcessData;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.util.java.JsonUtil;
 import password.pwm.util.java.JsonUtil;
-import password.pwm.util.secure.PwmBlockAlgorithm;
-import password.pwm.util.secure.PwmRandom;
+import password.pwm.util.java.LazySupplier;
+import password.pwm.util.java.XmlDocument;
+import password.pwm.util.java.XmlElement;
+import password.pwm.util.java.XmlFactory;
+import password.pwm.util.secure.PwmHashAlgorithm;
 import password.pwm.util.secure.PwmSecurityKey;
 import password.pwm.util.secure.PwmSecurityKey;
 import password.pwm.util.secure.SecureEngine;
 import password.pwm.util.secure.SecureEngine;
 
 
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
 import java.io.Serializable;
 import java.io.Serializable;
+import java.util.List;
 import java.util.Locale;
 import java.util.Locale;
 
 
 public abstract class AbstractValue implements StoredValue
 public abstract class AbstractValue implements StoredValue
 {
 {
-    static final String ENC_PW_PREFIX = "ENC-PW:";
+    private final transient LazySupplier<String> valueHashSupplier = new LazySupplier<>( () -> valueHashComputer( AbstractValue.this ) );
 
 
-    public String toString( )
+    public String toString()
     {
     {
         return toDebugString( null );
         return toDebugString( null );
     }
     }
@@ -57,84 +61,39 @@ public abstract class AbstractValue implements StoredValue
         return ( Serializable ) this.toNativeObject();
         return ( Serializable ) this.toNativeObject();
     }
     }
 
 
-    public boolean requiresStoredUpdate( )
-    {
-        return false;
-    }
-
     @Override
     @Override
-    public int currentSyntaxVersion( )
+    public int currentSyntaxVersion()
     {
     {
         return 0;
         return 0;
     }
     }
 
 
     @Override
     @Override
-    public String valueHash( ) throws PwmUnrecoverableException
+    public final String valueHash()
     {
     {
-        return SecureEngine.hash( JsonUtil.serialize( ( Serializable ) this.toNativeObject() ), PwmConstants.SETTING_CHECKSUM_HASH_METHOD );
+        return valueHashSupplier.get();
     }
     }
 
 
-    static String decryptPwValue( final String input, final PwmSecurityKey pwmSecurityKey ) throws PwmOperationalException
+    static String valueHashComputer( final StoredValue storedValue )
     {
     {
-        if ( input == null )
+        try
         {
         {
-            return "";
-        }
+            final PwmSecurityKey testingKey = new PwmSecurityKey( "test" );
+            final XmlOutputProcessData xmlOutputProcessData = XmlOutputProcessData.builder()
+                    .pwmSecurityKey( testingKey )
+                    .storedValueEncoderMode( StoredValueEncoder.Mode.PLAIN )
+                    .build();
+            final List<XmlElement> xmlValues = storedValue.toXmlValues( StoredConfigXmlConstants.XML_ELEMENT_VALUE, xmlOutputProcessData );
+            final XmlDocument document = XmlFactory.getFactory().newDocument( "root" );
+            document.getRootElement().addContent( xmlValues );
+            final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+            XmlFactory.getFactory().outputDocument( document, byteArrayOutputStream );
+            final String stringToHash = new String( byteArrayOutputStream.toByteArray(), PwmConstants.DEFAULT_CHARSET );
+            return SecureEngine.hash( stringToHash, PwmHashAlgorithm.SHA512 );
 
 
-        if ( input.startsWith( ENC_PW_PREFIX ) )
-        {
-            try
-            {
-                final String pwValueSuffix = input.substring( ENC_PW_PREFIX.length(), input.length() );
-                final String decrpytedValue = SecureEngine.decryptStringValue( pwValueSuffix, pwmSecurityKey, PwmBlockAlgorithm.CONFIG );
-                final StoredPwData storedPwData = JsonUtil.deserialize( decrpytedValue, StoredPwData.class );
-                return storedPwData.getValue();
-            }
-            catch ( Exception e )
-            {
-                final String errorMsg = "unable to decrypt password value for setting: " + e.getMessage();
-                final ErrorInformation errorInfo = new ErrorInformation( PwmError.CONFIG_FORMAT_ERROR, errorMsg );
-                throw new PwmOperationalException( errorInfo );
-            }
         }
         }
-
-        return input;
-    }
-
-    static String encryptPwValue( final String input, final PwmSecurityKey pwmSecurityKey )
-            throws PwmOperationalException
-    {
-        if ( input == null )
+        catch ( final IOException | PwmUnrecoverableException e )
         {
         {
-            return "";
+            throw new IllegalStateException( e );
         }
         }
-
-        if ( !input.startsWith( ENC_PW_PREFIX ) )
-        {
-            try
-            {
-                final String salt = PwmRandom.getInstance().alphaNumericString( 32 );
-                final StoredPwData storedPwData = new StoredPwData( salt, input );
-                final String jsonData = JsonUtil.serialize( storedPwData );
-                final String encryptedValue = SecureEngine.encryptToString( jsonData, pwmSecurityKey, PwmBlockAlgorithm.CONFIG );
-                return ENC_PW_PREFIX + encryptedValue;
-            }
-            catch ( Exception e )
-            {
-                final String errorMsg = "unable to encrypt password value for setting: " + e.getMessage();
-                final ErrorInformation errorInfo = new ErrorInformation( PwmError.CONFIG_FORMAT_ERROR, errorMsg );
-                throw new PwmOperationalException( errorInfo );
-            }
-        }
-
-        return input;
     }
     }
-
-    @Value
-    static class StoredPwData implements Serializable
-    {
-        private String salt;
-        private String value;
-    }
-
 }
 }

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

@@ -25,9 +25,9 @@ import password.pwm.PwmConstants;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.PwmSettingSyntax;
 import password.pwm.config.PwmSettingSyntax;
 import password.pwm.config.StoredValue;
 import password.pwm.config.StoredValue;
-import password.pwm.config.stored.StoredConfiguration;
+import password.pwm.config.stored.StoredConfigXmlConstants;
+import password.pwm.config.stored.XmlOutputProcessData;
 import password.pwm.config.value.data.ActionConfiguration;
 import password.pwm.config.value.data.ActionConfiguration;
-import password.pwm.config.value.data.ActionConfigurationOldVersion1;
 import password.pwm.error.PwmOperationalException;
 import password.pwm.error.PwmOperationalException;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.JsonUtil;
 import password.pwm.util.java.JsonUtil;
@@ -46,6 +46,7 @@ import java.util.HashSet;
 import java.util.List;
 import java.util.List;
 import java.util.Locale;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Map;
+import java.util.Optional;
 import java.util.Set;
 import java.util.Set;
 
 
 public class ActionValue extends AbstractValue implements StoredValue
 public class ActionValue extends AbstractValue implements StoredValue
@@ -98,8 +99,8 @@ public class ActionValue extends AbstractValue implements StoredValue
                 final List<ActionConfiguration> values = new ArrayList<>();
                 final List<ActionConfiguration> values = new ArrayList<>();
 
 
                 final boolean oldType = PwmSettingSyntax.STRING_ARRAY.toString().equals(
                 final boolean oldType = PwmSettingSyntax.STRING_ARRAY.toString().equals(
-                        settingElement.getAttributeValue( "syntax" ) );
-                final List<XmlElement> valueElements = settingElement.getChildren( "value" );
+                        settingElement.getAttributeValue( StoredConfigXmlConstants.XML_ATTRIBUTE_SYNTAX ) );
+                final List<XmlElement> valueElements = settingElement.getChildren( StoredConfigXmlConstants.XML_ELEMENT_VALUE );
                 for ( final XmlElement loopValueElement : valueElements )
                 for ( final XmlElement loopValueElement : valueElements )
                 {
                 {
                     final String stringValue = loopValueElement.getText();
                     final String stringValue = loopValueElement.getText();
@@ -109,17 +110,28 @@ public class ActionValue extends AbstractValue implements StoredValue
                         {
                         {
                             if ( oldType )
                             if ( oldType )
                             {
                             {
-                                if ( loopValueElement.getAttributeValue( "locale" ) == null )
+                                if ( loopValueElement.getAttributeValue( StoredConfigXmlConstants.XML_ATTRIBUTE_LOCALE ) == null )
                                 {
                                 {
-                                    final ActionConfigurationOldVersion1 oldVersion1 = ActionConfigurationOldVersion1.parseOldConfigString( stringValue );
+                                    final ActionConfiguration.ActionConfigurationOldVersion1 oldVersion1 = ActionConfiguration.ActionConfigurationOldVersion1
+                                            .parseOldConfigString( stringValue );
                                     values.add( convertOldVersion1Values( oldVersion1 ) );
                                     values.add( convertOldVersion1Values( oldVersion1 ) );
                                 }
                                 }
                             }
                             }
                             else
                             else
                             {
                             {
-                                final ActionConfigurationOldVersion1 parsedAc = JsonUtil.deserialize( stringValue, ActionConfigurationOldVersion1.class );
-                                parsedAc.setPassword( decryptPwValue( parsedAc.getPassword(), pwmSecurityKey ) );
-                                values.add( convertOldVersion1Values( parsedAc ) );
+                                final ActionConfiguration.ActionConfigurationOldVersion1 parsedAc = JsonUtil
+                                        .deserialize( stringValue, ActionConfiguration.ActionConfigurationOldVersion1.class );
+                                if ( parsedAc != null )
+                                {
+                                    final Optional<String> decodedValue = StoredValueEncoder.decode(
+                                            parsedAc.getPassword(),
+                                            StoredValueEncoder.Mode.ENCODED,
+                                            pwmSecurityKey );
+                                    decodedValue.ifPresent( s ->
+                                    {
+                                        values.add( convertOldVersion1Values( parsedAc.toBuilder().password( s ).build() ) );
+                                    } );
+                                }
                             }
                             }
                         }
                         }
                         else if ( syntaxVersion == 2 )
                         else if ( syntaxVersion == 2 )
@@ -136,14 +148,23 @@ public class ActionValue extends AbstractValue implements StoredValue
                                 // decrypt pw
                                 // decrypt pw
                                 try
                                 try
                                 {
                                 {
-                                    clonedWebActions.add( webAction.toBuilder()
-                                            .password( decryptPwValue( webAction.getPassword(), pwmSecurityKey ) )
-                                            .successStatus( successStatus )
-                                            .build() );
+
+
+                                    final Optional<String> decodedValue = StoredValueEncoder.decode(
+                                            webAction.getPassword(),
+                                            StoredValueEncoder.Mode.ENCODED,
+                                            pwmSecurityKey );
+                                    decodedValue.ifPresent( s ->
+                                    {
+                                        clonedWebActions.add( webAction.toBuilder()
+                                                .password( s )
+                                                .successStatus( successStatus )
+                                                .build() );
+                                    } );
                                 }
                                 }
-                                catch ( PwmOperationalException e )
+                                catch ( final PwmOperationalException e )
                                 {
                                 {
-                                    LOGGER.warn( "error decoding stored pw value: " + e.getMessage() );
+                                    LOGGER.warn( "error decoding stored pw value on setting '" + pwmSetting.getKey() + "': " + e.getMessage() );
                                 }
                                 }
                             }
                             }
 
 
@@ -162,7 +183,7 @@ public class ActionValue extends AbstractValue implements StoredValue
         };
         };
     }
     }
 
 
-    public List<XmlElement> toXmlValues( final String valueElementName, final PwmSecurityKey pwmSecurityKey  )
+    public List<XmlElement> toXmlValues( final String valueElementName, final XmlOutputProcessData xmlOutputProcessData )
     {
     {
         final List<XmlElement> returnList = new ArrayList<>();
         final List<XmlElement> returnList = new ArrayList<>();
         for ( final ActionConfiguration value : values )
         for ( final ActionConfiguration value : values )
@@ -170,15 +191,22 @@ public class ActionValue extends AbstractValue implements StoredValue
             final List<ActionConfiguration.WebAction> clonedWebActions = new ArrayList<>();
             final List<ActionConfiguration.WebAction> clonedWebActions = new ArrayList<>();
             for ( final ActionConfiguration.WebAction webAction : value.getWebActions() )
             for ( final ActionConfiguration.WebAction webAction : value.getWebActions() )
             {
             {
-                try
-                {
-                    clonedWebActions.add( webAction.toBuilder()
-                            .password( encryptPwValue( webAction.getPassword(), pwmSecurityKey ) )
-                            .build() );
-                }
-                catch ( PwmOperationalException e )
+                if ( !StringUtil.isEmpty( webAction.getPassword() ) )
                 {
                 {
-                    LOGGER.warn( "error encoding stored pw value: " + e.getMessage() );
+                    try
+                    {
+                        final String encodedValue = StoredValueEncoder.encode(
+                                webAction.getPassword(),
+                                xmlOutputProcessData.getStoredValueEncoderMode(),
+                                xmlOutputProcessData.getPwmSecurityKey() );
+                        clonedWebActions.add( webAction.toBuilder()
+                                .password( encodedValue )
+                                .build() );
+                    }
+                    catch ( final PwmOperationalException e )
+                    {
+                        LOGGER.warn( "error encoding stored pw value: " + e.getMessage() );
+                    }
                 }
                 }
             }
             }
 
 
@@ -225,7 +253,7 @@ public class ActionValue extends AbstractValue implements StoredValue
             {
             {
                 loopConfig.validate();
                 loopConfig.validate();
             }
             }
-            catch ( PwmOperationalException e )
+            catch ( final PwmOperationalException e )
             {
             {
                 return Collections.singletonList( "format error: " + e.getErrorInformation().toDebugStr() );
                 return Collections.singletonList( "format error: " + e.getErrorInformation().toDebugStr() );
             }
             }
@@ -367,14 +395,14 @@ public class ActionValue extends AbstractValue implements StoredValue
 
 
     private static int figureCurrentStoredSyntax( final XmlElement settingElement )
     private static int figureCurrentStoredSyntax( final XmlElement settingElement )
     {
     {
-        final String storedSyntaxVersionString = settingElement.getAttributeValue( StoredConfiguration.XML_ATTRIBUTE_SYNTAX_VERSION );
+        final String storedSyntaxVersionString = settingElement.getAttributeValue( StoredConfigXmlConstants.XML_ATTRIBUTE_SYNTAX_VERSION );
         if ( !StringUtil.isEmpty( storedSyntaxVersionString ) )
         if ( !StringUtil.isEmpty( storedSyntaxVersionString ) )
         {
         {
             try
             try
             {
             {
                 return Integer.parseInt( storedSyntaxVersionString );
                 return Integer.parseInt( storedSyntaxVersionString );
             }
             }
-            catch ( NumberFormatException e )
+            catch ( final NumberFormatException e )
             {
             {
                 LOGGER.debug( () -> "unable to parse syntax version for setting " + e.getMessage() );
                 LOGGER.debug( () -> "unable to parse syntax version for setting " + e.getMessage() );
             }
             }
@@ -382,7 +410,7 @@ public class ActionValue extends AbstractValue implements StoredValue
         return 0;
         return 0;
     }
     }
 
 
-    private static ActionConfiguration convertOldVersion1Values( final ActionConfigurationOldVersion1 oldAction )
+    private static ActionConfiguration convertOldVersion1Values( final ActionConfiguration.ActionConfigurationOldVersion1 oldAction )
     {
     {
         final ActionConfiguration.ActionConfigurationBuilder builder = ActionConfiguration.builder();
         final ActionConfiguration.ActionConfigurationBuilder builder = ActionConfiguration.builder();
         builder.name( oldAction.getName() );
         builder.name( oldAction.getName() );

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

@@ -23,7 +23,8 @@ package password.pwm.config.value;
 import password.pwm.PwmConstants;
 import password.pwm.PwmConstants;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.StoredValue;
 import password.pwm.config.StoredValue;
-import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.config.stored.StoredConfigXmlConstants;
+import password.pwm.config.stored.XmlOutputProcessData;
 import password.pwm.i18n.Display;
 import password.pwm.i18n.Display;
 import password.pwm.util.java.JsonUtil;
 import password.pwm.util.java.JsonUtil;
 import password.pwm.util.java.XmlElement;
 import password.pwm.util.java.XmlElement;
@@ -34,10 +35,11 @@ import java.io.Serializable;
 import java.util.Collections;
 import java.util.Collections;
 import java.util.List;
 import java.util.List;
 import java.util.Locale;
 import java.util.Locale;
+import java.util.Optional;
 
 
 public class BooleanValue implements StoredValue
 public class BooleanValue implements StoredValue
 {
 {
-    boolean value;
+    private final boolean value;
 
 
     public BooleanValue( final boolean value )
     public BooleanValue( final boolean value )
     {
     {
@@ -56,9 +58,13 @@ public class BooleanValue implements StoredValue
 
 
             public BooleanValue fromXmlElement( final PwmSetting pwmSetting, final XmlElement settingElement, final PwmSecurityKey input )
             public BooleanValue fromXmlElement( final PwmSetting pwmSetting, final XmlElement settingElement, final PwmSecurityKey input )
             {
             {
-                final XmlElement valueElement = settingElement.getChild( "value" );
-                final String value = valueElement.getText();
-                return new BooleanValue( Boolean.valueOf( value ) );
+                final Optional<XmlElement> valueElement = settingElement.getChild( StoredConfigXmlConstants.XML_ELEMENT_VALUE );
+                if ( valueElement.isPresent() )
+                {
+                    final String value = valueElement.get().getTextTrim();
+                    return new BooleanValue( Boolean.valueOf( value ) );
+                }
+                return new BooleanValue( false );
             }
             }
 
 
         };
         };
@@ -70,7 +76,7 @@ public class BooleanValue implements StoredValue
     }
     }
 
 
     @Override
     @Override
-    public List<XmlElement> toXmlValues( final String valueElementName, final PwmSecurityKey pwmSecurityKey  )
+    public List<XmlElement> toXmlValues( final String valueElementName, final XmlOutputProcessData xmlOutputProcessData )
     {
     {
         final XmlElement valueElement = XmlFactory.getFactory().newElement( valueElementName );
         final XmlElement valueElement = XmlFactory.getFactory().newElement( valueElementName );
         valueElement.addText( String.valueOf( value ) );
         valueElement.addText( String.valueOf( value ) );
@@ -99,12 +105,6 @@ public class BooleanValue implements StoredValue
         return value;
         return value;
     }
     }
 
 
-    @Override
-    public boolean requiresStoredUpdate( )
-    {
-        return false;
-    }
-
     @Override
     @Override
     public int currentSyntaxVersion( )
     public int currentSyntaxVersion( )
     {
     {
@@ -112,7 +112,7 @@ public class BooleanValue implements StoredValue
     }
     }
 
 
     @Override
     @Override
-    public String valueHash( ) throws PwmUnrecoverableException
+    public String valueHash()
     {
     {
         return value ? "1" : "0";
         return value ? "1" : "0";
     }
     }

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

@@ -23,6 +23,7 @@ package password.pwm.config.value;
 import com.google.gson.reflect.TypeToken;
 import com.google.gson.reflect.TypeToken;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.StoredValue;
 import password.pwm.config.StoredValue;
+import password.pwm.config.stored.XmlOutputProcessData;
 import password.pwm.config.value.data.ChallengeItemConfiguration;
 import password.pwm.config.value.data.ChallengeItemConfiguration;
 import password.pwm.util.i18n.LocaleHelper;
 import password.pwm.util.i18n.LocaleHelper;
 import password.pwm.util.java.JsonUtil;
 import password.pwm.util.java.JsonUtil;
@@ -43,11 +44,11 @@ public class ChallengeValue extends AbstractValue implements StoredValue
     private static final PwmLogger LOGGER = PwmLogger.forClass( ChallengeValue.class );
     private static final PwmLogger LOGGER = PwmLogger.forClass( ChallengeValue.class );
 
 
     //locale str as key.
     //locale str as key.
-    final Map<String, List<ChallengeItemConfiguration>> values;
+    private final Map<String, List<ChallengeItemConfiguration>> values;
 
 
     ChallengeValue( final Map<String, List<ChallengeItemConfiguration>> values )
     ChallengeValue( final Map<String, List<ChallengeItemConfiguration>> values )
     {
     {
-        this.values = values;
+        this.values = values == null ? Collections.emptyMap() : Collections.unmodifiableMap( values );
     }
     }
 
 
     public static StoredValueFactory factory( )
     public static StoredValueFactory factory( )
@@ -111,7 +112,7 @@ public class ChallengeValue extends AbstractValue implements StoredValue
         };
         };
     }
     }
 
 
-    public List<XmlElement> toXmlValues( final String valueElementName, final PwmSecurityKey pwmSecurityKey  )
+    public List<XmlElement> toXmlValues( final String valueElementName, final XmlOutputProcessData xmlOutputProcessData )
     {
     {
         final List<XmlElement> returnList = new ArrayList<>();
         final List<XmlElement> returnList = new ArrayList<>();
         for ( final Map.Entry<String, List<ChallengeItemConfiguration>> entry : values.entrySet() )
         for ( final Map.Entry<String, List<ChallengeItemConfiguration>> entry : values.entrySet() )
@@ -207,7 +208,7 @@ public class ChallengeValue extends AbstractValue implements StoredValue
             {
             {
                 minLength = Integer.parseInt( s1[ 1 ] );
                 minLength = Integer.parseInt( s1[ 1 ] );
             }
             }
-            catch ( Exception e )
+            catch ( final Exception e )
             {
             {
                 LOGGER.debug( () -> "unexpected error parsing config input '" + inputString + "' " + e.getMessage() );
                 LOGGER.debug( () -> "unexpected error parsing config input '" + inputString + "' " + e.getMessage() );
             }
             }
@@ -218,7 +219,7 @@ public class ChallengeValue extends AbstractValue implements StoredValue
             {
             {
                 maxLength = Integer.parseInt( s1[ 2 ] );
                 maxLength = Integer.parseInt( s1[ 2 ] );
             }
             }
-            catch ( Exception e )
+            catch ( final Exception e )
             {
             {
                 LOGGER.debug( () -> "unexpected error parsing config input '" + inputString + "' " + e.getMessage() );
                 LOGGER.debug( () -> "unexpected error parsing config input '" + inputString + "' " + e.getMessage() );
             }
             }
@@ -231,7 +232,12 @@ public class ChallengeValue extends AbstractValue implements StoredValue
             adminDefined = false;
             adminDefined = false;
         }
         }
 
 
-        return new ChallengeItemConfiguration( challengeText, minLength, maxLength, adminDefined );
+        return ChallengeItemConfiguration.builder()
+                .text( challengeText )
+                .minLength( minLength )
+                .maxLength( maxLength )
+                .adminDefined( adminDefined )
+                .build();
     }
     }
 
 
     public String toDebugString( final Locale locale )
     public String toDebugString( final Locale locale )

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

@@ -21,7 +21,8 @@
 package password.pwm.config.value;
 package password.pwm.config.value;
 
 
 import com.google.gson.reflect.TypeToken;
 import com.google.gson.reflect.TypeToken;
-import password.pwm.config.CustomLinkConfiguration;
+import password.pwm.config.stored.XmlOutputProcessData;
+import password.pwm.config.value.data.CustomLinkConfiguration;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.StoredValue;
 import password.pwm.config.StoredValue;
 import password.pwm.error.PwmOperationalException;
 import password.pwm.error.PwmOperationalException;
@@ -39,11 +40,11 @@ import java.util.Set;
 
 
 public class CustomLinkValue extends AbstractValue implements StoredValue
 public class CustomLinkValue extends AbstractValue implements StoredValue
 {
 {
-    final List<CustomLinkConfiguration> values;
+    private final List<CustomLinkConfiguration> values;
 
 
     public CustomLinkValue( final List<CustomLinkConfiguration> values )
     public CustomLinkValue( final List<CustomLinkConfiguration> values )
     {
     {
-        this.values = values;
+        this.values = values == null ? Collections.emptyList() : Collections.unmodifiableList( values );
     }
     }
 
 
     public static StoredValueFactory factory( )
     public static StoredValueFactory factory( )
@@ -88,7 +89,7 @@ public class CustomLinkValue extends AbstractValue implements StoredValue
         };
         };
     }
     }
 
 
-    public List<XmlElement> toXmlValues( final String valueElementName, final PwmSecurityKey pwmSecurityKey  )
+    public List<XmlElement> toXmlValues( final String valueElementName, final XmlOutputProcessData xmlOutputProcessData )
     {
     {
         final List<XmlElement> returnList = new ArrayList<>();
         final List<XmlElement> returnList = new ArrayList<>();
         for ( final CustomLinkConfiguration value : values )
         for ( final CustomLinkConfiguration value : values )

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

@@ -25,6 +25,7 @@ import com.google.gson.reflect.TypeToken;
 import password.pwm.bean.EmailItemBean;
 import password.pwm.bean.EmailItemBean;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.StoredValue;
 import password.pwm.config.StoredValue;
+import password.pwm.config.stored.XmlOutputProcessData;
 import password.pwm.error.PwmOperationalException;
 import password.pwm.error.PwmOperationalException;
 import password.pwm.util.i18n.LocaleHelper;
 import password.pwm.util.i18n.LocaleHelper;
 import password.pwm.util.java.JsonUtil;
 import password.pwm.util.java.JsonUtil;
@@ -42,11 +43,11 @@ import java.util.TreeMap;
 public class EmailValue extends AbstractValue implements StoredValue
 public class EmailValue extends AbstractValue implements StoredValue
 {
 {
     //key is locale identifier
     //key is locale identifier
-    final Map<String, EmailItemBean> values;
+    private final Map<String, EmailItemBean> values;
 
 
     EmailValue( final Map<String, EmailItemBean> values )
     EmailValue( final Map<String, EmailItemBean> values )
     {
     {
-        this.values = values;
+        this.values = values == null ? Collections.emptyMap() : Collections.unmodifiableMap( values );
     }
     }
 
 
     public static StoredValueFactory factory( )
     public static StoredValueFactory factory( )
@@ -99,7 +100,7 @@ public class EmailValue extends AbstractValue implements StoredValue
         };
         };
     }
     }
 
 
-    public List<XmlElement> toXmlValues( final String valueElementName, final PwmSecurityKey pwmSecurityKey  )
+    public List<XmlElement> toXmlValues( final String valueElementName, final XmlOutputProcessData xmlOutputProcessData )
     {
     {
         final List<XmlElement> returnList = new ArrayList<>();
         final List<XmlElement> returnList = new ArrayList<>();
         for ( final Map.Entry<String, EmailItemBean> entry : values.entrySet() )
         for ( final Map.Entry<String, EmailItemBean> entry : values.entrySet() )

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

@@ -20,11 +20,12 @@
 
 
 package password.pwm.config.value;
 package password.pwm.config.value;
 
 
+import lombok.Builder;
 import lombok.Value;
 import lombok.Value;
-import password.pwm.PwmConstants;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.StoredValue;
 import password.pwm.config.StoredValue;
-import password.pwm.error.PwmOperationalException;
+import password.pwm.config.stored.StoredConfigXmlConstants;
+import password.pwm.config.stored.XmlOutputProcessData;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.http.bean.ImmutableByteArray;
 import password.pwm.http.bean.ImmutableByteArray;
 import password.pwm.util.java.JsonUtil;
 import password.pwm.util.java.JsonUtil;
@@ -45,36 +46,23 @@ import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.List;
 import java.util.Locale;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Map;
+import java.util.Optional;
 
 
 public class FileValue extends AbstractValue implements StoredValue
 public class FileValue extends AbstractValue implements StoredValue
 {
 {
     private static final PwmLogger LOGGER = PwmLogger.forClass( FileValue.class );
     private static final PwmLogger LOGGER = PwmLogger.forClass( FileValue.class );
 
 
-    private Map<FileInformation, FileContent> values = new LinkedHashMap<>();
+    private static final int ENCODING_LINE_LENGTH = 120;
+    private static final String XML_ELEMENT_FILE_INFORMATION = "FileInformation";
+    private static final String XML_ELEMENT_FILE_CONTENT = "FileContent";
 
 
+    private final Map<FileInformation, FileContent> values;
+
+    @Value
     public static class FileInformation implements Serializable
     public static class FileInformation implements Serializable
     {
     {
         private String filename;
         private String filename;
         private String filetype;
         private String filetype;
-
-        public FileInformation(
-                final String filename,
-                final String filetype
-        )
-        {
-            this.filename = filename;
-            this.filetype = filetype;
-        }
-
-        public String getFilename( )
-        {
-            return filename;
-        }
-
-        public String getFiletype( )
-        {
-            return filetype;
-        }
     }
     }
 
 
     @Value
     @Value
@@ -82,30 +70,24 @@ public class FileValue extends AbstractValue implements StoredValue
     {
     {
         private ImmutableByteArray contents;
         private ImmutableByteArray contents;
 
 
-
-        public static FileContent fromEncodedString( final String input )
+        static FileContent fromEncodedString( final String input )
                 throws IOException
                 throws IOException
         {
         {
-            final byte[] convertedBytes = StringUtil.base64Decode( input );
+            final String whitespaceStrippedInput = StringUtil.stripAllWhitespace( input );
+            final byte[] convertedBytes = StringUtil.base64Decode( whitespaceStrippedInput );
             return new FileContent( ImmutableByteArray.of( convertedBytes ) );
             return new FileContent( ImmutableByteArray.of( convertedBytes ) );
         }
         }
 
 
-        public String toEncodedString( )
+        String toEncodedString( )
                 throws IOException
                 throws IOException
         {
         {
             return StringUtil.base64Encode( contents.copyOf(), StringUtil.Base64Options.GZIP );
             return StringUtil.base64Encode( contents.copyOf(), StringUtil.Base64Options.GZIP );
         }
         }
 
 
-        public String md5sum( )
-                throws PwmUnrecoverableException
-        {
-            return SecureEngine.hash( new ByteArrayInputStream( contents.copyOf() ), PwmHashAlgorithm.MD5 );
-        }
-
-        public String sha1sum( )
+        String sha512sum( )
                 throws PwmUnrecoverableException
                 throws PwmUnrecoverableException
         {
         {
-            return SecureEngine.hash( new ByteArrayInputStream( contents.copyOf() ), PwmHashAlgorithm.SHA1 );
+            return SecureEngine.hash( new ByteArrayInputStream( contents.copyOf() ), PwmHashAlgorithm.SHA512 );
         }
         }
 
 
         public int size( )
         public int size( )
@@ -116,7 +98,7 @@ public class FileValue extends AbstractValue implements StoredValue
 
 
     public FileValue( final Map<FileInformation, FileContent> values )
     public FileValue( final Map<FileInformation, FileContent> values )
     {
     {
-        this.values = values;
+        this.values = values == null ? Collections.emptyMap() : Collections.unmodifiableMap( values );
     }
     }
 
 
     public static StoredValueFactory factory( )
     public static StoredValueFactory factory( )
@@ -125,30 +107,29 @@ public class FileValue extends AbstractValue implements StoredValue
         {
         {
 
 
             public FileValue fromXmlElement( final PwmSetting pwmSetting, final XmlElement settingElement, final PwmSecurityKey input )
             public FileValue fromXmlElement( final PwmSetting pwmSetting, final XmlElement settingElement, final PwmSecurityKey input )
-                    throws PwmOperationalException
             {
             {
-                final List<XmlElement> valueElements = settingElement.getChildren( "value" );
+                final List<XmlElement> valueElements = settingElement.getChildren( StoredConfigXmlConstants.XML_ELEMENT_VALUE );
                 final Map<FileInformation, FileContent> values = new LinkedHashMap<>();
                 final Map<FileInformation, FileContent> values = new LinkedHashMap<>();
                 for ( final XmlElement loopValueElement : valueElements )
                 for ( final XmlElement loopValueElement : valueElements )
                 {
                 {
-                    final XmlElement loopFileInformation = loopValueElement.getChild( "FileInformation" );
-                    if ( loopFileInformation != null )
+                    final Optional<XmlElement> loopFileInformation = loopValueElement.getChild( XML_ELEMENT_FILE_INFORMATION );
+                    if ( loopFileInformation.isPresent() )
                     {
                     {
-                        final String loopFileInformationJson = loopFileInformation.getText();
+                        final String loopFileInformationJson = loopFileInformation.get().getText();
                         final FileInformation fileInformation = JsonUtil.deserialize( loopFileInformationJson,
                         final FileInformation fileInformation = JsonUtil.deserialize( loopFileInformationJson,
                                 FileInformation.class );
                                 FileInformation.class );
 
 
-                        final XmlElement loopFileContentElement = loopValueElement.getChild( "FileContent" );
-                        if ( loopFileContentElement != null )
+                        final Optional<XmlElement> loopFileContentElement = loopValueElement.getChild( XML_ELEMENT_FILE_CONTENT );
+                        if ( loopFileContentElement.isPresent() )
                         {
                         {
-                            final String fileContentString = loopFileContentElement.getText();
+                            final String fileContentString = loopFileContentElement.get().getText();
                             final FileContent fileContent;
                             final FileContent fileContent;
                             try
                             try
                             {
                             {
                                 fileContent = FileContent.fromEncodedString( fileContentString );
                                 fileContent = FileContent.fromEncodedString( fileContentString );
                                 values.put( fileInformation, fileContent );
                                 values.put( fileInformation, fileContent );
                             }
                             }
-                            catch ( IOException e )
+                            catch ( final IOException e )
                             {
                             {
                                 LOGGER.error( "error reading file contents item: " + e.getMessage(), e );
                                 LOGGER.error( "error reading file contents item: " + e.getMessage(), e );
                             }
                             }
@@ -165,7 +146,7 @@ public class FileValue extends AbstractValue implements StoredValue
         };
         };
     }
     }
 
 
-    public List<XmlElement> toXmlValues( final String valueElementName, final PwmSecurityKey pwmSecurityKey )
+    public List<XmlElement> toXmlValues( final String valueElementName, final XmlOutputProcessData xmlOutputProcessData )
     {
     {
         final List<XmlElement> returnList = new ArrayList<>();
         final List<XmlElement> returnList = new ArrayList<>();
         for ( final Map.Entry<FileInformation, FileContent> entry : this.values.entrySet() )
         for ( final Map.Entry<FileInformation, FileContent> entry : this.values.entrySet() )
@@ -174,16 +155,19 @@ public class FileValue extends AbstractValue implements StoredValue
             final FileContent fileContent = entry.getValue();
             final FileContent fileContent = entry.getValue();
             final XmlElement valueElement = XmlFactory.getFactory().newElement( valueElementName );
             final XmlElement valueElement = XmlFactory.getFactory().newElement( valueElementName );
 
 
-            final XmlElement fileInformationElement = XmlFactory.getFactory().newElement( "FileInformation" );
+            final XmlElement fileInformationElement = XmlFactory.getFactory().newElement( XML_ELEMENT_FILE_INFORMATION );
             fileInformationElement.addText( JsonUtil.serialize( fileInformation ) );
             fileInformationElement.addText( JsonUtil.serialize( fileInformation ) );
             valueElement.addContent( fileInformationElement );
             valueElement.addContent( fileInformationElement );
 
 
-            final XmlElement fileContentElement = XmlFactory.getFactory().newElement( "FileContent" );
+            final XmlElement fileContentElement = XmlFactory.getFactory().newElement( XML_ELEMENT_FILE_CONTENT );
+
             try
             try
             {
             {
-                fileContentElement.addText( fileContent.toEncodedString() );
+                final String encodedLineBreaks = StringUtil.insertRepeatedLineBreaks(
+                        fileContent.toEncodedString(), ENCODING_LINE_LENGTH );
+                fileContentElement.addText( encodedLineBreaks );
             }
             }
-            catch ( IOException e )
+            catch ( final IOException e )
             {
             {
                 LOGGER.error( "unexpected error writing setting to xml, IO error during base64 encoding: " + e.getMessage() );
                 LOGGER.error( "unexpected error writing setting to xml, IO error during base64 encoding: " + e.getMessage() );
             }
             }
@@ -221,7 +205,7 @@ public class FileValue extends AbstractValue implements StoredValue
         return ( Serializable ) asMetaData();
         return ( Serializable ) asMetaData();
     }
     }
 
 
-    public List<Map<String, Object>> asMetaData( )
+    List<Map<String, Object>> asMetaData( )
     {
     {
         final List<Map<String, Object>> output = new ArrayList<>();
         final List<Map<String, Object>> output = new ArrayList<>();
         for ( final Map.Entry<FileInformation, FileContent> entry : this.values.entrySet() )
         for ( final Map.Entry<FileInformation, FileContent> entry : this.values.entrySet() )
@@ -234,9 +218,9 @@ public class FileValue extends AbstractValue implements StoredValue
             details.put( "size", fileContent.size() );
             details.put( "size", fileContent.size() );
             try
             try
             {
             {
-                details.put( "md5sum", fileContent.md5sum() );
+                details.put( "sha512sum", fileContent.sha512sum() );
             }
             }
-            catch ( PwmUnrecoverableException e )
+            catch ( final PwmUnrecoverableException e )
             {
             {
                 LOGGER.trace( () -> "error generating file hash" );
                 LOGGER.trace( () -> "error generating file hash" );
             }
             }
@@ -256,65 +240,30 @@ public class FileValue extends AbstractValue implements StoredValue
         {
         {
             final FileValue.FileInformation fileInformation = entry.getKey();
             final FileValue.FileInformation fileInformation = entry.getKey();
             final FileContent fileContent = entry.getValue();
             final FileContent fileContent = entry.getValue();
-            final FileInfo loopInfo = new FileInfo();
-            loopInfo.name = fileInformation.getFilename();
-            loopInfo.type = fileInformation.getFiletype();
-            loopInfo.size = fileContent.size();
             try
             try
             {
             {
-                loopInfo.md5sum = fileContent.md5sum();
-                loopInfo.sha1sum = fileContent.sha1sum();
+                returnObj.add( FileInfo.builder()
+                        .name( fileInformation.getFilename() )
+                        .type( fileInformation.getFiletype() )
+                        .size( fileContent.size() )
+                        .sha512sum( fileContent.sha512sum() )
+                        .build() );
             }
             }
-            catch ( PwmUnrecoverableException e )
+            catch ( final PwmUnrecoverableException e )
             {
             {
-                LOGGER.warn( "error generating hash for certificate: " + e.getMessage() );
+                throw new IllegalStateException( e );
             }
             }
-            returnObj.add( loopInfo );
         }
         }
         return Collections.unmodifiableList( returnObj );
         return Collections.unmodifiableList( returnObj );
     }
     }
 
 
-    @Override
-    public String valueHash( ) throws PwmUnrecoverableException
-    {
-        return SecureEngine.hash( JsonUtil.serializeCollection( toInfoMap() ), PwmConstants.SETTING_CHECKSUM_HASH_METHOD );
-    }
-
+    @Value
+    @Builder
     public static class FileInfo implements Serializable
     public static class FileInfo implements Serializable
     {
     {
-        public String name;
-        public String type;
-        public int size;
-        public String md5sum;
-        public String sha1sum;
-
-        private FileInfo( )
-        {
-        }
-
-        public String getName( )
-        {
-            return name;
-        }
-
-        public String getType( )
-        {
-            return type;
-        }
-
-        public int getSize( )
-        {
-            return size;
-        }
-
-        public String getMd5sum( )
-        {
-            return md5sum;
-        }
-
-        public String getSha1sum( )
-        {
-            return sha1sum;
-        }
+        private String name;
+        private String type;
+        private long size;
+        private String sha512sum;
     }
     }
 }
 }

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

@@ -24,6 +24,7 @@ import com.google.gson.reflect.TypeToken;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.PwmSettingSyntax;
 import password.pwm.config.PwmSettingSyntax;
 import password.pwm.config.StoredValue;
 import password.pwm.config.StoredValue;
+import password.pwm.config.stored.XmlOutputProcessData;
 import password.pwm.config.value.data.FormConfiguration;
 import password.pwm.config.value.data.FormConfiguration;
 import password.pwm.error.PwmOperationalException;
 import password.pwm.error.PwmOperationalException;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.JavaHelper;
@@ -42,13 +43,11 @@ import java.util.Set;
 
 
 public class FormValue extends AbstractValue implements StoredValue
 public class FormValue extends AbstractValue implements StoredValue
 {
 {
-    final List<FormConfiguration> values;
-
-    private boolean needsXmlUpdate;
+    private final List<FormConfiguration> values;
 
 
     public FormValue( final List<FormConfiguration> values )
     public FormValue( final List<FormConfiguration> values )
     {
     {
-        this.values = values;
+        this.values = values == null ? Collections.emptyList() : Collections.unmodifiableList( values );
     }
     }
 
 
     public static StoredValueFactory factory( )
     public static StoredValueFactory factory( )
@@ -59,14 +58,14 @@ public class FormValue extends AbstractValue implements StoredValue
             {
             {
                 if ( input == null )
                 if ( input == null )
                 {
                 {
-                    return new FormValue( Collections.<FormConfiguration>emptyList() );
+                    return new FormValue( Collections.emptyList() );
                 }
                 }
                 else
                 else
                 {
                 {
                     List<FormConfiguration> srcList = JsonUtil.deserialize( input, new TypeToken<List<FormConfiguration>>()
                     List<FormConfiguration> srcList = JsonUtil.deserialize( input, new TypeToken<List<FormConfiguration>>()
                     {
                     {
                     } );
                     } );
-                    srcList = srcList == null ? Collections.<FormConfiguration>emptyList() : srcList;
+                    srcList = srcList == null ? Collections.emptyList() : srcList;
                     while ( srcList.contains( null ) )
                     while ( srcList.contains( null ) )
                     {
                     {
                         srcList.remove( null );
                         srcList.remove( null );
@@ -98,13 +97,12 @@ public class FormValue extends AbstractValue implements StoredValue
                     }
                     }
                 }
                 }
                 final FormValue formValue = new FormValue( values );
                 final FormValue formValue = new FormValue( values );
-                formValue.needsXmlUpdate = oldType;
                 return formValue;
                 return formValue;
             }
             }
         };
         };
     }
     }
 
 
-    public List<XmlElement> toXmlValues( final String valueElementName, final PwmSecurityKey pwmSecurityKey  )
+    public List<XmlElement> toXmlValues( final String valueElementName, final XmlOutputProcessData xmlOutputProcessData )
     {
     {
         final List<XmlElement> returnList = new ArrayList<>();
         final List<XmlElement> returnList = new ArrayList<>();
         for ( final FormConfiguration value : values )
         for ( final FormConfiguration value : values )
@@ -147,7 +145,7 @@ public class FormValue extends AbstractValue implements StoredValue
             {
             {
                 loopConfig.validate();
                 loopConfig.validate();
             }
             }
-            catch ( PwmOperationalException e )
+            catch ( final PwmOperationalException e )
             {
             {
                 return Collections.singletonList( "format error: " + e.getErrorInformation().toDebugStr() );
                 return Collections.singletonList( "format error: " + e.getErrorInformation().toDebugStr() );
             }
             }
@@ -156,11 +154,6 @@ public class FormValue extends AbstractValue implements StoredValue
         return Collections.emptyList();
         return Collections.emptyList();
     }
     }
 
 
-    public boolean isNeedsXmlUpdate( )
-    {
-        return needsXmlUpdate;
-    }
-
     public String toDebugString( final Locale locale )
     public String toDebugString( final Locale locale )
     {
     {
         if ( values != null && !values.isEmpty() )
         if ( values != null && !values.isEmpty() )

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

@@ -23,6 +23,7 @@ package password.pwm.config.value;
 import com.google.gson.reflect.TypeToken;
 import com.google.gson.reflect.TypeToken;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.StoredValue;
 import password.pwm.config.StoredValue;
+import password.pwm.config.stored.XmlOutputProcessData;
 import password.pwm.util.i18n.LocaleHelper;
 import password.pwm.util.i18n.LocaleHelper;
 import password.pwm.util.java.JsonUtil;
 import password.pwm.util.java.JsonUtil;
 import password.pwm.util.java.XmlElement;
 import password.pwm.util.java.XmlElement;
@@ -40,11 +41,11 @@ import java.util.regex.Pattern;
 
 
 public class LocalizedStringArrayValue extends AbstractValue implements StoredValue
 public class LocalizedStringArrayValue extends AbstractValue implements StoredValue
 {
 {
-    final Map<String, List<String>> values;
+    private final Map<String, List<String>> values;
 
 
     LocalizedStringArrayValue( final Map<String, List<String>> values )
     LocalizedStringArrayValue( final Map<String, List<String>> values )
     {
     {
-        this.values = values;
+        this.values = values == null ? Collections.emptyMap() : Collections.unmodifiableMap( values );
     }
     }
 
 
     public static StoredValueFactory factory( )
     public static StoredValueFactory factory( )
@@ -89,7 +90,7 @@ public class LocalizedStringArrayValue extends AbstractValue implements StoredVa
         };
         };
     }
     }
 
 
-    public List<XmlElement> toXmlValues( final String valueElementName, final PwmSecurityKey pwmSecurityKey  )
+    public List<XmlElement> toXmlValues( final String valueElementName, final XmlOutputProcessData xmlOutputProcessData )
     {
     {
         final List<XmlElement> returnList = new ArrayList<>();
         final List<XmlElement> returnList = new ArrayList<>();
         for ( final Map.Entry<String, List<String>> entry : values.entrySet() )
         for ( final Map.Entry<String, List<String>> entry : values.entrySet() )

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

@@ -23,6 +23,8 @@ package password.pwm.config.value;
 import com.google.gson.reflect.TypeToken;
 import com.google.gson.reflect.TypeToken;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.StoredValue;
 import password.pwm.config.StoredValue;
+import password.pwm.config.stored.StoredConfigXmlConstants;
+import password.pwm.config.stored.XmlOutputProcessData;
 import password.pwm.util.i18n.LocaleHelper;
 import password.pwm.util.i18n.LocaleHelper;
 import password.pwm.util.java.JsonUtil;
 import password.pwm.util.java.JsonUtil;
 import password.pwm.util.java.XmlElement;
 import password.pwm.util.java.XmlElement;
@@ -40,11 +42,11 @@ import java.util.regex.Pattern;
 
 
 public class LocalizedStringValue extends AbstractValue implements StoredValue
 public class LocalizedStringValue extends AbstractValue implements StoredValue
 {
 {
-    final Map<String, String> value;
+    private final Map<String, String> value;
 
 
     public LocalizedStringValue( final Map<String, String> values )
     public LocalizedStringValue( final Map<String, String> values )
     {
     {
-        this.value = Collections.unmodifiableMap( values );
+        this.value = values == null ? Collections.emptyMap() : Collections.unmodifiableMap( values );
     }
     }
 
 
     public static StoredValueFactory factory( )
     public static StoredValueFactory factory( )
@@ -69,11 +71,11 @@ public class LocalizedStringValue extends AbstractValue implements StoredValue
 
 
             public LocalizedStringValue fromXmlElement( final PwmSetting pwmSetting, final XmlElement settingElement, final PwmSecurityKey key )
             public LocalizedStringValue fromXmlElement( final PwmSetting pwmSetting, final XmlElement settingElement, final PwmSecurityKey key )
             {
             {
-                final List<XmlElement> elements = settingElement.getChildren( "value" );
+                final List<XmlElement> elements = settingElement.getChildren( StoredConfigXmlConstants.XML_ELEMENT_VALUE );
                 final Map<String, String> values = new TreeMap<>();
                 final Map<String, String> values = new TreeMap<>();
                 for ( final XmlElement loopValueElement : elements )
                 for ( final XmlElement loopValueElement : elements )
                 {
                 {
-                    final String localeString = loopValueElement.getAttributeValue( "locale" );
+                    final String localeString = loopValueElement.getAttributeValue( StoredConfigXmlConstants.XML_ATTRIBUTE_LOCALE );
                     final String value = loopValueElement.getText();
                     final String value = loopValueElement.getText();
                     values.put( localeString == null ? "" : localeString, value );
                     values.put( localeString == null ? "" : localeString, value );
                 }
                 }
@@ -82,7 +84,7 @@ public class LocalizedStringValue extends AbstractValue implements StoredValue
         };
         };
     }
     }
 
 
-    public List<XmlElement> toXmlValues( final String valueElementName, final PwmSecurityKey pwmSecurityKey  )
+    public List<XmlElement> toXmlValues( final String valueElementName, final XmlOutputProcessData xmlOutputProcessData )
     {
     {
         final List<XmlElement> returnList = new ArrayList<>();
         final List<XmlElement> returnList = new ArrayList<>();
         for ( final Map.Entry<String, String> entry : value.entrySet() )
         for ( final Map.Entry<String, String> entry : value.entrySet() )

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

@@ -24,6 +24,8 @@ import com.google.gson.reflect.TypeToken;
 import password.pwm.PwmConstants;
 import password.pwm.PwmConstants;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.StoredValue;
 import password.pwm.config.StoredValue;
+import password.pwm.config.stored.StoredConfigXmlConstants;
+import password.pwm.config.stored.XmlOutputProcessData;
 import password.pwm.config.value.data.NamedSecretData;
 import password.pwm.config.value.data.NamedSecretData;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmError;
@@ -31,6 +33,7 @@ import password.pwm.error.PwmOperationalException;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.util.PasswordData;
 import password.pwm.util.PasswordData;
 import password.pwm.util.java.JsonUtil;
 import password.pwm.util.java.JsonUtil;
+import password.pwm.util.java.LazySupplier;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.java.XmlElement;
 import password.pwm.util.java.XmlElement;
 import password.pwm.util.java.XmlFactory;
 import password.pwm.util.java.XmlFactory;
@@ -45,23 +48,27 @@ import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.List;
 import java.util.Locale;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Map;
+import java.util.Optional;
 
 
 public class NamedSecretValue implements StoredValue
 public class NamedSecretValue implements StoredValue
 {
 {
+
+    private final transient LazySupplier<String> valueHashSupplier = new LazySupplier<>( () -> AbstractValue.valueHashComputer( NamedSecretValue.this ) );
+
     private static final String ELEMENT_NAME = "name";
     private static final String ELEMENT_NAME = "name";
     private static final String ELEMENT_PASSWORD = "password";
     private static final String ELEMENT_PASSWORD = "password";
     private static final String ELEMENT_USAGE = "usage";
     private static final String ELEMENT_USAGE = "usage";
 
 
-    private Map<String, NamedSecretData> values;
+    private final Map<String, NamedSecretData> values;
 
 
     NamedSecretValue( )
     NamedSecretValue( )
     {
     {
+        values = Collections.emptyMap();
     }
     }
 
 
-
     public NamedSecretValue( final Map<String, NamedSecretData> values )
     public NamedSecretValue( final Map<String, NamedSecretData> values )
     {
     {
-        this.values = values;
+        this.values = values == null ? Collections.emptyMap() : Collections.unmodifiableMap( values );
     }
     }
 
 
     public static StoredValue.StoredValueFactory factory( )
     public static StoredValue.StoredValueFactory factory( )
@@ -78,7 +85,7 @@ public class NamedSecretValue implements StoredValue
                     final Map<String, NamedSecretData> linkedValues = new LinkedHashMap<>( values );
                     final Map<String, NamedSecretData> linkedValues = new LinkedHashMap<>( values );
                     return new NamedSecretValue( linkedValues );
                     return new NamedSecretValue( linkedValues );
                 }
                 }
-                catch ( Exception e )
+                catch ( final Exception e )
                 {
                 {
                     throw new IllegalStateException(
                     throw new IllegalStateException(
                             "NamedPasswordValue can not be json de-serialized: " + e.getMessage() );
                             "NamedPasswordValue can not be json de-serialized: " + e.getMessage() );
@@ -93,34 +100,33 @@ public class NamedSecretValue implements StoredValue
                     throws PwmOperationalException, PwmUnrecoverableException
                     throws PwmOperationalException, PwmUnrecoverableException
             {
             {
                 final Map<String, NamedSecretData> values = new LinkedHashMap<>();
                 final Map<String, NamedSecretData> values = new LinkedHashMap<>();
-                final List<XmlElement> valueElements = settingElement.getChildren( "value" );
+                final List<XmlElement> valueElements = settingElement.getChildren( StoredConfigXmlConstants.XML_ELEMENT_VALUE );
 
 
                 try
                 try
                 {
                 {
-                    if ( valueElements != null )
+                    for ( final XmlElement value : valueElements )
                     {
                     {
-                        for ( final XmlElement value : valueElements )
+                        final Optional<XmlElement> nameElement = value.getChild( ELEMENT_NAME );
+                        final Optional<XmlElement> passwordElement = value.getChild( ELEMENT_PASSWORD );
+                        if ( nameElement.isPresent() && passwordElement.isPresent() )
                         {
                         {
-                            if ( value.getChild( ELEMENT_NAME ) != null && value.getChild( ELEMENT_PASSWORD ) != null )
+                            final String name = nameElement.get().getText();
+                            final String encodedValue = passwordElement.get().getText();
+                            final PasswordData passwordData = new PasswordData( SecureEngine.decryptStringValue( encodedValue, key, PwmBlockAlgorithm.CONFIG ) );
+                            final List<XmlElement> usages = value.getChildren( ELEMENT_USAGE );
+                            final List<String> strUsages = new ArrayList<>();
+                            if ( usages != null )
                             {
                             {
-                                final String name = value.getChild( ELEMENT_NAME ).getText();
-                                final String encodedValue = value.getChild( ELEMENT_PASSWORD ).getText();
-                                final PasswordData passwordData = new PasswordData( SecureEngine.decryptStringValue( encodedValue, key, PwmBlockAlgorithm.CONFIG ) );
-                                final List<XmlElement> usages = value.getChildren( ELEMENT_USAGE );
-                                final List<String> strUsages = new ArrayList<>();
-                                if ( usages != null )
+                                for ( final XmlElement usageElement : usages )
                                 {
                                 {
-                                    for ( final XmlElement usageElement : usages )
-                                    {
-                                        strUsages.add( usageElement.getText() );
-                                    }
+                                    strUsages.add( usageElement.getText() );
                                 }
                                 }
-                                values.put( name, new NamedSecretData( passwordData, Collections.unmodifiableList( strUsages ) ) );
                             }
                             }
+                            values.put( name, new NamedSecretData( passwordData, Collections.unmodifiableList( strUsages ) ) );
                         }
                         }
                     }
                     }
                 }
                 }
-                catch ( Exception e )
+                catch ( final Exception e )
                 {
                 {
                     final String errorMsg = "unable to decode encrypted password value for setting: " + e.getMessage();
                     final String errorMsg = "unable to decode encrypted password value for setting: " + e.getMessage();
                     final ErrorInformation errorInfo = new ErrorInformation( PwmError.CONFIG_FORMAT_ERROR, errorMsg );
                     final ErrorInformation errorInfo = new ErrorInformation( PwmError.CONFIG_FORMAT_ERROR, errorMsg );
@@ -154,7 +160,7 @@ public class NamedSecretValue implements StoredValue
         return 0;
         return 0;
     }
     }
 
 
-    public List<XmlElement> toXmlValues( final String valueElementName, final PwmSecurityKey key )
+    public List<XmlElement> toXmlValues( final String valueElementName, final XmlOutputProcessData xmlOutputProcessData )
     {
     {
         if ( values == null )
         if ( values == null )
         {
         {
@@ -168,7 +174,7 @@ public class NamedSecretValue implements StoredValue
             {
             {
                 final String name = entry.getKey();
                 final String name = entry.getKey();
                 final PasswordData passwordData = entry.getValue().getPassword();
                 final PasswordData passwordData = entry.getValue().getPassword();
-                final String encodedValue = SecureEngine.encryptToString( passwordData.getStringValue(), key, PwmBlockAlgorithm.CONFIG );
+                final String encodedValue = SecureEngine.encryptToString( passwordData.getStringValue(), xmlOutputProcessData.getPwmSecurityKey(), PwmBlockAlgorithm.CONFIG );
                 final XmlElement newValueElement = XmlFactory.getFactory().newElement( "value" );
                 final XmlElement newValueElement = XmlFactory.getFactory().newElement( "value" );
                 final XmlElement nameElement = XmlFactory.getFactory().newElement( ELEMENT_NAME );
                 final XmlElement nameElement = XmlFactory.getFactory().newElement( ELEMENT_NAME );
                 nameElement.addText( name );
                 nameElement.addText( name );
@@ -189,7 +195,7 @@ public class NamedSecretValue implements StoredValue
                 valuesElement.add( newValueElement );
                 valuesElement.add( newValueElement );
             }
             }
         }
         }
-        catch ( Exception e )
+        catch ( final Exception e )
         {
         {
             throw new RuntimeException( "missing required AES and SHA1 libraries, or other crypto fault: " + e.getMessage() );
             throw new RuntimeException( "missing required AES and SHA1 libraries, or other crypto fault: " + e.getMessage() );
         }
         }
@@ -239,21 +245,15 @@ public class NamedSecretValue implements StoredValue
             }
             }
             return copiedValues;
             return copiedValues;
         }
         }
-        catch ( PwmUnrecoverableException e )
+        catch ( final PwmUnrecoverableException e )
         {
         {
             throw new IllegalStateException( e.getErrorInformation().toDebugStr() );
             throw new IllegalStateException( e.getErrorInformation().toDebugStr() );
         }
         }
     }
     }
 
 
-    public boolean requiresStoredUpdate( )
-    {
-        return false;
-    }
-
     @Override
     @Override
-    public String valueHash( ) throws PwmUnrecoverableException
+    public String valueHash()
     {
     {
-        return values == null ? "" : SecureEngine.hash( JsonUtil.serializeMap( values ), PwmConstants.SETTING_CHECKSUM_HASH_METHOD );
+        return valueHashSupplier.get();
     }
     }
-
 }
 }

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

@@ -22,6 +22,7 @@ package password.pwm.config.value;
 
 
 import password.pwm.config.PwmSetting;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.StoredValue;
 import password.pwm.config.StoredValue;
+import password.pwm.config.stored.XmlOutputProcessData;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.JsonUtil;
 import password.pwm.util.java.JsonUtil;
 import password.pwm.util.java.XmlElement;
 import password.pwm.util.java.XmlElement;
@@ -38,11 +39,11 @@ import java.util.stream.Collectors;
 
 
 public class NumericArrayValue extends AbstractValue implements StoredValue
 public class NumericArrayValue extends AbstractValue implements StoredValue
 {
 {
-    List<Long> values;
+    private final List<Long> values;
 
 
     public NumericArrayValue( final List<Long> values )
     public NumericArrayValue( final List<Long> values )
     {
     {
-        this.values = values;
+        this.values = values == null ? Collections.emptyList() : Collections.unmodifiableList( values );
     }
     }
 
 
     public static StoredValueFactory factory( )
     public static StoredValueFactory factory( )
@@ -72,7 +73,7 @@ public class NumericArrayValue extends AbstractValue implements StoredValue
     }
     }
 
 
     @Override
     @Override
-    public List<XmlElement> toXmlValues( final String valueElementName, final PwmSecurityKey pwmSecurityKey  )
+    public List<XmlElement> toXmlValues( final String valueElementName, final XmlOutputProcessData xmlOutputProcessData )
     {
     {
         final List<XmlElement> returnList = new ArrayList<>();
         final List<XmlElement> returnList = new ArrayList<>();
         for ( final Long value : this.values )
         for ( final Long value : this.values )

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

@@ -23,6 +23,8 @@ package password.pwm.config.value;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.PwmSettingProperty;
 import password.pwm.config.PwmSettingProperty;
 import password.pwm.config.StoredValue;
 import password.pwm.config.StoredValue;
+import password.pwm.config.stored.StoredConfigXmlConstants;
+import password.pwm.config.stored.XmlOutputProcessData;
 import password.pwm.util.java.JsonUtil;
 import password.pwm.util.java.JsonUtil;
 import password.pwm.util.java.XmlElement;
 import password.pwm.util.java.XmlElement;
 import password.pwm.util.java.XmlFactory;
 import password.pwm.util.java.XmlFactory;
@@ -30,10 +32,11 @@ import password.pwm.util.secure.PwmSecurityKey;
 
 
 import java.util.Collections;
 import java.util.Collections;
 import java.util.List;
 import java.util.List;
+import java.util.Optional;
 
 
 public class NumericValue extends AbstractValue implements StoredValue
 public class NumericValue extends AbstractValue implements StoredValue
 {
 {
-    long value;
+    private final long value;
 
 
     public NumericValue( final long value )
     public NumericValue( final long value )
     {
     {
@@ -51,9 +54,16 @@ public class NumericValue extends AbstractValue implements StoredValue
 
 
             public NumericValue fromXmlElement( final PwmSetting pwmSetting, final XmlElement settingElement, final PwmSecurityKey input )
             public NumericValue fromXmlElement( final PwmSetting pwmSetting, final XmlElement settingElement, final PwmSecurityKey input )
             {
             {
-                final XmlElement valueElement = settingElement.getChild( "value" );
-                final String value = valueElement.getText();
-                return new NumericValue( normalizeValue( pwmSetting, Long.parseLong( value ) ) );
+                final Optional<XmlElement> valueElement = settingElement.getChild( StoredConfigXmlConstants.XML_ELEMENT_VALUE );
+                if ( valueElement.isPresent() )
+                {
+                    final String value = valueElement.get().getText();
+                    return new NumericValue( normalizeValue( pwmSetting, Long.parseLong( value ) ) );
+                }
+                else
+                {
+                    return new NumericValue( 0 );
+                }
             }
             }
         };
         };
     }
     }
@@ -77,7 +87,7 @@ public class NumericValue extends AbstractValue implements StoredValue
     }
     }
 
 
     @Override
     @Override
-    public List<XmlElement> toXmlValues( final String valueElementName, final PwmSecurityKey pwmSecurityKey  )
+    public List<XmlElement> toXmlValues( final String valueElementName, final XmlOutputProcessData xmlOutputProcessData )
     {
     {
         final XmlElement valueElement = XmlFactory.getFactory().newElement( valueElementName );
         final XmlElement valueElement = XmlFactory.getFactory().newElement( valueElementName );
         valueElement.addText( Long.toString( value ) );
         valueElement.addText( Long.toString( value ) );

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

@@ -23,6 +23,7 @@ package password.pwm.config.value;
 import com.google.gson.reflect.TypeToken;
 import com.google.gson.reflect.TypeToken;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.StoredValue;
 import password.pwm.config.StoredValue;
+import password.pwm.config.stored.XmlOutputProcessData;
 import password.pwm.error.PwmOperationalException;
 import password.pwm.error.PwmOperationalException;
 import password.pwm.util.java.JsonUtil;
 import password.pwm.util.java.JsonUtil;
 import password.pwm.util.java.XmlElement;
 import password.pwm.util.java.XmlElement;
@@ -39,11 +40,11 @@ import java.util.TreeSet;
 
 
 public class OptionListValue extends AbstractValue implements StoredValue
 public class OptionListValue extends AbstractValue implements StoredValue
 {
 {
-    final Set<String> values;
+    private final Set<String> values;
 
 
     public OptionListValue( final Set<String> values )
     public OptionListValue( final Set<String> values )
     {
     {
-        this.values = new TreeSet( values );
+        this.values = values == null ? Collections.emptySet() : Collections.unmodifiableSet( new TreeSet<>( values ) );
     }
     }
 
 
     public static StoredValueFactory factory( )
     public static StoredValueFactory factory( )
@@ -88,7 +89,7 @@ public class OptionListValue extends AbstractValue implements StoredValue
         };
         };
     }
     }
 
 
-    public List<XmlElement> toXmlValues( final String valueElementName, final PwmSecurityKey pwmSecurityKey  )
+    public List<XmlElement> toXmlValues( final String valueElementName, final XmlOutputProcessData xmlOutputProcessData )
     {
     {
         final List<XmlElement> returnList = new ArrayList<>();
         final List<XmlElement> returnList = new ArrayList<>();
         for ( final String value : values )
         for ( final String value : values )

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

@@ -24,33 +24,36 @@ package password.pwm.config.value;
 import password.pwm.PwmConstants;
 import password.pwm.PwmConstants;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.StoredValue;
 import password.pwm.config.StoredValue;
+import password.pwm.config.stored.StoredConfigXmlConstants;
+import password.pwm.config.stored.XmlOutputProcessData;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmOperationalException;
 import password.pwm.error.PwmOperationalException;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.util.PasswordData;
 import password.pwm.util.PasswordData;
 import password.pwm.util.java.JsonUtil;
 import password.pwm.util.java.JsonUtil;
+import password.pwm.util.java.LazySupplier;
 import password.pwm.util.java.XmlElement;
 import password.pwm.util.java.XmlElement;
 import password.pwm.util.java.XmlFactory;
 import password.pwm.util.java.XmlFactory;
-import password.pwm.util.secure.PwmBlockAlgorithm;
 import password.pwm.util.secure.PwmSecurityKey;
 import password.pwm.util.secure.PwmSecurityKey;
-import password.pwm.util.secure.SecureEngine;
 
 
 import java.io.Serializable;
 import java.io.Serializable;
 import java.util.Collections;
 import java.util.Collections;
 import java.util.List;
 import java.util.List;
 import java.util.Locale;
 import java.util.Locale;
+import java.util.Optional;
 
 
 public class PasswordValue implements StoredValue
 public class PasswordValue implements StoredValue
 {
 {
-    private PasswordData value;
+    private final transient LazySupplier<String> valueHashSupplier = new LazySupplier<>( () -> AbstractValue.valueHashComputer( PasswordValue.this ) );
+
+    private final PasswordData value;
 
 
     PasswordValue( )
     PasswordValue( )
     {
     {
+        value = null;
     }
     }
 
 
-    boolean requiresStoredUpdate;
-
     public PasswordValue( final PasswordData passwordData )
     public PasswordValue( final PasswordData passwordData )
     {
     {
         value = passwordData;
         value = passwordData;
@@ -69,7 +72,7 @@ public class PasswordValue implements StoredValue
                     {
                     {
                         return new PasswordValue( new PasswordData( strValue ) );
                         return new PasswordValue( new PasswordData( strValue ) );
                     }
                     }
-                    catch ( PwmUnrecoverableException e )
+                    catch ( final PwmUnrecoverableException e )
                     {
                     {
                         throw new IllegalStateException(
                         throw new IllegalStateException(
                                 "PasswordValue can not be json de-serialized: " + e.getMessage() );
                                 "PasswordValue can not be json de-serialized: " + e.getMessage() );
@@ -85,50 +88,54 @@ public class PasswordValue implements StoredValue
             )
             )
                     throws PwmOperationalException, PwmUnrecoverableException
                     throws PwmOperationalException, PwmUnrecoverableException
             {
             {
-                final XmlElement valueElement = settingElement.getChild( "value" );
-                final String rawValue = valueElement.getText();
-
-                final PasswordValue newPasswordValue = new PasswordValue();
-                if ( rawValue == null || rawValue.isEmpty() )
+                final Optional<XmlElement> valueElement = settingElement.getChild( StoredConfigXmlConstants.XML_ELEMENT_VALUE );
+                if ( valueElement.isPresent() )
                 {
                 {
-                    return newPasswordValue;
-                }
-
-                final boolean plainTextSetting;
-                {
-                    final String plainTextAttributeStr = valueElement.getAttributeValue( "plaintext" );
-                    plainTextSetting = plainTextAttributeStr != null && Boolean.parseBoolean( plainTextAttributeStr );
-                }
+                    final String rawValue = valueElement.get().getText();
 
 
-                if ( plainTextSetting )
-                {
-                    newPasswordValue.value = new PasswordData( rawValue );
-                    newPasswordValue.requiresStoredUpdate = true;
-                }
-                else
-                {
-                    try
+                    final PasswordValue newPasswordValue = new PasswordValue();
+                    if ( rawValue == null || rawValue.isEmpty() )
                     {
                     {
-                        newPasswordValue.value = new PasswordData( SecureEngine.decryptStringValue( rawValue, key, PwmBlockAlgorithm.CONFIG ) );
                         return newPasswordValue;
                         return newPasswordValue;
                     }
                     }
-                    catch ( Exception e )
+
+                    final boolean plainTextSetting;
                     {
                     {
-                        final String errorMsg = "unable to decode encrypted password value for setting: " + e.getMessage();
-                        final ErrorInformation errorInfo = new ErrorInformation( PwmError.CONFIG_FORMAT_ERROR, errorMsg );
-                        throw new PwmOperationalException( errorInfo );
+                        final String plainTextAttributeStr = valueElement.get().getAttributeValue( "plaintext" );
+                        plainTextSetting = plainTextAttributeStr != null && Boolean.parseBoolean( plainTextAttributeStr );
+                    }
+
+                    if ( plainTextSetting )
+                    {
+                        return new PasswordValue( new PasswordData( rawValue ) );
+                    }
+                    else
+                    {
+                        try
+                        {
+                            final Optional<String> encodedValue = StoredValueEncoder.decode( rawValue, StoredValueEncoder.Mode.CONFIG_PW, key );
+                            if ( encodedValue.isPresent() )
+                            {
+                                return new PasswordValue( new PasswordData( encodedValue.get() ) );
+                            }
+                            else
+                            {
+                                return new PasswordValue( new PasswordData( "" ) );
+                            }
+                        }
+                        catch ( final Exception e )
+                        {
+                            final String errorMsg = "unable to decode encrypted password value for setting: " + e.getMessage();
+                            final ErrorInformation errorInfo = new ErrorInformation( PwmError.CONFIG_FORMAT_ERROR, errorMsg );
+                            throw new PwmOperationalException( errorInfo );
+                        }
                     }
                     }
                 }
                 }
-                return newPasswordValue;
+                return new PasswordValue();
             }
             }
         };
         };
     }
     }
 
 
-    public List<XmlElement> toXmlValues( final String valueElementName )
-    {
-        throw new IllegalStateException( "password xml output requires hash key" );
-    }
-
     @Override
     @Override
     public Object toNativeObject( )
     public Object toNativeObject( )
     {
     {
@@ -147,7 +154,7 @@ public class PasswordValue implements StoredValue
         return 0;
         return 0;
     }
     }
 
 
-    public List<XmlElement> toXmlValues( final String valueElementName, final PwmSecurityKey key )
+    public List<XmlElement> toXmlValues( final String valueElementName, final XmlOutputProcessData xmlOutputProcessData )
     {
     {
         if ( value == null )
         if ( value == null )
         {
         {
@@ -157,10 +164,14 @@ public class PasswordValue implements StoredValue
         final XmlElement valueElement = XmlFactory.getFactory().newElement( valueElementName );
         final XmlElement valueElement = XmlFactory.getFactory().newElement( valueElementName );
         try
         try
         {
         {
-            final String encodedValue = SecureEngine.encryptToString( value.getStringValue(), key, PwmBlockAlgorithm.CONFIG );
+            final String encodedValue = StoredValueEncoder.encode(
+                    value.getStringValue(),
+                    xmlOutputProcessData.getStoredValueEncoderMode(),
+                    xmlOutputProcessData.getPwmSecurityKey() );
+
             valueElement.addText( encodedValue );
             valueElement.addText( encodedValue );
         }
         }
-        catch ( Exception e )
+        catch ( final Exception e )
         {
         {
             throw new RuntimeException( "missing required AES and SHA1 libraries, or other crypto fault: " + e.getMessage() );
             throw new RuntimeException( "missing required AES and SHA1 libraries, or other crypto fault: " + e.getMessage() );
         }
         }
@@ -184,14 +195,9 @@ public class PasswordValue implements StoredValue
         return PwmConstants.LOG_REMOVED_VALUE_REPLACEMENT;
         return PwmConstants.LOG_REMOVED_VALUE_REPLACEMENT;
     }
     }
 
 
-    public boolean requiresStoredUpdate( )
-    {
-        return requiresStoredUpdate;
-    }
-
     @Override
     @Override
-    public String valueHash( ) throws PwmUnrecoverableException
+    public String valueHash()
     {
     {
-        return value == null ? "" : SecureEngine.hash( JsonUtil.serialize( value.getStringValue() ), PwmConstants.SETTING_CHECKSUM_HASH_METHOD );
+        return valueHashSupplier.get();
     }
     }
 }
 }

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

@@ -23,14 +23,14 @@ package password.pwm.config.value;
 import password.pwm.bean.PrivateKeyCertificate;
 import password.pwm.bean.PrivateKeyCertificate;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.StoredValue;
 import password.pwm.config.StoredValue;
+import password.pwm.config.stored.StoredConfigXmlConstants;
+import password.pwm.config.stored.XmlOutputProcessData;
 import password.pwm.util.java.JsonUtil;
 import password.pwm.util.java.JsonUtil;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.java.XmlElement;
 import password.pwm.util.java.XmlElement;
 import password.pwm.util.java.XmlFactory;
 import password.pwm.util.java.XmlFactory;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.logging.PwmLogger;
-import password.pwm.util.secure.PwmBlockAlgorithm;
 import password.pwm.util.secure.PwmSecurityKey;
 import password.pwm.util.secure.PwmSecurityKey;
-import password.pwm.util.secure.SecureEngine;
 import password.pwm.util.secure.X509Utils;
 import password.pwm.util.secure.X509Utils;
 
 
 import java.io.Serializable;
 import java.io.Serializable;
@@ -44,6 +44,7 @@ import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.List;
 import java.util.Locale;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Map;
+import java.util.Optional;
 
 
 public class PrivateKeyValue extends AbstractValue
 public class PrivateKeyValue extends AbstractValue
 {
 {
@@ -52,7 +53,7 @@ public class PrivateKeyValue extends AbstractValue
     private static final String ELEMENT_NAME_CERTIFICATE = "certificate";
     private static final String ELEMENT_NAME_CERTIFICATE = "certificate";
     private static final String ELEMENT_NAME_KEY = "key";
     private static final String ELEMENT_NAME_KEY = "key";
 
 
-    private PrivateKeyCertificate privateKeyCertificate;
+    private final PrivateKeyCertificate privateKeyCertificate;
 
 
     public static StoredValue.StoredValueFactory factory( )
     public static StoredValue.StoredValueFactory factory( )
     {
     {
@@ -60,14 +61,14 @@ public class PrivateKeyValue extends AbstractValue
         {
         {
             public PrivateKeyValue fromXmlElement( final PwmSetting pwmSetting, final XmlElement settingElement, final PwmSecurityKey key )
             public PrivateKeyValue fromXmlElement( final PwmSetting pwmSetting, final XmlElement settingElement, final PwmSecurityKey key )
             {
             {
-                if ( settingElement != null && settingElement.getChild( "value" ) != null )
+                if ( settingElement != null && settingElement.getChild( StoredConfigXmlConstants.XML_ELEMENT_VALUE  ).isPresent() )
                 {
                 {
 
 
-                    final XmlElement valueElement = settingElement.getChild( "value" );
-                    if ( valueElement != null )
+                    final Optional<XmlElement> valueElement = settingElement.getChild( StoredConfigXmlConstants.XML_ELEMENT_VALUE );
+                    if ( valueElement.isPresent() )
                     {
                     {
                         final List<X509Certificate> certificates = new ArrayList<>();
                         final List<X509Certificate> certificates = new ArrayList<>();
-                        for ( final XmlElement certificateElement : valueElement.getChildren( ELEMENT_NAME_CERTIFICATE ) )
+                        for ( final XmlElement certificateElement : valueElement.get().getChildren( ELEMENT_NAME_CERTIFICATE ) )
                         {
                         {
                             try
                             try
                             {
                             {
@@ -75,7 +76,7 @@ public class PrivateKeyValue extends AbstractValue
                                 final X509Certificate cert = X509Utils.certificateFromBase64( b64Text );
                                 final X509Certificate cert = X509Utils.certificateFromBase64( b64Text );
                                 certificates.add( cert );
                                 certificates.add( cert );
                             }
                             }
-                            catch ( Exception e )
+                            catch ( final Exception e )
                             {
                             {
                                 LOGGER.error( "error reading certificate: " + e.getMessage(), e );
                                 LOGGER.error( "error reading certificate: " + e.getMessage(), e );
                             }
                             }
@@ -86,15 +87,25 @@ public class PrivateKeyValue extends AbstractValue
                         PrivateKey privateKey = null;
                         PrivateKey privateKey = null;
                         try
                         try
                         {
                         {
-                            final XmlElement keyElement = valueElement.getChild( ELEMENT_NAME_KEY );
-                            final String encryptedText = keyElement.getText();
-                            final String decryptedText = SecureEngine.decryptStringValue( encryptedText, key, PwmBlockAlgorithm.CONFIG );
-                            final byte[] privateKeyBytes = StringUtil.base64Decode( decryptedText );
-                            privateKey = KeyFactory.getInstance( "RSA" ).generatePrivate( new PKCS8EncodedKeySpec( privateKeyBytes ) );
+                            final Optional<XmlElement> keyElement = valueElement.get().getChild( ELEMENT_NAME_KEY );
+                            if ( keyElement.isPresent() )
+                            {
+                                final String encryptedText = keyElement.get().getText();
+                                final Optional<String> decryptedText = StoredValueEncoder.decode( encryptedText, StoredValueEncoder.Mode.CONFIG_PW, key );
+                                if ( decryptedText.isPresent() )
+                                {
+                                    final byte[] privateKeyBytes = StringUtil.base64Decode( decryptedText.get() );
+                                    privateKey = KeyFactory.getInstance( "RSA" ).generatePrivate( new PKCS8EncodedKeySpec( privateKeyBytes ) );
+                                }
+                            }
+                            else
+                            {
+                                LOGGER.error( "error reading privateKey for setting: '" + pwmSetting.getKey() + "': missing 'value' element" );
+                            }
                         }
                         }
-                        catch ( Exception e )
+                        catch ( final Exception e )
                         {
                         {
-                            LOGGER.error( "error reading privateKey: " + e.getMessage(), e );
+                            LOGGER.error( "error reading privateKey for setting: '" + pwmSetting.getKey() + "': " + e.getMessage(), e );
                         }
                         }
 
 
                         if ( !certificates.isEmpty() && privateKey != null )
                         if ( !certificates.isEmpty() && privateKey != null )
@@ -143,9 +154,9 @@ public class PrivateKeyValue extends AbstractValue
         return 0;
         return 0;
     }
     }
 
 
-    public List<XmlElement> toXmlValues( final String valueElementName, final PwmSecurityKey key )
+    public List<XmlElement> toXmlValues( final String valueElementName, final XmlOutputProcessData xmlOutputProcessData )
     {
     {
-        final XmlElement valueElement = XmlFactory.getFactory().newElement( "value" );
+        final XmlElement valueElement = XmlFactory.getFactory().newElement( StoredConfigXmlConstants.XML_ELEMENT_VALUE );
         if ( privateKeyCertificate != null )
         if ( privateKeyCertificate != null )
         {
         {
             try
             try
@@ -161,12 +172,16 @@ public class PrivateKeyValue extends AbstractValue
                 {
                 {
                     final XmlElement keyElement = XmlFactory.getFactory().newElement( ELEMENT_NAME_KEY );
                     final XmlElement keyElement = XmlFactory.getFactory().newElement( ELEMENT_NAME_KEY );
                     final String b64EncodedKey = StringUtil.base64Encode( privateKeyCertificate.getKey().getEncoded() );
                     final String b64EncodedKey = StringUtil.base64Encode( privateKeyCertificate.getKey().getEncoded() );
-                    final String encryptedKey = SecureEngine.encryptToString( b64EncodedKey, key, PwmBlockAlgorithm.CONFIG );
+                    final String encryptedKey = StoredValueEncoder.encode(
+                            b64EncodedKey,
+                            xmlOutputProcessData.getStoredValueEncoderMode(),
+                            xmlOutputProcessData.getPwmSecurityKey() );
+
                     keyElement.addText( encryptedKey );
                     keyElement.addText( encryptedKey );
                     valueElement.addContent( keyElement );
                     valueElement.addContent( keyElement );
                 }
                 }
             }
             }
-            catch ( Exception e )
+            catch ( final Exception e )
             {
             {
                 throw new RuntimeException( "missing required AES and SHA1 libraries, or other crypto fault: " + e.getMessage() );
                 throw new RuntimeException( "missing required AES and SHA1 libraries, or other crypto fault: " + e.getMessage() );
             }
             }

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

@@ -24,6 +24,8 @@ import com.google.gson.reflect.TypeToken;
 import password.pwm.PwmConstants;
 import password.pwm.PwmConstants;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.StoredValue;
 import password.pwm.config.StoredValue;
+import password.pwm.config.stored.StoredConfigXmlConstants;
+import password.pwm.config.stored.XmlOutputProcessData;
 import password.pwm.config.value.data.RemoteWebServiceConfiguration;
 import password.pwm.config.value.data.RemoteWebServiceConfiguration;
 import password.pwm.error.PwmOperationalException;
 import password.pwm.error.PwmOperationalException;
 import password.pwm.util.java.JsonUtil;
 import password.pwm.util.java.JsonUtil;
@@ -43,6 +45,7 @@ import java.util.List;
 import java.util.Locale;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Objects;
+import java.util.Optional;
 import java.util.Set;
 import java.util.Set;
 
 
 public class RemoteWebServiceValue extends AbstractValue implements StoredValue
 public class RemoteWebServiceValue extends AbstractValue implements StoredValue
@@ -74,7 +77,7 @@ public class RemoteWebServiceValue extends AbstractValue implements StoredValue
                             }
                             }
                     );
                     );
 
 
-                    srcList = srcList == null ? Collections.emptyList() : srcList;
+                    srcList = srcList == null ? new ArrayList<>() : srcList;
                     srcList.removeIf( Objects::isNull );
                     srcList.removeIf( Objects::isNull );
                     return new RemoteWebServiceValue( Collections.unmodifiableList( srcList ) );
                     return new RemoteWebServiceValue( Collections.unmodifiableList( srcList ) );
                 }
                 }
@@ -87,7 +90,7 @@ public class RemoteWebServiceValue extends AbstractValue implements StoredValue
             )
             )
                     throws PwmOperationalException
                     throws PwmOperationalException
             {
             {
-                final List<XmlElement> valueElements = settingElement.getChildren( "value" );
+                final List<XmlElement> valueElements = settingElement.getChildren( StoredConfigXmlConstants.XML_ELEMENT_VALUE );
                 final List<RemoteWebServiceConfiguration> values = new ArrayList<>();
                 final List<RemoteWebServiceConfiguration> values = new ArrayList<>();
                 for ( final XmlElement loopValueElement : valueElements )
                 for ( final XmlElement loopValueElement : valueElements )
                 {
                 {
@@ -95,8 +98,15 @@ public class RemoteWebServiceValue extends AbstractValue implements StoredValue
                     if ( value != null && value.length() > 0 )
                     if ( value != null && value.length() > 0 )
                     {
                     {
                         final RemoteWebServiceConfiguration parsedValue = JsonUtil.deserialize( value, RemoteWebServiceConfiguration.class );
                         final RemoteWebServiceConfiguration parsedValue = JsonUtil.deserialize( value, RemoteWebServiceConfiguration.class );
-                        parsedValue.setPassword( decryptPwValue( parsedValue.getPassword(), pwmSecurityKey ) );
-                        values.add( parsedValue );
+                        final Optional<String> decodedValue = StoredValueEncoder.decode(
+                                parsedValue.getPassword(),
+                                StoredValueEncoder.Mode.ENCODED,
+                                pwmSecurityKey
+                        );
+                        decodedValue.ifPresent( ( s ) ->
+                        {
+                            values.add( parsedValue.toBuilder().password( s ).build() );
+                        } );
                     }
                     }
                 }
                 }
                 return new RemoteWebServiceValue( values );
                 return new RemoteWebServiceValue( values );
@@ -104,22 +114,27 @@ public class RemoteWebServiceValue extends AbstractValue implements StoredValue
         };
         };
     }
     }
 
 
-    public List<XmlElement> toXmlValues( final String valueElementName, final PwmSecurityKey pwmSecurityKey  )
+    public List<XmlElement> toXmlValues( final String valueElementName, final XmlOutputProcessData xmlOutputProcessData )
     {
     {
         final List<XmlElement> returnList = new ArrayList<>();
         final List<XmlElement> returnList = new ArrayList<>();
         for ( final RemoteWebServiceConfiguration value : values )
         for ( final RemoteWebServiceConfiguration value : values )
         {
         {
             final XmlElement valueElement = XmlFactory.getFactory().newElement( valueElementName );
             final XmlElement valueElement = XmlFactory.getFactory().newElement( valueElementName );
-            final RemoteWebServiceConfiguration clonedValue = JsonUtil.cloneUsingJson( value, RemoteWebServiceConfiguration.class );
+
+            String encodedValue = value.getPassword();
             try
             try
             {
             {
-                clonedValue.setPassword( encryptPwValue( clonedValue.getPassword(), pwmSecurityKey ) );
+                encodedValue = StoredValueEncoder.encode(
+                        value.getPassword(),
+                        xmlOutputProcessData.getStoredValueEncoderMode(),
+                        xmlOutputProcessData.getPwmSecurityKey() );
             }
             }
-            catch ( PwmOperationalException e )
+            catch ( final PwmOperationalException e )
             {
             {
                 LOGGER.warn( "error decoding stored pw value: " + e.getMessage() );
                 LOGGER.warn( "error decoding stored pw value: " + e.getMessage() );
             }
             }
 
 
+            final RemoteWebServiceConfiguration clonedValue = value.toBuilder().password( encodedValue ).build();
             valueElement.addText( JsonUtil.serialize( clonedValue ) );
             valueElement.addText( JsonUtil.serialize( clonedValue ) );
             returnList.add( valueElement );
             returnList.add( valueElement );
         }
         }
@@ -200,12 +215,14 @@ public class RemoteWebServiceValue extends AbstractValue implements StoredValue
         final ArrayList<RemoteWebServiceConfiguration> output = new ArrayList<>();
         final ArrayList<RemoteWebServiceConfiguration> output = new ArrayList<>();
         for ( final RemoteWebServiceConfiguration remoteWebServiceConfiguration : values )
         for ( final RemoteWebServiceConfiguration remoteWebServiceConfiguration : values )
         {
         {
-            final RemoteWebServiceConfiguration clone = JsonUtil.cloneUsingJson( remoteWebServiceConfiguration, RemoteWebServiceConfiguration.class );
-            if ( !StringUtil.isEmpty( clone.getPassword() ) )
+            if ( !StringUtil.isEmpty( remoteWebServiceConfiguration.getPassword() ) )
+            {
+                output.add( remoteWebServiceConfiguration.toBuilder().password( PwmConstants.LOG_REMOVED_VALUE_REPLACEMENT ).build() );
+            }
+            else
             {
             {
-                clone.setPassword( PwmConstants.LOG_REMOVED_VALUE_REPLACEMENT );
+                output.add( remoteWebServiceConfiguration );
             }
             }
-            output.add( clone );
         }
         }
         return output;
         return output;
     }
     }

+ 268 - 0
server/src/main/java/password/pwm/config/value/StoredValueEncoder.java

@@ -0,0 +1,268 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2019 The PWM Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package password.pwm.config.value;
+
+import lombok.Value;
+import password.pwm.PwmConstants;
+import password.pwm.error.ErrorInformation;
+import password.pwm.error.PwmError;
+import password.pwm.error.PwmOperationalException;
+import password.pwm.util.java.JsonUtil;
+import password.pwm.util.java.StringUtil;
+import password.pwm.util.logging.PwmLogger;
+import password.pwm.util.secure.PwmBlockAlgorithm;
+import password.pwm.util.secure.PwmRandom;
+import password.pwm.util.secure.PwmSecurityKey;
+import password.pwm.util.secure.SecureEngine;
+
+import java.io.Serializable;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+
+public abstract class StoredValueEncoder
+{
+    private StoredValueEncoder()
+    {
+    }
+
+    public enum Mode
+    {
+        PLAIN( new PlaintextModeEngine(), "PLAIN" + DELIMITER, "PLAINTEXT" + DELIMITER, "RAW" + DELIMITER ),
+        STRIPPED( new StrippedModeEngine(), "REMOVED" + DELIMITER ),
+        CONFIG_PW( new ConfigPwModeEngine(), "CONFIG-PW" + DELIMITER ),
+        ENCODED( new EncodedModeEngine(), "ENC-PW" + DELIMITER, "ENCODED" + DELIMITER ),;
+
+        private final List<String> prefixes;
+        private final SecureOutputEngine secureOutputEngine;
+
+        Mode( final SecureOutputEngine secureOutputEngine, final String... prefixes )
+        {
+            this.secureOutputEngine = secureOutputEngine;
+            this.prefixes = Collections.unmodifiableList( Arrays.asList( prefixes ) );
+        }
+
+        public List<String> getPrefixes()
+        {
+            return prefixes;
+        }
+
+        public String getPrefix()
+        {
+            return prefixes.iterator().next();
+        }
+
+        public SecureOutputEngine getSecureOutputEngine()
+        {
+            return secureOutputEngine;
+        }
+    }
+
+
+    private static final PwmLogger LOGGER = PwmLogger.forClass( StoredValueEncoder.class );
+    private static final String DELIMITER = ":";
+
+    public static Optional<String> decode(
+            final String input,
+            final Mode modeHint,
+            final PwmSecurityKey pwmSecurityKey
+    )
+            throws PwmOperationalException
+    {
+        if ( StringUtil.isEmpty( input ) )
+        {
+            return Optional.empty();
+        }
+
+        final ParsedInput parsedInput = ParsedInput.parseInput( input );
+        final Mode requestedMode = modeHint == null ? Mode.PLAIN : modeHint;
+        final Mode effectiveMode = parsedInput.getMode() == null
+                ? requestedMode
+                : parsedInput.getMode();
+        return Optional.ofNullable( effectiveMode.getSecureOutputEngine().decode( parsedInput, pwmSecurityKey ) );
+    }
+
+    public static String encode( final String realValue, final Mode mode, final PwmSecurityKey pwmSecurityKey )
+            throws PwmOperationalException
+    {
+        return mode.getSecureOutputEngine().encode( realValue, pwmSecurityKey );
+    }
+
+    @Value
+    private static class ParsedInput
+    {
+        private Mode mode;
+        private String value;
+
+        static ParsedInput parseInput( final String value )
+        {
+            if ( !StringUtil.isEmpty( value ) )
+            {
+                for ( final Mode mode : Mode.values() )
+                {
+                    for ( final String prefix : mode.getPrefixes() )
+                    {
+                        if ( value.startsWith( prefix ) )
+                        {
+                            return new ParsedInput( mode, value.substring( prefix.length() ) );
+                        }
+                    }
+                }
+            }
+
+            return new ParsedInput( null, value );
+        }
+    }
+
+    private interface SecureOutputEngine
+    {
+        String encode( String rawOutput, PwmSecurityKey pwmSecurityKey ) throws PwmOperationalException;
+
+        String decode( ParsedInput input, PwmSecurityKey pwmSecurityKey ) throws PwmOperationalException;
+    }
+
+
+    private static class PlaintextModeEngine implements SecureOutputEngine
+    {
+        @Override
+        public String encode( final String rawValue, final PwmSecurityKey pwmSecurityKey ) throws PwmOperationalException
+        {
+            return Mode.PLAIN.getPrefix() + rawValue;
+        }
+
+        @Override
+        public String decode( final ParsedInput input, final PwmSecurityKey pwmSecurityKey ) throws PwmOperationalException
+        {
+            return input.getValue();
+        }
+    }
+
+    private static class StrippedModeEngine implements SecureOutputEngine
+    {
+        @Override
+        public String encode( final String rawValue, final PwmSecurityKey pwmSecurityKey ) throws PwmOperationalException
+        {
+            return PwmConstants.LOG_REMOVED_VALUE_REPLACEMENT;
+        }
+
+        @Override
+        public String decode( final ParsedInput input, final PwmSecurityKey pwmSecurityKey ) throws PwmOperationalException
+        {
+            return PwmConstants.LOG_REMOVED_VALUE_REPLACEMENT;
+        }
+    }
+
+    private static class ConfigPwModeEngine implements SecureOutputEngine
+    {
+        @Override
+        public String encode( final String rawValue, final PwmSecurityKey pwmSecurityKey ) throws PwmOperationalException
+        {
+            try
+            {
+                final String encryptedValue = SecureEngine.encryptToString( rawValue, pwmSecurityKey, PwmBlockAlgorithm.CONFIG );
+                return Mode.CONFIG_PW + encryptedValue;
+            }
+            catch ( final Exception e )
+            {
+                final String errorMsg = "unable to encrypt config-password value for setting: " + e.getMessage();
+                final ErrorInformation errorInfo = new ErrorInformation( PwmError.CONFIG_FORMAT_ERROR, errorMsg );
+                throw new PwmOperationalException( errorInfo );
+            }
+        }
+
+        @Override
+        public String decode( final ParsedInput input, final PwmSecurityKey pwmSecurityKey ) throws PwmOperationalException
+        {
+            try
+            {
+                return SecureEngine.decryptStringValue( input.getValue(), pwmSecurityKey, PwmBlockAlgorithm.CONFIG );
+            }
+            catch ( final Exception e )
+            {
+                final String errorMsg = "unable to decrypt config password value for setting: " + e.getMessage();
+                final ErrorInformation errorInfo = new ErrorInformation( PwmError.CONFIG_FORMAT_ERROR, errorMsg );
+                LOGGER.warn( errorInfo.toDebugStr() );
+                throw new PwmOperationalException( errorInfo );
+            }
+        }
+    }
+
+    private static class EncodedModeEngine implements SecureOutputEngine
+    {
+        @Override
+        public String encode( final String rawValue, final PwmSecurityKey pwmSecurityKey ) throws PwmOperationalException
+        {
+            if ( rawValue == null )
+            {
+                return Mode.ENCODED.getPrefix();
+            }
+
+            // make sure value isn't already encoded
+            if ( ParsedInput.parseInput( rawValue ).getMode() == null )
+            {
+                try
+                {
+                    final String salt = PwmRandom.getInstance().alphaNumericString( 32 );
+                    final StoredPwData storedPwData = new StoredPwData( salt, rawValue );
+                    final String jsonData = JsonUtil.serialize( storedPwData );
+                    final String encryptedValue = SecureEngine.encryptToString( jsonData, pwmSecurityKey, PwmBlockAlgorithm.CONFIG );
+                    return Mode.ENCODED.getPrefix() + encryptedValue;
+                }
+                catch ( final Exception e )
+                {
+                    final String errorMsg = "unable to encrypt password value for setting: " + e.getMessage();
+                    final ErrorInformation errorInfo = new ErrorInformation( PwmError.CONFIG_FORMAT_ERROR, errorMsg );
+                    throw new PwmOperationalException( errorInfo );
+                }
+            }
+
+            return rawValue;
+        }
+
+        @Override
+        public String decode( final ParsedInput input, final PwmSecurityKey pwmSecurityKey ) throws PwmOperationalException
+        {
+            try
+            {
+                final String pwValueSuffix = input.getValue( );
+                final String decryptedValue = SecureEngine.decryptStringValue( pwValueSuffix, pwmSecurityKey, PwmBlockAlgorithm.CONFIG );
+                final StoredPwData storedPwData = JsonUtil.deserialize( decryptedValue, StoredPwData.class );
+                return storedPwData.getValue();
+            }
+            catch ( final Exception e )
+            {
+                final String errorMsg = "unable to decrypt password value for setting: " + e.getMessage();
+                final ErrorInformation errorInfo = new ErrorInformation( PwmError.CONFIG_FORMAT_ERROR, errorMsg );
+                LOGGER.warn( errorInfo.toDebugStr() );
+                throw new PwmOperationalException( errorInfo );
+            }
+        }
+    }
+
+    @Value
+    private static class StoredPwData implements Serializable
+    {
+        private String salt;
+        private String value;
+    }
+
+}

+ 6 - 4
server/src/main/java/password/pwm/config/value/StringArrayValue.java

@@ -22,6 +22,8 @@ package password.pwm.config.value;
 
 
 import password.pwm.config.PwmSetting;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.StoredValue;
 import password.pwm.config.StoredValue;
+import password.pwm.config.stored.StoredConfigXmlConstants;
+import password.pwm.config.stored.XmlOutputProcessData;
 import password.pwm.util.java.JsonUtil;
 import password.pwm.util.java.JsonUtil;
 import password.pwm.util.java.XmlElement;
 import password.pwm.util.java.XmlElement;
 import password.pwm.util.java.XmlFactory;
 import password.pwm.util.java.XmlFactory;
@@ -37,11 +39,11 @@ import java.util.regex.Pattern;
 
 
 public class StringArrayValue extends AbstractValue implements StoredValue
 public class StringArrayValue extends AbstractValue implements StoredValue
 {
 {
-    final List<String> values;
+    private final List<String> values;
 
 
     public StringArrayValue( final List<String> values )
     public StringArrayValue( final List<String> values )
     {
     {
-        this.values = values;
+        this.values = values == null ? Collections.emptyList() : Collections.unmodifiableList( values );
     }
     }
 
 
     public static StoredValueFactory factory( )
     public static StoredValueFactory factory( )
@@ -68,7 +70,7 @@ public class StringArrayValue extends AbstractValue implements StoredValue
 
 
             public StringArrayValue fromXmlElement( final PwmSetting pwmSetting, final XmlElement settingElement, final PwmSecurityKey key )
             public StringArrayValue fromXmlElement( final PwmSetting pwmSetting, final XmlElement settingElement, final PwmSecurityKey key )
             {
             {
-                final List<XmlElement> valueElements = settingElement.getChildren( "value" );
+                final List<XmlElement> valueElements = settingElement.getChildren( StoredConfigXmlConstants.XML_ELEMENT_VALUE );
                 final List<String> values = new ArrayList<>();
                 final List<String> values = new ArrayList<>();
                 for ( final XmlElement loopValueElement : valueElements )
                 for ( final XmlElement loopValueElement : valueElements )
                 {
                 {
@@ -80,7 +82,7 @@ public class StringArrayValue extends AbstractValue implements StoredValue
         };
         };
     }
     }
 
 
-    public List<XmlElement> toXmlValues( final String valueElementName, final PwmSecurityKey pwmSecurityKey  )
+    public List<XmlElement> toXmlValues( final String valueElementName, final XmlOutputProcessData xmlOutputProcessData )
     {
     {
         final List<XmlElement> returnList = new ArrayList<>();
         final List<XmlElement> returnList = new ArrayList<>();
         for ( final String value : this.values )
         for ( final String value : this.values )

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

@@ -23,6 +23,8 @@ package password.pwm.config.value;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.PwmSettingFlag;
 import password.pwm.config.PwmSettingFlag;
 import password.pwm.config.StoredValue;
 import password.pwm.config.StoredValue;
+import password.pwm.config.stored.StoredConfigXmlConstants;
+import password.pwm.config.stored.XmlOutputProcessData;
 import password.pwm.config.value.data.FormConfiguration;
 import password.pwm.config.value.data.FormConfiguration;
 import password.pwm.util.java.JsonUtil;
 import password.pwm.util.java.JsonUtil;
 import password.pwm.util.java.XmlElement;
 import password.pwm.util.java.XmlElement;
@@ -32,6 +34,7 @@ import password.pwm.util.secure.PwmSecurityKey;
 import java.util.Collections;
 import java.util.Collections;
 import java.util.List;
 import java.util.List;
 import java.util.Locale;
 import java.util.Locale;
+import java.util.Optional;
 import java.util.regex.Matcher;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 import java.util.regex.Pattern;
 
 
@@ -60,13 +63,14 @@ public class StringValue extends AbstractValue implements StoredValue
 
 
             public StringValue fromXmlElement( final PwmSetting pwmSetting, final XmlElement settingElement, final PwmSecurityKey key )
             public StringValue fromXmlElement( final PwmSetting pwmSetting, final XmlElement settingElement, final PwmSecurityKey key )
             {
             {
-                final XmlElement valueElement = settingElement.getChild( "value" );
-                return new StringValue( valueElement == null ? "" : valueElement.getText() );
+                final Optional<XmlElement> valueElement = settingElement.getChild( StoredConfigXmlConstants.XML_ELEMENT_VALUE );
+                final String value = valueElement.map( XmlElement::getText ).orElse( "" );
+                return new StringValue( value );
             }
             }
         };
         };
     }
     }
 
 
-    public List<XmlElement> toXmlValues( final String valueElementName, final PwmSecurityKey pwmSecurityKey  )
+    public List<XmlElement> toXmlValues( final String valueElementName, final XmlOutputProcessData xmlOutputProcessData )
     {
     {
         final XmlElement valueElement = XmlFactory.getFactory().newElement( valueElementName );
         final XmlElement valueElement = XmlFactory.getFactory().newElement( valueElementName );
         valueElement.addText( value );
         valueElement.addText( value );

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

@@ -24,7 +24,8 @@ import com.google.gson.reflect.TypeToken;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.commons.lang3.StringUtils;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.StoredValue;
 import password.pwm.config.StoredValue;
-import password.pwm.config.stored.StoredConfigurationImpl;
+import password.pwm.config.stored.StoredConfigXmlConstants;
+import password.pwm.config.stored.XmlOutputProcessData;
 import password.pwm.config.value.data.UserPermission;
 import password.pwm.config.value.data.UserPermission;
 import password.pwm.error.PwmOperationalException;
 import password.pwm.error.PwmOperationalException;
 import password.pwm.i18n.Display;
 import password.pwm.i18n.Display;
@@ -40,13 +41,13 @@ import java.util.Locale;
 
 
 public class UserPermissionValue extends AbstractValue implements StoredValue
 public class UserPermissionValue extends AbstractValue implements StoredValue
 {
 {
-    final List<UserPermission> values;
+    private final List<UserPermission> values;
 
 
     private boolean needsXmlUpdate;
     private boolean needsXmlUpdate;
 
 
     public UserPermissionValue( final List<UserPermission> values )
     public UserPermissionValue( final List<UserPermission> values )
     {
     {
-        this.values = values;
+        this.values = values == null ? Collections.emptyList() : Collections.unmodifiableList( values );
     }
     }
 
 
     public static StoredValueFactory factory( )
     public static StoredValueFactory factory( )
@@ -57,14 +58,14 @@ public class UserPermissionValue extends AbstractValue implements StoredValue
             {
             {
                 if ( input == null )
                 if ( input == null )
                 {
                 {
-                    return new UserPermissionValue( Collections.<UserPermission>emptyList() );
+                    return new UserPermissionValue( Collections.emptyList() );
                 }
                 }
                 else
                 else
                 {
                 {
                     List<UserPermission> srcList = JsonUtil.deserialize( input, new TypeToken<List<UserPermission>>()
                     List<UserPermission> srcList = JsonUtil.deserialize( input, new TypeToken<List<UserPermission>>()
                     {
                     {
                     } );
                     } );
-                    srcList = srcList == null ? Collections.<UserPermission>emptyList() : srcList;
+                    srcList = srcList == null ? Collections.emptyList() : srcList;
                     while ( srcList.contains( null ) )
                     while ( srcList.contains( null ) )
                     {
                     {
                         srcList.remove( null );
                         srcList.remove( null );
@@ -77,7 +78,7 @@ public class UserPermissionValue extends AbstractValue implements StoredValue
                     throws PwmOperationalException
                     throws PwmOperationalException
             {
             {
                 final boolean newType = "2".equals(
                 final boolean newType = "2".equals(
-                        settingElement.getAttributeValue( StoredConfigurationImpl.XML_ATTRIBUTE_SYNTAX_VERSION ) );
+                        settingElement.getAttributeValue( StoredConfigXmlConstants.XML_ATTRIBUTE_SYNTAX_VERSION ) );
                 final List<XmlElement> valueElements = settingElement.getChildren( "value" );
                 final List<XmlElement> valueElements = settingElement.getChildren( "value" );
                 final List<UserPermission> values = new ArrayList<>();
                 final List<UserPermission> values = new ArrayList<>();
                 for ( final XmlElement loopValueElement : valueElements )
                 for ( final XmlElement loopValueElement : valueElements )
@@ -92,7 +93,10 @@ public class UserPermissionValue extends AbstractValue implements StoredValue
                         }
                         }
                         else
                         else
                         {
                         {
-                            values.add( new UserPermission( UserPermission.Type.ldapQuery, null, value, null ) );
+                            values.add( UserPermission.builder()
+                                    .type( UserPermission.Type.ldapQuery )
+                                    .ldapQuery( value )
+                                    .build() );
                         }
                         }
                     }
                     }
                 }
                 }
@@ -103,7 +107,7 @@ public class UserPermissionValue extends AbstractValue implements StoredValue
         };
         };
     }
     }
 
 
-    public List<XmlElement> toXmlValues( final String valueElementName, final PwmSecurityKey pwmSecurityKey  )
+    public List<XmlElement> toXmlValues( final String valueElementName, final XmlOutputProcessData xmlOutputProcessData )
     {
     {
         final List<XmlElement> returnList = new ArrayList<>();
         final List<XmlElement> returnList = new ArrayList<>();
         for ( final UserPermission value : values )
         for ( final UserPermission value : values )
@@ -129,7 +133,7 @@ public class UserPermissionValue extends AbstractValue implements StoredValue
             {
             {
                 validateLdapSearchFilter( userPermission.getLdapQuery() );
                 validateLdapSearchFilter( userPermission.getLdapQuery() );
             }
             }
-            catch ( IllegalArgumentException e )
+            catch ( final IllegalArgumentException e )
             {
             {
                 returnObj.add( e.getMessage() + " for filter " + userPermission.getLdapQuery() );
                 returnObj.add( e.getMessage() + " for filter " + userPermission.getLdapQuery() );
             }
             }

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

@@ -39,7 +39,7 @@ public class ValueFactory
     {
     {
         try
         try
         {
         {
-            final StoredValue.StoredValueFactory factory = setting.getSyntax().getStoredValueImpl();
+            final StoredValue.StoredValueFactory factory = setting.getSyntax().getFactory();
             return factory.fromJson( input );
             return factory.fromJson( input );
         }
         }
         catch ( Exception e )
         catch ( Exception e )
@@ -59,7 +59,7 @@ public class ValueFactory
     {
     {
         try
         try
         {
         {
-            final StoredValue.StoredValueFactory factory = setting.getSyntax().getStoredValueImpl();
+            final StoredValue.StoredValueFactory factory = setting.getSyntax().getFactory();
             return factory.fromXmlElement( setting, settingElement, key );
             return factory.fromXmlElement( setting, settingElement, key );
         }
         }
         catch ( Exception e )
         catch ( Exception e )
@@ -71,7 +71,7 @@ public class ValueFactory
                 errorMsg.append( ", cause: " ).append( e.getCause().getMessage() );
                 errorMsg.append( ", cause: " ).append( e.getCause().getMessage() );
             }
             }
             LOGGER.error( errorMsg, e );
             LOGGER.error( errorMsg, e );
-            throw new IllegalStateException( "unable to read xml element '" + settingElement.getName() + "' from setting '" + setting.getKey() + "' error: " + e.getMessage() );
+            throw new IllegalStateException( "unable to read xml element '" + settingElement.getName() + "' from setting '" + setting.getKey() + "' error: " + e.getMessage(), e );
         }
         }
     }
     }
 }
 }

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

@@ -24,6 +24,8 @@ import lombok.Value;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.StoredValue;
 import password.pwm.config.StoredValue;
 import password.pwm.config.option.IdentityVerificationMethod;
 import password.pwm.config.option.IdentityVerificationMethod;
+import password.pwm.config.stored.StoredConfigXmlConstants;
+import password.pwm.config.stored.XmlOutputProcessData;
 import password.pwm.error.PwmOperationalException;
 import password.pwm.error.PwmOperationalException;
 import password.pwm.i18n.Display;
 import password.pwm.i18n.Display;
 import password.pwm.util.i18n.LocaleHelper;
 import password.pwm.util.i18n.LocaleHelper;
@@ -36,17 +38,17 @@ import password.pwm.util.secure.PwmSecurityKey;
 import java.io.Serializable;
 import java.io.Serializable;
 import java.util.ArrayList;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Collections;
-import java.util.HashMap;
 import java.util.LinkedHashMap;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.List;
 import java.util.Locale;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Map;
+import java.util.Optional;
 
 
 public class VerificationMethodValue extends AbstractValue implements StoredValue
 public class VerificationMethodValue extends AbstractValue implements StoredValue
 {
 {
     private static final PwmLogger LOGGER = PwmLogger.forClass( VerificationMethodValue.class );
     private static final PwmLogger LOGGER = PwmLogger.forClass( VerificationMethodValue.class );
 
 
-    private VerificationMethodSettings value = new VerificationMethodSettings();
+    private final VerificationMethodSettings value;
 
 
 
 
     public enum EnabledState
     public enum EnabledState
@@ -58,16 +60,18 @@ public class VerificationMethodValue extends AbstractValue implements StoredValu
 
 
     public static class VerificationMethodSettings implements Serializable
     public static class VerificationMethodSettings implements Serializable
     {
     {
-        private Map<IdentityVerificationMethod, VerificationMethodSetting> methodSettings = new HashMap<>();
-        private int minOptionalRequired;
+        private final Map<IdentityVerificationMethod, VerificationMethodSetting> methodSettings;
+        private final int minOptionalRequired;
 
 
         public VerificationMethodSettings( )
         public VerificationMethodSettings( )
         {
         {
+            methodSettings = Collections.emptyMap();
+            minOptionalRequired = 0;
         }
         }
 
 
         public VerificationMethodSettings( final Map<IdentityVerificationMethod, VerificationMethodSetting> methodSettings, final int minOptionalRequired )
         public VerificationMethodSettings( final Map<IdentityVerificationMethod, VerificationMethodSetting> methodSettings, final int minOptionalRequired )
         {
         {
-            this.methodSettings = methodSettings;
+            this.methodSettings = methodSettings == null ? Collections.emptyMap() : Collections.unmodifiableMap( methodSettings );
             this.minOptionalRequired = minOptionalRequired;
             this.minOptionalRequired = minOptionalRequired;
         }
         }
 
 
@@ -87,7 +91,7 @@ public class VerificationMethodValue extends AbstractValue implements StoredValu
     @Value
     @Value
     public static class VerificationMethodSetting implements Serializable
     public static class VerificationMethodSetting implements Serializable
     {
     {
-        private final EnabledState enabledState;
+        private EnabledState enabledState;
     }
     }
 
 
     public VerificationMethodValue( )
     public VerificationMethodValue( )
@@ -127,16 +131,20 @@ public class VerificationMethodValue extends AbstractValue implements StoredValu
             public VerificationMethodValue fromXmlElement( final PwmSetting pwmSetting, final XmlElement settingElement, final PwmSecurityKey key )
             public VerificationMethodValue fromXmlElement( final PwmSetting pwmSetting, final XmlElement settingElement, final PwmSecurityKey key )
                     throws PwmOperationalException
                     throws PwmOperationalException
             {
             {
-                final XmlElement valueElement = settingElement.getChild( "value" );
-                final String inputStr = valueElement.getText();
-                final VerificationMethodSettings settings = JsonUtil.deserialize( inputStr, VerificationMethodSettings.class );
-                return new VerificationMethodValue( settings );
+                final Optional<XmlElement> valueElement = settingElement.getChild( StoredConfigXmlConstants.XML_ELEMENT_VALUE );
+                if ( valueElement.isPresent() )
+                {
+                    final String inputStr = valueElement.get().getText();
+                    final VerificationMethodSettings settings = JsonUtil.deserialize( inputStr, VerificationMethodSettings.class );
+                    return new VerificationMethodValue( settings );
+                }
+                return  new VerificationMethodValue(  );
             }
             }
         };
         };
     }
     }
 
 
     @Override
     @Override
-    public List<XmlElement> toXmlValues( final String valueElementName, final PwmSecurityKey pwmSecurityKey  )
+    public List<XmlElement> toXmlValues( final String valueElementName, final XmlOutputProcessData xmlOutputProcessData )
     {
     {
         final XmlElement valueElement = XmlFactory.getFactory().newElement( valueElementName );
         final XmlElement valueElement = XmlFactory.getFactory().newElement( valueElementName );
         valueElement.addText( JsonUtil.serialize( value ) );
         valueElement.addText( JsonUtil.serialize( value ) );

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

@@ -22,8 +22,11 @@ package password.pwm.config.value;
 
 
 import password.pwm.config.PwmSetting;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.StoredValue;
 import password.pwm.config.StoredValue;
+import password.pwm.config.stored.StoredConfigXmlConstants;
+import password.pwm.config.stored.XmlOutputProcessData;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.JavaHelper;
+import password.pwm.util.java.StringUtil;
 import password.pwm.util.java.XmlElement;
 import password.pwm.util.java.XmlElement;
 import password.pwm.util.java.XmlFactory;
 import password.pwm.util.java.XmlFactory;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.logging.PwmLogger;
@@ -47,7 +50,7 @@ import java.util.Map;
 public class X509CertificateValue extends AbstractValue implements StoredValue
 public class X509CertificateValue extends AbstractValue implements StoredValue
 {
 {
     private static final PwmLogger LOGGER = PwmLogger.forClass( X509CertificateValue.class );
     private static final PwmLogger LOGGER = PwmLogger.forClass( X509CertificateValue.class );
-    private X509Certificate[] certificates;
+    private final X509Certificate[] certificates;
 
 
     public static StoredValueFactory factory( )
     public static StoredValueFactory factory( )
     {
     {
@@ -56,7 +59,7 @@ public class X509CertificateValue extends AbstractValue implements StoredValue
             public X509CertificateValue fromXmlElement( final PwmSetting pwmSetting, final XmlElement settingElement, final PwmSecurityKey key )
             public X509CertificateValue fromXmlElement( final PwmSetting pwmSetting, final XmlElement settingElement, final PwmSecurityKey key )
             {
             {
                 final List<X509Certificate> certificates = new ArrayList<>();
                 final List<X509Certificate> certificates = new ArrayList<>();
-                final List<XmlElement> valueElements = settingElement.getChildren( "value" );
+                final List<XmlElement> valueElements = settingElement.getChildren( StoredConfigXmlConstants.XML_ELEMENT_VALUE );
                 for ( final XmlElement loopValueElement : valueElements )
                 for ( final XmlElement loopValueElement : valueElements )
                 {
                 {
                     final String b64encodedStr = loopValueElement.getText();
                     final String b64encodedStr = loopValueElement.getText();
@@ -64,7 +67,7 @@ public class X509CertificateValue extends AbstractValue implements StoredValue
                     {
                     {
                         certificates.add( X509Utils.certificateFromBase64( b64encodedStr ) );
                         certificates.add( X509Utils.certificateFromBase64( b64encodedStr ) );
                     }
                     }
-                    catch ( Exception e )
+                    catch ( final Exception e )
                     {
                     {
                         LOGGER.error( "error decoding certificate: " + e.getMessage() );
                         LOGGER.error( "error decoding certificate: " + e.getMessage() );
                     }
                     }
@@ -99,12 +102,12 @@ public class X509CertificateValue extends AbstractValue implements StoredValue
         {
         {
             throw new NullPointerException( "certificates cannot be null" );
             throw new NullPointerException( "certificates cannot be null" );
         }
         }
-        this.certificates = certificates.toArray( new X509Certificate[ certificates.size() ] );
+        this.certificates = certificates.toArray( new X509Certificate[0] );
     }
     }
 
 
 
 
     @Override
     @Override
-    public List<XmlElement> toXmlValues( final String valueElementName, final PwmSecurityKey pwmSecurityKey  )
+    public List<XmlElement> toXmlValues( final String valueElementName, final XmlOutputProcessData xmlOutputProcessData )
     {
     {
         final List<XmlElement> returnList = new ArrayList<>();
         final List<XmlElement> returnList = new ArrayList<>();
         for ( final X509Certificate value : certificates )
         for ( final X509Certificate value : certificates )
@@ -112,9 +115,11 @@ public class X509CertificateValue extends AbstractValue implements StoredValue
             final XmlElement valueElement = XmlFactory.getFactory().newElement( valueElementName );
             final XmlElement valueElement = XmlFactory.getFactory().newElement( valueElementName );
             try
             try
             {
             {
-                valueElement.addText( X509Utils.certificateToBase64( value ) );
+                final String b64Value = X509Utils.certificateToBase64( value );
+                final String splitValue = StringUtil.insertRepeatedLineBreaks( b64Value, 80 );
+                valueElement.addText( splitValue );
             }
             }
-            catch ( CertificateEncodingException e )
+            catch ( final CertificateEncodingException e )
             {
             {
                 LOGGER.error( "error encoding certificate: " + e.getMessage() );
                 LOGGER.error( "error encoding certificate: " + e.getMessage() );
             }
             }
@@ -154,7 +159,7 @@ public class X509CertificateValue extends AbstractValue implements StoredValue
                 sb.append( " SHA1 Hash: " ).append( SecureEngine.hash( new ByteArrayInputStream( cert.getEncoded() ),
                 sb.append( " SHA1 Hash: " ).append( SecureEngine.hash( new ByteArrayInputStream( cert.getEncoded() ),
                         PwmHashAlgorithm.SHA1 ) ).append( "\n" );
                         PwmHashAlgorithm.SHA1 ) ).append( "\n" );
             }
             }
-            catch ( PwmUnrecoverableException | CertificateEncodingException e )
+            catch ( final PwmUnrecoverableException | CertificateEncodingException e )
             {
             {
                 LOGGER.warn( "error generating hash for certificate: " + e.getMessage() );
                 LOGGER.warn( "error generating hash for certificate: " + e.getMessage() );
             }
             }

+ 144 - 0
server/src/main/java/password/pwm/config/value/data/ActionConfiguration.java

@@ -123,4 +123,148 @@ public class ActionConfiguration implements Serializable
             }
             }
         }
         }
     }
     }
+
+    @Value
+    @Builder( toBuilder = true )
+    public static class ActionConfigurationOldVersion1 implements Serializable
+    {
+        public enum Type
+        {
+            webservice,
+            ldap,;
+        }
+
+        public enum WebMethod
+        {
+            delete( ActionConfiguration.WebMethod.delete ),
+            get( ActionConfiguration.WebMethod.get ),
+            post( ActionConfiguration.WebMethod.post ),
+            put( ActionConfiguration.WebMethod.put ),
+            patch( ActionConfiguration.WebMethod.patch ),;
+
+            private final ActionConfiguration.WebMethod newMethod;
+
+            WebMethod( final ActionConfiguration.WebMethod newMethod )
+            {
+                this.newMethod = newMethod;
+            }
+
+            public ActionConfiguration.WebMethod getNewMethod( )
+            {
+                return newMethod;
+            }
+        }
+
+        public enum LdapMethod
+        {
+            replace( ActionConfiguration.LdapMethod.replace ),
+            add( ActionConfiguration.LdapMethod.add ),
+            remove( ActionConfiguration.LdapMethod.remove ),;
+
+            private final ActionConfiguration.LdapMethod newMethod;
+
+            LdapMethod( final ActionConfiguration.LdapMethod newType )
+            {
+                this.newMethod = newType;
+            }
+
+            public ActionConfiguration.LdapMethod getNewMethod( )
+            {
+                return newMethod;
+            }
+        }
+
+        private String name;
+        private String description;
+
+        @Builder.Default
+        private Type type = Type.webservice;
+
+        @Builder.Default
+        private WebMethod method = WebMethod.get;
+
+        private Map<String, String> headers;
+        private String url;
+        private String body;
+        private String username;
+        private String password;
+        private List<X509Certificate> certificates;
+
+        private LdapMethod ldapMethod = LdapMethod.replace;
+        private String attributeName;
+        private String attributeValue;
+
+        public static ActionConfigurationOldVersion1 parseOldConfigString( final String value )
+        {
+            final String[] splitString = value.split( "=" );
+            final String attributeName = splitString[ 0 ];
+            final String attributeValue = splitString[ 1 ];
+            return ActionConfigurationOldVersion1.builder()
+                    .name( attributeName )
+                    .description( attributeName )
+                    .type( Type.ldap )
+                    .attributeName( attributeName )
+                    .attributeValue( attributeValue )
+                    .build();
+        }
+
+        public void validate( ) throws PwmOperationalException
+        {
+            if ( this.getName() == null || this.getName().length() < 1 )
+            {
+                throw new PwmOperationalException( new ErrorInformation( PwmError.CONFIG_FORMAT_ERROR, null, new String[]
+                        {
+                                " form field name is required",
+                        }
+                ) );
+            }
+
+            if ( this.getType() == null )
+            {
+                throw new PwmOperationalException( new ErrorInformation( PwmError.CONFIG_FORMAT_ERROR, null, new String[]
+                        {
+                                " type is required for field " + this.getName(),
+                        }
+                ) );
+            }
+
+            if ( this.getType() == Type.webservice )
+            {
+                if ( this.getMethod() == null )
+                {
+                    throw new PwmOperationalException( new ErrorInformation( PwmError.CONFIG_FORMAT_ERROR, null, new String[]
+                            {
+                                    " method for webservice action " + this.getName() + " is required",
+                            }
+                    ) );
+                }
+                if ( this.getUrl() == null || this.getUrl().length() < 1 )
+                {
+                    throw new PwmOperationalException( new ErrorInformation( PwmError.CONFIG_FORMAT_ERROR, null, new String[]
+                            {
+                                    " url for webservice action " + this.getName() + " is required",
+                            } ) );
+                }
+            }
+            else if ( this.getType() == Type.ldap )
+            {
+                if ( this.getAttributeName() == null || this.getAttributeName().length() < 1 )
+                {
+                    throw new PwmOperationalException( new ErrorInformation( PwmError.CONFIG_FORMAT_ERROR, null, new String[]
+                            {
+                                    " attribute name for ldap action " + this.getName() + " is required",
+                            }
+                    ) );
+                }
+                if ( this.getAttributeValue() == null || this.getAttributeValue().length() < 1 )
+                {
+                    throw new PwmOperationalException( new ErrorInformation( PwmError.CONFIG_FORMAT_ERROR, null, new String[]
+                            {
+                                    " attribute value for ldap action " + this.getName() + " is required",
+                            }
+                    ) );
+                }
+            }
+        }
+    }
 }
 }

+ 0 - 180
server/src/main/java/password/pwm/config/value/data/ActionConfigurationOldVersion1.java

@@ -1,180 +0,0 @@
-/*
- * Password Management Servlets (PWM)
- * http://www.pwm-project.org
- *
- * Copyright (c) 2006-2009 Novell, Inc.
- * Copyright (c) 2009-2019 The PWM Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package password.pwm.config.value.data;
-
-import lombok.Data;
-import password.pwm.error.ErrorInformation;
-import password.pwm.error.PwmError;
-import password.pwm.error.PwmOperationalException;
-import password.pwm.util.java.JsonUtil;
-
-import java.io.Serializable;
-import java.security.cert.X509Certificate;
-import java.util.List;
-import java.util.Map;
-
-@Data
-public class ActionConfigurationOldVersion1 implements Serializable
-{
-    public enum Type
-    {
-        webservice,
-        ldap,;
-    }
-
-    public enum WebMethod
-    {
-        delete( ActionConfiguration.WebMethod.delete ),
-        get( ActionConfiguration.WebMethod.get ),
-        post( ActionConfiguration.WebMethod.post ),
-        put( ActionConfiguration.WebMethod.put ),
-        patch( ActionConfiguration.WebMethod.patch ),;
-
-        private final ActionConfiguration.WebMethod newMethod;
-
-        WebMethod( final ActionConfiguration.WebMethod newMethod )
-        {
-            this.newMethod = newMethod;
-        }
-
-        public ActionConfiguration.WebMethod getNewMethod( )
-        {
-            return newMethod;
-        }
-    }
-
-    public enum LdapMethod
-    {
-        replace( ActionConfiguration.LdapMethod.replace ),
-        add( ActionConfiguration.LdapMethod.add ),
-        remove( ActionConfiguration.LdapMethod.remove ),;
-
-        private final ActionConfiguration.LdapMethod newMethod;
-
-        LdapMethod( final ActionConfiguration.LdapMethod newType )
-        {
-            this.newMethod = newType;
-        }
-
-        public ActionConfiguration.LdapMethod getNewMethod( )
-        {
-            return newMethod;
-        }
-    }
-
-    private String name;
-    private String description;
-
-    private ActionConfigurationOldVersion1.Type type = ActionConfigurationOldVersion1.Type.webservice;
-
-    private ActionConfigurationOldVersion1.WebMethod method = ActionConfigurationOldVersion1.WebMethod.get;
-    private Map<String, String> headers;
-    private String url;
-    private String body;
-    private String username;
-    private String password;
-    private List<X509Certificate> certificates;
-
-
-    private ActionConfigurationOldVersion1.LdapMethod ldapMethod = ActionConfigurationOldVersion1.LdapMethod.replace;
-    private String attributeName;
-    private String attributeValue;
-
-    public static ActionConfigurationOldVersion1 parseOldConfigString( final String value )
-    {
-        final String[] splitString = value.split( "=" );
-        final String attributeName = splitString[ 0 ];
-        final String attributeValue = splitString[ 1 ];
-        final ActionConfigurationOldVersion1 actionConfiguration = new ActionConfigurationOldVersion1();
-        actionConfiguration.name = attributeName;
-        actionConfiguration.description = attributeName;
-        actionConfiguration.type = ActionConfigurationOldVersion1.Type.ldap;
-        actionConfiguration.attributeName = attributeName;
-        actionConfiguration.attributeValue = attributeValue;
-        return actionConfiguration;
-    }
-
-    public void validate( ) throws PwmOperationalException
-    {
-        if ( this.getName() == null || this.getName().length() < 1 )
-        {
-            throw new PwmOperationalException( new ErrorInformation( PwmError.CONFIG_FORMAT_ERROR, null, new String[]
-                    {
-                            " form field name is required",
-                    }
-            ) );
-        }
-
-        if ( this.getType() == null )
-        {
-            throw new PwmOperationalException( new ErrorInformation( PwmError.CONFIG_FORMAT_ERROR, null, new String[]
-                    {
-                            " type is required for field " + this.getName(),
-                    }
-            ) );
-        }
-
-        if ( this.getType() == ActionConfigurationOldVersion1.Type.webservice )
-        {
-            if ( this.getMethod() == null )
-            {
-                throw new PwmOperationalException( new ErrorInformation( PwmError.CONFIG_FORMAT_ERROR, null, new String[]
-                        {
-                                " method for webservice action " + this.getName() + " is required",
-                        }
-                ) );
-            }
-            if ( this.getUrl() == null || this.getUrl().length() < 1 )
-            {
-                throw new PwmOperationalException( new ErrorInformation( PwmError.CONFIG_FORMAT_ERROR, null, new String[]
-                        {
-                                " url for webservice action " + this.getName() + " is required",
-                        } ) );
-            }
-        }
-        else if ( this.getType() == ActionConfigurationOldVersion1.Type.ldap )
-        {
-            if ( this.getAttributeName() == null || this.getAttributeName().length() < 1 )
-            {
-                throw new PwmOperationalException( new ErrorInformation( PwmError.CONFIG_FORMAT_ERROR, null, new String[]
-                        {
-                                " attribute name for ldap action " + this.getName() + " is required",
-                        }
-                ) );
-            }
-            if ( this.getAttributeValue() == null || this.getAttributeValue().length() < 1 )
-            {
-                throw new PwmOperationalException( new ErrorInformation( PwmError.CONFIG_FORMAT_ERROR, null, new String[]
-                        {
-                                " attribute value for ldap action " + this.getName() + " is required",
-                        }
-                ) );
-            }
-        }
-    }
-
-    public ActionConfigurationOldVersion1 copyWithNewCertificate( final List<X509Certificate> certificates )
-    {
-        final ActionConfigurationOldVersion1 clone = JsonUtil.cloneUsingJson( this, ActionConfigurationOldVersion1.class );
-        clone.certificates = certificates;
-        return clone;
-    }
-}

+ 4 - 15
server/src/main/java/password/pwm/config/value/data/ChallengeItemConfiguration.java

@@ -20,11 +20,13 @@
 
 
 package password.pwm.config.value.data;
 package password.pwm.config.value.data;
 
 
-import lombok.Getter;
+import lombok.Builder;
+import lombok.Value;
 
 
 import java.io.Serializable;
 import java.io.Serializable;
 
 
-@Getter
+@Value
+@Builder
 public class ChallengeItemConfiguration implements Serializable
 public class ChallengeItemConfiguration implements Serializable
 {
 {
     private String text;
     private String text;
@@ -37,17 +39,4 @@ public class ChallengeItemConfiguration implements Serializable
     private int points;
     private int points;
     private String setupGuide;
     private String setupGuide;
     private String regex;
     private String regex;
-
-    public ChallengeItemConfiguration(
-            final String challengeText,
-            final int minimumLength,
-            final int maximumLength,
-            final boolean adminDefined
-    )
-    {
-        this.text = challengeText;
-        this.minLength = minimumLength;
-        this.maxLength = maximumLength;
-        this.adminDefined = adminDefined;
-    }
 }
 }

+ 3 - 3
server/src/main/java/password/pwm/config/CustomLinkConfiguration.java → server/src/main/java/password/pwm/config/value/data/CustomLinkConfiguration.java

@@ -18,9 +18,9 @@
  * limitations under the License.
  * limitations under the License.
  */
  */
 
 
-package password.pwm.config;
+package password.pwm.config.value.data;
 
 
-import lombok.Getter;
+import lombok.Value;
 import password.pwm.util.i18n.LocaleHelper;
 import password.pwm.util.i18n.LocaleHelper;
 import password.pwm.util.java.JsonUtil;
 import password.pwm.util.java.JsonUtil;
 
 
@@ -32,7 +32,7 @@ import java.util.Map;
 /**
 /**
  * @author Richard A. Keil
  * @author Richard A. Keil
  */
  */
-@Getter
+@Value
 public class CustomLinkConfiguration implements Serializable
 public class CustomLinkConfiguration implements Serializable
 {
 {
 
 

+ 14 - 27
server/src/main/java/password/pwm/config/value/data/FormConfiguration.java

@@ -20,10 +20,8 @@
 
 
 package password.pwm.config.value.data;
 package password.pwm.config.value.data;
 
 
-import lombok.AccessLevel;
-import lombok.AllArgsConstructor;
 import lombok.Builder;
 import lombok.Builder;
-import lombok.Getter;
+import lombok.Value;
 import password.pwm.AppProperty;
 import password.pwm.AppProperty;
 import password.pwm.PwmConstants;
 import password.pwm.PwmConstants;
 import password.pwm.config.Configuration;
 import password.pwm.config.Configuration;
@@ -51,15 +49,10 @@ import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 import java.util.regex.Pattern;
 import java.util.regex.PatternSyntaxException;
 import java.util.regex.PatternSyntaxException;
 
 
-/**
- * @author Jason D. Rivard
- */
-@Getter
-@Builder
-@AllArgsConstructor( access = AccessLevel.PRIVATE )
+@Value
+@Builder( toBuilder = true )
 public class FormConfiguration implements Serializable
 public class FormConfiguration implements Serializable
 {
 {
-
     public enum Type
     public enum Type
     {
     {
         text,
         text,
@@ -150,21 +143,21 @@ public class FormConfiguration implements Serializable
             throw new NullPointerException( "config can not be null" );
             throw new NullPointerException( "config can not be null" );
         }
         }
 
 
-        final FormConfiguration formItem = new FormConfiguration();
+        final FormConfiguration.FormConfigurationBuilder builder = FormConfiguration.builder();
         final StringTokenizer st = new StringTokenizer( config, ":" );
         final StringTokenizer st = new StringTokenizer( config, ":" );
 
 
         // attribute name
         // attribute name
-        formItem.name = st.nextToken();
+        builder.name( st.nextToken() );
 
 
         // label
         // label
-        formItem.labels = Collections.singletonMap( "", st.nextToken() );
+        builder.labels( Collections.singletonMap( "", st.nextToken() ) );
 
 
         // type
         // type
         {
         {
             final String typeStr = st.nextToken();
             final String typeStr = st.nextToken();
             try
             try
             {
             {
-                formItem.type = Type.valueOf( typeStr.toLowerCase() );
+                builder.type( Type.valueOf( typeStr.toLowerCase() ) );
             }
             }
             catch ( IllegalArgumentException e )
             catch ( IllegalArgumentException e )
             {
             {
@@ -177,7 +170,7 @@ public class FormConfiguration implements Serializable
         //minimum length
         //minimum length
         try
         try
         {
         {
-            formItem.minimumLength = Integer.parseInt( st.nextToken() );
+            builder.minimumLength( Integer.parseInt( st.nextToken() ) );
         }
         }
         catch ( NumberFormatException e )
         catch ( NumberFormatException e )
         {
         {
@@ -189,9 +182,9 @@ public class FormConfiguration implements Serializable
         //maximum length
         //maximum length
         try
         try
         {
         {
-            formItem.maximumLength = Integer.parseInt( st.nextToken() );
+            builder.maximumLength( Integer.parseInt( st.nextToken() ) );
         }
         }
-        catch ( NumberFormatException e )
+        catch ( final NumberFormatException e )
         {
         {
             throw new PwmOperationalException( new ErrorInformation( PwmError.CONFIG_FORMAT_ERROR, null, new String[] {
             throw new PwmOperationalException( new ErrorInformation( PwmError.CONFIG_FORMAT_ERROR, null, new String[] {
                     "invalid maximum length type for form config: " + e.getMessage(),
                     "invalid maximum length type for form config: " + e.getMessage(),
@@ -199,17 +192,17 @@ public class FormConfiguration implements Serializable
         }
         }
 
 
         //required
         //required
-        formItem.required = Boolean.TRUE.toString().equalsIgnoreCase( st.nextToken() );
+        builder.required( Boolean.TRUE.toString().equalsIgnoreCase( st.nextToken() ) );
 
 
         //confirmation
         //confirmation
-        formItem.confirmationRequired = Boolean.TRUE.toString().equalsIgnoreCase( st.nextToken() );
+        builder.confirmationRequired( Boolean.TRUE.toString().equalsIgnoreCase( st.nextToken() ) );
 
 
-        return formItem;
+        return builder.build();
     }
     }
 
 
     public void validate( ) throws PwmOperationalException
     public void validate( ) throws PwmOperationalException
     {
     {
-        if ( this.getName() == null || this.getName().length() < 1 )
+        if ( StringUtil.isEmpty( this.getName() ) )
         {
         {
             throw new PwmOperationalException( new ErrorInformation( PwmError.CONFIG_FORMAT_ERROR, null, new String[] {
             throw new PwmOperationalException( new ErrorInformation( PwmError.CONFIG_FORMAT_ERROR, null, new String[] {
                     " form field name is required",
                     " form field name is required",
@@ -255,12 +248,6 @@ public class FormConfiguration implements Serializable
         }
         }
     }
     }
 
 
-    public FormConfiguration( )
-    {
-        labels = Collections.singletonMap( "", "" );
-        regexErrors = Collections.singletonMap( "", "" );
-    }
-
     public String getLabel( final Locale locale )
     public String getLabel( final Locale locale )
     {
     {
         return LocaleHelper.resolveStringKeyLocaleMap( locale, labels );
         return LocaleHelper.resolveStringKeyLocaleMap( locale, labels );

+ 2 - 4
server/src/main/java/password/pwm/config/value/data/NamedSecretData.java

@@ -20,15 +20,13 @@
 
 
 package password.pwm.config.value.data;
 package password.pwm.config.value.data;
 
 
-import lombok.AllArgsConstructor;
-import lombok.Getter;
+import lombok.Value;
 import password.pwm.util.PasswordData;
 import password.pwm.util.PasswordData;
 
 
 import java.io.Serializable;
 import java.io.Serializable;
 import java.util.List;
 import java.util.List;
 
 
-@Getter
-@AllArgsConstructor
+@Value
 public class NamedSecretData implements Serializable
 public class NamedSecretData implements Serializable
 {
 {
     private PasswordData password;
     private PasswordData password;

+ 4 - 4
server/src/main/java/password/pwm/config/value/data/RemoteWebServiceConfiguration.java

@@ -20,16 +20,16 @@
 
 
 package password.pwm.config.value.data;
 package password.pwm.config.value.data;
 
 
-import lombok.Getter;
-import lombok.Setter;
+import lombok.Builder;
+import lombok.Value;
 
 
 import java.io.Serializable;
 import java.io.Serializable;
 import java.security.cert.X509Certificate;
 import java.security.cert.X509Certificate;
 import java.util.List;
 import java.util.List;
 import java.util.Map;
 import java.util.Map;
 
 
-@Getter
-@Setter
+@Value
+@Builder( toBuilder = true )
 public class RemoteWebServiceConfiguration implements Serializable
 public class RemoteWebServiceConfiguration implements Serializable
 {
 {
 
 

+ 2 - 4
server/src/main/java/password/pwm/config/value/data/ShortcutItem.java

@@ -20,15 +20,13 @@
 
 
 package password.pwm.config.value.data;
 package password.pwm.config.value.data;
 
 
-import lombok.AllArgsConstructor;
-import lombok.Getter;
+import lombok.Value;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.logging.PwmLogger;
 
 
 import java.io.Serializable;
 import java.io.Serializable;
 import java.net.URI;
 import java.net.URI;
 
 
-@Getter
-@AllArgsConstructor
+@Value
 public class ShortcutItem implements Serializable
 public class ShortcutItem implements Serializable
 {
 {
 
 

+ 8 - 31
server/src/main/java/password/pwm/config/value/data/UserPermission.java

@@ -20,10 +20,13 @@
 
 
 package password.pwm.config.value.data;
 package password.pwm.config.value.data;
 
 
-import password.pwm.PwmConstants;
+import lombok.Builder;
+import lombok.Value;
 
 
 import java.io.Serializable;
 import java.io.Serializable;
 
 
+@Value
+@Builder
 public class UserPermission implements Serializable
 public class UserPermission implements Serializable
 {
 {
     public enum Type
     public enum Type
@@ -32,38 +35,12 @@ public class UserPermission implements Serializable
         ldapGroup,
         ldapGroup,
     }
     }
 
 
-    private String ldapProfileID = PwmConstants.PROFILE_ID_ALL;
+    @Builder.Default
+    private Type type = Type.ldapQuery;
+
+    private String ldapProfileID;
     private String ldapQuery;
     private String ldapQuery;
     private String ldapBase;
     private String ldapBase;
-    private Type type;
-
-    public UserPermission(
-            final Type type,
-            final String ldapProfileID,
-            final String ldapQuery,
-            final String ldapBase
-    )
-    {
-        this.type = type;
-        this.ldapProfileID = ldapProfileID;
-        this.ldapQuery = ldapQuery;
-        this.ldapBase = ldapBase;
-    }
-
-    public String getLdapProfileID( )
-    {
-        return ldapProfileID == null ? null : ldapProfileID.trim();
-    }
-
-    public String getLdapQuery( )
-    {
-        return ldapQuery;
-    }
-
-    public String getLdapBase( )
-    {
-        return ldapBase;
-    }
 
 
     public Type getType( )
     public Type getType( )
     {
     {

+ 29 - 0
server/src/main/java/password/pwm/error/PwmInternalException.java

@@ -0,0 +1,29 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2019 The PWM Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package password.pwm.error;
+
+public class PwmInternalException extends RuntimeException
+{
+    public PwmInternalException( final String message, final Throwable cause )
+    {
+        super( message, cause );
+    }
+}

+ 10 - 10
server/src/main/java/password/pwm/health/CertificateChecker.java

@@ -27,9 +27,8 @@ import password.pwm.config.Configuration;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.PwmSettingSyntax;
 import password.pwm.config.PwmSettingSyntax;
 import password.pwm.config.profile.LdapProfile;
 import password.pwm.config.profile.LdapProfile;
-import password.pwm.config.stored.StoredConfigReference;
-import password.pwm.config.stored.StoredConfigurationImpl;
-import password.pwm.config.stored.StoredConfigurationUtil;
+import password.pwm.config.stored.StoredConfigItemKey;
+import password.pwm.config.stored.StoredConfiguration;
 import password.pwm.config.value.ActionValue;
 import password.pwm.config.value.ActionValue;
 import password.pwm.config.value.data.ActionConfiguration;
 import password.pwm.config.value.data.ActionConfiguration;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.ErrorInformation;
@@ -46,6 +45,7 @@ import java.time.Instant;
 import java.util.ArrayList;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Collections;
 import java.util.List;
 import java.util.List;
+import java.util.Set;
 
 
 public class CertificateChecker implements HealthChecker
 public class CertificateChecker implements HealthChecker
 {
 {
@@ -92,24 +92,24 @@ public class CertificateChecker implements HealthChecker
     private static List<HealthRecord> doActionHealthCheck( final Configuration configuration ) throws PwmUnrecoverableException
     private static List<HealthRecord> doActionHealthCheck( final Configuration configuration ) throws PwmUnrecoverableException
     {
     {
 
 
-        final StoredConfigurationImpl storedConfiguration = configuration.getStoredConfiguration();
+        final StoredConfiguration storedConfiguration = configuration.getStoredConfiguration();
 
 
         final List<HealthRecord> returnList = new ArrayList<>();
         final List<HealthRecord> returnList = new ArrayList<>();
-        final List<StoredConfigReference> modifiedReferences = StoredConfigurationUtil.modifiedSettings( storedConfiguration );
-        for ( final StoredConfigReference storedConfigReference : modifiedReferences )
+        final Set<StoredConfigItemKey> modifiedReferences = storedConfiguration.modifiedItems();
+        for ( final StoredConfigItemKey storedConfigItemKey : modifiedReferences )
         {
         {
-            if ( storedConfigReference.getRecordType() == StoredConfigReference.RecordType.SETTING )
+            if ( storedConfigItemKey.getRecordType() == StoredConfigItemKey.RecordType.SETTING )
             {
             {
-                final PwmSetting pwmSetting = PwmSetting.forKey( storedConfigReference.getRecordID() );
+                final PwmSetting pwmSetting = PwmSetting.forKey( storedConfigItemKey.getRecordID() );
                 if ( pwmSetting != null && pwmSetting.getSyntax() == PwmSettingSyntax.ACTION )
                 if ( pwmSetting != null && pwmSetting.getSyntax() == PwmSettingSyntax.ACTION )
                 {
                 {
-                    final ActionValue value = ( ActionValue ) storedConfiguration.readSetting( pwmSetting, storedConfigReference.getProfileID() );
+                    final ActionValue value = ( ActionValue ) storedConfiguration.readSetting( pwmSetting, storedConfigItemKey.getProfileID() );
                     for ( final ActionConfiguration actionConfiguration : value.toNativeObject() )
                     for ( final ActionConfiguration actionConfiguration : value.toNativeObject() )
                     {
                     {
                         for ( final ActionConfiguration.WebAction webAction : actionConfiguration.getWebActions()  )
                         for ( final ActionConfiguration.WebAction webAction : actionConfiguration.getWebActions()  )
                         {
                         {
                             final List<X509Certificate> certificates = webAction.getCertificates();
                             final List<X509Certificate> certificates = webAction.getCertificates();
-                            returnList.addAll( doHealthCheck( configuration, pwmSetting, storedConfigReference.getProfileID(), certificates ) );
+                            returnList.addAll( doHealthCheck( configuration, pwmSetting, storedConfigItemKey.getProfileID(), certificates ) );
                         }
                         }
                     }
                     }
                 }
                 }

+ 138 - 153
server/src/main/java/password/pwm/health/ConfigurationChecker.java

@@ -24,21 +24,20 @@ import password.pwm.AppProperty;
 import password.pwm.PwmApplication;
 import password.pwm.PwmApplication;
 import password.pwm.PwmApplicationMode;
 import password.pwm.PwmApplicationMode;
 import password.pwm.PwmConstants;
 import password.pwm.PwmConstants;
-import password.pwm.bean.SessionLabel;
 import password.pwm.config.Configuration;
 import password.pwm.config.Configuration;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.PwmSettingSyntax;
 import password.pwm.config.PwmSettingSyntax;
 import password.pwm.config.StoredValue;
 import password.pwm.config.StoredValue;
 import password.pwm.config.option.DataStorageMethod;
 import password.pwm.config.option.DataStorageMethod;
 import password.pwm.config.option.MessageSendMethod;
 import password.pwm.config.option.MessageSendMethod;
+import password.pwm.config.profile.ActivateUserProfile;
 import password.pwm.config.profile.ForgottenPasswordProfile;
 import password.pwm.config.profile.ForgottenPasswordProfile;
 import password.pwm.config.profile.HelpdeskProfile;
 import password.pwm.config.profile.HelpdeskProfile;
 import password.pwm.config.profile.LdapProfile;
 import password.pwm.config.profile.LdapProfile;
 import password.pwm.config.profile.NewUserProfile;
 import password.pwm.config.profile.NewUserProfile;
 import password.pwm.config.profile.PwmPasswordPolicy;
 import password.pwm.config.profile.PwmPasswordPolicy;
-import password.pwm.config.profile.ActivateUserProfile;
+import password.pwm.config.stored.StoredConfigItemKey;
 import password.pwm.config.value.data.FormConfiguration;
 import password.pwm.config.value.data.FormConfiguration;
-import password.pwm.error.PwmException;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.i18n.Config;
 import password.pwm.i18n.Config;
 import password.pwm.util.PasswordData;
 import password.pwm.util.PasswordData;
@@ -49,6 +48,7 @@ import password.pwm.util.password.PasswordUtility;
 
 
 import java.net.URI;
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.net.URISyntaxException;
+import java.time.Instant;
 import java.util.ArrayList;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.Collections;
@@ -100,183 +100,173 @@ public class ConfigurationChecker implements HealthChecker
 
 
     public List<HealthRecord> doHealthCheck( final Configuration config, final Locale locale )
     public List<HealthRecord> doHealthCheck( final Configuration config, final Locale locale )
     {
     {
-
-
-        final List<HealthRecord> records = new ArrayList<>();
-
         if ( config.readSettingAsBoolean( PwmSetting.HIDE_CONFIGURATION_HEALTH_WARNINGS ) )
         if ( config.readSettingAsBoolean( PwmSetting.HIDE_CONFIGURATION_HEALTH_WARNINGS ) )
         {
         {
-            return records;
+            return Collections.emptyList();
         }
         }
 
 
-        records.addAll( allChecks( config, locale ) );
+        return Collections.unmodifiableList( allChecks( config, locale ) );
+    }
 
 
-        final String siteUrl = config.readSettingAsString( PwmSetting.PWM_SITE_URL );
-        final String separator = LocaleHelper.getLocalizedMessage( locale, Config.Display_SettingNavigationSeparator, null );
 
 
-        if ( siteUrl == null || siteUrl.isEmpty() || siteUrl.equals(
-                PwmSetting.PWM_SITE_URL.getDefaultValue( config.getTemplate() ).toNativeObject() ) )
+    private List<HealthRecord> allChecks(
+            final Configuration config,
+            final Locale locale
+    )
+    {
+        final List<HealthRecord> records = new ArrayList<>();
+        for ( final Class<? extends ConfigHealthCheck> clazz : ALL_CHECKS )
         {
         {
-            records.add(
-                    HealthRecord.forMessage( HealthMessage.Config_NoSiteURL, PwmSetting.PWM_SITE_URL.toMenuLocationDebug( null, locale ) ) );
+            final ConfigHealthCheck healthCheckClass;
+            try
+            {
+                healthCheckClass = clazz.getDeclaredConstructor().newInstance();
+                records.addAll( healthCheckClass.healthCheck( config, locale ) );
+            }
+            catch ( Exception e )
+            {
+                LOGGER.error( "unexpected error during health check operation for class " + clazz.toString() + ", error:" + e.getMessage(), e );
+            }
         }
         }
+        return records;
+    }
 
 
+    private static final List<Class<? extends ConfigHealthCheck>> ALL_CHECKS = Collections.unmodifiableList( Arrays.asList(
+            VerifyBasicConfigs.class,
+            VerifyPasswordStrengthLevels.class,
+            VerifyPasswordPolicyConfigs.class,
+            VerifyResponseLdapAttribute.class,
+            VerifyDbConfiguredIfNeeded.class,
+            VerifyIfDeprecatedSendMethodValuesUsed.class,
+            VerifyIfDeprecatedJsFormOptionUsed.class
+    ) );
 
 
-
-        if ( config.readSettingAsBoolean( PwmSetting.LDAP_ENABLE_WIRE_TRACE ) )
+    static class VerifyBasicConfigs implements ConfigHealthCheck
+    {
+        @Override
+        public List<HealthRecord> healthCheck( final Configuration config, final Locale locale )
         {
         {
-            records.add(
-                    HealthRecord.forMessage( HealthMessage.Config_LDAPWireTrace, PwmSetting.LDAP_ENABLE_WIRE_TRACE.toMenuLocationDebug( null, locale ) ) );
-        }
+            final List<HealthRecord> records = new ArrayList<>();
+            final String siteUrl = config.readSettingAsString( PwmSetting.PWM_SITE_URL );
+            final String separator = LocaleHelper.getLocalizedMessage( locale, Config.Display_SettingNavigationSeparator, null );
 
 
-        if ( Boolean.parseBoolean( config.readAppProperty( AppProperty.LDAP_PROMISCUOUS_ENABLE ) ) )
-        {
-            final String appPropertyKey = "AppProperty" + separator + AppProperty.LDAP_PROMISCUOUS_ENABLE.getKey();
-            records.add( HealthRecord.forMessage( HealthMessage.Config_PromiscuousLDAP, appPropertyKey ) );
-        }
+            if ( siteUrl == null || siteUrl.isEmpty() || siteUrl.equals(
+                    PwmSetting.PWM_SITE_URL.getDefaultValue( config.getTemplate() ).toNativeObject() ) )
+            {
+                records.add(
+                        HealthRecord.forMessage( HealthMessage.Config_NoSiteURL, PwmSetting.PWM_SITE_URL.toMenuLocationDebug( null, locale ) ) );
+            }
 
 
-        if ( config.readSettingAsBoolean( PwmSetting.DISPLAY_SHOW_DETAILED_ERRORS ) )
-        {
-            records.add( HealthRecord.forMessage( HealthMessage.Config_ShowDetailedErrors, PwmSetting.DISPLAY_SHOW_DETAILED_ERRORS.toMenuLocationDebug( null, locale ) ) );
-        }
+            if ( config.readSettingAsBoolean( PwmSetting.LDAP_ENABLE_WIRE_TRACE ) )
+            {
+                records.add(
+                        HealthRecord.forMessage( HealthMessage.Config_LDAPWireTrace, PwmSetting.LDAP_ENABLE_WIRE_TRACE.toMenuLocationDebug( null, locale ) ) );
+            }
 
 
-        for ( final LdapProfile ldapProfile : config.getLdapProfiles().values() )
-        {
-            final String testUserDN = ldapProfile.readSettingAsString( PwmSetting.LDAP_TEST_USER_DN );
-            if ( testUserDN == null || testUserDN.length() < 1 )
+            if ( Boolean.parseBoolean( config.readAppProperty( AppProperty.LDAP_PROMISCUOUS_ENABLE ) ) )
             {
             {
-                records.add( HealthRecord.forMessage( HealthMessage.Config_AddTestUser, PwmSetting.LDAP_TEST_USER_DN.toMenuLocationDebug( ldapProfile.getIdentifier(), locale ) ) );
+                final String appPropertyKey = "AppProperty" + separator + AppProperty.LDAP_PROMISCUOUS_ENABLE.getKey();
+                records.add( HealthRecord.forMessage( HealthMessage.Config_PromiscuousLDAP, appPropertyKey ) );
             }
             }
-        }
 
 
-        for ( final LdapProfile ldapProfile : config.getLdapProfiles().values() )
-        {
-            final List<String> ldapServerURLs = ldapProfile.getLdapUrls();
-            if ( ldapServerURLs != null && !ldapServerURLs.isEmpty() )
+            if ( config.readSettingAsBoolean( PwmSetting.DISPLAY_SHOW_DETAILED_ERRORS ) )
             {
             {
-                for ( final String urlStringValue : ldapServerURLs )
+                records.add( HealthRecord.forMessage( HealthMessage.Config_ShowDetailedErrors, PwmSetting.DISPLAY_SHOW_DETAILED_ERRORS.toMenuLocationDebug( null, locale ) ) );
+            }
+
+            for ( final LdapProfile ldapProfile : config.getLdapProfiles().values() )
+            {
+                final String testUserDN = ldapProfile.readSettingAsString( PwmSetting.LDAP_TEST_USER_DN );
+                if ( testUserDN == null || testUserDN.length() < 1 )
                 {
                 {
-                    try
+                    records.add( HealthRecord.forMessage(
+                            HealthMessage.Config_AddTestUser,
+                            PwmSetting.LDAP_TEST_USER_DN.toMenuLocationDebug( ldapProfile.getIdentifier(), locale )
+                    ) );
+                }
+            }
+
+            for ( final LdapProfile ldapProfile : config.getLdapProfiles().values() )
+            {
+                final List<String> ldapServerURLs = ldapProfile.getLdapUrls();
+                if ( ldapServerURLs != null && !ldapServerURLs.isEmpty() )
+                {
+                    for ( final String urlStringValue : ldapServerURLs )
                     {
                     {
-                        final URI url = new URI( urlStringValue );
-                        final boolean secure = "ldaps".equalsIgnoreCase( url.getScheme() );
-                        if ( !secure )
+                        try
                         {
                         {
-                            records.add( HealthRecord.forMessage(
-                                    HealthMessage.Config_LDAPUnsecure,
-                                    PwmSetting.LDAP_SERVER_URLS.toMenuLocationDebug( ldapProfile.getIdentifier(), locale )
+                            final URI url = new URI( urlStringValue );
+                            final boolean secure = "ldaps".equalsIgnoreCase( url.getScheme() );
+                            if ( !secure )
+                            {
+                                records.add( HealthRecord.forMessage(
+                                        HealthMessage.Config_LDAPUnsecure,
+                                        PwmSetting.LDAP_SERVER_URLS.toMenuLocationDebug( ldapProfile.getIdentifier(), locale )
+                                ) );
+                            }
+                        }
+                        catch ( URISyntaxException e )
+                        {
+                            records.add( HealthRecord.forMessage( HealthMessage.Config_ParseError,
+                                    e.getMessage(),
+                                    PwmSetting.LDAP_SERVER_URLS.toMenuLocationDebug( ldapProfile.getIdentifier(), locale ),
+                                    urlStringValue
                             ) );
                             ) );
                         }
                         }
                     }
                     }
-                    catch ( URISyntaxException e )
-                    {
-                        records.add( HealthRecord.forMessage( HealthMessage.Config_ParseError,
-                                e.getMessage(),
-                                PwmSetting.LDAP_SERVER_URLS.toMenuLocationDebug( ldapProfile.getIdentifier(), locale ),
-                                urlStringValue
-                        ) );
-                    }
                 }
                 }
             }
             }
-        }
-
-        records.addAll( passwordStrengthChecks( config, locale ) );
 
 
-        return records;
+            return records;
+        }
     }
     }
 
 
-    private List<HealthRecord> passwordStrengthChecks(
-            final Configuration config,
-            final Locale locale
-    )
+    static class VerifyPasswordStrengthLevels implements ConfigHealthCheck
     {
     {
-        final List<HealthRecord> records = new ArrayList<>(  );
-
-        for ( final PwmSetting setting : PwmSetting.values() )
+        @Override
+        public List<HealthRecord> healthCheck( final Configuration config, final Locale locale )
         {
         {
-            if (
-                    setting.getSyntax() == PwmSettingSyntax.PASSWORD
-                            && !setting.getCategory().hasProfiles()
-                            && !config.isDefaultValue( setting )
-            )
+            final List<HealthRecord> records = new ArrayList<>();
+
+            try
             {
             {
-                try
+                for ( final StoredConfigItemKey key : config.getStoredConfiguration().modifiedItems() )
                 {
                 {
-                    final PasswordData passwordValue = config.readSettingAsPassword( setting );
-                    final String stringValue = passwordValue.getStringValue();
-                    if ( !StringUtil.isEmpty( stringValue ) )
+                    final Instant startTime = Instant.now();
+                    if ( key.getRecordType() == StoredConfigItemKey.RecordType.SETTING )
                     {
                     {
-                        final int strength = PasswordUtility.judgePasswordStrength( config, stringValue );
-                        if ( strength < 50 )
+                        final PwmSetting pwmSetting = key.toPwmSetting();
+                        if ( pwmSetting.getSyntax() == PwmSettingSyntax.PASSWORD )
                         {
                         {
-                            records.add( HealthRecord.forMessage( HealthMessage.Config_WeakPassword,
-                                    setting.toMenuLocationDebug( null, locale ), String.valueOf( strength ) ) );
+                            final StoredValue storedValue = config.getStoredConfiguration().readSetting( pwmSetting, key.getProfileID() );
+                            final PasswordData passwordValue = ( PasswordData ) storedValue.toNativeObject();
+                            if ( passwordValue != null )
+                            {
+                                final String stringValue = passwordValue.getStringValue();
+
+                                if ( !StringUtil.isEmpty( stringValue ) )
+                                {
+                                    final int strength = PasswordUtility.judgePasswordStrength( config, stringValue );
+                                    if ( strength < 50 )
+                                    {
+                                        records.add( HealthRecord.forMessage( HealthMessage.Config_WeakPassword,
+                                                pwmSetting.toMenuLocationDebug( key.getProfileID(), locale ), String.valueOf( strength ) ) );
+                                    }
+                                }
+                            }
                         }
                         }
                     }
                     }
                 }
                 }
-                catch ( Exception e )
-                {
-                    LOGGER.error( SessionLabel.HEALTH_SESSION_LABEL, "error while inspecting setting "
-                            + setting.toMenuLocationDebug( null, locale ) + ", error: " + e.getMessage() );
-                }
             }
             }
-        }
-        for ( final LdapProfile profile : config.getLdapProfiles().values() )
-        {
-            final PwmSetting setting = PwmSetting.LDAP_PROXY_USER_PASSWORD;
-            try
-            {
-                final PasswordData passwordValue = profile.readSettingAsPassword( setting );
-                final int strength = PasswordUtility.judgePasswordStrength( config, passwordValue == null ? null : passwordValue.getStringValue() );
-                if ( strength < 50 )
-                {
-                    records.add( HealthRecord.forMessage( HealthMessage.Config_WeakPassword,
-                            setting.toMenuLocationDebug( profile.getIdentifier(), locale ),
-                            String.valueOf( strength ) ) );
-                }
-            }
-            catch ( PwmException e )
+            catch ( PwmUnrecoverableException e )
             {
             {
-                LOGGER.error(
-                        SessionLabel.HEALTH_SESSION_LABEL,
-                        "error while inspecting setting " + setting.toMenuLocationDebug( profile.getIdentifier(), locale )
-                                + ", error: " + e.getMessage() );
+                LOGGER.error( "unexpected error examining password strength of configuration: " );
             }
             }
-        }
 
 
-        return records;
-    }
-
-    private List<HealthRecord> allChecks(
-            final Configuration config,
-            final Locale locale
-    )
-    {
-        final List<HealthRecord> records = new ArrayList<>();
-        for ( final Class<? extends ConfigHealthCheck> clazz : ALL_CHECKS )
-        {
-            final ConfigHealthCheck healthCheckClass;
-            try
-            {
-                healthCheckClass = clazz.newInstance();
-                records.addAll( healthCheckClass.healthCheck( config, locale ) );
-            }
-            catch ( Exception e )
-            {
-                LOGGER.error( "unexpected error during health check operation for class " + clazz.toString() + ", error:" + e.getMessage(), e );
-            }
+            return Collections.unmodifiableList( records );
         }
         }
-        return records;
     }
     }
 
 
-    private static final List<Class<? extends ConfigHealthCheck>> ALL_CHECKS = Collections.unmodifiableList( Arrays.asList(
-            VerifyPasswordPolicyConfigs.class,
-            VerifyResponseLdapAttribute.class,
-            VerifyDbConfiguredIfNeeded.class,
-            VerifyIfDeprecatedSendMethodValuesUsed.class,
-            VerifyIfDeprecatedJsFormOptionUsed.class
-    ) );
-
     static class VerifyResponseLdapAttribute implements ConfigHealthCheck
     static class VerifyResponseLdapAttribute implements ConfigHealthCheck
     {
     {
         @Override
         @Override
@@ -403,30 +393,25 @@ public class ConfigurationChecker implements HealthChecker
                 {
                 {
                     if ( loopSetting.getCategory().hasProfiles() )
                     if ( loopSetting.getCategory().hasProfiles() )
                     {
                     {
-                        try
+
+                        final List<String> profiles = config.getStoredConfiguration().profilesForSetting( loopSetting );
+                        for ( final String profile : profiles )
                         {
                         {
-                            final List<String> profiles = config.getStoredConfiguration().profilesForSetting( loopSetting );
-                            for ( final String profile : profiles )
+                            final StoredValue storedValue = config.getStoredConfiguration().readSetting( loopSetting, profile );
+                            final List<FormConfiguration> forms = (List<FormConfiguration>) storedValue.toNativeObject();
+                            for ( final FormConfiguration form : forms )
                             {
                             {
-                                final StoredValue storedValue = config.getStoredConfiguration().readSetting( loopSetting, profile );
-                                final List<FormConfiguration> forms = (List<FormConfiguration>) storedValue.toNativeObject();
-                                for ( final FormConfiguration form : forms )
+                                if ( !StringUtil.isEmpty( form.getJavascript() ) )
                                 {
                                 {
-                                    if ( !StringUtil.isEmpty( form.getJavascript() ) )
-                                    {
-                                        records.add( HealthRecord.forMessage(
-                                                HealthMessage.Config_DeprecatedJSForm,
-                                                loopSetting.toMenuLocationDebug( profile, locale ),
-                                                PwmSetting.DISPLAY_CUSTOM_JAVASCRIPT.toMenuLocationDebug( null, locale )
-                                        ) );
-                                    }
+                                    records.add( HealthRecord.forMessage(
+                                            HealthMessage.Config_DeprecatedJSForm,
+                                            loopSetting.toMenuLocationDebug( profile, locale ),
+                                            PwmSetting.DISPLAY_CUSTOM_JAVASCRIPT.toMenuLocationDebug( null, locale )
+                                    ) );
                                 }
                                 }
                             }
                             }
                         }
                         }
-                        catch ( PwmUnrecoverableException e )
-                        {
-                            LOGGER.error( "unexpected error examining profiles for deprecated form  js option check: " + e.getMessage() );
-                        }
+
                     }
                     }
                     else
                     else
                     {
                     {

+ 5 - 5
server/src/main/java/password/pwm/health/HealthRecord.java

@@ -183,10 +183,10 @@ public class HealthRecord implements Serializable, Comparable<HealthRecord>
     {
     {
         final List<password.pwm.ws.server.rest.bean.HealthRecord> healthRecordBeans = password.pwm.ws.server.rest.bean.HealthRecord.fromHealthRecords(
         final List<password.pwm.ws.server.rest.bean.HealthRecord> healthRecordBeans = password.pwm.ws.server.rest.bean.HealthRecord.fromHealthRecords(
                 profileRecords, locale, configuration );
                 profileRecords, locale, configuration );
-        final HealthData healthData = new HealthData();
-        healthData.timestamp = Instant.now();
-        healthData.overall = HealthMonitor.getMostSevereHealthStatus( profileRecords ).toString();
-        healthData.records = healthRecordBeans;
-        return healthData;
+        return HealthData.builder()
+                .timestamp( Instant.now() )
+                .overall( HealthMonitor.getMostSevereHealthStatus( profileRecords ).toString() )
+                .records( healthRecordBeans )
+                .build();
     }
     }
 }
 }

+ 55 - 53
server/src/main/java/password/pwm/http/ContextManager.java

@@ -32,7 +32,8 @@ import password.pwm.config.StoredValue;
 import password.pwm.config.profile.LdapProfile;
 import password.pwm.config.profile.LdapProfile;
 import password.pwm.config.stored.ConfigurationProperty;
 import password.pwm.config.stored.ConfigurationProperty;
 import password.pwm.config.stored.ConfigurationReader;
 import password.pwm.config.stored.ConfigurationReader;
-import password.pwm.config.stored.StoredConfigurationImpl;
+import password.pwm.config.stored.StoredConfiguration;
+import password.pwm.config.stored.StoredConfigurationModifier;
 import password.pwm.config.value.X509CertificateValue;
 import password.pwm.config.value.X509CertificateValue;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmError;
@@ -42,7 +43,9 @@ import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.util.PropertyConfigurationImporter;
 import password.pwm.util.PropertyConfigurationImporter;
 import password.pwm.util.PwmScheduler;
 import password.pwm.util.PwmScheduler;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.JavaHelper;
+import password.pwm.util.java.StringUtil;
 import password.pwm.util.java.TimeDuration;
 import password.pwm.util.java.TimeDuration;
+import password.pwm.util.logging.PwmLogManager;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.secure.X509Utils;
 import password.pwm.util.secure.X509Utils;
 
 
@@ -59,10 +62,12 @@ import java.nio.file.Path;
 import java.security.cert.X509Certificate;
 import java.security.cert.X509Certificate;
 import java.time.Instant;
 import java.time.Instant;
 import java.util.Collection;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.Date;
 import java.util.Date;
 import java.util.List;
 import java.util.List;
 import java.util.Locale;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Map;
+import java.util.Optional;
 import java.util.Set;
 import java.util.Set;
 import java.util.concurrent.Executors;
 import java.util.concurrent.Executors;
 import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.ScheduledExecutorService;
@@ -75,6 +80,8 @@ public class ContextManager implements Serializable
     private static final PwmLogger LOGGER = PwmLogger.forClass( ContextManager.class );
     private static final PwmLogger LOGGER = PwmLogger.forClass( ContextManager.class );
     private static final SessionLabel SESSION_LABEL = SessionLabel.CONTEXT_SESSION_LABEL;
     private static final SessionLabel SESSION_LABEL = SessionLabel.CONTEXT_SESSION_LABEL;
 
 
+    private static final TimeDuration RESTART_DELAY = TimeDuration.of( 5, TimeDuration.Unit.SECONDS );
+
     private transient ServletContext servletContext;
     private transient ServletContext servletContext;
     private transient ScheduledExecutorService taskMaster;
     private transient ScheduledExecutorService taskMaster;
 
 
@@ -225,7 +232,6 @@ public class ContextManager implements Serializable
             configurationFile = locateConfigurationFile( applicationPath, PwmConstants.DEFAULT_CONFIG_FILE_FILENAME );
             configurationFile = locateConfigurationFile( applicationPath, PwmConstants.DEFAULT_CONFIG_FILE_FILENAME );
 
 
             configReader = new ConfigurationReader( configurationFile );
             configReader = new ConfigurationReader( configurationFile );
-            configReader.getStoredConfiguration().lock();
             configuration = configReader.getConfiguration();
             configuration = configReader.getConfiguration();
 
 
             mode = startupErrorInformation == null ? configReader.getConfigMode() : PwmApplicationMode.ERROR;
             mode = startupErrorInformation == null ? configReader.getConfigMode() : PwmApplicationMode.ERROR;
@@ -251,7 +257,13 @@ public class ContextManager implements Serializable
         }
         }
 
 
         final Collection<PwmEnvironment.ApplicationFlag> applicationFlags = parameterReader.readApplicationFlags();
         final Collection<PwmEnvironment.ApplicationFlag> applicationFlags = parameterReader.readApplicationFlags();
-        final Map<PwmEnvironment.ApplicationParameter, String> applicationParams = parameterReader.readApplicationParams();
+        final Map<PwmEnvironment.ApplicationParameter, String> applicationParams = parameterReader.readApplicationParams( applicationPath );
+
+        if ( applicationParams != null && applicationParams.containsKey( PwmEnvironment.ApplicationParameter.InitConsoleLogLevel ) )
+        {
+            final String logLevel = applicationParams.get( PwmEnvironment.ApplicationParameter.InitConsoleLogLevel );
+            PwmLogManager.preInitConsoleLogLevel( logLevel );
+        }
 
 
         try
         try
         {
         {
@@ -293,8 +305,6 @@ public class ContextManager implements Serializable
                 taskMaster.scheduleWithFixedDelay( new ConfigFileWatcher(), fileScanFrequencyMs, fileScanFrequencyMs, TimeUnit.MILLISECONDS );
                 taskMaster.scheduleWithFixedDelay( new ConfigFileWatcher(), fileScanFrequencyMs, fileScanFrequencyMs, TimeUnit.MILLISECONDS );
             }
             }
 
 
-            checkConfigForSaveOnRestart( configReader, pwmApplication );
-
             checkConfigForAutoImportLdapCerts( configReader );
             checkConfigForAutoImportLdapCerts( configReader );
         }
         }
 
 
@@ -306,38 +316,6 @@ public class ContextManager implements Serializable
         LOGGER.trace( SESSION_LABEL, () -> "initialization complete (" + TimeDuration.compactFromCurrent( startTime ) + ")" );
         LOGGER.trace( SESSION_LABEL, () -> "initialization complete (" + TimeDuration.compactFromCurrent( startTime ) + ")" );
     }
     }
 
 
-    private void checkConfigForSaveOnRestart(
-            final ConfigurationReader configReader,
-            final PwmApplication pwmApplication
-    )
-    {
-        if ( configReader == null || configReader.getStoredConfiguration() == null )
-        {
-            return;
-        }
-
-        if ( !Boolean.parseBoolean( configReader.getStoredConfiguration().readConfigProperty( ConfigurationProperty.CONFIG_ON_START ) ) )
-        {
-            return;
-        }
-
-        LOGGER.warn( SESSION_LABEL, "configuration file contains property \""
-                + ConfigurationProperty.CONFIG_ON_START.getKey() + "\"=true, will save configuration and set property to false." );
-
-        try
-        {
-            final StoredConfigurationImpl newConfig = StoredConfigurationImpl.copy( configReader.getStoredConfiguration() );
-            newConfig.writeConfigProperty( ConfigurationProperty.CONFIG_ON_START, "false" );
-            configReader.saveConfiguration( newConfig, pwmApplication, SESSION_LABEL );
-            requestPwmApplicationRestart();
-        }
-        catch ( Exception e )
-        {
-            LOGGER.error( SESSION_LABEL, "error while saving configuration file commanded by property \""
-                    + ConfigurationProperty.CONFIG_ON_START + "\"=true, error: " + e.getMessage() );
-        }
-    }
-
     private void checkConfigForAutoImportLdapCerts(
     private void checkConfigForAutoImportLdapCerts(
             final ConfigurationReader configReader
             final ConfigurationReader configReader
     )
     )
@@ -347,15 +325,17 @@ public class ContextManager implements Serializable
             return;
             return;
         }
         }
 
 
-        if ( !Boolean.parseBoolean( configReader.getStoredConfiguration().readConfigProperty( ConfigurationProperty.IMPORT_LDAP_CERTIFICATES ) ) )
         {
         {
-            return;
+            final Optional<String> importLdapCerts = configReader.getStoredConfiguration().readConfigProperty( ConfigurationProperty.IMPORT_LDAP_CERTIFICATES );
+            if ( !importLdapCerts.isPresent() || !Boolean.parseBoolean( importLdapCerts.get() ) )
+            {
+                return;
+            }
         }
         }
 
 
         LOGGER.info( SESSION_LABEL, () -> "configuration file contains property \"" + ConfigurationProperty.IMPORT_LDAP_CERTIFICATES.getKey()
         LOGGER.info( SESSION_LABEL, () -> "configuration file contains property \"" + ConfigurationProperty.IMPORT_LDAP_CERTIFICATES.getKey()
-                + "\"=true, will import attempt ldap certificate import every 5 seconds until successful" );
-        final long secondsDelay = 5;
-        taskMaster.scheduleWithFixedDelay( new AutoImportLdapCertJob(), secondsDelay, secondsDelay, TimeUnit.SECONDS );
+                + "\"=true, will import attempt ldap certificate import every " + RESTART_DELAY.asLongString() + " until successful" );
+        taskMaster.scheduleWithFixedDelay( new AutoImportLdapCertJob(), RESTART_DELAY.asMillis(), RESTART_DELAY.asMillis(), TimeUnit.MILLISECONDS );
     }
     }
 
 
     private void handleStartupError( final String msgPrefix, final Throwable throwable )
     private void handleStartupError( final String msgPrefix, final Throwable throwable )
@@ -459,7 +439,7 @@ public class ContextManager implements Serializable
                     try
                     try
                     {
                     {
                         final PropertyConfigurationImporter importer = new PropertyConfigurationImporter();
                         final PropertyConfigurationImporter importer = new PropertyConfigurationImporter();
-                        final StoredConfigurationImpl storedConfiguration = importer.readConfiguration( new FileInputStream( silentPropertiesFile ) );
+                        final StoredConfiguration storedConfiguration = importer.readConfiguration( new FileInputStream( silentPropertiesFile ) );
                         configReader.saveConfiguration( storedConfiguration, pwmApplication, SESSION_LABEL );
                         configReader.saveConfiguration( storedConfiguration, pwmApplication, SESSION_LABEL );
                         LOGGER.info( SESSION_LABEL, () -> "file " + silentPropertiesFile.getAbsolutePath() + " has been successfully imported and saved as configuration file" );
                         LOGGER.info( SESSION_LABEL, () -> "file " + silentPropertiesFile.getAbsolutePath() + " has been successfully imported and saved as configuration file" );
                         requestPwmApplicationRestart();
                         requestPwmApplicationRestart();
@@ -674,17 +654,38 @@ public class ContextManager implements Serializable
             return PwmEnvironment.ParseHelper.readApplicationFlagsFromSystem( contextPath );
             return PwmEnvironment.ParseHelper.readApplicationFlagsFromSystem( contextPath );
         }
         }
 
 
-        Map<PwmEnvironment.ApplicationParameter, String> readApplicationParams( )
+        Map<PwmEnvironment.ApplicationParameter, String> readApplicationParams( final File applicationPath  )
         {
         {
-            final String contextAppParamsValue = readEnvironmentParameter( PwmEnvironment.EnvironmentParameter.applicationParamFile );
+            // attempt to read app params finle from specified env param file value
+            {
+                final String contextAppParamsValue = readEnvironmentParameter( PwmEnvironment.EnvironmentParameter.applicationParamFile );
+                if ( !StringUtil.isEmpty( contextAppParamsValue ) )
+                {
+                    return PwmEnvironment.ParseHelper.readAppParametersFromPath( contextAppParamsValue );
+                }
+            }
 
 
-            if ( contextAppParamsValue != null && !contextAppParamsValue.isEmpty() )
+            // attempt to read app params file from specified system file value
             {
             {
-                return PwmEnvironment.ParseHelper.parseApplicationParamValueParameter( contextAppParamsValue );
+                final String contextPath = servletContext.getContextPath().replace( "/", "" );
+                final Map<PwmEnvironment.ApplicationParameter, String> results = PwmEnvironment.ParseHelper.readApplicationParmsFromSystem( contextPath );
+                if ( !results.isEmpty() )
+                {
+                    return results;
+                }
             }
             }
 
 
-            final String contextPath = servletContext.getContextPath().replace( "/", "" );
-            return PwmEnvironment.ParseHelper.readApplicationParmsFromSystem( contextPath );
+            // attempt to read via application.properties in applicationPath
+            if ( applicationPath != null && applicationPath.exists() )
+            {
+                final File appPropertiesFile = new File( applicationPath.getPath() + File.separator + "application.properties" );
+                if ( appPropertiesFile.exists() )
+                {
+                    return PwmEnvironment.ParseHelper.readAppParametersFromPath( appPropertiesFile.getPath() );
+                }
+            }
+
+            return Collections.emptyMap();
         }
         }
 
 
 
 
@@ -736,7 +737,7 @@ public class ContextManager implements Serializable
             LOGGER.trace( SESSION_LABEL, () -> "beginning auto-import ldap cert due to config property '"
             LOGGER.trace( SESSION_LABEL, () -> "beginning auto-import ldap cert due to config property '"
                     + ConfigurationProperty.IMPORT_LDAP_CERTIFICATES.getKey() + "'" );
                     + ConfigurationProperty.IMPORT_LDAP_CERTIFICATES.getKey() + "'" );
             final Configuration configuration = new Configuration( configReader.getStoredConfiguration() );
             final Configuration configuration = new Configuration( configReader.getStoredConfiguration() );
-            final StoredConfigurationImpl newStoredConfig = StoredConfigurationImpl.copy( configReader.getStoredConfiguration() );
+            final StoredConfigurationModifier modifiedConfig = StoredConfigurationModifier.newModifier( configReader.getStoredConfiguration() );
 
 
             int importedCerts = 0;
             int importedCerts = 0;
             for ( final LdapProfile ldapProfile : configuration.getLdapProfiles().values() )
             for ( final LdapProfile ldapProfile : configuration.getLdapProfiles().values() )
@@ -753,7 +754,8 @@ public class ContextManager implements Serializable
                             LOGGER.trace( SESSION_LABEL, () -> "imported cert: " + X509Utils.makeDebugText( cert ) );
                             LOGGER.trace( SESSION_LABEL, () -> "imported cert: " + X509Utils.makeDebugText( cert ) );
                         }
                         }
                         final StoredValue storedValue = new X509CertificateValue( certs );
                         final StoredValue storedValue = new X509CertificateValue( certs );
-                        newStoredConfig.writeSetting( PwmSetting.LDAP_SERVER_CERTS, ldapProfile.getIdentifier(), storedValue, null );
+
+                        modifiedConfig.writeSetting( PwmSetting.LDAP_SERVER_CERTS, ldapProfile.getIdentifier(), storedValue, null );
                     }
                     }
 
 
                 }
                 }
@@ -765,8 +767,8 @@ public class ContextManager implements Serializable
                 LOGGER.trace( SESSION_LABEL, () -> "completed auto-import ldap cert due to config property '"
                 LOGGER.trace( SESSION_LABEL, () -> "completed auto-import ldap cert due to config property '"
                         + ConfigurationProperty.IMPORT_LDAP_CERTIFICATES.getKey() + "'"
                         + ConfigurationProperty.IMPORT_LDAP_CERTIFICATES.getKey() + "'"
                         + ", imported " + totalImportedCerts + " certificates" );
                         + ", imported " + totalImportedCerts + " certificates" );
-                newStoredConfig.writeConfigProperty( ConfigurationProperty.IMPORT_LDAP_CERTIFICATES, "false" );
-                configReader.saveConfiguration( newStoredConfig, pwmApplication, SESSION_LABEL );
+                modifiedConfig.writeConfigProperty( ConfigurationProperty.IMPORT_LDAP_CERTIFICATES, "false" );
+                configReader.saveConfiguration( modifiedConfig.newStoredConfiguration(), pwmApplication, SESSION_LABEL );
                 requestPwmApplicationRestart();
                 requestPwmApplicationRestart();
             }
             }
             else
             else

+ 4 - 44
server/src/main/java/password/pwm/http/bean/ConfigManagerBean.java

@@ -20,18 +20,19 @@
 
 
 package password.pwm.http.bean;
 package password.pwm.http.bean;
 
 
+import lombok.Data;
 import password.pwm.config.option.SessionBeanMode;
 import password.pwm.config.option.SessionBeanMode;
-import password.pwm.config.stored.StoredConfigurationImpl;
+import password.pwm.config.stored.StoredConfiguration;
 
 
 import java.util.Collections;
 import java.util.Collections;
 import java.util.Set;
 import java.util.Set;
 
 
+@Data
 public class ConfigManagerBean extends PwmSessionBean
 public class ConfigManagerBean extends PwmSessionBean
 {
 {
-    private transient StoredConfigurationImpl storedConfiguration;
+    private transient StoredConfiguration storedConfiguration;
     private boolean passwordVerified;
     private boolean passwordVerified;
     private boolean configUnlockedWarningShown;
     private boolean configUnlockedWarningShown;
-
     private String prePasswordEntryUrl;
     private String prePasswordEntryUrl;
 
 
     public ConfigManagerBean( )
     public ConfigManagerBean( )
@@ -43,47 +44,6 @@ public class ConfigManagerBean extends PwmSessionBean
         return Type.AUTHENTICATED;
         return Type.AUTHENTICATED;
     }
     }
 
 
-
-    public StoredConfigurationImpl getStoredConfiguration( )
-    {
-        return storedConfiguration;
-    }
-
-    public void setConfiguration( final StoredConfigurationImpl storedConfiguration )
-    {
-        this.storedConfiguration = storedConfiguration;
-    }
-
-    public boolean isPasswordVerified( )
-    {
-        return passwordVerified;
-    }
-
-    public void setPasswordVerified( final boolean passwordVerified )
-    {
-        this.passwordVerified = passwordVerified;
-    }
-
-    public String getPrePasswordEntryUrl( )
-    {
-        return prePasswordEntryUrl;
-    }
-
-    public void setPrePasswordEntryUrl( final String prePasswordEntryUrl )
-    {
-        this.prePasswordEntryUrl = prePasswordEntryUrl;
-    }
-
-    public boolean isConfigUnlockedWarningShown( )
-    {
-        return configUnlockedWarningShown;
-    }
-
-    public void setConfigUnlockedWarningShown( final boolean configUnlockedWarningShown )
-    {
-        this.configUnlockedWarningShown = configUnlockedWarningShown;
-    }
-
     @Override
     @Override
     public Set<SessionBeanMode> supportedModes( )
     public Set<SessionBeanMode> supportedModes( )
     {
     {

+ 5 - 4
server/src/main/java/password/pwm/http/filter/ConfigAccessFilter.java

@@ -24,7 +24,8 @@ import password.pwm.AppProperty;
 import password.pwm.Permission;
 import password.pwm.Permission;
 import password.pwm.PwmApplicationMode;
 import password.pwm.PwmApplicationMode;
 import password.pwm.config.stored.ConfigurationReader;
 import password.pwm.config.stored.ConfigurationReader;
-import password.pwm.config.stored.StoredConfigurationImpl;
+import password.pwm.config.stored.StoredConfiguration;
+import password.pwm.config.stored.StoredConfigurationUtil;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmException;
 import password.pwm.error.PwmException;
@@ -97,7 +98,7 @@ public class ConfigAccessFilter extends AbstractPwmFilter
             throws IOException, PwmUnrecoverableException, ServletException
             throws IOException, PwmUnrecoverableException, ServletException
     {
     {
         final ConfigurationReader runningConfigReader = ContextManager.getContextManager( pwmRequest.getHttpServletRequest().getSession() ).getConfigReader();
         final ConfigurationReader runningConfigReader = ContextManager.getContextManager( pwmRequest.getHttpServletRequest().getSession() ).getConfigReader();
-        final StoredConfigurationImpl storedConfig = runningConfigReader.getStoredConfiguration();
+        final StoredConfiguration storedConfig = runningConfigReader.getStoredConfiguration();
 
 
         checkPreconditions( pwmRequest, storedConfig );
         checkPreconditions( pwmRequest, storedConfig );
 
 
@@ -117,12 +118,12 @@ public class ConfigAccessFilter extends AbstractPwmFilter
 
 
     private static void checkPreconditions(
     private static void checkPreconditions(
             final PwmRequest pwmRequest,
             final PwmRequest pwmRequest,
-            final StoredConfigurationImpl storedConfig
+            final StoredConfiguration storedConfig
     )
     )
             throws PwmUnrecoverableException
             throws PwmUnrecoverableException
     {
     {
 
 
-        if ( !storedConfig.hasPassword() )
+        if ( !StoredConfigurationUtil.hasPassword( storedConfig ) )
         {
         {
             final String errorMsg = "config file does not have a configuration password";
             final String errorMsg = "config file does not have a configuration password";
             final ErrorInformation errorInformation = new ErrorInformation( PwmError.CONFIG_FORMAT_ERROR, errorMsg, new String[]
             final ErrorInformation errorInformation = new ErrorInformation( PwmError.CONFIG_FORMAT_ERROR, errorMsg, new String[]

+ 0 - 7
server/src/main/java/password/pwm/http/servlet/ClientApiServlet.java

@@ -32,7 +32,6 @@ import password.pwm.config.PwmSetting;
 import password.pwm.config.option.SelectableContextMode;
 import password.pwm.config.option.SelectableContextMode;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmError;
-import password.pwm.error.PwmException;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.http.HttpHeader;
 import password.pwm.http.HttpHeader;
 import password.pwm.http.HttpMethod;
 import password.pwm.http.HttpMethod;
@@ -223,12 +222,6 @@ public class ClientApiServlet extends ControlledPwmServlet
             final RestResultBean restResultBean = RestResultBean.withData( jsonOutput );
             final RestResultBean restResultBean = RestResultBean.withData( jsonOutput );
             pwmRequest.outputJsonResult( restResultBean );
             pwmRequest.outputJsonResult( restResultBean );
         }
         }
-        catch ( PwmException e )
-        {
-            final ErrorInformation errorInformation = e.getErrorInformation();
-            LOGGER.debug( pwmRequest, errorInformation );
-            pwmRequest.respondWithError( errorInformation );
-        }
         catch ( Exception e )
         catch ( Exception e )
         {
         {
             final String errorMessage = "unexpected error executing web service: " + e.getMessage();
             final String errorMessage = "unexpected error executing web service: " + e.getMessage();

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

@@ -38,7 +38,10 @@ import password.pwm.config.SettingUIFunction;
 import password.pwm.config.StoredValue;
 import password.pwm.config.StoredValue;
 import password.pwm.config.profile.PwmPasswordPolicy;
 import password.pwm.config.profile.PwmPasswordPolicy;
 import password.pwm.config.stored.ConfigurationProperty;
 import password.pwm.config.stored.ConfigurationProperty;
-import password.pwm.config.stored.StoredConfigurationImpl;
+import password.pwm.config.stored.StoredConfigItemKey;
+import password.pwm.config.stored.StoredConfiguration;
+import password.pwm.config.stored.StoredConfigurationModifier;
+import password.pwm.config.stored.StoredConfigurationUtil;
 import password.pwm.config.stored.ValueMetaData;
 import password.pwm.config.stored.ValueMetaData;
 import password.pwm.config.value.ActionValue;
 import password.pwm.config.value.ActionValue;
 import password.pwm.config.value.FileValue;
 import password.pwm.config.value.FileValue;
@@ -51,7 +54,6 @@ import password.pwm.error.PwmError;
 import password.pwm.error.PwmException;
 import password.pwm.error.PwmException;
 import password.pwm.error.PwmOperationalException;
 import password.pwm.error.PwmOperationalException;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.error.PwmUnrecoverableException;
-import password.pwm.health.ConfigurationChecker;
 import password.pwm.health.DatabaseStatusChecker;
 import password.pwm.health.DatabaseStatusChecker;
 import password.pwm.health.HealthRecord;
 import password.pwm.health.HealthRecord;
 import password.pwm.health.HealthStatus;
 import password.pwm.health.HealthStatus;
@@ -72,6 +74,7 @@ import password.pwm.i18n.Message;
 import password.pwm.i18n.PwmLocaleBundle;
 import password.pwm.i18n.PwmLocaleBundle;
 import password.pwm.ldap.LdapBrowser;
 import password.pwm.ldap.LdapBrowser;
 import password.pwm.util.PasswordData;
 import password.pwm.util.PasswordData;
+import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.JsonUtil;
 import password.pwm.util.java.JsonUtil;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.java.TimeDuration;
 import password.pwm.util.java.TimeDuration;
@@ -104,6 +107,8 @@ import java.util.StringTokenizer;
 import java.util.TreeMap;
 import java.util.TreeMap;
 import java.util.TreeSet;
 import java.util.TreeSet;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
 
 
 @WebServlet(
 @WebServlet(
         name = "ConfigEditorServlet",
         name = "ConfigEditorServlet",
@@ -173,8 +178,8 @@ public class ConfigEditorServlet extends ControlledPwmServlet
 
 
         if ( configManagerBean.getStoredConfiguration() == null )
         if ( configManagerBean.getStoredConfiguration() == null )
         {
         {
-            final StoredConfigurationImpl loadedConfig = ConfigManagerServlet.readCurrentConfiguration( pwmRequest );
-            configManagerBean.setConfiguration( loadedConfig );
+            final StoredConfiguration loadedConfig = ConfigManagerServlet.readCurrentConfiguration( pwmRequest );
+            configManagerBean.setStoredConfiguration( loadedConfig );
         }
         }
 
 
         return ProcessStatus.Continue;
         return ProcessStatus.Continue;
@@ -208,8 +213,10 @@ public class ConfigEditorServlet extends ControlledPwmServlet
         try
         try
         {
         {
             final Class implementingClass = Class.forName( functionName );
             final Class implementingClass = Class.forName( functionName );
-            final SettingUIFunction function = ( SettingUIFunction ) implementingClass.newInstance();
-            final Serializable result = function.provideFunction( pwmRequest, configManagerBean.getStoredConfiguration(), pwmSetting, profileID, extraData );
+            final SettingUIFunction function = ( SettingUIFunction ) implementingClass.getDeclaredConstructor().newInstance();
+            final StoredConfigurationModifier modifier = StoredConfigurationModifier.newModifier( configManagerBean.getStoredConfiguration() );
+            final Serializable result = function.provideFunction( pwmRequest, modifier, pwmSetting, profileID, extraData );
+            configManagerBean.setStoredConfiguration( modifier.newStoredConfiguration() );
             final RestResultBean restResultBean = RestResultBean.forSuccessMessage( result, pwmRequest, Message.Success_Unknown );
             final RestResultBean restResultBean = RestResultBean.forSuccessMessage( result, pwmRequest, Message.Success_Unknown );
             pwmRequest.outputJsonResult( restResultBean );
             pwmRequest.outputJsonResult( restResultBean );
         }
         }
@@ -238,7 +245,7 @@ public class ConfigEditorServlet extends ControlledPwmServlet
             throws IOException, PwmUnrecoverableException
             throws IOException, PwmUnrecoverableException
     {
     {
         final ConfigManagerBean configManagerBean = getBean( pwmRequest );
         final ConfigManagerBean configManagerBean = getBean( pwmRequest );
-        final StoredConfigurationImpl storedConfig = configManagerBean.getStoredConfiguration();
+        final StoredConfiguration storedConfig = configManagerBean.getStoredConfiguration();
 
 
         final String key = pwmRequest.readParameterAsString( "key" );
         final String key = pwmRequest.readParameterAsString( "key" );
         final Object returnValue;
         final Object returnValue;
@@ -249,16 +256,18 @@ public class ConfigEditorServlet extends ControlledPwmServlet
         {
         {
             final StringTokenizer st = new StringTokenizer( key, "-" );
             final StringTokenizer st = new StringTokenizer( key, "-" );
             st.nextToken();
             st.nextToken();
-            final PwmLocaleBundle bundleName = PwmLocaleBundle.valueOf( st.nextToken() );
+            final String localeBundleName = st.nextToken();
+            final PwmLocaleBundle pwmLocaleBundle = PwmLocaleBundle.forKey( localeBundleName )
+                    .orElseThrow( () -> new IllegalArgumentException( "unknown locale bundle name '" + localeBundleName + "'" ) );
             final String keyName = st.nextToken();
             final String keyName = st.nextToken();
-            final Map<String, String> bundleMap = storedConfig.readLocaleBundleMap( bundleName.getTheClass().getName(), keyName );
+            final Map<String, String> bundleMap = storedConfig.readLocaleBundleMap( pwmLocaleBundle, keyName );
             if ( bundleMap == null || bundleMap.isEmpty() )
             if ( bundleMap == null || bundleMap.isEmpty() )
             {
             {
                 final Map<String, String> defaultValueMap = new LinkedHashMap<>();
                 final Map<String, String> defaultValueMap = new LinkedHashMap<>();
-                final String defaultLocaleValue = ResourceBundle.getBundle( bundleName.getTheClass().getName(), PwmConstants.DEFAULT_LOCALE ).getString( keyName );
+                final String defaultLocaleValue = ResourceBundle.getBundle( pwmLocaleBundle.getTheClass().getName(), PwmConstants.DEFAULT_LOCALE ).getString( keyName );
                 for ( final Locale locale : pwmRequest.getConfig().getKnownLocales() )
                 for ( final Locale locale : pwmRequest.getConfig().getKnownLocales() )
                 {
                 {
-                    final ResourceBundle localeBundle = ResourceBundle.getBundle( bundleName.getTheClass().getName(), locale );
+                    final ResourceBundle localeBundle = ResourceBundle.getBundle( pwmLocaleBundle.getTheClass().getName(), locale );
                     if ( locale.toString().equalsIgnoreCase( PwmConstants.DEFAULT_LOCALE.toString() ) )
                     if ( locale.toString().equalsIgnoreCase( PwmConstants.DEFAULT_LOCALE.toString() ) )
                     {
                     {
                         defaultValueMap.put( "", defaultLocaleValue );
                         defaultValueMap.put( "", defaultLocaleValue );
@@ -359,7 +368,7 @@ public class ConfigEditorServlet extends ControlledPwmServlet
             throws IOException, PwmUnrecoverableException
             throws IOException, PwmUnrecoverableException
     {
     {
         final ConfigManagerBean configManagerBean = getBean( pwmRequest );
         final ConfigManagerBean configManagerBean = getBean( pwmRequest );
-        final StoredConfigurationImpl storedConfig = configManagerBean.getStoredConfiguration();
+        final StoredConfigurationModifier modifier = StoredConfigurationModifier.newModifier( configManagerBean.getStoredConfiguration() );
         final String key = pwmRequest.readParameterAsString( "key" );
         final String key = pwmRequest.readParameterAsString( "key" );
         final String bodyString = pwmRequest.readRequestBodyAsString();
         final String bodyString = pwmRequest.readRequestBodyAsString();
         final PwmSetting setting = PwmSetting.forKey( key );
         final PwmSetting setting = PwmSetting.forKey( key );
@@ -372,12 +381,13 @@ public class ConfigEditorServlet extends ControlledPwmServlet
         {
         {
             final StringTokenizer st = new StringTokenizer( key, "-" );
             final StringTokenizer st = new StringTokenizer( key, "-" );
             st.nextToken();
             st.nextToken();
-            final PwmLocaleBundle bundleName = PwmLocaleBundle.valueOf( st.nextToken() );
+            final PwmLocaleBundle pwmLocaleBundle = PwmLocaleBundle.forKey( st.nextToken() )
+                    .orElseThrow( () -> new IllegalArgumentException( "unknown locale bundle name" ) );
             final String keyName = st.nextToken();
             final String keyName = st.nextToken();
             final Map<String, String> valueMap = JsonUtil.deserializeStringMap( bodyString );
             final Map<String, String> valueMap = JsonUtil.deserializeStringMap( bodyString );
             final Map<String, String> outputMap = new LinkedHashMap<>( valueMap );
             final Map<String, String> outputMap = new LinkedHashMap<>( valueMap );
 
 
-            storedConfig.writeLocaleBundleMap( bundleName.getTheClass().getName(), keyName, outputMap );
+            modifier.writeLocaleBundleMap( pwmLocaleBundle, keyName, outputMap );
             returnMap.put( "isDefault", outputMap.isEmpty() );
             returnMap.put( "isDefault", outputMap.isEmpty() );
             returnMap.put( "key", key );
             returnMap.put( "key", key );
         }
         }
@@ -392,9 +402,9 @@ public class ConfigEditorServlet extends ControlledPwmServlet
                 {
                 {
                     returnMap.put( "errorMessage", setting.getLabel( pwmRequest.getLocale() ) + ": " + errorMsgs.get( 0 ) );
                     returnMap.put( "errorMessage", setting.getLabel( pwmRequest.getLocale() ) + ": " + errorMsgs.get( 0 ) );
                 }
                 }
-                storedConfig.writeSetting( setting, profileID, storedValue, loggedInUser );
+                modifier.writeSetting( setting, profileID, storedValue, loggedInUser );
             }
             }
-            catch ( Exception e )
+            catch ( final Exception e )
             {
             {
                 final String errorMsg = "error writing default value for setting " + setting.toString() + ", error: " + e.getMessage();
                 final String errorMsg = "error writing default value for setting " + setting.toString() + ", error: " + e.getMessage();
                 LOGGER.error( errorMsg, e );
                 LOGGER.error( errorMsg, e );
@@ -403,8 +413,9 @@ public class ConfigEditorServlet extends ControlledPwmServlet
             returnMap.put( "key", key );
             returnMap.put( "key", key );
             returnMap.put( "category", setting.getCategory().toString() );
             returnMap.put( "category", setting.getCategory().toString() );
             returnMap.put( "syntax", setting.getSyntax().toString() );
             returnMap.put( "syntax", setting.getSyntax().toString() );
-            returnMap.put( "isDefault", storedConfig.isDefaultValue( setting, profileID ) );
+            returnMap.put( "isDefault", configManagerBean.getStoredConfiguration().isDefaultValue( setting, profileID ) );
         }
         }
+        configManagerBean.setStoredConfiguration( modifier.newStoredConfiguration() );
         pwmRequest.outputJsonResult( RestResultBean.withData( returnMap ) );
         pwmRequest.outputJsonResult( RestResultBean.withData( returnMap ) );
         return ProcessStatus.Halt;
         return ProcessStatus.Halt;
     }
     }
@@ -416,7 +427,7 @@ public class ConfigEditorServlet extends ControlledPwmServlet
             throws IOException, PwmUnrecoverableException
             throws IOException, PwmUnrecoverableException
     {
     {
         final ConfigManagerBean configManagerBean = getBean( pwmRequest );
         final ConfigManagerBean configManagerBean = getBean( pwmRequest );
-        final StoredConfigurationImpl storedConfig = configManagerBean.getStoredConfiguration();
+        final StoredConfigurationModifier modifier = StoredConfigurationModifier.newModifier( configManagerBean.getStoredConfiguration() );
         final UserIdentity loggedInUser = pwmRequest.getUserInfoIfLoggedIn();
         final UserIdentity loggedInUser = pwmRequest.getUserInfoIfLoggedIn();
         final String key = pwmRequest.readParameterAsString( "key" );
         final String key = pwmRequest.readParameterAsString( "key" );
         final PwmSetting setting = PwmSetting.forKey( key );
         final PwmSetting setting = PwmSetting.forKey( key );
@@ -425,16 +436,18 @@ public class ConfigEditorServlet extends ControlledPwmServlet
         {
         {
             final StringTokenizer st = new StringTokenizer( key, "-" );
             final StringTokenizer st = new StringTokenizer( key, "-" );
             st.nextToken();
             st.nextToken();
-            final PwmLocaleBundle bundleName = PwmLocaleBundle.valueOf( st.nextToken() );
+            final PwmLocaleBundle pwmLocaleBundle = PwmLocaleBundle.forKey( st.nextToken() )
+                    .orElseThrow( () -> new IllegalArgumentException( "unknown locale bundle name" ) );
             final String keyName = st.nextToken();
             final String keyName = st.nextToken();
-            storedConfig.resetLocaleBundleMap( bundleName.getTheClass().getName(), keyName );
+            modifier.resetLocaleBundleMap( pwmLocaleBundle, keyName );
         }
         }
         else
         else
         {
         {
             final String profileID = setting.getCategory().hasProfiles() ? pwmRequest.readParameterAsString( "profile" ) : null;
             final String profileID = setting.getCategory().hasProfiles() ? pwmRequest.readParameterAsString( "profile" ) : null;
-            storedConfig.resetSetting( setting, profileID, loggedInUser );
+            modifier.resetSetting( setting, profileID, loggedInUser );
         }
         }
 
 
+        configManagerBean.setStoredConfiguration( modifier.newStoredConfiguration() );
         pwmRequest.outputJsonResult( RestResultBean.forSuccessMessage( pwmRequest, Message.Success_Unknown ) );
         pwmRequest.outputJsonResult( RestResultBean.forSuccessMessage( pwmRequest, Message.Success_Unknown ) );
         return ProcessStatus.Halt;
         return ProcessStatus.Halt;
     }
     }
@@ -446,24 +459,26 @@ public class ConfigEditorServlet extends ControlledPwmServlet
             throws IOException, ServletException, PwmUnrecoverableException
             throws IOException, ServletException, PwmUnrecoverableException
     {
     {
         final ConfigManagerBean configManagerBean = getBean( pwmRequest );
         final ConfigManagerBean configManagerBean = getBean( pwmRequest );
+        final StoredConfigurationModifier modifier = StoredConfigurationModifier.newModifier( configManagerBean.getStoredConfiguration() );
 
 
         try
         try
         {
         {
             final Map<String, String> postData = pwmRequest.readBodyAsJsonStringMap();
             final Map<String, String> postData = pwmRequest.readBodyAsJsonStringMap();
             final String password = postData.get( "password" );
             final String password = postData.get( "password" );
-            configManagerBean.getStoredConfiguration().setPassword( password );
+            StoredConfigurationUtil.setPassword( modifier, password );
             configManagerBean.setPasswordVerified( true );
             configManagerBean.setPasswordVerified( true );
             LOGGER.debug( pwmRequest, () -> "config password updated" );
             LOGGER.debug( pwmRequest, () -> "config password updated" );
             final RestResultBean restResultBean = RestResultBean.forConfirmMessage( pwmRequest, Config.Confirm_ConfigPasswordStored );
             final RestResultBean restResultBean = RestResultBean.forConfirmMessage( pwmRequest, Config.Confirm_ConfigPasswordStored );
 
 
             pwmRequest.outputJsonResult( restResultBean );
             pwmRequest.outputJsonResult( restResultBean );
         }
         }
-        catch ( PwmOperationalException e )
+        catch ( final PwmOperationalException e )
         {
         {
             final RestResultBean restResultBean = RestResultBean.fromError( e.getErrorInformation(), pwmRequest );
             final RestResultBean restResultBean = RestResultBean.fromError( e.getErrorInformation(), pwmRequest );
             pwmRequest.outputJsonResult( restResultBean );
             pwmRequest.outputJsonResult( restResultBean );
         }
         }
 
 
+        configManagerBean.setStoredConfiguration( modifier.newStoredConfiguration() );
         return ProcessStatus.Halt;
         return ProcessStatus.Halt;
     }
     }
 
 
@@ -473,7 +488,7 @@ public class ConfigEditorServlet extends ControlledPwmServlet
     {
     {
         final ConfigManagerBean configManagerBean = getBean( pwmRequest );
         final ConfigManagerBean configManagerBean = getBean( pwmRequest );
         final PwmSession pwmSession = pwmRequest.getPwmSession();
         final PwmSession pwmSession = pwmRequest.getPwmSession();
-        final List<String> validationErrors = configManagerBean.getStoredConfiguration().validateValues();
+        final List<String> validationErrors = StoredConfigurationUtil.validateValues( configManagerBean.getStoredConfiguration() );
         if ( !validationErrors.isEmpty() )
         if ( !validationErrors.isEmpty() )
         {
         {
             final String errorString = validationErrors.get( 0 );
             final String errorString = validationErrors.get( 0 );
@@ -491,12 +506,12 @@ public class ConfigEditorServlet extends ControlledPwmServlet
             try
             try
             {
             {
                 ConfigManagerServlet.saveConfiguration( pwmRequest, configManagerBean.getStoredConfiguration() );
                 ConfigManagerServlet.saveConfiguration( pwmRequest, configManagerBean.getStoredConfiguration() );
-                configManagerBean.setConfiguration( null );
-                configManagerBean.setConfiguration( null );
+                configManagerBean.setStoredConfiguration( null );
+                configManagerBean.setStoredConfiguration( null );
                 LOGGER.debug( pwmSession, () -> "save configuration operation completed" );
                 LOGGER.debug( pwmSession, () -> "save configuration operation completed" );
                 pwmRequest.outputJsonResult( RestResultBean.forSuccessMessage( pwmRequest, Message.Success_Unknown ) );
                 pwmRequest.outputJsonResult( RestResultBean.forSuccessMessage( pwmRequest, Message.Success_Unknown ) );
             }
             }
-            catch ( PwmUnrecoverableException e )
+            catch ( final PwmUnrecoverableException e )
             {
             {
                 final ErrorInformation errorInfo = e.getErrorInformation();
                 final ErrorInformation errorInfo = e.getErrorInformation();
                 pwmRequest.outputJsonResult( RestResultBean.fromError( errorInfo, pwmRequest ) );
                 pwmRequest.outputJsonResult( RestResultBean.fromError( errorInfo, pwmRequest ) );
@@ -514,7 +529,7 @@ public class ConfigEditorServlet extends ControlledPwmServlet
             throws IOException, ServletException, PwmUnrecoverableException
             throws IOException, ServletException, PwmUnrecoverableException
     {
     {
         final ConfigManagerBean configManagerBean = getBean( pwmRequest );
         final ConfigManagerBean configManagerBean = getBean( pwmRequest );
-        configManagerBean.setConfiguration( null );
+        configManagerBean.setStoredConfiguration( null );
         pwmRequest.outputJsonResult( RestResultBean.forSuccessMessage( pwmRequest, Message.Success_Unknown ) );
         pwmRequest.outputJsonResult( RestResultBean.forSuccessMessage( pwmRequest, Message.Success_Unknown ) );
         return ProcessStatus.Halt;
         return ProcessStatus.Halt;
     }
     }
@@ -527,19 +542,19 @@ public class ConfigEditorServlet extends ControlledPwmServlet
             throws IOException, PwmUnrecoverableException
             throws IOException, PwmUnrecoverableException
     {
     {
         final ConfigManagerBean configManagerBean = getBean( pwmRequest );
         final ConfigManagerBean configManagerBean = getBean( pwmRequest );
+        final StoredConfigurationModifier modifer = StoredConfigurationModifier.newModifier( configManagerBean.getStoredConfiguration() );
         {
         {
             final String updateDescriptionTextCmd = pwmRequest.readParameterAsString( "updateNotesText" );
             final String updateDescriptionTextCmd = pwmRequest.readParameterAsString( "updateNotesText" );
-            if ( updateDescriptionTextCmd != null && "true".equalsIgnoreCase( updateDescriptionTextCmd ) )
+            if ( StringUtil.nullSafeEqualsIgnoreCase( "true", updateDescriptionTextCmd ) )
             {
             {
                 try
                 try
                 {
                 {
                     final String bodyString = pwmRequest.readRequestBodyAsString();
                     final String bodyString = pwmRequest.readRequestBodyAsString();
                     final String value = JsonUtil.deserialize( bodyString, String.class );
                     final String value = JsonUtil.deserialize( bodyString, String.class );
-                    configManagerBean.getStoredConfiguration().writeConfigProperty( ConfigurationProperty.NOTES,
-                            value );
+                    modifer.writeConfigProperty( ConfigurationProperty.NOTES, value );
                     LOGGER.trace( () -> "updated notesText" );
                     LOGGER.trace( () -> "updated notesText" );
                 }
                 }
-                catch ( Exception e )
+                catch ( final Exception e )
                 {
                 {
                     LOGGER.error( "error updating notesText: " + e.getMessage() );
                     LOGGER.error( "error updating notesText: " + e.getMessage() );
                 }
                 }
@@ -551,20 +566,19 @@ public class ConfigEditorServlet extends ControlledPwmServlet
                     try
                     try
                     {
                     {
                         final PwmSettingTemplate template = PwmSettingTemplate.valueOf( requestedTemplate );
                         final PwmSettingTemplate template = PwmSettingTemplate.valueOf( requestedTemplate );
-                        configManagerBean.getStoredConfiguration().writeConfigProperty(
-                                ConfigurationProperty.LDAP_TEMPLATE, template.toString() );
+                        modifer.writeConfigProperty( ConfigurationProperty.LDAP_TEMPLATE, template.toString() );
                         LOGGER.trace( () -> "setting template to: " + requestedTemplate );
                         LOGGER.trace( () -> "setting template to: " + requestedTemplate );
                     }
                     }
-                    catch ( IllegalArgumentException e )
+                    catch ( final IllegalArgumentException e )
                     {
                     {
-                        configManagerBean.getStoredConfiguration().writeConfigProperty(
-                                ConfigurationProperty.LDAP_TEMPLATE, PwmSettingTemplate.DEFAULT.toString() );
+                        modifer.writeConfigProperty( ConfigurationProperty.LDAP_TEMPLATE, PwmSettingTemplate.DEFAULT.toString() );
                         LOGGER.error( "unknown template set request: " + requestedTemplate );
                         LOGGER.error( "unknown template set request: " + requestedTemplate );
                     }
                     }
                 }
                 }
             }
             }
         }
         }
 
 
+        configManagerBean.setStoredConfiguration( modifer.newStoredConfiguration() );
         return ProcessStatus.Halt;
         return ProcessStatus.Halt;
     }
     }
 
 
@@ -575,35 +589,22 @@ public class ConfigEditorServlet extends ControlledPwmServlet
             throws IOException, PwmUnrecoverableException
             throws IOException, PwmUnrecoverableException
     {
     {
         final ConfigManagerBean configManagerBean = getBean( pwmRequest );
         final ConfigManagerBean configManagerBean = getBean( pwmRequest );
-        final Locale locale = pwmRequest.getLocale();
-        final HashMap<String, Object> returnObj = new HashMap<>();
-        returnObj.put( "html", configManagerBean.getStoredConfiguration().changeLogAsDebugString( locale, true ) );
-        returnObj.put( "modified", configManagerBean.getStoredConfiguration().isModified() );
 
 
-        try
-        {
-            final ConfigurationChecker configurationChecker = new ConfigurationChecker();
-            final Configuration config = new Configuration( configManagerBean.getStoredConfiguration() );
-            final List<HealthRecord> healthRecords = configurationChecker.doHealthCheck(
-                    config,
-                    pwmRequest.getLocale()
-            );
-            final HealthData healthData = new HealthData();
-            healthData.setOverall( "CONFIG" );
-            healthData.setRecords( password.pwm.ws.server.rest.bean.HealthRecord.fromHealthRecords( healthRecords, locale, config ) );
 
 
-            returnObj.put( "health", healthData );
-        }
-        catch ( Exception e )
-        {
-            LOGGER.error( pwmRequest, "error generating health records: " + e.getMessage() );
-        }
+        final Map<String, Object> returnObj = new ConcurrentHashMap<>();
+        final ExecutorService executor = Executors.newFixedThreadPool( 3 );
+        executor.execute( () -> ConfigEditorServletUtils.outputChangeLogData( pwmRequest, configManagerBean, returnObj ) );
+        executor.execute( () -> returnObj.put( "health", ConfigEditorServletUtils.configurationHealth( pwmRequest, configManagerBean ) ) );
+        JavaHelper.closeAndWaitExecutor( executor, TimeDuration.MINUTE );
 
 
-        final RestResultBean restResultBean = RestResultBean.withData( returnObj );
+        final RestResultBean restResultBean = RestResultBean.withData( new HashMap<>( returnObj ) );
         pwmRequest.outputJsonResult( restResultBean );
         pwmRequest.outputJsonResult( restResultBean );
+
         return ProcessStatus.Halt;
         return ProcessStatus.Halt;
     }
     }
 
 
+
+
     @ActionHandler( action = "search" )
     @ActionHandler( action = "search" )
     private ProcessStatus restSearchSettings(
     private ProcessStatus restSearchSettings(
             final PwmRequest pwmRequest
             final PwmRequest pwmRequest
@@ -617,19 +618,19 @@ public class ConfigEditorServlet extends ControlledPwmServlet
         final Locale locale = pwmRequest.getLocale();
         final Locale locale = pwmRequest.getLocale();
         final RestResultBean restResultBean;
         final RestResultBean restResultBean;
         final String searchTerm = valueMap.get( "search" );
         final String searchTerm = valueMap.get( "search" );
-        final StoredConfigurationImpl storedConfiguration = configManagerBean.getStoredConfiguration();
+        final StoredConfiguration storedConfiguration = configManagerBean.getStoredConfiguration();
 
 
         if ( searchTerm != null && !searchTerm.isEmpty() )
         if ( searchTerm != null && !searchTerm.isEmpty() )
         {
         {
-            final ArrayList<StoredConfigurationImpl.ConfigRecordID> searchResults = new ArrayList<>( configManagerBean.getStoredConfiguration().search( searchTerm, locale ) );
+            final Set<StoredConfigItemKey> searchResults = StoredConfigurationUtil.search( storedConfiguration, searchTerm, locale );
             final ConcurrentHashMap<String, Map<String, SearchResultItem>> returnData = new ConcurrentHashMap<>();
             final ConcurrentHashMap<String, Map<String, SearchResultItem>> returnData = new ConcurrentHashMap<>();
 
 
             searchResults
             searchResults
                     .parallelStream()
                     .parallelStream()
-                    .filter( recordID -> recordID.getRecordType() == StoredConfigurationImpl.ConfigRecordID.RecordType.SETTING )
+                    .filter( key -> key.getRecordType() == StoredConfigItemKey.RecordType.SETTING )
                     .forEach( recordID ->
                     .forEach( recordID ->
                     {
                     {
-                        final PwmSetting setting = ( PwmSetting ) recordID.getRecordID();
+                        final PwmSetting setting = recordID.toPwmSetting();
                         final SearchResultItem item = new SearchResultItem(
                         final SearchResultItem item = new SearchResultItem(
                                 setting.getCategory().toString(),
                                 setting.getCategory().toString(),
                                 storedConfiguration.readSetting( setting, recordID.getProfileID() ).toDebugString( locale ),
                                 storedConfiguration.readSetting( setting, recordID.getProfileID() ).toDebugString( locale ),
@@ -655,7 +656,7 @@ public class ConfigEditorServlet extends ControlledPwmServlet
         }
         }
         else
         else
         {
         {
-            restResultBean = RestResultBean.withData( new ArrayList<StoredConfigurationImpl.ConfigRecordID>() );
+            restResultBean = RestResultBean.withData( new ArrayList() );
         }
         }
 
 
         pwmRequest.outputJsonResult( restResultBean );
         pwmRequest.outputJsonResult( restResultBean );
@@ -751,11 +752,13 @@ public class ConfigEditorServlet extends ControlledPwmServlet
             throws PwmUnrecoverableException, IOException, ServletException
             throws PwmUnrecoverableException, IOException, ServletException
     {
     {
         final ConfigManagerBean configManagerBean = getBean( pwmRequest );
         final ConfigManagerBean configManagerBean = getBean( pwmRequest );
+        final StoredConfigurationModifier modifier = StoredConfigurationModifier.newModifier( configManagerBean.getStoredConfiguration() );
+
+
         final String key = pwmRequest.readParameterAsString( "key" );
         final String key = pwmRequest.readParameterAsString( "key" );
         final PwmSetting setting = PwmSetting.forKey( key );
         final PwmSetting setting = PwmSetting.forKey( key );
         final int maxFileSize = Integer.parseInt( pwmRequest.getConfig().readAppProperty( AppProperty.CONFIG_MAX_JDBC_JAR_SIZE ) );
         final int maxFileSize = Integer.parseInt( pwmRequest.getConfig().readAppProperty( AppProperty.CONFIG_MAX_JDBC_JAR_SIZE ) );
 
 
-
         if ( setting == PwmSetting.HTTPS_CERT )
         if ( setting == PwmSetting.HTTPS_CERT )
         {
         {
             try
             try
@@ -767,7 +770,7 @@ public class ConfigEditorServlet extends ControlledPwmServlet
                 {
                 {
                     keyStoreFormat = HttpsServerCertificateManager.KeyStoreFormat.valueOf( pwmRequest.readParameterAsString( "format" ) );
                     keyStoreFormat = HttpsServerCertificateManager.KeyStoreFormat.valueOf( pwmRequest.readParameterAsString( "format" ) );
                 }
                 }
-                catch ( IllegalArgumentException e )
+                catch ( final IllegalArgumentException e )
                 {
                 {
                     throw new PwmUnrecoverableException( new ErrorInformation( PwmError.ERROR_MISSING_PARAMETER, "unknown format type: " + e.getMessage(), new String[]
                     throw new PwmUnrecoverableException( new ErrorInformation( PwmError.ERROR_MISSING_PARAMETER, "unknown format type: " + e.getMessage(), new String[]
                             {
                             {
@@ -780,7 +783,7 @@ public class ConfigEditorServlet extends ControlledPwmServlet
                 final ByteArrayInputStream fileIs = new ByteArrayInputStream( fileUploads.get( PwmConstants.PARAM_FILE_UPLOAD ).getContent().copyOf() );
                 final ByteArrayInputStream fileIs = new ByteArrayInputStream( fileUploads.get( PwmConstants.PARAM_FILE_UPLOAD ).getContent().copyOf() );
 
 
                 HttpsServerCertificateManager.importKey(
                 HttpsServerCertificateManager.importKey(
-                        configManagerBean.getStoredConfiguration(),
+                        modifier,
                         keyStoreFormat,
                         keyStoreFormat,
                         fileIs,
                         fileIs,
                         passwordData,
                         passwordData,
@@ -790,7 +793,7 @@ public class ConfigEditorServlet extends ControlledPwmServlet
                 pwmRequest.outputJsonResult( RestResultBean.forSuccessMessage( pwmRequest, Message.Success_Unknown ) );
                 pwmRequest.outputJsonResult( RestResultBean.forSuccessMessage( pwmRequest, Message.Success_Unknown ) );
                 return ProcessStatus.Halt;
                 return ProcessStatus.Halt;
             }
             }
-            catch ( PwmException e )
+            catch ( final PwmException e )
             {
             {
                 LOGGER.error( pwmRequest, "error during https certificate upload: " + e.getMessage() );
                 LOGGER.error( pwmRequest, "error during https certificate upload: " + e.getMessage() );
                 pwmRequest.respondWithError( e.getErrorInformation(), false );
                 pwmRequest.respondWithError( e.getErrorInformation(), false );
@@ -805,10 +808,11 @@ public class ConfigEditorServlet extends ControlledPwmServlet
                     ? pwmRequest.getPwmSession().getUserInfo().getUserIdentity()
                     ? pwmRequest.getPwmSession().getUserInfo().getUserIdentity()
                     : null;
                     : null;
 
 
-            configManagerBean.getStoredConfiguration().writeSetting( setting, fileValue, userIdentity );
+            modifier.writeSetting( setting, null, fileValue, userIdentity );
             pwmRequest.outputJsonResult( RestResultBean.forSuccessMessage( pwmRequest, Message.Success_Unknown ) );
             pwmRequest.outputJsonResult( RestResultBean.forSuccessMessage( pwmRequest, Message.Success_Unknown ) );
         }
         }
 
 
+        configManagerBean.setStoredConfiguration( modifier.newStoredConfiguration() );
         return ProcessStatus.Halt;
         return ProcessStatus.Halt;
     }
     }
 
 
@@ -836,7 +840,7 @@ public class ConfigEditorServlet extends ControlledPwmServlet
         }
         }
 
 
         {
         {
-            final StoredConfigurationImpl storedConfiguration = configManagerBean.getStoredConfiguration();
+            final StoredConfiguration storedConfiguration = configManagerBean.getStoredConfiguration();
             final List<PwmSettingCategory> categories = NavTreeHelper.filteredCategories(
             final List<PwmSettingCategory> categories = NavTreeHelper.filteredCategories(
                     pwmRequest.getPwmApplication(),
                     pwmRequest.getPwmApplication(),
                     storedConfiguration,
                     storedConfiguration,
@@ -867,7 +871,7 @@ public class ConfigEditorServlet extends ControlledPwmServlet
                         categoryInfo.setName( localeBundle.getTheClass().getSimpleName() );
                         categoryInfo.setName( localeBundle.getTheClass().getSimpleName() );
                         categoryInfo.setParent( "DISPLAY_TEXT" );
                         categoryInfo.setParent( "DISPLAY_TEXT" );
                         categoryInfo.setType( NavTreeHelper.NavItemType.displayText );
                         categoryInfo.setType( NavTreeHelper.NavItemType.displayText );
-                        categoryInfo.setKeys( new TreeSet<>( modifiedSettingsOnly ? modifiedKeys : localeBundle.getKeys() ) );
+                        categoryInfo.setKeys( new TreeSet<>( modifiedSettingsOnly ? modifiedKeys : localeBundle.getDisplayKeys() ) );
                         navigationData.add( categoryInfo );
                         navigationData.add( categoryInfo );
                         includeDisplayText = true;
                         includeDisplayText = true;
                     }
                     }
@@ -903,7 +907,7 @@ public class ConfigEditorServlet extends ControlledPwmServlet
                 configManagerBean.getStoredConfiguration(),
                 configManagerBean.getStoredConfiguration(),
                 pwmRequest.getSessionLabel(),
                 pwmRequest.getSessionLabel(),
                 pwmRequest.getLocale()
                 pwmRequest.getLocale()
-                )
+        )
         );
         );
 
 
         if ( pwmRequest.getPwmApplication().getApplicationMode() == PwmApplicationMode.CONFIGURATION && !PwmConstants.TRIAL_MODE )
         if ( pwmRequest.getPwmApplication().getApplicationMode() == PwmApplicationMode.CONFIGURATION && !PwmConstants.TRIAL_MODE )
@@ -922,11 +926,11 @@ public class ConfigEditorServlet extends ControlledPwmServlet
 
 
     public static Map<String, Object> generateSettingData(
     public static Map<String, Object> generateSettingData(
             final PwmApplication pwmApplication,
             final PwmApplication pwmApplication,
-            final StoredConfigurationImpl storedConfiguration,
+            final StoredConfiguration storedConfiguration,
             final SessionLabel sessionLabel,
             final SessionLabel sessionLabel,
             final Locale locale
             final Locale locale
 
 
-            ) throws PwmUnrecoverableException
+    ) throws PwmUnrecoverableException
     {
     {
         final LinkedHashMap<String, Object> returnMap = new LinkedHashMap<>();
         final LinkedHashMap<String, Object> returnMap = new LinkedHashMap<>();
         final MacroMachine macroMachine = MacroMachine.forNonUserSpecific( pwmApplication, sessionLabel );
         final MacroMachine macroMachine = MacroMachine.forNonUserSpecific( pwmApplication, sessionLabel );
@@ -963,7 +967,7 @@ public class ConfigEditorServlet extends ControlledPwmServlet
         }
         }
         {
         {
             final LinkedHashMap<String, Object> varMap = new LinkedHashMap<>();
             final LinkedHashMap<String, Object> varMap = new LinkedHashMap<>();
-            varMap.put( "ldapProfileIds", storedConfiguration.readSetting( PwmSetting.LDAP_PROFILE_LIST ).toNativeObject() );
+            varMap.put( "ldapProfileIds", storedConfiguration.readSetting( PwmSetting.LDAP_PROFILE_LIST, null ).toNativeObject() );
             varMap.put( "currentTemplate", storedConfiguration.getTemplateSet() );
             varMap.put( "currentTemplate", storedConfiguration.getTemplateSet() );
             varMap.put( "configurationNotes", storedConfiguration.readConfigProperty( ConfigurationProperty.NOTES ) );
             varMap.put( "configurationNotes", storedConfiguration.readConfigProperty( ConfigurationProperty.NOTES ) );
             returnMap.put( "var", varMap );
             returnMap.put( "var", varMap );
@@ -1049,6 +1053,7 @@ public class ConfigEditorServlet extends ControlledPwmServlet
             throws IOException, ServletException, PwmUnrecoverableException
             throws IOException, ServletException, PwmUnrecoverableException
     {
     {
         final ConfigManagerBean configManagerBean = getBean( pwmRequest );
         final ConfigManagerBean configManagerBean = getBean( pwmRequest );
+        final StoredConfigurationModifier modifier = StoredConfigurationModifier.newModifier( configManagerBean.getStoredConfiguration() );
         final Map<String, String> inputMap = pwmRequest.readBodyAsJsonStringMap( PwmHttpRequestWrapper.Flag.BypassValidation );
         final Map<String, String> inputMap = pwmRequest.readBodyAsJsonStringMap( PwmHttpRequestWrapper.Flag.BypassValidation );
 
 
         final String settingKey = inputMap.get( "setting" );
         final String settingKey = inputMap.get( "setting" );
@@ -1069,13 +1074,15 @@ public class ConfigEditorServlet extends ControlledPwmServlet
         final String destinationID = inputMap.get( "destinationID" );
         final String destinationID = inputMap.get( "destinationID" );
         try
         try
         {
         {
-            configManagerBean.getStoredConfiguration().copyProfileID( category, sourceID, destinationID, pwmRequest.getUserInfoIfLoggedIn() );
+            modifier.copyProfileID( category, sourceID, destinationID, pwmRequest.getUserInfoIfLoggedIn() );
             pwmRequest.outputJsonResult( RestResultBean.forSuccessMessage( pwmRequest, Message.Success_Unknown ) );
             pwmRequest.outputJsonResult( RestResultBean.forSuccessMessage( pwmRequest, Message.Success_Unknown ) );
         }
         }
-        catch ( PwmUnrecoverableException e )
+        catch ( final PwmUnrecoverableException e )
         {
         {
             pwmRequest.outputJsonResult( RestResultBean.fromError( e.getErrorInformation(), pwmRequest ) );
             pwmRequest.outputJsonResult( RestResultBean.fromError( e.getErrorInformation(), pwmRequest ) );
         }
         }
+
+        configManagerBean.setStoredConfiguration( modifier.newStoredConfiguration() );
         return ProcessStatus.Halt;
         return ProcessStatus.Halt;
     }
     }
 
 

+ 81 - 0
server/src/main/java/password/pwm/http/servlet/configeditor/ConfigEditorServletUtils.java

@@ -21,19 +21,32 @@
 package password.pwm.http.servlet.configeditor;
 package password.pwm.http.servlet.configeditor;
 
 
 import password.pwm.PwmConstants;
 import password.pwm.PwmConstants;
+import password.pwm.config.Configuration;
+import password.pwm.config.stored.StoredConfigItemKey;
+import password.pwm.config.stored.StoredConfigurationUtil;
 import password.pwm.config.value.FileValue;
 import password.pwm.config.value.FileValue;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmException;
 import password.pwm.error.PwmException;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.health.ConfigurationChecker;
+import password.pwm.health.HealthRecord;
 import password.pwm.http.PwmRequest;
 import password.pwm.http.PwmRequest;
+import password.pwm.http.bean.ConfigManagerBean;
+import password.pwm.util.java.StringUtil;
+import password.pwm.util.java.TimeDuration;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.ws.server.RestResultBean;
 import password.pwm.ws.server.RestResultBean;
+import password.pwm.ws.server.rest.bean.HealthData;
 
 
 import javax.servlet.ServletException;
 import javax.servlet.ServletException;
 import java.io.IOException;
 import java.io.IOException;
+import java.time.Instant;
 import java.util.LinkedHashMap;
 import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Locale;
 import java.util.Map;
 import java.util.Map;
+import java.util.Set;
 
 
 public class ConfigEditorServletUtils
 public class ConfigEditorServletUtils
 {
 {
@@ -83,4 +96,72 @@ public class ConfigEditorServletUtils
         return null;
         return null;
     }
     }
 
 
+    static void outputChangeLogData(
+            final PwmRequest pwmRequest,
+            final ConfigManagerBean configManagerBean,
+            final Map<String, Object> outputMap
+    )
+    {
+            final Locale locale = pwmRequest.getLocale();
+
+            final Set<StoredConfigItemKey> changeLog = StoredConfigurationUtil.changedValues(
+                    pwmRequest.getPwmApplication().getConfig().getStoredConfiguration(),
+                    configManagerBean.getStoredConfiguration() );
+
+            final Map<String, String> changeLogMap = StoredConfigurationUtil.makeDebugMap(
+                    configManagerBean.getStoredConfiguration(),
+                    changeLog,
+                    locale );
+
+            final StringBuilder output = new StringBuilder();
+            if ( changeLogMap.isEmpty() )
+            {
+                output.append( "No setting changes." );
+            }
+            else
+            {
+                for ( final Map.Entry<String, String> entry : changeLogMap.entrySet() )
+                {
+                    output.append( "<div class=\"changeLogKey\">" );
+                    output.append( entry.getKey() );
+                    output.append( "</div><div class=\"changeLogValue\">" );
+                    output.append( StringUtil.escapeHtml( entry.getValue() ) );
+                    output.append( "</div>" );
+                }
+            }
+            outputMap.put( "html", output.toString() );
+            outputMap.put( "modified", !changeLog.isEmpty() );
+
+    }
+
+    static HealthData configurationHealth(
+            final PwmRequest pwmRequest,
+            final ConfigManagerBean configManagerBean
+    )
+    {
+        final Instant startTime = Instant.now();
+        try
+        {
+            final Locale locale = pwmRequest.getLocale();
+            final ConfigurationChecker configurationChecker = new ConfigurationChecker();
+            final Configuration config = new Configuration( configManagerBean.getStoredConfiguration() );
+            final List<HealthRecord> healthRecords = configurationChecker.doHealthCheck(
+                    config,
+                    pwmRequest.getLocale()
+            );
+
+            LOGGER.debug( () -> "config health check done in " + TimeDuration.compactFromCurrent( startTime ) );
+
+            return HealthData.builder()
+                    .overall( "CONFIG" )
+                    .records( password.pwm.ws.server.rest.bean.HealthRecord.fromHealthRecords( healthRecords, locale, config ) )
+                    .build();
+        }
+        catch ( Exception e )
+        {
+            LOGGER.error( pwmRequest, "error generating health records: " + e.getMessage() );
+        }
+
+        return HealthData.builder().build();
+    }
 }
 }

+ 9 - 9
server/src/main/java/password/pwm/http/servlet/configeditor/NavTreeHelper.java

@@ -27,7 +27,7 @@ import password.pwm.config.Configuration;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.PwmSettingCategory;
 import password.pwm.config.PwmSettingCategory;
 import password.pwm.config.StoredValue;
 import password.pwm.config.StoredValue;
-import password.pwm.config.stored.StoredConfigurationImpl;
+import password.pwm.config.stored.StoredConfiguration;
 import password.pwm.config.stored.StoredConfigurationUtil;
 import password.pwm.config.stored.StoredConfigurationUtil;
 import password.pwm.i18n.Config;
 import password.pwm.i18n.Config;
 import password.pwm.i18n.PwmLocaleBundle;
 import password.pwm.i18n.PwmLocaleBundle;
@@ -47,13 +47,13 @@ class NavTreeHelper
     static Set<String> determineModifiedKeysSettings(
     static Set<String> determineModifiedKeysSettings(
             final PwmLocaleBundle bundle,
             final PwmLocaleBundle bundle,
             final Configuration config,
             final Configuration config,
-            final StoredConfigurationImpl storedConfiguration
+            final StoredConfiguration storedConfiguration
     )
     )
     {
     {
         final Set<String> modifiedKeys = new TreeSet<>();
         final Set<String> modifiedKeys = new TreeSet<>();
-        for ( final String key : bundle.getKeys() )
+        for ( final String key : bundle.getDisplayKeys() )
         {
         {
-            final Map<String, String> storedBundle = storedConfiguration.readLocaleBundleMap( bundle.getTheClass().getName(), key );
+            final Map<String, String> storedBundle = storedConfiguration.readLocaleBundleMap( bundle, key );
             if ( !storedBundle.isEmpty() )
             if ( !storedBundle.isEmpty() )
             {
             {
                 for ( final Locale locale : config.getKnownLocales() )
                 for ( final Locale locale : config.getKnownLocales() )
@@ -77,7 +77,7 @@ class NavTreeHelper
     static boolean categoryMatcher(
     static boolean categoryMatcher(
             final PwmApplication pwmApplication,
             final PwmApplication pwmApplication,
             final PwmSettingCategory category,
             final PwmSettingCategory category,
-            final StoredConfigurationImpl storedConfiguration,
+            final StoredConfiguration storedConfiguration,
             final boolean modifiedOnly,
             final boolean modifiedOnly,
             final int minLevel,
             final int minLevel,
             final String text
             final String text
@@ -138,7 +138,7 @@ class NavTreeHelper
 
 
     static List<PwmSettingCategory> filteredCategories(
     static List<PwmSettingCategory> filteredCategories(
             final PwmApplication pwmApplication,
             final PwmApplication pwmApplication,
-            final StoredConfigurationImpl storedConfiguration,
+            final StoredConfiguration storedConfiguration,
             final Locale locale,
             final Locale locale,
             final boolean modifiedSettingsOnly,
             final boolean modifiedSettingsOnly,
             final double level,
             final double level,
@@ -170,7 +170,7 @@ class NavTreeHelper
      */
      */
     static List<NavTreeItem> makeSettingNavItems(
     static List<NavTreeItem> makeSettingNavItems(
             final List<PwmSettingCategory> categories,
             final List<PwmSettingCategory> categories,
-            final StoredConfigurationImpl storedConfiguration,
+            final StoredConfiguration storedConfiguration,
             final Locale locale
             final Locale locale
     )
     )
     {
     {
@@ -277,7 +277,7 @@ class NavTreeHelper
     }
     }
 
 
     private static boolean settingMatches(
     private static boolean settingMatches(
-            final StoredConfigurationImpl storedConfiguration,
+            final StoredConfiguration storedConfiguration,
             final PwmSetting setting,
             final PwmSetting setting,
             final String profileID,
             final String profileID,
             final boolean modifiedOnly,
             final boolean modifiedOnly,
@@ -313,7 +313,7 @@ class NavTreeHelper
             final StoredValue storedValue = storedConfiguration.readSetting( setting, profileID );
             final StoredValue storedValue = storedConfiguration.readSetting( setting, profileID );
             for ( final String term : StringUtil.whitespaceSplit( text ) )
             for ( final String term : StringUtil.whitespaceSplit( text ) )
             {
             {
-                if ( storedConfiguration.matchSetting( setting, storedValue, term, PwmConstants.DEFAULT_LOCALE ) )
+                if ( StoredConfigurationUtil.matchSetting( storedConfiguration, setting, storedValue, term, PwmConstants.DEFAULT_LOCALE ) )
                 {
                 {
                     return true;
                     return true;
                 }
                 }

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

@@ -23,7 +23,9 @@ package password.pwm.http.servlet.configguide;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.PwmSettingTemplate;
 import password.pwm.config.PwmSettingTemplate;
 import password.pwm.config.StoredValue;
 import password.pwm.config.StoredValue;
-import password.pwm.config.stored.StoredConfigurationImpl;
+import password.pwm.config.stored.StoredConfiguration;
+import password.pwm.config.stored.StoredConfigurationFactory;
+import password.pwm.config.stored.StoredConfigurationModifier;
 import password.pwm.config.value.BooleanValue;
 import password.pwm.config.value.BooleanValue;
 import password.pwm.config.value.ChallengeValue;
 import password.pwm.config.value.ChallengeValue;
 import password.pwm.config.value.FileValue;
 import password.pwm.config.value.FileValue;
@@ -69,7 +71,7 @@ public class ConfigGuideForm
 
 
     private static void updateStoredConfigTemplateValue(
     private static void updateStoredConfigTemplateValue(
             final Map<ConfigGuideFormField, String> formData,
             final Map<ConfigGuideFormField, String> formData,
-            final StoredConfigurationImpl storedConfiguration,
+            final StoredConfigurationModifier modifier,
             final PwmSetting pwmSetting,
             final PwmSetting pwmSetting,
             final ConfigGuideFormField formField,
             final ConfigGuideFormField formField,
             final PwmSettingTemplate.Type type
             final PwmSettingTemplate.Type type
@@ -80,20 +82,20 @@ public class ConfigGuideForm
         if ( !StringUtil.isEmpty( formValue ) )
         if ( !StringUtil.isEmpty( formValue ) )
         {
         {
             final PwmSettingTemplate template = PwmSettingTemplate.templateForString( formValue, type );
             final PwmSettingTemplate template = PwmSettingTemplate.templateForString( formValue, type );
-            storedConfiguration.writeSetting( pwmSetting, null, new StringValue( template.toString() ), null );
+            modifier.writeSetting( pwmSetting, null, new StringValue( template.toString() ), null );
         }
         }
     }
     }
 
 
     private static final String LDAP_PROFILE_NAME = "default";
     private static final String LDAP_PROFILE_NAME = "default";
 
 
-    public static StoredConfigurationImpl generateStoredConfig(
+    public static StoredConfiguration generateStoredConfig(
             final ConfigGuideBean configGuideBean
             final ConfigGuideBean configGuideBean
     )
     )
             throws PwmUnrecoverableException
             throws PwmUnrecoverableException
     {
     {
 
 
         final Map<ConfigGuideFormField, String> formData = configGuideBean.getFormData();
         final Map<ConfigGuideFormField, String> formData = configGuideBean.getFormData();
-        final StoredConfigurationImpl storedConfiguration = StoredConfigurationImpl.newStoredConfiguration();
+        final StoredConfigurationModifier storedConfiguration = StoredConfigurationModifier.newModifier( StoredConfigurationFactory.newConfig() );
 
 
         // templates
         // templates
         updateStoredConfigTemplateValue(
         updateStoredConfigTemplateValue(
@@ -170,8 +172,11 @@ public class ConfigGuideForm
         {
         {
             // set admin query
             // set admin query
             final String groupDN = formData.get( ConfigGuideFormField.PARAM_LDAP_ADMIN_GROUP );
             final String groupDN = formData.get( ConfigGuideFormField.PARAM_LDAP_ADMIN_GROUP );
-            final List<UserPermission> userPermissions = Collections.singletonList( new UserPermission( UserPermission.Type.ldapGroup, null, null, groupDN ) );
-            storedConfiguration.writeSetting( PwmSetting.QUERY_MATCH_PWM_ADMIN, new UserPermissionValue( userPermissions ), null );
+            final List<UserPermission> userPermissions = Collections.singletonList( UserPermission.builder()
+                    .type( UserPermission.Type.ldapGroup )
+                    .ldapBase( groupDN )
+                    .build() );
+            storedConfiguration.writeSetting( PwmSetting.QUERY_MATCH_PWM_ADMIN, null, new UserPermissionValue( userPermissions ), null );
         }
         }
 
 
         {
         {
@@ -215,12 +220,12 @@ public class ConfigGuideForm
         }
         }
 
 
         // set site url
         // set site url
-        storedConfiguration.writeSetting( PwmSetting.PWM_SITE_URL, new StringValue( formData.get( ConfigGuideFormField.PARAM_APP_SITEURL ) ), null );
+        storedConfiguration.writeSetting( PwmSetting.PWM_SITE_URL, null, new StringValue( formData.get( ConfigGuideFormField.PARAM_APP_SITEURL ) ), null );
 
 
         // enable debug mode
         // enable debug mode
         storedConfiguration.writeSetting( PwmSetting.DISPLAY_SHOW_DETAILED_ERRORS, null, new BooleanValue( true ), null );
         storedConfiguration.writeSetting( PwmSetting.DISPLAY_SHOW_DETAILED_ERRORS, null, new BooleanValue( true ), null );
 
 
-        return storedConfiguration;
+        return storedConfiguration.newStoredConfiguration();
     }
     }
 
 
     static String figureLdapUrlFromFormConfig( final Map<ConfigGuideFormField, String> ldapForm )
     static String figureLdapUrlFromFormConfig( final Map<ConfigGuideFormField, String> ldapForm )
@@ -236,7 +241,7 @@ public class ConfigGuideForm
     {
     {
         try
         try
         {
         {
-            final StoredConfigurationImpl storedConfiguration = generateStoredConfig( configGuideBean );
+            final StoredConfiguration storedConfiguration = generateStoredConfig( configGuideBean );
             final String uriString = PwmSetting.LDAP_SERVER_URLS.getExample( storedConfiguration.getTemplateSet() );
             final String uriString = PwmSetting.LDAP_SERVER_URLS.getExample( storedConfiguration.getTemplateSet() );
             final URI uri = new URI( uriString );
             final URI uri = new URI( uriString );
             return uri.getHost();
             return uri.getHost();

+ 30 - 24
server/src/main/java/password/pwm/http/servlet/configguide/ConfigGuideServlet.java

@@ -33,7 +33,10 @@ import password.pwm.config.StoredValue;
 import password.pwm.config.function.UserMatchViewerFunction;
 import password.pwm.config.function.UserMatchViewerFunction;
 import password.pwm.config.profile.LdapProfile;
 import password.pwm.config.profile.LdapProfile;
 import password.pwm.config.stored.ConfigurationProperty;
 import password.pwm.config.stored.ConfigurationProperty;
-import password.pwm.config.stored.StoredConfigurationImpl;
+import password.pwm.config.stored.StoredConfiguration;
+import password.pwm.config.stored.StoredConfigurationFactory;
+import password.pwm.config.stored.StoredConfigurationModifier;
+import password.pwm.config.stored.StoredConfigurationUtil;
 import password.pwm.config.value.FileValue;
 import password.pwm.config.value.FileValue;
 import password.pwm.config.value.ValueFactory;
 import password.pwm.config.value.ValueFactory;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.ErrorInformation;
@@ -97,7 +100,7 @@ public class ConfigGuideServlet extends ControlledPwmServlet
 
 
     private static final PwmLogger LOGGER = PwmLogger.getLogger( ConfigGuideServlet.class.getName() );
     private static final PwmLogger LOGGER = PwmLogger.getLogger( ConfigGuideServlet.class.getName() );
 
 
-    private static final String LDAP_PROFILE_KEY = "default";
+    private static final String LDAP_PROFILE_KEY = PwmConstants.PROFILE_ID_DEFAULT;
 
 
     public enum ConfigGuideAction implements AbstractPwmServlet.ProcessAction
     public enum ConfigGuideAction implements AbstractPwmServlet.ProcessAction
     {
     {
@@ -230,8 +233,8 @@ public class ConfigGuideServlet extends ControlledPwmServlet
     {
     {
         final ConfigGuideBean configGuideBean = getBean( pwmRequest );
         final ConfigGuideBean configGuideBean = getBean( pwmRequest );
 
 
-        final StoredConfigurationImpl storedConfigurationImpl = ConfigGuideForm.generateStoredConfig( configGuideBean );
-        final Configuration tempConfiguration = new Configuration( storedConfigurationImpl );
+        final StoredConfiguration storedConfiguration = ConfigGuideForm.generateStoredConfig( configGuideBean );
+        final Configuration tempConfiguration = new Configuration( storedConfiguration );
         final PwmApplication tempApplication = new PwmApplication( pwmRequest.getPwmApplication()
         final PwmApplication tempApplication = new PwmApplication( pwmRequest.getPwmApplication()
                 .getPwmEnvironment()
                 .getPwmEnvironment()
                 .makeRuntimeInstance( tempConfiguration ) );
                 .makeRuntimeInstance( tempConfiguration ) );
@@ -285,7 +288,7 @@ public class ConfigGuideServlet extends ControlledPwmServlet
                     final Collection<UserIdentity> results = userMatchViewerFunction.discoverMatchingUsers(
                     final Collection<UserIdentity> results = userMatchViewerFunction.discoverMatchingUsers(
                             pwmRequest.getPwmApplication(),
                             pwmRequest.getPwmApplication(),
                             2,
                             2,
-                            storedConfigurationImpl,
+                            storedConfiguration,
                             PwmSetting.QUERY_MATCH_PWM_ADMIN,
                             PwmSetting.QUERY_MATCH_PWM_ADMIN,
                             null
                             null
                     );
                     );
@@ -335,11 +338,11 @@ public class ConfigGuideServlet extends ControlledPwmServlet
                 JavaHelper.unhandledSwitchStatement( configGuideBean.getStep() );
                 JavaHelper.unhandledSwitchStatement( configGuideBean.getStep() );
         }
         }
 
 
-        final HealthData jsonOutput = new HealthData();
-        jsonOutput.records = password.pwm.ws.server.rest.bean.HealthRecord.fromHealthRecords( records,
-                pwmRequest.getLocale(), tempConfiguration );
-        jsonOutput.timestamp = Instant.now();
-        jsonOutput.overall = HealthMonitor.getMostSevereHealthStatus( records ).toString();
+        final HealthData jsonOutput = HealthData.builder()
+                .records( password.pwm.ws.server.rest.bean.HealthRecord.fromHealthRecords( records, pwmRequest.getLocale(), tempConfiguration ) )
+                .timestamp( Instant.now() )
+                .overall( HealthMonitor.getMostSevereHealthStatus( records ).toString() )
+                .build();
         final RestResultBean restResultBean = RestResultBean.withData( jsonOutput );
         final RestResultBean restResultBean = RestResultBean.withData( jsonOutput );
         pwmRequest.outputJsonResult( restResultBean );
         pwmRequest.outputJsonResult( restResultBean );
         return ProcessStatus.Halt;
         return ProcessStatus.Halt;
@@ -356,8 +359,9 @@ public class ConfigGuideServlet extends ControlledPwmServlet
         try
         try
         {
         {
             final UserMatchViewerFunction userMatchViewerFunction = new UserMatchViewerFunction();
             final UserMatchViewerFunction userMatchViewerFunction = new UserMatchViewerFunction();
-            final StoredConfigurationImpl storedConfiguration = ConfigGuideForm.generateStoredConfig( configGuideBean );
-            final Serializable output = userMatchViewerFunction.provideFunction( pwmRequest, storedConfiguration, PwmSetting.QUERY_MATCH_PWM_ADMIN, null, null );
+            final StoredConfiguration storedConfiguration = ConfigGuideForm.generateStoredConfig( configGuideBean );
+            final StoredConfigurationModifier modifier = StoredConfigurationModifier.newModifier( storedConfiguration );
+            final Serializable output = userMatchViewerFunction.provideFunction( pwmRequest, modifier, PwmSetting.QUERY_MATCH_PWM_ADMIN, null, null );
             pwmRequest.outputJsonResult( RestResultBean.withData( output ) );
             pwmRequest.outputJsonResult( RestResultBean.withData( output ) );
         }
         }
         catch ( PwmException e )
         catch ( PwmException e )
@@ -382,11 +386,13 @@ public class ConfigGuideServlet extends ControlledPwmServlet
     {
     {
         final ConfigGuideBean configGuideBean = getBean( pwmRequest );
         final ConfigGuideBean configGuideBean = getBean( pwmRequest );
 
 
-        final StoredConfigurationImpl storedConfiguration = ConfigGuideForm.generateStoredConfig( configGuideBean );
+        StoredConfiguration storedConfiguration = ConfigGuideForm.generateStoredConfig( configGuideBean );
         if ( configGuideBean.getStep() == GuideStep.LDAP_PROXY )
         if ( configGuideBean.getStep() == GuideStep.LDAP_PROXY )
         {
         {
-            storedConfiguration.resetSetting( PwmSetting.LDAP_PROXY_USER_DN, LDAP_PROFILE_KEY, null );
-            storedConfiguration.resetSetting( PwmSetting.LDAP_PROXY_USER_PASSWORD, LDAP_PROFILE_KEY, null );
+            final StoredConfigurationModifier modifier = StoredConfigurationModifier.newModifier( storedConfiguration );
+            modifier.resetSetting( PwmSetting.LDAP_PROXY_USER_DN, LDAP_PROFILE_KEY, null );
+            modifier.resetSetting( PwmSetting.LDAP_PROXY_USER_PASSWORD, LDAP_PROFILE_KEY, null );
+            storedConfiguration = modifier.newStoredConfiguration();
         }
         }
 
 
         final Instant startTime = Instant.now();
         final Instant startTime = Instant.now();
@@ -571,10 +577,10 @@ public class ConfigGuideServlet extends ControlledPwmServlet
         final ContextManager contextManager = ContextManager.getContextManager( pwmRequest );
         final ContextManager contextManager = ContextManager.getContextManager( pwmRequest );
         try
         try
         {
         {
-            final StoredConfigurationImpl storedConfiguration = new StoredConfigurationImpl();
+            final StoredConfigurationModifier storedConfiguration = StoredConfigurationModifier.newModifier( StoredConfigurationFactory.newConfig() );
             storedConfiguration.writeConfigProperty( ConfigurationProperty.CONFIG_IS_EDITABLE, "true" );
             storedConfiguration.writeConfigProperty( ConfigurationProperty.CONFIG_IS_EDITABLE, "true" );
-            storedConfiguration.setPassword( password );
-            ConfigGuideUtils.writeConfig( contextManager, storedConfiguration );
+            StoredConfigurationUtil.setPassword( storedConfiguration, password );
+            ConfigGuideUtils.writeConfig( contextManager, storedConfiguration.newStoredConfiguration() );
             pwmRequest.outputJsonResult( RestResultBean.forSuccessMessage( pwmRequest, Message.Success_Unknown ) );
             pwmRequest.outputJsonResult( RestResultBean.forSuccessMessage( pwmRequest, Message.Success_Unknown ) );
             pwmRequest.invalidateSession();
             pwmRequest.invalidateSession();
         }
         }
@@ -591,15 +597,15 @@ public class ConfigGuideServlet extends ControlledPwmServlet
     {
     {
         final String profileID = "default";
         final String profileID = "default";
         final ConfigGuideBean configGuideBean = getBean( pwmRequest );
         final ConfigGuideBean configGuideBean = getBean( pwmRequest );
-        final StoredConfigurationImpl storedConfigurationImpl = ConfigGuideForm.generateStoredConfig( configGuideBean );
+        final StoredConfiguration storedConfiguration = ConfigGuideForm.generateStoredConfig( configGuideBean );
 
 
         final String key = pwmRequest.readParameterAsString( "key" );
         final String key = pwmRequest.readParameterAsString( "key" );
         final LinkedHashMap<String, Object> returnMap = new LinkedHashMap<>();
         final LinkedHashMap<String, Object> returnMap = new LinkedHashMap<>();
         final PwmSetting theSetting = PwmSetting.forKey( key );
         final PwmSetting theSetting = PwmSetting.forKey( key );
 
 
         final Object returnValue;
         final Object returnValue;
-        returnValue = storedConfigurationImpl.readSetting( theSetting, profileID ).toNativeObject();
-        returnMap.put( "isDefault", storedConfigurationImpl.isDefaultValue( theSetting, profileID ) );
+        returnValue = storedConfiguration.readSetting( theSetting, profileID ).toNativeObject();
+        returnMap.put( "isDefault", storedConfiguration.isDefaultValue( theSetting, profileID ) );
         returnMap.put( "key", key );
         returnMap.put( "key", key );
         returnMap.put( "category", theSetting.getCategory().toString() );
         returnMap.put( "category", theSetting.getCategory().toString() );
         returnMap.put( "syntax", theSetting.getSyntax().toString() );
         returnMap.put( "syntax", theSetting.getSyntax().toString() );
@@ -619,7 +625,7 @@ public class ConfigGuideServlet extends ControlledPwmServlet
         final String bodyString = pwmRequest.readRequestBodyAsString();
         final String bodyString = pwmRequest.readRequestBodyAsString();
         final PwmSetting setting = PwmSetting.forKey( key );
         final PwmSetting setting = PwmSetting.forKey( key );
         final ConfigGuideBean configGuideBean = getBean( pwmRequest );
         final ConfigGuideBean configGuideBean = getBean( pwmRequest );
-        final StoredConfigurationImpl storedConfigurationImpl = ConfigGuideForm.generateStoredConfig( configGuideBean );
+        final StoredConfiguration storedConfigurationImpl = ConfigGuideForm.generateStoredConfig( configGuideBean );
 
 
 
 
         final LinkedHashMap<String, Object> returnMap = new LinkedHashMap<>();
         final LinkedHashMap<String, Object> returnMap = new LinkedHashMap<>();
@@ -659,11 +665,11 @@ public class ConfigGuideServlet extends ControlledPwmServlet
             throws IOException, PwmUnrecoverableException
             throws IOException, PwmUnrecoverableException
     {
     {
         final ConfigGuideBean configGuideBean = getBean( pwmRequest );
         final ConfigGuideBean configGuideBean = getBean( pwmRequest );
-        final StoredConfigurationImpl storedConfigurationImpl = ConfigGuideForm.generateStoredConfig( configGuideBean );
+        final StoredConfiguration storedConfiguration = ConfigGuideForm.generateStoredConfig( configGuideBean );
 
 
         final LinkedHashMap<String, Object> returnMap = new LinkedHashMap<>( ConfigEditorServlet.generateSettingData(
         final LinkedHashMap<String, Object> returnMap = new LinkedHashMap<>( ConfigEditorServlet.generateSettingData(
                 pwmRequest.getPwmApplication(),
                 pwmRequest.getPwmApplication(),
-                storedConfigurationImpl,
+                storedConfiguration,
                 pwmRequest.getSessionLabel(),
                 pwmRequest.getSessionLabel(),
                 pwmRequest.getLocale()
                 pwmRequest.getLocale()
         )
         )

+ 19 - 13
server/src/main/java/password/pwm/http/servlet/configguide/ConfigGuideUtils.java

@@ -30,7 +30,10 @@ import password.pwm.PwmConstants;
 import password.pwm.config.Configuration;
 import password.pwm.config.Configuration;
 import password.pwm.config.stored.ConfigurationProperty;
 import password.pwm.config.stored.ConfigurationProperty;
 import password.pwm.config.stored.ConfigurationReader;
 import password.pwm.config.stored.ConfigurationReader;
-import password.pwm.config.stored.StoredConfigurationImpl;
+import password.pwm.config.stored.StoredConfiguration;
+import password.pwm.config.stored.StoredConfigurationFactory;
+import password.pwm.config.stored.StoredConfigurationModifier;
+import password.pwm.config.stored.StoredConfigurationUtil;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmException;
 import password.pwm.error.PwmException;
@@ -70,13 +73,14 @@ public class ConfigGuideUtils
     static void writeConfig(
     static void writeConfig(
             final ContextManager contextManager,
             final ContextManager contextManager,
             final ConfigGuideBean configGuideBean
             final ConfigGuideBean configGuideBean
-    ) throws PwmOperationalException, PwmUnrecoverableException
+    )
+            throws PwmOperationalException, PwmUnrecoverableException
     {
     {
-        final StoredConfigurationImpl storedConfiguration = ConfigGuideForm.generateStoredConfig( configGuideBean );
+        final StoredConfigurationModifier storedConfiguration = StoredConfigurationModifier.newModifier( ConfigGuideForm.generateStoredConfig( configGuideBean ) );
         final String configPassword = configGuideBean.getFormData().get( ConfigGuideFormField.PARAM_CONFIG_PASSWORD );
         final String configPassword = configGuideBean.getFormData().get( ConfigGuideFormField.PARAM_CONFIG_PASSWORD );
         if ( configPassword != null && configPassword.length() > 0 )
         if ( configPassword != null && configPassword.length() > 0 )
         {
         {
-            storedConfiguration.setPassword( configPassword );
+            StoredConfigurationUtil.setPassword( storedConfiguration, configPassword );
         }
         }
         else
         else
         {
         {
@@ -84,31 +88,33 @@ public class ConfigGuideUtils
         }
         }
 
 
         storedConfiguration.writeConfigProperty( ConfigurationProperty.CONFIG_IS_EDITABLE, "false" );
         storedConfiguration.writeConfigProperty( ConfigurationProperty.CONFIG_IS_EDITABLE, "false" );
-        ConfigGuideUtils.writeConfig( contextManager, storedConfiguration );
+        ConfigGuideUtils.writeConfig( contextManager, storedConfiguration.newStoredConfiguration() );
     }
     }
 
 
     static void writeConfig(
     static void writeConfig(
             final ContextManager contextManager,
             final ContextManager contextManager,
-            final StoredConfigurationImpl storedConfiguration
-    ) throws PwmOperationalException, PwmUnrecoverableException
+            final StoredConfiguration storedConfiguration
+    )
+            throws PwmOperationalException, PwmUnrecoverableException
     {
     {
         final ConfigurationReader configReader = contextManager.getConfigReader();
         final ConfigurationReader configReader = contextManager.getConfigReader();
         final PwmApplication pwmApplication = contextManager.getPwmApplication();
         final PwmApplication pwmApplication = contextManager.getPwmApplication();
 
 
         try
         try
         {
         {
+            final StoredConfigurationModifier modifier = StoredConfigurationModifier.newModifier( storedConfiguration );
             // add a random security key
             // add a random security key
-            storedConfiguration.initNewRandomSecurityKey();
+            StoredConfigurationUtil.initNewRandomSecurityKey( modifier );
 
 
-            configReader.saveConfiguration( storedConfiguration, pwmApplication, null );
+            configReader.saveConfiguration( modifier.newStoredConfiguration(), pwmApplication, null );
 
 
             contextManager.requestPwmApplicationRestart();
             contextManager.requestPwmApplicationRestart();
         }
         }
-        catch ( PwmException e )
+        catch ( final PwmException e )
         {
         {
             throw new PwmOperationalException( e.getErrorInformation() );
             throw new PwmOperationalException( e.getErrorInformation() );
         }
         }
-        catch ( Exception e )
+        catch ( final Exception e )
         {
         {
             final ErrorInformation errorInformation = new ErrorInformation( PwmError.ERROR_INVALID_CONFIG, "unable to save configuration: " + e.getLocalizedMessage() );
             final ErrorInformation errorInformation = new ErrorInformation( PwmError.ERROR_INVALID_CONFIG, "unable to save configuration: " + e.getLocalizedMessage() );
             throw new PwmOperationalException( errorInformation );
             throw new PwmOperationalException( errorInformation );
@@ -232,8 +238,8 @@ public class ConfigGuideUtils
             {
             {
                 try
                 try
                 {
                 {
-                    final StoredConfigurationImpl storedConfig = StoredConfigurationImpl.fromXml( uploadedFile );
-                    final List<String> configErrors = storedConfig.validateValues();
+                    final StoredConfiguration storedConfig = StoredConfigurationFactory.fromXml( uploadedFile );
+                    final List<String> configErrors = StoredConfigurationUtil.validateValues( storedConfig );
                     if ( configErrors != null && !configErrors.isEmpty() )
                     if ( configErrors != null && !configErrors.isEmpty() )
                     {
                     {
                         throw new PwmOperationalException( new ErrorInformation( PwmError.CONFIG_FORMAT_ERROR, configErrors.get( 0 ) ) );
                         throw new PwmOperationalException( new ErrorInformation( PwmError.CONFIG_FORMAT_ERROR, configErrors.get( 0 ) ) );

+ 9 - 9
server/src/main/java/password/pwm/http/servlet/configmanager/ConfigManagerCertificatesServlet.java

@@ -28,9 +28,8 @@ import password.pwm.config.Configuration;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.PwmSettingSyntax;
 import password.pwm.config.PwmSettingSyntax;
 import password.pwm.config.StoredValue;
 import password.pwm.config.StoredValue;
-import password.pwm.config.stored.StoredConfigReference;
-import password.pwm.config.stored.StoredConfigurationImpl;
-import password.pwm.config.stored.StoredConfigurationUtil;
+import password.pwm.config.stored.StoredConfigItemKey;
+import password.pwm.config.stored.StoredConfiguration;
 import password.pwm.config.value.data.ActionConfiguration;
 import password.pwm.config.value.data.ActionConfiguration;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.http.HttpMethod;
 import password.pwm.http.HttpMethod;
@@ -54,6 +53,7 @@ import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Collections;
 import java.util.List;
 import java.util.List;
+import java.util.Set;
 
 
 @WebServlet(
 @WebServlet(
         name = "ConfigManagerCertificateServlet",
         name = "ConfigManagerCertificateServlet",
@@ -115,14 +115,14 @@ public class ConfigManagerCertificatesServlet extends AbstractPwmServlet
 
 
     List<CertificateDebugDataItem> makeCertificateDebugData( final Configuration configuration ) throws PwmUnrecoverableException
     List<CertificateDebugDataItem> makeCertificateDebugData( final Configuration configuration ) throws PwmUnrecoverableException
     {
     {
-        final StoredConfigurationImpl storedConfiguration = configuration.getStoredConfiguration();
-        final List<StoredConfigReference> modifiedSettings = StoredConfigurationUtil.modifiedSettings( storedConfiguration );
+        final StoredConfiguration storedConfiguration = configuration.getStoredConfiguration();
+        final Set<StoredConfigItemKey> modifiedSettings = storedConfiguration.modifiedItems();
 
 
         final List<CertificateDebugDataItem> certificateDebugDataItems = new ArrayList<>();
         final List<CertificateDebugDataItem> certificateDebugDataItems = new ArrayList<>();
 
 
-        for ( final StoredConfigReference ref : modifiedSettings )
+        for ( final StoredConfigItemKey ref : modifiedSettings )
         {
         {
-            if ( ref.getRecordType() == StoredConfigReference.RecordType.SETTING )
+            if ( ref.getRecordType() == StoredConfigItemKey.RecordType.SETTING )
             {
             {
                 final PwmSetting pwmSetting = PwmSetting.forKey( ref.getRecordID() );
                 final PwmSetting pwmSetting = PwmSetting.forKey( ref.getRecordID() );
                 if ( pwmSetting.getSyntax() == PwmSettingSyntax.X509CERT )
                 if ( pwmSetting.getSyntax() == PwmSettingSyntax.X509CERT )
@@ -134,7 +134,7 @@ public class ConfigManagerCertificatesServlet extends AbstractPwmServlet
                     }
                     }
                     else
                     else
                     {
                     {
-                        storedValue = storedConfiguration.readSetting( pwmSetting );
+                        storedValue = storedConfiguration.readSetting( pwmSetting, null );
                     }
                     }
                     final X509Certificate[] arrayCerts = ( X509Certificate[] ) storedValue.toNativeObject();
                     final X509Certificate[] arrayCerts = ( X509Certificate[] ) storedValue.toNativeObject();
                     final List<X509Certificate> certificates = arrayCerts == null ? Collections.emptyList() : Arrays.asList( arrayCerts );
                     final List<X509Certificate> certificates = arrayCerts == null ? Collections.emptyList() : Arrays.asList( arrayCerts );
@@ -149,7 +149,7 @@ public class ConfigManagerCertificatesServlet extends AbstractPwmServlet
                     }
                     }
                     else
                     else
                     {
                     {
-                        storedValue = storedConfiguration.readSetting( pwmSetting );
+                        storedValue = storedConfiguration.readSetting( pwmSetting, null );
                     }
                     }
                     final List<ActionConfiguration> actionConfigurations = ( List ) storedValue.toNativeObject();
                     final List<ActionConfiguration> actionConfigurations = ( List ) storedValue.toNativeObject();
                     for ( final ActionConfiguration actionConfiguration : actionConfigurations )
                     for ( final ActionConfiguration actionConfiguration : actionConfigurations )

+ 56 - 26
server/src/main/java/password/pwm/http/servlet/configmanager/ConfigManagerLoginServlet.java

@@ -32,7 +32,7 @@ import password.pwm.config.PwmSetting;
 import password.pwm.config.stored.ConfigurationProperty;
 import password.pwm.config.stored.ConfigurationProperty;
 import password.pwm.config.stored.ConfigurationReader;
 import password.pwm.config.stored.ConfigurationReader;
 import password.pwm.config.stored.StoredConfiguration;
 import password.pwm.config.stored.StoredConfiguration;
-import password.pwm.config.stored.StoredConfigurationImpl;
+import password.pwm.config.stored.StoredConfigurationUtil;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.error.PwmUnrecoverableException;
@@ -60,10 +60,12 @@ import javax.servlet.annotation.WebServlet;
 import java.io.IOException;
 import java.io.IOException;
 import java.io.Serializable;
 import java.io.Serializable;
 import java.time.Instant;
 import java.time.Instant;
+import java.time.temporal.ChronoUnit;
 import java.util.ArrayList;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Collections;
 import java.util.List;
 import java.util.List;
+import java.util.Optional;
 
 
 @WebServlet(
 @WebServlet(
         name = "ConfigManagerLogin",
         name = "ConfigManagerLogin",
@@ -133,12 +135,12 @@ public class ConfigManagerLoginServlet extends AbstractPwmServlet
     {
     {
         final PwmApplication pwmApplication = pwmRequest.getPwmApplication();
         final PwmApplication pwmApplication = pwmRequest.getPwmApplication();
         final ConfigurationReader runningConfigReader = ContextManager.getContextManager( pwmRequest.getHttpServletRequest().getSession() ).getConfigReader();
         final ConfigurationReader runningConfigReader = ContextManager.getContextManager( pwmRequest.getHttpServletRequest().getSession() ).getConfigReader();
-        final StoredConfigurationImpl storedConfig = runningConfigReader.getStoredConfiguration();
+        final StoredConfiguration storedConfig = runningConfigReader.getStoredConfiguration();
 
 
         final String password = pwmRequest.readParameterAsString( "password" );
         final String password = pwmRequest.readParameterAsString( "password" );
         if ( !StringUtil.isEmpty( password ) )
         if ( !StringUtil.isEmpty( password ) )
         {
         {
-            if ( storedConfig.verifyPassword( password, pwmRequest.getConfig() ) )
+            if ( StoredConfigurationUtil.verifyPassword( storedConfig, password ) )
             {
             {
                 LOGGER.trace( pwmRequest, () -> "valid configuration password accepted" );
                 LOGGER.trace( pwmRequest, () -> "valid configuration password accepted" );
                 updateLoginHistory( pwmRequest, pwmRequest.getUserInfoIfLoggedIn(), true );
                 updateLoginHistory( pwmRequest, pwmRequest.getUserInfoIfLoggedIn(), true );
@@ -300,11 +302,9 @@ public class ConfigManagerLoginServlet extends AbstractPwmServlet
 
 
         if ( persistentSeconds > 0 )
         if ( persistentSeconds > 0 )
         {
         {
-            final TimeDuration persistenceDuration = TimeDuration.of( persistentSeconds, TimeDuration.Unit.SECONDS );
-            final Instant expirationDate = persistenceDuration.incrementFromInstant( Instant.now() );
-            final StoredConfigurationImpl storedConfig = pwmRequest.getConfig().getStoredConfiguration();
+            final StoredConfiguration storedConfig = pwmRequest.getConfig().getStoredConfiguration();
             final String persistentLoginValue = makePersistentLoginPassword( pwmRequest, storedConfig );
             final String persistentLoginValue = makePersistentLoginPassword( pwmRequest, storedConfig );
-            final PersistentLoginInfo persistentLoginInfo = new PersistentLoginInfo( expirationDate, persistentLoginValue );
+            final PersistentLoginInfo persistentLoginInfo = new PersistentLoginInfo( Instant.now(), persistentLoginValue );
             final String cookieValue = pwmRequest.getPwmApplication().getSecureService().encryptObjectToString( persistentLoginInfo );
             final String cookieValue = pwmRequest.getPwmApplication().getSecureService().encryptObjectToString( persistentLoginInfo );
             pwmRequest.getPwmResponse().writeCookie(
             pwmRequest.getPwmResponse().writeCookie(
                     COOKIE_NAME,
                     COOKIE_NAME,
@@ -312,10 +312,7 @@ public class ConfigManagerLoginServlet extends AbstractPwmServlet
                     persistentSeconds,
                     persistentSeconds,
                     COOKIE_PATH
                     COOKIE_PATH
             );
             );
-            LOGGER.debug( pwmRequest, () -> "set persistent config login cookie (expires "
-                    + JavaHelper.toIsoDate( expirationDate )
-                    + ")"
-            );
+            LOGGER.debug( pwmRequest, () -> "issued persistent config login cookie" );
         }
         }
     }
     }
 
 
@@ -330,7 +327,7 @@ public class ConfigManagerLoginServlet extends AbstractPwmServlet
         }
         }
 
 
         final ConfigurationReader runningConfigReader = ContextManager.getContextManager( pwmRequest.getHttpServletRequest().getSession() ).getConfigReader();
         final ConfigurationReader runningConfigReader = ContextManager.getContextManager( pwmRequest.getHttpServletRequest().getSession() ).getConfigReader();
-        final StoredConfigurationImpl storedConfig = runningConfigReader.getStoredConfiguration();
+        final StoredConfiguration storedConfig = runningConfigReader.getStoredConfiguration();
 
 
         try
         try
         {
         {
@@ -338,25 +335,41 @@ public class ConfigManagerLoginServlet extends AbstractPwmServlet
             if ( !StringUtil.isEmpty( cookieValue ) )
             if ( !StringUtil.isEmpty( cookieValue ) )
             {
             {
                 final PersistentLoginInfo persistentLoginInfo = pwmRequest.getPwmApplication().getSecureService().decryptObject( cookieValue, PersistentLoginInfo.class );
                 final PersistentLoginInfo persistentLoginInfo = pwmRequest.getPwmApplication().getSecureService().decryptObject( cookieValue, PersistentLoginInfo.class );
-                if ( persistentLoginInfo != null )
+                if ( persistentLoginInfo != null && persistentLoginInfo.getIssueTimestamp() != null )
                 {
                 {
-                    if ( persistentLoginInfo.getExpireDate().isAfter( Instant.now() ) )
+                    final int maxLoginSeconds = figureMaxLoginSeconds( pwmRequest );
+                    final TimeDuration cookieAge = TimeDuration.fromCurrent( persistentLoginInfo.getIssueTimestamp() );
+
+                    if ( cookieAge.isShorterThan( TimeDuration.of( maxLoginSeconds, TimeDuration.Unit.SECONDS ) ) )
                     {
                     {
                         final String persistentLoginPassword = makePersistentLoginPassword( pwmRequest, storedConfig );
                         final String persistentLoginPassword = makePersistentLoginPassword( pwmRequest, storedConfig );
                         if ( StringUtil.nullSafeEquals( persistentLoginPassword, persistentLoginInfo.getPassword() ) )
                         if ( StringUtil.nullSafeEquals( persistentLoginPassword, persistentLoginInfo.getPassword() ) )
                         {
                         {
-                            LOGGER.debug( pwmRequest, () -> "accepting persistent config login from cookie (expires "
-                                    + JavaHelper.toIsoDate( persistentLoginInfo.getExpireDate() )
+                            final Instant expireTime = Instant.now().plus( maxLoginSeconds, ChronoUnit.SECONDS );
+                            LOGGER.debug( pwmRequest, () -> "accepting persistent config login from cookie (expires at "
+                                    + expireTime.toString()
                                     + ")"
                                     + ")"
                             );
                             );
 
 
                             final ConfigManagerBean configManagerBean = pwmRequest.getPwmApplication().getSessionStateService().getBean( pwmRequest, ConfigManagerBean.class );
                             final ConfigManagerBean configManagerBean = pwmRequest.getPwmApplication().getSessionStateService().getBean( pwmRequest, ConfigManagerBean.class );
                             configManagerBean.setPasswordVerified( true );
                             configManagerBean.setPasswordVerified( true );
                         }
                         }
+                        else
+                        {
+                            LOGGER.debug( pwmRequest, () -> "discarding persistent login cookie with incorrect password value" );
+                            pwmRequest.getPwmResponse().removeCookie( COOKIE_NAME, COOKIE_PATH );
+                        }
                     }
                     }
-
+                    else
+                    {
+                        LOGGER.debug( pwmRequest, () -> "removing expired (" + cookieAge.asCompactString() + ") persistent config login cookie" );
+                        pwmRequest.getPwmResponse().removeCookie( COOKIE_NAME, COOKIE_PATH );
+                    }
+                }
+                else
+                {
+                    LOGGER.debug( pwmRequest, () -> "discarding invalid persistent login cookie " );
                     pwmRequest.getPwmResponse().removeCookie( COOKIE_NAME, COOKIE_PATH );
                     pwmRequest.getPwmResponse().removeCookie( COOKIE_NAME, COOKIE_PATH );
-                    LOGGER.debug( pwmRequest, () -> "removing non-working persistent config login cookie" );
                 }
                 }
             }
             }
         }
         }
@@ -370,8 +383,8 @@ public class ConfigManagerLoginServlet extends AbstractPwmServlet
     @Value
     @Value
     private static class PersistentLoginInfo implements Serializable
     private static class PersistentLoginInfo implements Serializable
     {
     {
-        @SerializedName( "e" )
-        private Instant expireDate;
+        @SerializedName( "i" )
+        private Instant issueTimestamp;
 
 
         @SerializedName( "p" )
         @SerializedName( "p" )
         private String password;
         private String password;
@@ -393,25 +406,42 @@ public class ConfigManagerLoginServlet extends AbstractPwmServlet
             throws PwmUnrecoverableException
             throws PwmUnrecoverableException
     {
     {
         final int hashChars = 32;
         final int hashChars = 32;
-        String hashValue = storedConfig.readConfigProperty( ConfigurationProperty.PASSWORD_HASH );
 
 
-        if ( PwmApplicationMode.RUNNING == pwmRequest.getPwmApplication().getApplicationMode() )
+        if ( !persistentLoginEnabled( pwmRequest ) )
         {
         {
-            final PwmSession pwmSession = pwmRequest.getPwmSession();
-            hashValue += pwmSession.getUserInfo().getUserIdentity().toDelimitedKey();
+            throw PwmUnrecoverableException.newException( PwmError.ERROR_INTERNAL, "persistent login not enabled" );
         }
         }
 
 
+        final PwmSession pwmSession = pwmRequest.getPwmSession();
+        final String configPasswordHash = storedConfig.readConfigProperty( ConfigurationProperty.PASSWORD_HASH )
+                .orElseThrow( () -> PwmUnrecoverableException.newException( PwmError.ERROR_INTERNAL, "missing config password" ) );
+
+        final String hashValue = configPasswordHash + pwmSession.getUserInfo().getUserIdentity().toDelimitedKey();
+
         return StringUtil.truncate( SecureEngine.hash( hashValue, PwmHashAlgorithm.SHA512 ), hashChars );
         return StringUtil.truncate( SecureEngine.hash( hashValue, PwmHashAlgorithm.SHA512 ), hashChars );
     }
     }
 
 
 
 
     private static boolean persistentLoginEnabled(
     private static boolean persistentLoginEnabled(
             final PwmRequest pwmRequest
             final PwmRequest pwmRequest
-    )
+    ) throws PwmUnrecoverableException
     {
     {
+        if ( PwmApplicationMode.RUNNING != pwmRequest.getPwmApplication().getApplicationMode() )
+        {
+            LOGGER.debug( pwmRequest, () -> "app not in running mode, persistent login not possible." );
+            return false;
+        }
+
         if ( pwmRequest.getConfig().isDefaultValue( PwmSetting.PWM_SECURITY_KEY ) )
         if ( pwmRequest.getConfig().isDefaultValue( PwmSetting.PWM_SECURITY_KEY ) )
         {
         {
-            LOGGER.debug( pwmRequest, () -> "security not available, persistent login not possible." );
+            LOGGER.debug( pwmRequest, () -> "security key not available, persistent login not possible." );
+            return false;
+        }
+
+        final Optional<String> configPasswordHash = pwmRequest.getConfig().getStoredConfiguration().readConfigProperty( ConfigurationProperty.PASSWORD_HASH );
+        if ( !configPasswordHash.isPresent() )
+        {
+            LOGGER.debug( pwmRequest, () -> "config password is not present, persistent login not possible." );
             return false;
             return false;
         }
         }
 
 

+ 25 - 40
server/src/main/java/password/pwm/http/servlet/configmanager/ConfigManagerServlet.java

@@ -29,7 +29,10 @@ import password.pwm.PwmApplication;
 import password.pwm.PwmConstants;
 import password.pwm.PwmConstants;
 import password.pwm.config.stored.ConfigurationProperty;
 import password.pwm.config.stored.ConfigurationProperty;
 import password.pwm.config.stored.ConfigurationReader;
 import password.pwm.config.stored.ConfigurationReader;
-import password.pwm.config.stored.StoredConfigurationImpl;
+import password.pwm.config.stored.StoredConfiguration;
+import password.pwm.config.stored.StoredConfigurationFactory;
+import password.pwm.config.stored.StoredConfigurationModifier;
+import password.pwm.config.stored.StoredConfigurationUtil;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmException;
 import password.pwm.error.PwmException;
@@ -52,10 +55,6 @@ import password.pwm.http.servlet.configguide.ConfigGuideUtils;
 import password.pwm.i18n.Admin;
 import password.pwm.i18n.Admin;
 import password.pwm.i18n.Config;
 import password.pwm.i18n.Config;
 import password.pwm.i18n.Display;
 import password.pwm.i18n.Display;
-import password.pwm.svc.PwmService;
-import password.pwm.svc.event.AuditEvent;
-import password.pwm.svc.event.AuditRecord;
-import password.pwm.svc.event.AuditRecordFactory;
 import password.pwm.util.LDAPPermissionCalculator;
 import password.pwm.util.LDAPPermissionCalculator;
 import password.pwm.util.i18n.LocaleHelper;
 import password.pwm.util.i18n.LocaleHelper;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.JavaHelper;
@@ -72,6 +71,7 @@ import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashMap;
 import java.util.LinkedHashMap;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.List;
+import java.util.Map;
 import java.util.zip.ZipOutputStream;
 import java.util.zip.ZipOutputStream;
 
 
 @WebServlet(
 @WebServlet(
@@ -205,7 +205,7 @@ public class ConfigManagerServlet extends AbstractPwmServlet
         pwmRequest.setAttribute(
         pwmRequest.setAttribute(
                 PwmRequestAttribute.ConfigHasPassword,
                 PwmRequestAttribute.ConfigHasPassword,
                 LocaleHelper.booleanString(
                 LocaleHelper.booleanString(
-                        configurationReader.getStoredConfiguration().hasPassword(),
+                        StoredConfigurationUtil.hasPassword( configurationReader.getStoredConfiguration() ),
                         pwmRequest.getLocale(),
                         pwmRequest.getLocale(),
                         pwmRequest.getConfig()
                         pwmRequest.getConfig()
                 )
                 )
@@ -251,8 +251,8 @@ public class ConfigManagerServlet extends AbstractPwmServlet
 
 
         try
         try
         {
         {
-            final StoredConfigurationImpl storedConfiguration = readCurrentConfiguration( pwmRequest );
-            if ( !storedConfiguration.hasPassword() )
+            final StoredConfiguration storedConfiguration = readCurrentConfiguration( pwmRequest );
+            if ( !StoredConfigurationUtil.hasPassword( storedConfiguration ) )
             {
             {
                 final ErrorInformation errorInfo = new ErrorInformation( PwmError.CONFIG_FORMAT_ERROR, null, new String[]
                 final ErrorInformation errorInfo = new ErrorInformation( PwmError.CONFIG_FORMAT_ERROR, null, new String[]
                         {
                         {
@@ -265,10 +265,11 @@ public class ConfigManagerServlet extends AbstractPwmServlet
                 return;
                 return;
             }
             }
 
 
-            storedConfiguration.writeConfigProperty( ConfigurationProperty.CONFIG_IS_EDITABLE, "false" );
-            saveConfiguration( pwmRequest, storedConfiguration );
+            final StoredConfigurationModifier modifiedConfig = StoredConfigurationModifier.newModifier( storedConfiguration );
+            modifiedConfig.writeConfigProperty( ConfigurationProperty.CONFIG_IS_EDITABLE, "false" );
+            saveConfiguration( pwmRequest, modifiedConfig.newStoredConfiguration() );
             final ConfigManagerBean configManagerBean = pwmRequest.getPwmApplication().getSessionStateService().getBean( pwmRequest, ConfigManagerBean.class );
             final ConfigManagerBean configManagerBean = pwmRequest.getPwmApplication().getSessionStateService().getBean( pwmRequest, ConfigManagerBean.class );
-            configManagerBean.setConfiguration( null );
+            configManagerBean.setStoredConfiguration( null );
         }
         }
         catch ( PwmException e )
         catch ( PwmException e )
         {
         {
@@ -293,12 +294,12 @@ public class ConfigManagerServlet extends AbstractPwmServlet
 
 
     public static void saveConfiguration(
     public static void saveConfiguration(
             final PwmRequest pwmRequest,
             final PwmRequest pwmRequest,
-            final StoredConfigurationImpl storedConfiguration
+            final StoredConfiguration storedConfiguration
     )
     )
             throws PwmUnrecoverableException
             throws PwmUnrecoverableException
     {
     {
         {
         {
-            final List<String> errorStrings = storedConfiguration.validateValues();
+            final List<String> errorStrings = StoredConfigurationUtil.validateValues( storedConfiguration );
             if ( errorStrings != null && !errorStrings.isEmpty() )
             if ( errorStrings != null && !errorStrings.isEmpty() )
             {
             {
                 final String errorString = errorStrings.get( 0 );
                 final String errorString = errorStrings.get( 0 );
@@ -319,30 +320,17 @@ public class ConfigManagerServlet extends AbstractPwmServlet
                     pwmRequest.getSessionLabel()
                     pwmRequest.getSessionLabel()
             );
             );
 
 
-            final PwmApplication pwmApplication = pwmRequest.getPwmApplication();
-            if ( pwmApplication.getAuditManager() != null && pwmApplication.getAuditManager().status() == PwmService.STATUS.OPEN )
-            {
-                final String modifyMessage = "Configuration Changes: " + storedConfiguration.changeLogAsDebugString( PwmConstants.DEFAULT_LOCALE, false );
-                final AuditRecord auditRecord = new AuditRecordFactory( pwmApplication ).createUserAuditRecord(
-                        AuditEvent.MODIFY_CONFIGURATION,
-                        pwmRequest.getUserInfoIfLoggedIn(),
-                        pwmRequest.getSessionLabel(),
-                        modifyMessage
-                );
-                pwmApplication.getAuditManager().submit( auditRecord );
-            }
-
             contextManager.requestPwmApplicationRestart();
             contextManager.requestPwmApplicationRestart();
         }
         }
         catch ( Exception e )
         catch ( Exception e )
         {
         {
             final String errorString = "error saving file: " + e.getMessage();
             final String errorString = "error saving file: " + e.getMessage();
-            LOGGER.error( pwmRequest, errorString );
+            LOGGER.error( pwmRequest, errorString, e );
             throw new PwmUnrecoverableException( new ErrorInformation( PwmError.CONFIG_FORMAT_ERROR, null, new String[]
             throw new PwmUnrecoverableException( new ErrorInformation( PwmError.CONFIG_FORMAT_ERROR, null, new String[]
                     {
                     {
                             errorString,
                             errorString,
                     }
                     }
-            ) );
+            ), e );
         }
         }
 
 
     }
     }
@@ -361,11 +349,11 @@ public class ConfigManagerServlet extends AbstractPwmServlet
 
 
         try
         try
         {
         {
-            final StoredConfigurationImpl storedConfiguration = readCurrentConfiguration( pwmRequest );
+            final StoredConfiguration storedConfiguration = readCurrentConfiguration( pwmRequest );
             final OutputStream responseWriter = resp.getOutputStream();
             final OutputStream responseWriter = resp.getOutputStream();
             resp.setHeader( HttpHeader.ContentDisposition, "attachment;filename=" + PwmConstants.DEFAULT_CONFIG_FILE_FILENAME );
             resp.setHeader( HttpHeader.ContentDisposition, "attachment;filename=" + PwmConstants.DEFAULT_CONFIG_FILE_FILENAME );
             resp.setContentType( HttpContentType.xml );
             resp.setContentType( HttpContentType.xml );
-            storedConfiguration.toXml( responseWriter );
+            StoredConfigurationFactory.toXml( storedConfiguration, responseWriter );
             responseWriter.close();
             responseWriter.close();
         }
         }
         catch ( Exception e )
         catch ( Exception e )
@@ -392,28 +380,25 @@ public class ConfigManagerServlet extends AbstractPwmServlet
     }
     }
 
 
 
 
-    public static StoredConfigurationImpl readCurrentConfiguration( final PwmRequest pwmRequest )
+    public static StoredConfiguration readCurrentConfiguration( final PwmRequest pwmRequest )
             throws PwmUnrecoverableException
             throws PwmUnrecoverableException
     {
     {
-        final ContextManager contextManager = ContextManager.getContextManager( pwmRequest.getHttpServletRequest().getSession() );
-        final ConfigurationReader runningConfigReader = contextManager.getConfigReader();
-        final StoredConfigurationImpl runningConfig = runningConfigReader.getStoredConfiguration();
-        return StoredConfigurationImpl.copy( runningConfig );
+        return pwmRequest.getConfig().getStoredConfiguration();
     }
     }
 
 
     private void showSummary( final PwmRequest pwmRequest )
     private void showSummary( final PwmRequest pwmRequest )
             throws IOException, ServletException, PwmUnrecoverableException
             throws IOException, ServletException, PwmUnrecoverableException
     {
     {
-        final StoredConfigurationImpl storedConfiguration = readCurrentConfiguration( pwmRequest );
-        final LinkedHashMap<String, Object> outputMap = new LinkedHashMap<>( storedConfiguration.toOutputMap( pwmRequest.getLocale() ) );
-        pwmRequest.setAttribute( PwmRequestAttribute.ConfigurationSummaryOutput, outputMap );
+        final StoredConfiguration storedConfiguration = readCurrentConfiguration( pwmRequest );
+        final Map<String, String> outputMap = StoredConfigurationUtil.makeDebugMap( storedConfiguration, storedConfiguration.modifiedItems(), pwmRequest.getLocale() );
+        pwmRequest.setAttribute( PwmRequestAttribute.ConfigurationSummaryOutput, new LinkedHashMap<>( outputMap ) );
         pwmRequest.forwardToJsp( JspUrl.CONFIG_MANAGER_EDITOR_SUMMARY );
         pwmRequest.forwardToJsp( JspUrl.CONFIG_MANAGER_EDITOR_SUMMARY );
     }
     }
 
 
     private void showPermissions( final PwmRequest pwmRequest )
     private void showPermissions( final PwmRequest pwmRequest )
             throws IOException, ServletException, PwmUnrecoverableException
             throws IOException, ServletException, PwmUnrecoverableException
     {
     {
-        final StoredConfigurationImpl storedConfiguration = readCurrentConfiguration( pwmRequest );
+        final StoredConfiguration storedConfiguration = readCurrentConfiguration( pwmRequest );
         final LDAPPermissionCalculator ldapPermissionCalculator = new LDAPPermissionCalculator( storedConfiguration );
         final LDAPPermissionCalculator ldapPermissionCalculator = new LDAPPermissionCalculator( storedConfiguration );
         pwmRequest.setAttribute( PwmRequestAttribute.LdapPermissionItems, ldapPermissionCalculator );
         pwmRequest.setAttribute( PwmRequestAttribute.LdapPermissionItems, ldapPermissionCalculator );
         pwmRequest.forwardToJsp( JspUrl.CONFIG_MANAGER_PERMISSIONS );
         pwmRequest.forwardToJsp( JspUrl.CONFIG_MANAGER_PERMISSIONS );
@@ -434,7 +419,7 @@ public class ConfigManagerServlet extends AbstractPwmServlet
         try
         try
         {
         {
 
 
-            final StoredConfigurationImpl storedConfiguration = readCurrentConfiguration( pwmRequest );
+            final StoredConfiguration storedConfiguration = readCurrentConfiguration( pwmRequest );
             final LDAPPermissionCalculator ldapPermissionCalculator = new LDAPPermissionCalculator( storedConfiguration );
             final LDAPPermissionCalculator ldapPermissionCalculator = new LDAPPermissionCalculator( storedConfiguration );
 
 
             for ( final LDAPPermissionCalculator.PermissionRecord permissionRecord : ldapPermissionCalculator.getPermissionRecords() )
             for ( final LDAPPermissionCalculator.PermissionRecord permissionRecord : ldapPermissionCalculator.getPermissionRecords() )

+ 42 - 21
server/src/main/java/password/pwm/http/servlet/configmanager/DebugItemGenerator.java

@@ -31,7 +31,11 @@ import password.pwm.PwmConstants;
 import password.pwm.bean.SessionLabel;
 import password.pwm.bean.SessionLabel;
 import password.pwm.bean.UserIdentity;
 import password.pwm.bean.UserIdentity;
 import password.pwm.config.Configuration;
 import password.pwm.config.Configuration;
-import password.pwm.config.stored.StoredConfigurationImpl;
+import password.pwm.config.StoredValue;
+import password.pwm.config.stored.StoredConfigItemKey;
+import password.pwm.config.stored.StoredConfiguration;
+import password.pwm.config.stored.StoredConfigurationFactory;
+import password.pwm.config.stored.StoredConfigurationUtil;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.health.HealthRecord;
 import password.pwm.health.HealthRecord;
 import password.pwm.http.ContextManager;
 import password.pwm.http.ContextManager;
@@ -129,9 +133,8 @@ public class DebugItemGenerator
         this.pwmApplication = pwmApplication;
         this.pwmApplication = pwmApplication;
         this.sessionLabel = sessionLabel;
         this.sessionLabel = sessionLabel;
 
 
-        final StoredConfigurationImpl storedConfiguration = StoredConfigurationImpl.copy( pwmApplication.getConfig().getStoredConfiguration() );
-        storedConfiguration.resetAllPasswordValues( "value removed from " + getFilenameBase() + " configuration export" );
-        this.obfuscatedConfiguration = new Configuration( storedConfiguration );
+        final StoredConfiguration obfuscatedStoredConfig = StoredConfigurationUtil.copyConfigAndBlankAllPasswords( pwmApplication.getConfig().getStoredConfiguration() );
+        this.obfuscatedConfiguration = new Configuration( obfuscatedStoredConfig );
     }
     }
 
 
     private String getFilenameBase()
     private String getFilenameBase()
@@ -212,8 +215,20 @@ public class DebugItemGenerator
         @Override
         @Override
         public void outputItem( final DebugItemInput debugItemInput, final OutputStream outputStream ) throws Exception
         public void outputItem( final DebugItemInput debugItemInput, final OutputStream outputStream ) throws Exception
         {
         {
-            final StoredConfigurationImpl storedConfiguration = debugItemInput.getObfuscatedConfiguration().getStoredConfiguration();
-            final String jsonOutput = JsonUtil.serialize( storedConfiguration.toJsonDebugObject(), JsonUtil.Flag.PrettyPrint );
+            final StoredConfiguration storedConfiguration = debugItemInput.getObfuscatedConfiguration().getStoredConfiguration();
+            final TreeMap<String, Object> outputObject = new TreeMap<>();
+
+            for ( final StoredConfigItemKey storedConfigItemKey : storedConfiguration.modifiedItems() )
+            {
+                if ( storedConfigItemKey.getRecordType() == StoredConfigItemKey.RecordType.SETTING )
+                {
+                    final String key = storedConfigItemKey.getLabel( PwmConstants.DEFAULT_LOCALE );
+                    final StoredValue value = storedConfiguration.readSetting( storedConfigItemKey.toPwmSetting(), storedConfigItemKey.getProfileID() );
+                    outputObject.put( key, value );
+                }
+            }
+
+            final String jsonOutput = JsonUtil.serializeMap( outputObject, JsonUtil.Flag.PrettyPrint );
             outputStream.write( jsonOutput.getBytes( PwmConstants.DEFAULT_CHARSET ) );
             outputStream.write( jsonOutput.getBytes( PwmConstants.DEFAULT_CHARSET ) );
         }
         }
     }
     }
@@ -229,7 +244,8 @@ public class DebugItemGenerator
         @Override
         @Override
         public void outputItem( final DebugItemInput debugItemInput, final OutputStream outputStream ) throws Exception
         public void outputItem( final DebugItemInput debugItemInput, final OutputStream outputStream ) throws Exception
         {
         {
-            final StoredConfigurationImpl storedConfiguration = debugItemInput.getObfuscatedConfiguration().getStoredConfiguration();
+            final Locale locale = PwmConstants.DEFAULT_LOCALE;
+            final StoredConfiguration storedConfiguration = debugItemInput.getObfuscatedConfiguration().getStoredConfiguration();
 
 
             final StringWriter writer = new StringWriter();
             final StringWriter writer = new StringWriter();
             writer.write( "Configuration Debug Output for "
             writer.write( "Configuration Debug Output for "
@@ -239,19 +255,21 @@ public class DebugItemGenerator
             writer.write( "This file is " + PwmConstants.DEFAULT_CHARSET.displayName() + " encoded\n" );
             writer.write( "This file is " + PwmConstants.DEFAULT_CHARSET.displayName() + " encoded\n" );
 
 
             writer.write( "\n" );
             writer.write( "\n" );
-            final Map<String, String> modifiedSettings = new TreeMap<>(
-                    storedConfiguration.getModifiedSettingDebugValues( LOCALE, true )
-            );
+            final Set<StoredConfigItemKey> modifiedSettings = storedConfiguration.modifiedItems();
 
 
-            for ( final Map.Entry<String, String> entry : modifiedSettings.entrySet() )
+            for ( final StoredConfigItemKey storedConfigItemKey : modifiedSettings )
             {
             {
-                final String key = entry.getKey();
-                final String value = entry.getValue();
-                writer.write( ">> Setting > " + key );
-                writer.write( "\n" );
-                writer.write( value );
-                writer.write( "\n" );
-                writer.write( "\n" );
+                if ( storedConfigItemKey.getRecordType() == StoredConfigItemKey.RecordType.SETTING )
+                {
+                    final String key = storedConfigItemKey.toPwmSetting().toMenuLocationDebug( storedConfigItemKey.getProfileID(), locale );
+                    final String value = storedConfiguration.readSetting( storedConfigItemKey.toPwmSetting(), storedConfigItemKey.getProfileID() ).toDebugString( locale );
+                    writer.write( ">> Setting > " + key );
+                    writer.write( "\n" );
+                    writer.write( value );
+                    writer.write( "\n" );
+                    writer.write( "\n" );
+
+                }
             }
             }
 
 
             outputStream.write( writer.toString().getBytes( PwmConstants.DEFAULT_CHARSET ) );
             outputStream.write( writer.toString().getBytes( PwmConstants.DEFAULT_CHARSET ) );
@@ -269,11 +287,14 @@ public class DebugItemGenerator
         @Override
         @Override
         public void outputItem( final DebugItemInput debugItemInput, final OutputStream outputStream ) throws Exception
         public void outputItem( final DebugItemInput debugItemInput, final OutputStream outputStream ) throws Exception
         {
         {
-            final StoredConfigurationImpl storedConfiguration = debugItemInput.getObfuscatedConfiguration().getStoredConfiguration();
+            final StoredConfiguration storedConfiguration = debugItemInput.getObfuscatedConfiguration().getStoredConfiguration();
 
 
             // temporary output stream required because .toXml closes stream.
             // temporary output stream required because .toXml closes stream.
             final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
             final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
-            storedConfiguration.toXml( byteArrayOutputStream );
+            final StoredConfigurationFactory.OutputSettings outputSettings = StoredConfigurationFactory.OutputSettings.builder()
+                    .mode( StoredConfigurationFactory.OutputSettings.SecureOutputMode.STRIPPED )
+                    .build();
+            StoredConfigurationFactory.toXml( storedConfiguration, byteArrayOutputStream, outputSettings );
             outputStream.write( byteArrayOutputStream.toByteArray() );        }
             outputStream.write( byteArrayOutputStream.toByteArray() );        }
     }
     }
 
 
@@ -576,7 +597,7 @@ public class DebugItemGenerator
         public void outputItem( final DebugItemInput debugItemInput, final OutputStream outputStream ) throws Exception
         public void outputItem( final DebugItemInput debugItemInput, final OutputStream outputStream ) throws Exception
         {
         {
 
 
-            final StoredConfigurationImpl storedConfiguration = debugItemInput.getObfuscatedConfiguration().getStoredConfiguration();
+            final StoredConfiguration storedConfiguration = debugItemInput.getObfuscatedConfiguration().getStoredConfiguration();
             final LDAPPermissionCalculator ldapPermissionCalculator = new LDAPPermissionCalculator( storedConfiguration );
             final LDAPPermissionCalculator ldapPermissionCalculator = new LDAPPermissionCalculator( storedConfiguration );
 
 
             final CSVPrinter csvPrinter = JavaHelper.makeCsvPrinter( outputStream );
             final CSVPrinter csvPrinter = JavaHelper.makeCsvPrinter( outputStream );

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

@@ -27,7 +27,6 @@ import password.pwm.http.PwmHttpResponseWrapper;
 import password.pwm.http.PwmRequest;
 import password.pwm.http.PwmRequest;
 import password.pwm.http.PwmRequestAttribute;
 import password.pwm.http.PwmRequestAttribute;
 import password.pwm.http.bean.PwmSessionBean;
 import password.pwm.http.bean.PwmSessionBean;
-import password.pwm.util.PasswordData;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.java.TimeDuration;
 import password.pwm.util.java.TimeDuration;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.logging.PwmLogger;
@@ -196,8 +195,9 @@ class CryptoCookieBeanImpl implements SessionBeanProvider
     private PwmSecurityKey keyForSession( final PwmRequest pwmRequest )
     private PwmSecurityKey keyForSession( final PwmRequest pwmRequest )
             throws PwmUnrecoverableException
             throws PwmUnrecoverableException
     {
     {
-        final PasswordData configKey = pwmRequest.getConfig().readSettingAsPassword( PwmSetting.PWM_SECURITY_KEY );
+        final PwmSecurityKey pwmSecurityKey = pwmRequest.getConfig().getSecurityKey();
+        final String keyHash = pwmSecurityKey.keyHash( pwmRequest.getPwmApplication().getSecureService() );
         final String userGuid = pwmRequest.getPwmSession().getLoginInfoBean().getGuid();
         final String userGuid = pwmRequest.getPwmSession().getLoginInfoBean().getGuid();
-        return new PwmSecurityKey( configKey.getStringValue() + userGuid );
+        return new PwmSecurityKey( keyHash + userGuid );
     }
     }
 }
 }

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

@@ -332,7 +332,5 @@ public enum Display implements PwmDisplayBundle
         return this.toString();
         return this.toString();
 
 
     }
     }
-
-
 }
 }
 
 

+ 49 - 24
server/src/main/java/password/pwm/i18n/PwmLocaleBundle.java

@@ -21,35 +21,43 @@
 package password.pwm.i18n;
 package password.pwm.i18n;
 
 
 import password.pwm.PwmConstants;
 import password.pwm.PwmConstants;
+import password.pwm.util.java.JavaHelper;
+import password.pwm.util.java.StringUtil;
 
 
 import java.util.ArrayList;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.HashSet;
-import java.util.Iterator;
 import java.util.List;
 import java.util.List;
+import java.util.Optional;
 import java.util.ResourceBundle;
 import java.util.ResourceBundle;
 import java.util.Set;
 import java.util.Set;
 
 
 public enum PwmLocaleBundle
 public enum PwmLocaleBundle
 {
 {
-    DISPLAY( Display.class, false ),
-    ERRORS( Error.class, false ),
-    MESSAGE( Message.class, false ),
+    DISPLAY( Display.class ),
+    ERRORS( Error.class ),
+    MESSAGE( Message.class ),
 
 
-    CONFIG( Config.class, true ),
-    ADMIN( Admin.class, true ),
-    HEALTH( Health.class, true ),;
+    CONFIG( Config.class, Flag.AdminOnly ),
+    ADMIN( Admin.class, Flag.AdminOnly ),
+    HEALTH( Health.class, Flag.AdminOnly ),
+    CONFIG_GUIDE( ConfigGuide.class, Flag.AdminOnly ),;
 
 
     private final Class<? extends PwmDisplayBundle> theClass;
     private final Class<? extends PwmDisplayBundle> theClass;
-    private final boolean adminOnly;
-    private Set<String> keys;
 
 
-    PwmLocaleBundle( final Class<? extends PwmDisplayBundle> theClass, final boolean adminOnly )
+    enum Flag
+    {
+        AdminOnly,
+    }
+
+    private final Flag[] flags;
+
+    PwmLocaleBundle( final Class<? extends PwmDisplayBundle> theClass, final Flag... flags )
     {
     {
         this.theClass = theClass;
         this.theClass = theClass;
-        this.adminOnly = adminOnly;
+        this.flags = flags;
     }
     }
 
 
     public Class<? extends PwmDisplayBundle> getTheClass( )
     public Class<? extends PwmDisplayBundle> getTheClass( )
@@ -59,17 +67,40 @@ public enum PwmLocaleBundle
 
 
     public boolean isAdminOnly( )
     public boolean isAdminOnly( )
     {
     {
-        return adminOnly;
+        return JavaHelper.enumArrayContainsValue( flags, Flag.AdminOnly );
     }
     }
 
 
-    public Set<String> getKeys( )
+    public static Optional<PwmLocaleBundle> forKey( final String key )
     {
     {
-        if ( keys == null )
+        for ( final PwmLocaleBundle pwmLocaleBundle : PwmLocaleBundle.values() )
         {
         {
-            final ResourceBundle defaultBundle = ResourceBundle.getBundle( this.getTheClass().getName(), PwmConstants.DEFAULT_LOCALE );
-            keys = Collections.unmodifiableSet( new HashSet<>( defaultBundle.keySet() ) );
+            if ( StringUtil.caseIgnoreContains( pwmLocaleBundle.getLegacyKeys(), key ) )
+            {
+                return Optional.of( pwmLocaleBundle );
+            }
         }
         }
-        return keys;
+
+        return Optional.empty();
+    }
+
+    public String getKey()
+    {
+        return getTheClass().getSimpleName();
+    }
+
+    public Set<String> getLegacyKeys()
+    {
+        return Collections.unmodifiableSet( new HashSet<>( Arrays.asList(
+                this.getTheClass().getSimpleName(),
+                this.getTheClass().getName(),
+                "password.pwm." + this.getTheClass().getSimpleName()
+        ) ) );
+    }
+
+    public Set<String> getDisplayKeys( )
+    {
+        final ResourceBundle defaultBundle = ResourceBundle.getBundle( this.getTheClass().getName(), PwmConstants.DEFAULT_LOCALE );
+        return Collections.unmodifiableSet( new HashSet<>( defaultBundle.keySet() ) );
     }
     }
 
 
     public static Collection<PwmLocaleBundle> allValues( )
     public static Collection<PwmLocaleBundle> allValues( )
@@ -80,13 +111,7 @@ public enum PwmLocaleBundle
     public static Collection<PwmLocaleBundle> userFacingValues( )
     public static Collection<PwmLocaleBundle> userFacingValues( )
     {
     {
         final List<PwmLocaleBundle> returnValue = new ArrayList<>( allValues() );
         final List<PwmLocaleBundle> returnValue = new ArrayList<>( allValues() );
-        for ( final Iterator<PwmLocaleBundle> iter = returnValue.iterator(); iter.hasNext(); )
-        {
-            if ( iter.next().isAdminOnly() )
-            {
-                iter.remove();
-            }
-        }
+        returnValue.removeIf( PwmLocaleBundle::isAdminOnly );
         return Collections.unmodifiableList( returnValue );
         return Collections.unmodifiableList( returnValue );
     }
     }
 }
 }

+ 3 - 3
server/src/main/java/password/pwm/ldap/LdapBrowser.java

@@ -32,7 +32,7 @@ import com.novell.ldapchai.util.SearchHelper;
 import password.pwm.AppProperty;
 import password.pwm.AppProperty;
 import password.pwm.config.Configuration;
 import password.pwm.config.Configuration;
 import password.pwm.config.profile.LdapProfile;
 import password.pwm.config.profile.LdapProfile;
-import password.pwm.config.stored.StoredConfigurationImpl;
+import password.pwm.config.stored.StoredConfiguration;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.error.PwmUnrecoverableException;
@@ -51,14 +51,14 @@ import java.util.TreeMap;
 public class LdapBrowser
 public class LdapBrowser
 {
 {
     private static final PwmLogger LOGGER = PwmLogger.forClass( LdapBrowser.class );
     private static final PwmLogger LOGGER = PwmLogger.forClass( LdapBrowser.class );
-    private final StoredConfigurationImpl storedConfiguration;
+    private final StoredConfiguration storedConfiguration;
 
 
     private final ChaiProviderFactory chaiProviderFactory;
     private final ChaiProviderFactory chaiProviderFactory;
     private final Map<String, ChaiProvider> providerCache = new HashMap<>();
     private final Map<String, ChaiProvider> providerCache = new HashMap<>();
 
 
     public LdapBrowser(
     public LdapBrowser(
             final ChaiProviderFactory chaiProviderFactory,
             final ChaiProviderFactory chaiProviderFactory,
-            final StoredConfigurationImpl storedConfiguration
+            final StoredConfiguration storedConfiguration
     )
     )
             throws PwmUnrecoverableException
             throws PwmUnrecoverableException
     {
     {

+ 1 - 1
server/src/main/java/password/pwm/svc/node/NodeMachine.java

@@ -78,7 +78,7 @@ class NodeMachine
     public List<NodeInfo> nodes( ) throws PwmUnrecoverableException
     public List<NodeInfo> nodes( ) throws PwmUnrecoverableException
     {
     {
         final Map<String, NodeInfo> returnObj = new TreeMap<>();
         final Map<String, NodeInfo> returnObj = new TreeMap<>();
-        final String configHash = pwmApplication.getConfig().configurationHash();
+        final String configHash = pwmApplication.getConfig().configurationHash( pwmApplication.getSecureService() );
         for ( final StoredNodeData storedNodeData : knownNodes.values() )
         for ( final StoredNodeData storedNodeData : knownNodes.values() )
         {
         {
             final boolean configMatch = configHash.equals( storedNodeData.getConfigHash() );
             final boolean configMatch = configHash.equals( storedNodeData.getConfigHash() );

+ 1 - 1
server/src/main/java/password/pwm/svc/node/StoredNodeData.java

@@ -47,7 +47,7 @@ class StoredNodeData implements Serializable
                 pwmApplication.getStartupTime(),
                 pwmApplication.getStartupTime(),
                 pwmApplication.getInstanceID(),
                 pwmApplication.getInstanceID(),
                 pwmApplication.getRuntimeNonce(),
                 pwmApplication.getRuntimeNonce(),
-                pwmApplication.getConfig().configurationHash()
+                pwmApplication.getConfig().configurationHash( pwmApplication.getSecureService() )
         );
         );
     }
     }
 }
 }

برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است