Selaa lähdekoodia

fix comparable configitemkey error

Jason Rivard 4 vuotta sitten
vanhempi
commit
afa93341af
100 muutettua tiedostoa jossa 2190 lisäystä ja 1720 poistoa
  1. 5 9
      pom.xml
  2. 0 12
      server/src/main/java/password/pwm/AppProperty.java
  3. 2 1
      server/src/main/java/password/pwm/PwmAboutProperty.java
  4. 63 69
      server/src/main/java/password/pwm/PwmApplication.java
  5. 2 0
      server/src/main/java/password/pwm/PwmConstants.java
  6. 39 113
      server/src/main/java/password/pwm/PwmDomain.java
  7. 32 4
      server/src/main/java/password/pwm/bean/DomainID.java
  8. 4 4
      server/src/main/java/password/pwm/bean/TokenDestinationItem.java
  9. 41 15
      server/src/main/java/password/pwm/bean/UserIdentity.java
  10. 25 14
      server/src/main/java/password/pwm/config/AppConfig.java
  11. 1 1
      server/src/main/java/password/pwm/config/ConfigurationUtil.java
  12. 9 63
      server/src/main/java/password/pwm/config/DomainConfig.java
  13. 7 9
      server/src/main/java/password/pwm/config/PwmSetting.java
  14. 5 1
      server/src/main/java/password/pwm/config/PwmSettingCategory.java
  15. 51 8
      server/src/main/java/password/pwm/config/SettingReader.java
  16. 2 2
      server/src/main/java/password/pwm/config/SettingUIFunction.java
  17. 8 11
      server/src/main/java/password/pwm/config/function/AbstractUriCertImportFunction.java
  18. 14 12
      server/src/main/java/password/pwm/config/function/ActionCertImportFunction.java
  19. 5 5
      server/src/main/java/password/pwm/config/function/LdapCertImportFunction.java
  20. 9 5
      server/src/main/java/password/pwm/config/function/OAuthCertImportFunction.java
  21. 10 6
      server/src/main/java/password/pwm/config/function/RemoteWebServiceCertImportFunction.java
  22. 4 2
      server/src/main/java/password/pwm/config/function/SMSGatewayCertImportFunction.java
  23. 4 4
      server/src/main/java/password/pwm/config/function/SmtpCertImportFunction.java
  24. 8 5
      server/src/main/java/password/pwm/config/function/SyslogCertImportFunction.java
  25. 7 8
      server/src/main/java/password/pwm/config/function/UserMatchViewerFunction.java
  26. 0 9
      server/src/main/java/password/pwm/config/option/IdentityVerificationMethod.java
  27. 8 15
      server/src/main/java/password/pwm/config/profile/AbstractProfile.java
  28. 15 46
      server/src/main/java/password/pwm/config/profile/ChallengeProfile.java
  29. 1 3
      server/src/main/java/password/pwm/config/profile/EmailServerProfile.java
  30. 2 3
      server/src/main/java/password/pwm/config/profile/ForgottenPasswordProfile.java
  31. 1 1
      server/src/main/java/password/pwm/config/profile/LdapProfile.java
  32. 1 1
      server/src/main/java/password/pwm/config/profile/NewUserProfile.java
  33. 1 1
      server/src/main/java/password/pwm/config/profile/PwmPasswordPolicy.java
  34. 176 0
      server/src/main/java/password/pwm/config/stored/ConfigSearchMachine.java
  35. 65 64
      server/src/main/java/password/pwm/config/stored/ConfigurationCleaner.java
  36. 7 7
      server/src/main/java/password/pwm/config/stored/ConfigurationReader.java
  37. 5 5
      server/src/main/java/password/pwm/config/stored/StoredConfigData.java
  38. 42 44
      server/src/main/java/password/pwm/config/stored/StoredConfigKey.java
  39. 39 41
      server/src/main/java/password/pwm/config/stored/StoredConfigXmlSerializer.java
  40. 11 10
      server/src/main/java/password/pwm/config/stored/StoredConfigZipJsonSerializer.java
  41. 11 8
      server/src/main/java/password/pwm/config/stored/StoredConfigZipXmlSerializer.java
  42. 5 15
      server/src/main/java/password/pwm/config/stored/StoredConfiguration.java
  43. 23 85
      server/src/main/java/password/pwm/config/stored/StoredConfigurationImpl.java
  44. 25 96
      server/src/main/java/password/pwm/config/stored/StoredConfigurationModifier.java
  45. 173 179
      server/src/main/java/password/pwm/config/stored/StoredConfigurationUtil.java
  46. 2 4
      server/src/main/java/password/pwm/config/value/FormValue.java
  47. 11 3
      server/src/main/java/password/pwm/config/value/LocalizedStringArrayValue.java
  48. 14 0
      server/src/main/java/password/pwm/config/value/ValueFactory.java
  49. 15 4
      server/src/main/java/password/pwm/config/value/ValueTypeConverter.java
  50. 2 2
      server/src/main/java/password/pwm/config/value/VerificationMethodValue.java
  51. 10 9
      server/src/main/java/password/pwm/health/ApplianceStatusChecker.java
  52. 112 84
      server/src/main/java/password/pwm/health/CertificateChecker.java
  53. 98 78
      server/src/main/java/password/pwm/health/ConfigurationChecker.java
  54. 9 6
      server/src/main/java/password/pwm/health/DatabaseStatusChecker.java
  55. 21 20
      server/src/main/java/password/pwm/health/HealthMonitor.java
  56. 30 7
      server/src/main/java/password/pwm/health/HealthRecord.java
  57. 31 0
      server/src/main/java/password/pwm/health/HealthSupplier.java
  58. 4 3
      server/src/main/java/password/pwm/health/JavaChecker.java
  59. 90 32
      server/src/main/java/password/pwm/health/LDAPHealthChecker.java
  60. 8 7
      server/src/main/java/password/pwm/health/LocalDBHealthChecker.java
  61. 30 13
      server/src/main/java/password/pwm/http/ContextManager.java
  62. 4 16
      server/src/main/java/password/pwm/http/HttpHeader.java
  63. 35 35
      server/src/main/java/password/pwm/http/PwmHttpRequestWrapper.java
  64. 11 11
      server/src/main/java/password/pwm/http/PwmHttpResponseWrapper.java
  65. 16 14
      server/src/main/java/password/pwm/http/PwmRequest.java
  66. 3 0
      server/src/main/java/password/pwm/http/PwmRequestAttribute.java
  67. 1 1
      server/src/main/java/password/pwm/http/PwmResponse.java
  68. 4 3
      server/src/main/java/password/pwm/http/PwmURL.java
  69. 16 13
      server/src/main/java/password/pwm/http/filter/AuthenticationFilter.java
  70. 17 17
      server/src/main/java/password/pwm/http/filter/RequestInitializationFilter.java
  71. 6 2
      server/src/main/java/password/pwm/http/filter/SessionFilter.java
  72. 2 3
      server/src/main/java/password/pwm/http/servlet/ClientApiServlet.java
  73. 1 1
      server/src/main/java/password/pwm/http/servlet/ForgottenUsernameServlet.java
  74. 1 1
      server/src/main/java/password/pwm/http/servlet/GuestRegistrationServlet.java
  75. 13 0
      server/src/main/java/password/pwm/http/servlet/LoginServlet.java
  76. 1 1
      server/src/main/java/password/pwm/http/servlet/SetupOtpServlet.java
  77. 1 1
      server/src/main/java/password/pwm/http/servlet/activation/ActivateUserUtils.java
  78. 2 2
      server/src/main/java/password/pwm/http/servlet/admin/AdminServlet.java
  79. 25 25
      server/src/main/java/password/pwm/http/servlet/admin/AppDashboardData.java
  80. 89 38
      server/src/main/java/password/pwm/http/servlet/configeditor/ConfigEditorServlet.java
  81. 58 57
      server/src/main/java/password/pwm/http/servlet/configeditor/ConfigEditorServletUtils.java
  82. 28 0
      server/src/main/java/password/pwm/http/servlet/configeditor/DomainManageMode.java
  83. 160 0
      server/src/main/java/password/pwm/http/servlet/configeditor/DomainStateReader.java
  84. 5 4
      server/src/main/java/password/pwm/http/servlet/configeditor/SearchResultItem.java
  85. 41 21
      server/src/main/java/password/pwm/http/servlet/configeditor/data/NavTreeDataMaker.java
  86. 3 0
      server/src/main/java/password/pwm/http/servlet/configeditor/data/NavTreeSettings.java
  87. 3 3
      server/src/main/java/password/pwm/http/servlet/configeditor/data/SettingDataMaker.java
  88. 42 29
      server/src/main/java/password/pwm/http/servlet/configguide/ConfigGuideForm.java
  89. 37 26
      server/src/main/java/password/pwm/http/servlet/configguide/ConfigGuideServlet.java
  90. 9 7
      server/src/main/java/password/pwm/http/servlet/configguide/ConfigGuideUtils.java
  91. 19 21
      server/src/main/java/password/pwm/http/servlet/configmanager/ConfigManagerCertificatesServlet.java
  92. 5 3
      server/src/main/java/password/pwm/http/servlet/configmanager/ConfigManagerLocalDBServlet.java
  93. 5 6
      server/src/main/java/password/pwm/http/servlet/configmanager/ConfigManagerLoginServlet.java
  94. 7 5
      server/src/main/java/password/pwm/http/servlet/configmanager/ConfigManagerServlet.java
  95. 4 6
      server/src/main/java/password/pwm/http/servlet/configmanager/ConfigManagerWordlistServlet.java
  96. 43 45
      server/src/main/java/password/pwm/http/servlet/configmanager/DebugItemGenerator.java
  97. 5 4
      server/src/main/java/password/pwm/http/servlet/forgottenpw/ForgottenPasswordUtil.java
  98. 3 1
      server/src/main/java/password/pwm/http/servlet/helpdesk/HelpdeskCardInfoBean.java
  99. 5 5
      server/src/main/java/password/pwm/http/servlet/helpdesk/HelpdeskServlet.java
  100. 5 5
      server/src/main/java/password/pwm/http/servlet/newuser/NewUserFormUtils.java

+ 5 - 9
pom.xml

@@ -235,7 +235,7 @@
                     <dependency>
                         <groupId>com.puppycrawl.tools</groupId>
                         <artifactId>checkstyle</artifactId>
-                        <version>8.38</version>
+                        <version>8.39</version>
                     </dependency>
                 </dependencies>
                 <executions>
@@ -314,7 +314,7 @@
             <plugin>
                 <groupId>com.github.spotbugs</groupId>
                 <artifactId>spotbugs-maven-plugin</artifactId>
-                <version>4.1.4</version>
+                <version>4.2.0</version>
                 <dependencies>
                     <dependency>
                         <groupId>com.github.spotbugs</groupId>
@@ -359,14 +359,11 @@
                         </configuration>
                     </execution>
                 </executions>
-                <configuration>
-                    <!-- put your configurations here -->
-                </configuration>
             </plugin>
-            <plugin> <!-- checks owsp vulnerability database -->
+            <plugin>
                 <groupId>org.owasp</groupId>
                 <artifactId>dependency-check-maven</artifactId>
-                <version>6.0.3</version>
+                <version>6.0.4</version>
                 <executions>
                     <execution>
                         <goals>
@@ -403,7 +400,7 @@
         <dependency>
             <groupId>org.mockito</groupId>
             <artifactId>mockito-core</artifactId>
-            <version>3.6.28</version>
+            <version>3.7.0</version>
             <scope>test</scope>
         </dependency>
         <dependency>
@@ -436,6 +433,5 @@
             <version>1.27</version>
             <scope>test</scope>
         </dependency>
-
     </dependencies>
 </project>

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

@@ -403,18 +403,6 @@ public enum AppProperty
         return key;
     }
 
-    public static AppProperty forKey( final String key )
-    {
-        for ( final AppProperty appProperty : AppProperty.values() )
-        {
-            if ( appProperty.getKey().equals( key ) )
-            {
-                return appProperty;
-            }
-        }
-        return null;
-    }
-
     public String getDefaultValue( )
     {
         if ( defaultValue == null )

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

@@ -22,6 +22,7 @@ package password.pwm;
 
 import password.pwm.config.PwmSetting;
 import password.pwm.i18n.Display;
+import password.pwm.svc.sessiontrack.SessionTrackService;
 import password.pwm.util.db.DatabaseService;
 import password.pwm.util.i18n.LocaleHelper;
 import password.pwm.util.java.FileSystemUtility;
@@ -72,7 +73,7 @@ public enum PwmAboutProperty
     app_secureBlockAlgorithm( null, pwmApplication -> pwmApplication.getSecureService().getDefaultBlockAlgorithm().getLabel() ),
     app_secureHashAlgorithm( null, pwmApplication -> pwmApplication.getSecureService().getDefaultHashAlgorithm().toString() ),
     app_ldapProfileCount( null, pwmApplication -> Integer.toString( pwmApplication.getDefaultDomain().getConfig().getLdapProfiles().size() ) ),
-    app_ldapConnectionCount( null, pwmApplication -> Integer.toString( pwmApplication.getLdapConnectionService().connectionCount() ) ),
+    app_ldapConnectionCount( null, pwmApplication -> Long.toString( SessionTrackService.totalLdapConnectionCount( pwmApplication ) ) ),
     app_activeSessionCount( "Active Session Count", pwmApplication -> Integer.toString( pwmApplication.getSessionTrackService().sessionCount() ) ),
     app_activeRequestCount( "Active Request Count", pwmApplication -> Integer.toString( pwmApplication.getActiveServletRequests().get() ) ),
 

+ 63 - 69
server/src/main/java/password/pwm/PwmApplication.java

@@ -20,15 +20,13 @@
 
 package password.pwm;
 
-import com.novell.ldapchai.ChaiUser;
-import com.novell.ldapchai.exception.ChaiUnavailableException;
-import com.novell.ldapchai.provider.ChaiProvider;
 import password.pwm.bean.DomainID;
 import password.pwm.bean.SessionLabel;
 import password.pwm.bean.SmsItemBean;
-import password.pwm.bean.UserIdentity;
 import password.pwm.config.AppConfig;
+import password.pwm.config.DomainConfig;
 import password.pwm.config.PwmSetting;
+import password.pwm.config.PwmSettingScope;
 import password.pwm.config.stored.StoredConfiguration;
 import password.pwm.config.stored.StoredConfigurationUtil;
 import password.pwm.error.ErrorInformation;
@@ -37,11 +35,8 @@ import password.pwm.error.PwmException;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.health.HealthMonitor;
 import password.pwm.http.servlet.configeditor.data.SettingDataMaker;
-import password.pwm.http.servlet.peoplesearch.PeopleSearchService;
 import password.pwm.http.servlet.resource.ResourceServletService;
 import password.pwm.http.state.SessionStateService;
-import password.pwm.ldap.LdapConnectionService;
-import password.pwm.ldap.search.UserSearchEngine;
 import password.pwm.svc.PwmService;
 import password.pwm.svc.PwmServiceEnum;
 import password.pwm.svc.PwmServiceManager;
@@ -57,6 +52,7 @@ import password.pwm.svc.intruder.RecordType;
 import password.pwm.svc.node.NodeService;
 import password.pwm.svc.pwnotify.PwNotifyService;
 import password.pwm.svc.report.ReportService;
+import password.pwm.svc.secure.SystemSecureService;
 import password.pwm.svc.sessiontrack.SessionTrackService;
 import password.pwm.svc.sessiontrack.UserAgentUtils;
 import password.pwm.svc.shorturl.UrlShortenerService;
@@ -85,12 +81,9 @@ import password.pwm.util.logging.PwmLogLevel;
 import password.pwm.util.logging.PwmLogManager;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.macro.MacroRequest;
-import password.pwm.util.operations.CrService;
-import password.pwm.util.operations.OtpService;
 import password.pwm.util.queue.SmsQueueManager;
 import password.pwm.util.secure.HttpsServerCertificateManager;
 import password.pwm.util.secure.PwmRandom;
-import password.pwm.util.secure.SecureService;
 
 import java.io.ByteArrayOutputStream;
 import java.io.File;
@@ -98,16 +91,17 @@ import java.io.FileInputStream;
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.Serializable;
+import java.net.URI;
 import java.security.KeyStore;
 import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Collections;
-import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Optional;
+import java.util.TreeMap;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.function.Function;
@@ -123,7 +117,7 @@ public class PwmApplication
     private Map<DomainID, PwmDomain> domains;
     private String runtimeNonce = PwmRandom.getInstance().randomUUID().toString();
 
-    private final PwmServiceManager pwmServiceManager = new PwmServiceManager( this, DomainID.systemId() );
+    private final PwmServiceManager pwmServiceManager = new PwmServiceManager( this, DomainID.systemId(), PwmServiceEnum.forScope( PwmSettingScope.SYSTEM ) );
 
     private final Instant startupTime = Instant.now();
     private Instant installTime = Instant.now();
@@ -156,6 +150,30 @@ public class PwmApplication
         }
     }
 
+    public static Optional<String> deriveLocalServerHostname( final DomainConfig domainConfig )
+    {
+        if ( domainConfig != null )
+        {
+            final String siteUrl = domainConfig.readSettingAsString( PwmSetting.PWM_SITE_URL );
+            if ( !StringUtil.isEmpty( siteUrl ) )
+            {
+                try
+                {
+                    final URI parsedUri = URI.create( siteUrl );
+                    {
+                        final String uriHost = parsedUri.getHost();
+                        return Optional.ofNullable( uriHost );
+                    }
+                }
+                catch ( final IllegalArgumentException e )
+                {
+                    LOGGER.trace( () -> " error parsing siteURL hostname: " + e.getMessage() );
+                }
+            }
+        }
+        return Optional.empty();
+    }
+
     private void initialize( )
             throws PwmUnrecoverableException
     {
@@ -214,13 +232,13 @@ public class PwmApplication
             }
         }
 
-        this.localDBLogger = PwmLogManager.initializeLocalDBLogger( this.getDefaultDomain() );
+        this.localDBLogger = PwmLogManager.initializeLocalDBLogger( this );
 
         // log the loaded configuration
         LOGGER.debug( () -> "configuration load completed" );
 
         // read the pwm servlet instance id
-        instanceID = fetchInstanceID( localDB, this.getDefaultDomain() );
+        instanceID = fetchInstanceID( localDB, this );
         LOGGER.debug( () -> "using '" + getInstanceID() + "' for instance's ID (instanceID)" );
 
         // read the pwm installation date
@@ -234,6 +252,16 @@ public class PwmApplication
 
         pwmServiceManager.initAllServices();
 
+        {
+            final Instant domainInitStartTime = Instant.now();
+            for ( final PwmDomain pwmDomain : domains.values() )
+            {
+                LOGGER.trace( () -> "beginning domain initialization for domain " + pwmDomain.getDomainID().stringValue() );
+                pwmDomain.initialize();
+            }
+            LOGGER.trace( () -> "completed domain initialization for all domains", () -> TimeDuration.fromCurrent( domainInitStartTime ) );
+        }
+
         final boolean skipPostInit = pwmEnvironment.isInternalRuntimeInstance()
                 || pwmEnvironment.getFlags().contains( PwmEnvironment.ApplicationFlag.CommandLineInstance );
 
@@ -246,6 +274,7 @@ public class PwmApplication
 
             pwmScheduler.immediateExecuteInNewThread( this::postInitTasks, this.getClass().getSimpleName() + " postInit tasks" );
         }
+
     }
 
     public void reInit( final PwmEnvironment pwmEnvironment )
@@ -327,7 +356,7 @@ public class PwmApplication
         MBeanUtility.registerMBean( this );
 
         {
-            final ExecutorService executorService = PwmScheduler.makeSingleThreadExecutorService( this.getDefaultDomain(), PwmDomain.class );
+            final ExecutorService executorService = PwmScheduler.makeSingleThreadExecutorService( this, PwmDomain.class );
             pwmScheduler.scheduleDailyZuluZeroStartJob( new DailySummaryJob( this.getDefaultDomain() ), executorService, TimeDuration.ZERO );
         }
 
@@ -408,6 +437,11 @@ public class PwmApplication
         if ( !keepServicesRunning )
         {
             pwmServiceManager.shutdownAllServices();
+
+            for ( final PwmDomain pwmDomain : domains.values() )
+            {
+                pwmDomain.shutdown();
+            }
         }
 
         if ( localDBLogger != null )
@@ -547,7 +581,10 @@ public class PwmApplication
         };
 
         final StoredConfiguration storedConfiguration = pwmDomain.getConfig().getStoredConfiguration();
-        final Map<String, String> debugStrings = StoredConfigurationUtil.makeDebugMap( storedConfiguration, storedConfiguration.modifiedItems(), PwmConstants.DEFAULT_LOCALE );
+        final Map<String, String> debugStrings = StoredConfigurationUtil.makeDebugMap(
+                storedConfiguration,
+                storedConfiguration.keys(),
+                PwmConstants.DEFAULT_LOCALE );
 
         LOGGER.trace( () -> "--begin current configuration output--" );
         debugStrings.entrySet().stream()
@@ -678,10 +715,10 @@ public class PwmApplication
         return Instant.now();
     }
 
-    private String fetchInstanceID( final LocalDB localDB, final PwmDomain pwmDomain )
+    private String fetchInstanceID( final LocalDB localDB, final PwmApplication pwmApplication )
     {
         {
-            final String newInstanceID = pwmDomain.getPwmEnvironment().getParameters().get( PwmEnvironment.ApplicationParameter.InstanceID );
+            final String newInstanceID = pwmApplication.getPwmEnvironment().getParameters().get( PwmEnvironment.ApplicationParameter.InstanceID );
 
             if ( !StringUtil.isTrimEmpty( newInstanceID ) )
             {
@@ -724,26 +761,6 @@ public class PwmApplication
         return ( IntruderManager ) pwmServiceManager.getService( PwmServiceEnum.IntruderManager );
     }
 
-    public ChaiUser getProxiedChaiUser( final UserIdentity userIdentity )
-            throws PwmUnrecoverableException
-    {
-        try
-        {
-            final ChaiProvider proxiedProvider = getProxyChaiProvider( userIdentity.getLdapProfileID() );
-            return proxiedProvider.getEntryFactory().newChaiUser( userIdentity.getUserDN() );
-        }
-        catch ( final ChaiUnavailableException e )
-        {
-            throw PwmUnrecoverableException.fromChaiException( e );
-        }
-    }
-
-    public ChaiProvider getProxyChaiProvider( final String identifier )
-            throws PwmUnrecoverableException
-    {
-        return getLdapConnectionService().getProxyChaiProvider( identifier );
-    }
-
     public LocalDBLogger getLocalDBLogger( )
     {
         return localDBLogger;
@@ -808,14 +825,9 @@ public class PwmApplication
         return ( UrlShortenerService ) pwmServiceManager.getService( PwmServiceEnum.UrlShortenerService );
     }
 
-    public UserSearchEngine getUserSearchEngine( )
-    {
-        return ( UserSearchEngine ) pwmServiceManager.getService( PwmServiceEnum.UserSearchEngine );
-    }
-
-    public NodeService getClusterService( )
+    public NodeService getNodeService( )
     {
-        return ( NodeService ) pwmServiceManager.getService( PwmServiceEnum.ClusterService );
+        return ( NodeService ) pwmServiceManager.getService( PwmServiceEnum.NodeService );
     }
 
     public ErrorInformation getLastLocalDBFailure( )
@@ -828,11 +840,6 @@ public class PwmApplication
         return ( TokenService ) pwmServiceManager.getService( PwmServiceEnum.TokenService );
     }
 
-    public LdapConnectionService getLdapConnectionService( )
-    {
-        return ( LdapConnectionService ) pwmServiceManager.getService( PwmServiceEnum.LdapConnectionService );
-    }
-
     public SessionTrackService getSessionTrackService( )
     {
         return ( SessionTrackService ) pwmServiceManager.getService( PwmServiceEnum.SessionTrackService );
@@ -843,11 +850,6 @@ public class PwmApplication
         return ( ResourceServletService ) pwmServiceManager.getService( PwmServiceEnum.ResourceServletService );
     }
 
-    public PeopleSearchService getPeopleSearchService( )
-    {
-        return ( PeopleSearchService ) pwmServiceManager.getService( PwmServiceEnum.PeopleSearchService );
-    }
-
     public DatabaseAccessor getDatabaseAccessor( )
 
             throws PwmUnrecoverableException
@@ -865,16 +867,6 @@ public class PwmApplication
         return ( StatisticsManager ) pwmServiceManager.getService( PwmServiceEnum.StatisticsManager );
     }
 
-    public OtpService getOtpService( )
-    {
-        return ( OtpService ) pwmServiceManager.getService( PwmServiceEnum.OtpService );
-    }
-
-    public CrService getCrService( )
-    {
-        return ( CrService ) pwmServiceManager.getService( PwmServiceEnum.CrService );
-    }
-
     public SessionStateService getSessionStateService( )
     {
         return ( SessionStateService ) pwmServiceManager.getService( PwmServiceEnum.SessionStateSvc );
@@ -886,9 +878,9 @@ public class PwmApplication
         return ( CacheService ) pwmServiceManager.getService( PwmServiceEnum.CacheService );
     }
 
-    public SecureService getSecureService( )
+    public SystemSecureService getSecureService( )
     {
-        return ( SecureService ) pwmServiceManager.getService( PwmServiceEnum.SecureService );
+        return ( SystemSecureService ) pwmServiceManager.getService( PwmServiceEnum.SystemSecureService );
     }
 
 
@@ -973,10 +965,12 @@ public class PwmApplication
         }
 
         public static Map<DomainID, PwmDomain> initializeDomains( final PwmApplication pwmApplication )
+                throws PwmUnrecoverableException
         {
-            final Map<DomainID, PwmDomain> domainMap = new LinkedHashMap<>();
-            for ( final DomainID domainID : pwmApplication.getPwmEnvironment().getConfig().getDomainIDs() )
+            final Map<DomainID, PwmDomain> domainMap = new TreeMap<>();
+            for ( final String domainIdString : pwmApplication.getPwmEnvironment().getConfig().getDomainIDs() )
             {
+                final DomainID domainID = DomainID.create( domainIdString );
                 final PwmDomain newDomain = new PwmDomain( pwmApplication, domainID );
                 domainMap.put( domainID, newDomain );
             }

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

@@ -153,6 +153,8 @@ public abstract class PwmConstants
     public static final String PARAM_PASSWORD = "password";
     public static final String PARAM_CONTEXT = "context";
     public static final String PARAM_LDAP_PROFILE = "ldapProfile";
+    public static final String PARAM_DOMAIN = "domain";
+
     public static final String PARAM_SKIP_CAPTCHA = "skipCaptcha";
     public static final String PARAM_POST_LOGIN_URL = "posturl";
     public static final String PARAM_FILE_UPLOAD = "fileUpload";

+ 39 - 113
server/src/main/java/password/pwm/PwmDomain.java

@@ -21,50 +21,39 @@
 package password.pwm;
 
 import com.novell.ldapchai.ChaiUser;
+import com.novell.ldapchai.exception.ChaiUnavailableException;
 import com.novell.ldapchai.provider.ChaiProvider;
 import password.pwm.bean.DomainID;
-import password.pwm.bean.SessionLabel;
 import password.pwm.bean.UserIdentity;
 import password.pwm.config.DomainConfig;
+import password.pwm.config.PwmSettingScope;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmUnrecoverableException;
-import password.pwm.health.HealthMonitor;
 import password.pwm.http.servlet.peoplesearch.PeopleSearchService;
 import password.pwm.http.servlet.resource.ResourceServletService;
 import password.pwm.http.state.SessionStateService;
 import password.pwm.ldap.LdapConnectionService;
 import password.pwm.ldap.search.UserSearchEngine;
 import password.pwm.svc.PwmService;
+import password.pwm.svc.PwmServiceEnum;
+import password.pwm.svc.PwmServiceManager;
 import password.pwm.svc.cache.CacheService;
 import password.pwm.svc.event.AuditService;
 import password.pwm.svc.httpclient.HttpClientService;
 import password.pwm.svc.intruder.IntruderManager;
-import password.pwm.svc.node.NodeService;
 import password.pwm.svc.pwnotify.PwNotifyService;
 import password.pwm.svc.report.ReportService;
+import password.pwm.svc.secure.DomainSecureService;
 import password.pwm.svc.sessiontrack.SessionTrackService;
-import password.pwm.svc.shorturl.UrlShortenerService;
 import password.pwm.svc.stats.StatisticsManager;
 import password.pwm.svc.token.TokenService;
-import password.pwm.svc.wordlist.SeedlistService;
 import password.pwm.svc.wordlist.SharedHistoryManager;
-import password.pwm.svc.wordlist.WordlistService;
-import password.pwm.util.PwmScheduler;
-import password.pwm.util.db.DatabaseAccessor;
-import password.pwm.util.db.DatabaseService;
-import password.pwm.util.localdb.LocalDB;
-import password.pwm.util.logging.LocalDBLogger;
 import password.pwm.util.logging.PwmLogger;
-import password.pwm.util.macro.MacroRequest;
 import password.pwm.util.operations.CrService;
 import password.pwm.util.operations.OtpService;
-import password.pwm.util.queue.SmsQueueManager;
-import password.pwm.util.secure.SecureService;
 
-import java.io.Serializable;
 import java.util.List;
 import java.util.Objects;
-import java.util.Optional;
 
 /**
  * A repository for objects common to the servlet context.  A singleton
@@ -79,34 +68,33 @@ public class PwmDomain
     private final PwmApplication pwmApplication;
     private final DomainID domainID;
 
+    private final PwmServiceManager pwmServiceManager;
+
     public PwmDomain( final PwmApplication pwmApplication, final DomainID domainID )
     {
         this.pwmApplication = Objects.requireNonNull( pwmApplication );
         this.domainID = Objects.requireNonNull( domainID );
-    }
 
-    public DomainConfig getConfig( )
-    {
-        return pwmApplication.getConfig().getDomainConfigs().get( domainID );
+        this.pwmServiceManager = new PwmServiceManager( pwmApplication, domainID, PwmServiceEnum.forScope( PwmSettingScope.DOMAIN ) );
     }
 
-    public PwmApplicationMode getApplicationMode( )
+    public void initialize()
+            throws PwmUnrecoverableException
+
     {
-        return pwmApplication.getApplicationMode();
+        pwmServiceManager.initAllServices();
     }
 
-    public DatabaseAccessor getDatabaseAccessor( )
-
-            throws PwmUnrecoverableException
+    public DomainConfig getConfig( )
     {
-        return pwmApplication.getDatabaseAccessor();
+        return pwmApplication.getConfig().getDomainConfigs().get( domainID );
     }
 
-    public DatabaseService getDatabaseService( )
+    public PwmApplicationMode getApplicationMode( )
     {
-        return pwmApplication.getDatabaseService();
+        return pwmApplication.getApplicationMode();
     }
-
+    
     public StatisticsManager getStatisticsManager( )
     {
         return pwmApplication.getStatisticsManager();
@@ -114,12 +102,12 @@ public class PwmDomain
 
     public OtpService getOtpService( )
     {
-        return pwmApplication.getOtpService();
+        return ( OtpService ) pwmServiceManager.getService( PwmServiceEnum.OtpService );
     }
 
     public CrService getCrService( )
     {
-        return pwmApplication.getCrService();
+        return ( CrService ) pwmServiceManager.getService( PwmServiceEnum.CrService );
     }
 
     public SessionStateService getSessionStateService( )
@@ -132,14 +120,9 @@ public class PwmDomain
         return pwmApplication.getCacheService();
     }
 
-    public SecureService getSecureService( )
+    public DomainSecureService getSecureService( )
     {
-        return pwmApplication.getSecureService();
-    }
-
-    public LocalDB getLocalDB( )
-    {
-        return pwmApplication.getLocalDB();
+        return ( DomainSecureService ) pwmServiceManager.getService( PwmServiceEnum.DomainSecureService );
     }
 
     public PwmApplication getPwmApplication()
@@ -147,34 +130,14 @@ public class PwmDomain
         return pwmApplication;
     }
 
-    public PwmEnvironment getPwmEnvironment( )
-    {
-        return pwmApplication.getPwmEnvironment();
-    }
-
-    public <T extends Serializable> Optional<T> readAppAttribute( final AppAttribute appAttribute, final Class<T> returnClass )
-    {
-       return getPwmApplication().readAppAttribute( appAttribute, returnClass );
-    }
-
-    public void writeAppAttribute( final AppAttribute appAttribute, final Serializable value )
-    {
-        getPwmApplication().writeAppAttribute( appAttribute, value );
-    }
-
     public boolean determineIfDetailErrorMsgShown( )
     {
         return pwmApplication.determineIfDetailErrorMsgShown();
     }
 
-    public PwmScheduler getPwmScheduler()
-    {
-        return pwmApplication.getPwmScheduler();
-    }
-
-    public LdapConnectionService getLdapConnectionService()
+    public LdapConnectionService getLdapConnectionService( )
     {
-        return pwmApplication.getLdapConnectionService();
+        return ( LdapConnectionService ) pwmServiceManager.getService( PwmServiceEnum.LdapConnectionService );
     }
 
     public AuditService getAuditManager()
@@ -182,26 +145,29 @@ public class PwmDomain
         return pwmApplication.getAuditManager();
     }
 
-    public String getInstanceID()
-    {
-        return pwmApplication.getInstanceID();
-    }
-
     public SessionTrackService getSessionTrackService()
     {
         return pwmApplication.getSessionTrackService();
     }
 
-    public ChaiProvider getProxyChaiProvider( final String ldapProfileID )
+    public ChaiUser getProxiedChaiUser( final UserIdentity userIdentity )
             throws PwmUnrecoverableException
     {
-        return pwmApplication.getProxyChaiProvider( ldapProfileID );
+        try
+        {
+            final ChaiProvider proxiedProvider = getProxyChaiProvider( userIdentity.getLdapProfileID() );
+            return proxiedProvider.getEntryFactory().newChaiUser( userIdentity.getUserDN() );
+        }
+        catch ( final ChaiUnavailableException e )
+        {
+            throw PwmUnrecoverableException.fromChaiException( e );
+        }
     }
 
-    public ChaiUser getProxiedChaiUser( final UserIdentity userIdentity )
+    public ChaiProvider getProxyChaiProvider( final String identifier )
             throws PwmUnrecoverableException
     {
-        return pwmApplication.getProxiedChaiUser( userIdentity );
+        return getLdapConnectionService().getProxyChaiProvider( identifier );
     }
 
     public List<PwmService> getPwmServices( )
@@ -211,12 +177,7 @@ public class PwmDomain
 
     public UserSearchEngine getUserSearchEngine()
     {
-        return pwmApplication.getUserSearchEngine();
-    }
-
-    public UrlShortenerService getUrlShortener()
-    {
-        return pwmApplication.getUrlShortener();
+        return ( UserSearchEngine ) pwmServiceManager.getService( PwmServiceEnum.UserSearchEngine );
     }
 
     public HttpClientService getHttpClientService()
@@ -224,11 +185,6 @@ public class PwmDomain
         return pwmApplication.getHttpClientService();
     }
 
-    public NodeService getClusterService()
-    {
-        return pwmApplication.getClusterService();
-    }
-
     public IntruderManager getIntruderManager()
     {
         return pwmApplication.getIntruderManager();
@@ -239,54 +195,24 @@ public class PwmDomain
         return pwmApplication.getTokenService();
     }
 
-    public void sendSmsUsingQueue( final String smsNumber, final String modifiedMessage, final SessionLabel sessionLabel, final MacroRequest macroRequest )
-    {
-        pwmApplication.sendSmsUsingQueue( smsNumber, modifiedMessage, sessionLabel, macroRequest );
-    }
-
-    public WordlistService getWordlistService()
-    {
-        return pwmApplication.getWordlistService();
-    }
-
-    public LocalDBLogger getLocalDBLogger()
-    {
-        return pwmApplication.getLocalDBLogger();
-    }
-
     public SharedHistoryManager getSharedHistoryManager()
     {
         return pwmApplication.getSharedHistoryManager();
     }
 
-    public SeedlistService getSeedlistManager()
-    {
-        return pwmApplication.getSeedlistManager();
-    }
-
-    public SmsQueueManager getSmsQueue()
-    {
-        return pwmApplication.getSmsQueue();
-    }
-
     public ErrorInformation getLastLocalDBFailure()
     {
         return pwmApplication.getLastLocalDBFailure();
     }
 
-    public HealthMonitor getHealthMonitor()
-    {
-        return pwmApplication.getHealthMonitor();
-    }
-
     public ReportService getReportService()
     {
         return pwmApplication.getReportService();
     }
 
-    public PeopleSearchService getPeopleSearchService()
+    public PeopleSearchService getPeopleSearchService( )
     {
-        return pwmApplication.getPeopleSearchService();
+        return ( PeopleSearchService ) pwmServiceManager.getService( PwmServiceEnum.PeopleSearchService );
     }
 
     public PwNotifyService getPwNotifyService()
@@ -301,7 +227,7 @@ public class PwmDomain
 
     public void shutdown()
     {
-
+        pwmServiceManager.shutdownAllServices();
     }
 
     public DomainID getDomainID()

+ 32 - 4
server/src/main/java/password/pwm/bean/DomainID.java

@@ -22,8 +22,11 @@ package password.pwm.bean;
 
 import org.jetbrains.annotations.NotNull;
 import password.pwm.config.PwmSetting;
+import password.pwm.config.PwmSettingScope;
+import password.pwm.util.java.JavaHelper;
 
 import java.io.Serializable;
+import java.util.Comparator;
 import java.util.Objects;
 import java.util.regex.Pattern;
 
@@ -31,7 +34,14 @@ public class DomainID implements Comparable<DomainID>, Serializable
 {
     private static final String SYSTEM_ID = "system";
     private static final DomainID SYSTEM_DOMAIN_ID = new DomainID( SYSTEM_ID );
-    private static final Pattern REGEX_PATTERN = PwmSetting.DOMAIN_LIST.getRegExPattern();
+
+   // private static final Pattern PATTERN = Pattern.compile( "(?!.*system.*)([a-zA-Z][a-zA-Z0-9]{2,10})" );
+    private static final Pattern PATTERN = PwmSetting.DOMAIN_LIST.getRegExPattern();
+
+    // sort placing 'system' first then alphabetically.
+    private static final Comparator<DomainID> COMPARATOR = Comparator.comparing( DomainID::isSystem )
+            .reversed()
+            .thenComparing( DomainID::stringValue );
 
     private final String domainID;
 
@@ -42,13 +52,31 @@ public class DomainID implements Comparable<DomainID>, Serializable
 
     public static DomainID create( final String domainID )
     {
-        if ( !REGEX_PATTERN.matcher( domainID ).matches() )
+        final Pattern pattern = PATTERN;
+        if ( !pattern.matcher( domainID ).matches() )
         {
-            throw new IllegalArgumentException( "domainID value does not match required syntax pattern" );
+            throw new IllegalArgumentException( "domainID value '" + domainID + " ' does not match required syntax pattern" );
         }
         return new DomainID( domainID );
     }
 
+    public boolean inScope( final PwmSettingScope scope )
+    {
+        switch ( scope )
+        {
+            case SYSTEM:
+                return this.isSystem();
+
+            case DOMAIN:
+                return !this.isSystem();
+
+            default:
+                JavaHelper.unhandledSwitchStatement( scope );
+        }
+
+        return false;
+    }
+
     @Override
     public boolean equals( final Object o )
     {
@@ -73,7 +101,7 @@ public class DomainID implements Comparable<DomainID>, Serializable
     @Override
     public int compareTo( @NotNull final DomainID o )
     {
-        return this.domainID.compareTo( o.domainID );
+        return COMPARATOR.compare( this, o );
     }
 
     @Override

+ 4 - 4
server/src/main/java/password/pwm/bean/TokenDestinationItem.java

@@ -35,7 +35,7 @@ import password.pwm.ldap.UserInfo;
 import password.pwm.svc.token.TokenDestinationDisplayMasker;
 import password.pwm.util.i18n.LocaleHelper;
 import password.pwm.util.java.StringUtil;
-import password.pwm.util.secure.SecureService;
+import password.pwm.svc.secure.DomainSecureService;
 
 import java.io.Serializable;
 import java.util.ArrayList;
@@ -94,7 +94,7 @@ public class TokenDestinationItem implements Serializable
             throws PwmUnrecoverableException
     {
         final DomainConfig domainConfig = pwmDomain.getConfig();
-        final SecureService secureService = pwmDomain.getSecureService();
+        final DomainSecureService domainSecureService = pwmDomain.getSecureService();
 
         final TokenDestinationDisplayMasker tokenDestinationDisplayMasker = new TokenDestinationDisplayMasker( domainConfig );
 
@@ -110,7 +110,7 @@ public class TokenDestinationItem implements Serializable
         {
             if ( !StringUtil.isEmpty( emailValue ) )
             {
-                final String idHash = secureService.hash( emailValue + Type.email.name() );
+                final String idHash = domainSecureService.hash( emailValue + Type.email.name() );
                 final TokenDestinationItem item = TokenDestinationItem.builder()
                         .id( idHash )
                         .display( tokenDestinationDisplayMasker.maskEmail( emailValue ) )
@@ -131,7 +131,7 @@ public class TokenDestinationItem implements Serializable
         {
             if ( !StringUtil.isEmpty( smsValue ) )
             {
-                final String idHash = secureService.hash( smsValue + Type.sms.name() );
+                final String idHash = domainSecureService.hash( smsValue + Type.sms.name() );
                 final TokenDestinationItem item = TokenDestinationItem.builder()
                         .id( idHash )
                         .display( tokenDestinationDisplayMasker.maskPhone( smsValue ) )

+ 41 - 15
server/src/main/java/password/pwm/bean/UserIdentity.java

@@ -38,6 +38,7 @@ import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.JsonUtil;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.java.TimeDuration;
+import password.pwm.util.logging.PwmLogger;
 
 import java.io.Serializable;
 import java.util.Comparator;
@@ -47,6 +48,7 @@ import java.util.StringTokenizer;
 @SuppressFBWarnings( "SE_TRANSIENT_FIELD_NOT_RESTORED" )
 public class UserIdentity implements Serializable, Comparable<UserIdentity>
 {
+    private static final PwmLogger LOGGER = PwmLogger.forClass( UserIdentity.class );
     private static final long serialVersionUID = 1L;
 
     private static final String CRYPO_HEADER = "ui_C-";
@@ -62,6 +64,11 @@ public class UserIdentity implements Serializable, Comparable<UserIdentity>
                     UserIdentity::getDomainID,
                     Comparator.nullsLast( Comparator.naturalOrder() ) );
 
+    public static final String XML_DOMAIN = "domain";
+    public static final String XML_LDAP_PROFILE = "ldapProfile";
+    public static final String XML_DISTINGUISHED_NAME = "distinguishedName";
+    public static final String XML_USER_IDENTITY = "userIdentity";
+
 
     private transient String obfuscatedValue;
     private transient boolean canonical;
@@ -88,7 +95,7 @@ public class UserIdentity implements Serializable, Comparable<UserIdentity>
         this.canonical = canonical;
     }
 
-    public static UserIdentity createUserIdentity(
+    public static UserIdentity create(
             final String userDN,
             final String ldapProfile,
             final DomainID domainID,
@@ -167,7 +174,7 @@ public class UserIdentity implements Serializable, Comparable<UserIdentity>
 
     public String toDelimitedKey( )
     {
-        return this.getLdapProfileID() + DELIM_SEPARATOR + this.getUserDN() + DELIM_SEPARATOR + this.getDomainID().toString();
+        return JsonUtil.serialize( this );
     }
 
     public String toDisplayString( )
@@ -203,34 +210,53 @@ public class UserIdentity implements Serializable, Comparable<UserIdentity>
     {
         JavaHelper.requireNonEmpty( key );
 
+        try
+        {
+            return JsonUtil.deserialize( key, UserIdentity.class );
+        }
+        catch ( final Exception e )
+        {
+            LOGGER.trace( () -> "unable to deserialize UserIdentity: " + key + " using JSON method: " + e.getMessage() );
+        }
+
+        // old style
         final StringTokenizer st = new StringTokenizer( key, DELIM_SEPARATOR );
 
-        final DomainID domainID;
+        DomainID domainID = PwmConstants.DOMAIN_ID_PLACEHOLDER;
         if ( st.countTokens() < 2 )
         {
             throw new PwmUnrecoverableException( new ErrorInformation( PwmError.ERROR_INTERNAL, "not enough tokens while parsing delimited identity key" ) );
         }
-
-        if ( st.countTokens() > 2 )
-        {
-            final String domainStr = st.nextToken();
-            domainID = DomainID.create( domainStr );
-        }
-        else
+        else if ( st.countTokens() > 2 )
         {
-            domainID = PwmConstants.DOMAIN_ID_PLACEHOLDER;
+            String domainStr = "";
+            try
+            {
+                domainStr = st.nextToken();
+                domainID = DomainID.create( domainStr );
+            }
+            catch ( final Exception e )
+            {
+                final String fDomainStr = domainStr;
+                LOGGER.trace( () -> "error decoding DomainID '" + fDomainStr + "' from delimited UserIdentity: " + e.getMessage() );
+            }
         }
 
-
         if ( st.countTokens() > 3 )
         {
             throw new PwmUnrecoverableException( new ErrorInformation( PwmError.ERROR_INTERNAL, "too many string tokens while parsing delimited identity key" ) );
         }
         final String profileID = st.nextToken();
         final String userDN = st.nextToken();
-        return createUserIdentity( userDN, profileID, domainID );
+        return create( userDN, profileID, domainID );
     }
 
+    /**
+     * Attempt to de-serialize value using delimited or obfuscated key.
+     *
+     * @deprecated  Should be used by calling {@link #fromDelimitedKey(String)} or {@link #fromObfuscatedKey(String, PwmApplication)}.
+     */
+    @Deprecated
     public static UserIdentity fromKey( final String key, final PwmApplication pwmApplication )
             throws PwmUnrecoverableException
     {
@@ -294,7 +320,7 @@ public class UserIdentity implements Serializable, Comparable<UserIdentity>
             return this;
         }
 
-        final ChaiUser chaiUser = pwmApplication.getProxiedChaiUser( this );
+        final ChaiUser chaiUser = pwmApplication.getDomains().get( this.getDomainID() ).getProxiedChaiUser( this );
         final String userDN;
         try
         {
@@ -304,7 +330,7 @@ public class UserIdentity implements Serializable, Comparable<UserIdentity>
         {
             throw PwmUnrecoverableException.fromChaiException( e );
         }
-        final UserIdentity canonicalziedIdentity = createUserIdentity( userDN, this.getLdapProfileID(), this.getDomainID() );
+        final UserIdentity canonicalziedIdentity = create( userDN, this.getLdapProfileID(), this.getDomainID() );
         canonicalziedIdentity.canonical = true;
         return canonicalziedIdentity;
     }

+ 25 - 14
server/src/main/java/password/pwm/config/AppConfig.java

@@ -24,9 +24,11 @@ import password.pwm.AppProperty;
 import password.pwm.PwmConstants;
 import password.pwm.bean.DomainID;
 import password.pwm.bean.PrivateKeyCertificate;
+import password.pwm.config.option.CertificateMatchingMode;
 import password.pwm.config.profile.EmailServerProfile;
 import password.pwm.config.profile.ProfileDefinition;
 import password.pwm.config.stored.StoredConfiguration;
+import password.pwm.config.value.FileValue;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmUnrecoverableException;
@@ -38,8 +40,8 @@ import password.pwm.util.logging.PwmLogLevel;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.secure.PwmRandom;
 import password.pwm.util.secure.PwmSecurityKey;
-import password.pwm.util.secure.SecureService;
 
+import java.security.cert.X509Certificate;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.LinkedHashMap;
@@ -59,7 +61,7 @@ public class AppConfig
     private final StoredConfiguration storedConfiguration;
     private final SettingReader settingReader;
     private final Map<DomainID, DomainConfig> domainConfigMap;
-    private final List<DomainID> domainIDList;
+    private final List<String> domainIDList;
 
     private PwmSecurityKey tempInstanceKey = null;
 
@@ -69,16 +71,15 @@ public class AppConfig
         this.settingReader = new SettingReader( storedConfiguration, null, DomainID.systemId() );
 
         this.domainIDList = settingReader.readSettingAsStringArray( PwmSetting.DOMAIN_LIST ).stream()
-                .map( DomainID::create )
                 .collect( Collectors.toUnmodifiableList() );
 
         this.domainConfigMap = domainIDList.stream()
                 .collect( Collectors.toUnmodifiableMap(
-                        ( domainID ) -> domainID,
-                        ( domainID ) -> new DomainConfig( this, domainID ) ) );
+                        DomainID::create,
+                        ( domainID ) -> new DomainConfig( this, DomainID.create( domainID ) ) ) );
     }
 
-    public List<DomainID> getDomainIDs()
+    public List<String> getDomainIDs()
     {
         return domainIDList;
     }
@@ -173,15 +174,19 @@ public class AppConfig
         return settingReader.readSettingAsPrivateKey( setting );
     }
 
-    public String configurationHash( final SecureService secureService )
-            throws PwmUnrecoverableException
+    public <E extends Enum<E>> E readSettingAsEnum( final PwmSetting setting, final Class<E> enumClass )
     {
-        return storedConfiguration.valueHash();
+        return settingReader.readSettingAsEnum( setting, enumClass );
     }
 
-    public <E extends Enum<E>> E readSettingAsEnum( final PwmSetting setting, final Class<E> enumClass )
+    public Map<FileValue.FileInformation, FileValue.FileContent> readSettingAsFile( final PwmSetting pwmSetting )
     {
-        return settingReader.readSettingAsEnum( setting, enumClass );
+        return settingReader.readSettingAsFile( pwmSetting );
+    }
+
+    public List<X509Certificate> readSettingAsCertificate( final PwmSetting pwmSetting )
+    {
+        return settingReader.readSettingAsCertificate( pwmSetting );
     }
 
     private class ConfigurationSuppliers
@@ -279,6 +284,14 @@ public class AppConfig
         return settingReader.getProfileMap( ProfileDefinition.EmailServers );
     }
 
+    public CertificateMatchingMode readCertificateMatchingMode()
+    {
+        final CertificateMatchingMode mode = readSettingAsEnum( PwmSetting.CERTIFICATE_VALIDATION_MODE, CertificateMatchingMode.class );
+        return mode == null
+                ? CertificateMatchingMode.CA_ONLY
+                : mode;
+    }
+
     public boolean hasDbConfigured( )
     {
         return !StringUtil.isEmpty( readSettingAsString( PwmSetting.DATABASE_CLASS ) )
@@ -287,10 +300,8 @@ public class AppConfig
                 && readSettingAsPassword( PwmSetting.DATABASE_PASSWORD ) != null;
     }
 
-    private PasswordData readSettingAsPassword( final PwmSetting setting )
+    public PasswordData readSettingAsPassword( final PwmSetting setting )
     {
         return settingReader.readSettingAsPassword( setting );
     }
-
-
 }

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

@@ -64,7 +64,7 @@ public class ConfigurationUtil
 
     public static List<DataStorageMethod> getCrWritePreference( final DomainConfig domainConfig )
     {
-        final List<DataStorageMethod> writeMethods = domainConfig.getResponseStorageLocations( PwmSetting.FORGOTTEN_PASSWORD_WRITE_PREFERENCE );
+        final List<DataStorageMethod> writeMethods = new ArrayList<>( domainConfig.getResponseStorageLocations( PwmSetting.FORGOTTEN_PASSWORD_WRITE_PREFERENCE ) );
         if ( writeMethods.size() == 1 && writeMethods.get( 0 ) == DataStorageMethod.AUTO )
         {
             writeMethods.clear();

+ 9 - 63
server/src/main/java/password/pwm/config/DomainConfig.java

@@ -24,7 +24,6 @@ import password.pwm.AppProperty;
 import password.pwm.bean.DomainID;
 import password.pwm.bean.EmailItemBean;
 import password.pwm.bean.PrivateKeyCertificate;
-import password.pwm.config.option.CertificateMatchingMode;
 import password.pwm.config.option.DataStorageMethod;
 import password.pwm.config.option.TokenStorageMethod;
 import password.pwm.config.profile.ActivateUserProfile;
@@ -40,12 +39,9 @@ import password.pwm.config.profile.ProfileDefinition;
 import password.pwm.config.profile.PwmPasswordPolicy;
 import password.pwm.config.profile.SetupOtpProfile;
 import password.pwm.config.profile.UpdateProfileProfile;
-import password.pwm.config.stored.StoredConfigItemKey;
 import password.pwm.config.stored.StoredConfiguration;
 import password.pwm.config.stored.StoredConfigurationUtil;
 import password.pwm.config.value.FileValue;
-import password.pwm.config.value.StoredValue;
-import password.pwm.config.value.ValueTypeConverter;
 import password.pwm.config.value.data.ActionConfiguration;
 import password.pwm.config.value.data.FormConfiguration;
 import password.pwm.config.value.data.NamedSecretData;
@@ -62,13 +58,11 @@ import password.pwm.util.java.LazySupplier;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.secure.PwmRandom;
 import password.pwm.util.secure.PwmSecurityKey;
-import password.pwm.util.secure.SecureService;
 
 import java.security.cert.X509Certificate;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.LinkedHashMap;
-import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
@@ -125,9 +119,7 @@ public class DomainConfig
 
     public EmailItemBean readSettingAsEmail( final PwmSetting setting, final Locale locale )
     {
-        final Map<Locale, EmailItemBean> availableLocaleMap = ValueTypeConverter.valueToLocalizedEmail( setting, readStoredValue( setting ) );
-        final Locale matchedLocale = LocaleHelper.localeResolver( locale, availableLocaleMap.keySet() );
-        return availableLocaleMap.get( matchedLocale );
+        return settingReader.readSettingAsEmail( setting, locale );
     }
 
     public <E extends Enum<E>> E readSettingAsEnum( final PwmSetting setting, final Class<E> enumClass )
@@ -196,8 +188,6 @@ public class DomainConfig
         return localizedMap;
     }
 
-
-
     public List<String> getChallengeProfileIDs( )
     {
         return StoredConfigurationUtil.profilesForSetting( PwmSetting.CHALLENGE_PROFILE_LIST, storedConfiguration );
@@ -210,7 +200,7 @@ public class DomainConfig
             throw new IllegalArgumentException( "unknown challenge profileID specified: " + profile );
         }
 
-        return ChallengeProfile.readChallengeProfileFromConfig( profile, locale, storedConfiguration );
+        return ChallengeProfile.readChallengeProfileFromConfig( getDomainID(), profile, locale, storedConfiguration );
     }
 
     public long readSettingAsLong( final PwmSetting setting )
@@ -232,32 +222,27 @@ public class DomainConfig
 
     public List<String> readSettingAsStringArray( final PwmSetting setting )
     {
-        return ValueTypeConverter.valueToStringArray( readStoredValue( setting ) );
+        return settingReader.readSettingAsStringArray( setting );
     }
 
     public String readSettingAsLocalizedString( final PwmSetting setting, final Locale locale )
     {
-        return ValueTypeConverter.valueToLocalizedString( readStoredValue( setting ), locale );
-    }
-
-    public boolean isDefaultValue( final PwmSetting pwmSetting )
-    {
-        return storedConfiguration.isDefaultValue( pwmSetting, null );
+        return settingReader.readSettingAsLocalizedString( setting, locale );
     }
 
     public boolean readSettingAsBoolean( final PwmSetting setting )
     {
-        return ValueTypeConverter.valueToBoolean( readStoredValue( setting ) );
+        return settingReader.readSettingAsBoolean( setting );
     }
 
     public Map<FileValue.FileInformation, FileValue.FileContent> readSettingAsFile( final PwmSetting setting )
     {
-        return ValueTypeConverter.valueToFile( setting, readStoredValue( setting ) );
+        return settingReader.readSettingAsFile( setting );
     }
 
     public List<X509Certificate> readSettingAsCertificate( final PwmSetting setting )
     {
-        return ValueTypeConverter.valueToX509Certificates( setting, readStoredValue( setting ) );
+        return settingReader.readSettingAsCertificate( setting );
     }
 
     public PrivateKeyCertificate readSettingAsPrivateKey( final PwmSetting setting )
@@ -269,7 +254,7 @@ public class DomainConfig
 
     public PwmSecurityKey getSecurityKey( ) throws PwmUnrecoverableException
     {
-        return configurationSuppliers.pwmSecurityKey.call();
+        return getAppConfig().getSecurityKey();
     }
 
     public List<DataStorageMethod> getResponseStorageLocations( final PwmSetting setting )
@@ -323,18 +308,7 @@ public class DomainConfig
     {
         return appConfig.readAppProperty( property );
     }
-
-    private StoredValue readStoredValue( final PwmSetting setting )
-    {
-        if ( setting.getCategory().hasProfiles() )
-        {
-            throw new IllegalStateException( "attempt to read setting value for setting '"
-                    + setting.getKey() + "' as non-profiled setting " );
-        }
-
-        return storedConfiguration.readSetting( setting, null );
-    }
-
+    
     public DomainID getDomainID()
     {
         return domainID;
@@ -452,34 +426,6 @@ public class DomainConfig
         return this.storedConfiguration;
     }
 
-
-    public String configurationHash( final SecureService secureService )
-            throws PwmUnrecoverableException
-    {
-        return appConfig.configurationHash( secureService );
-    }
-
-    public Set<PwmSetting> nonDefaultSettings( )
-    {
-        final Set<PwmSetting> returnSet = new LinkedHashSet<>();
-        for ( final StoredConfigItemKey key : this.storedConfiguration.modifiedItems() )
-        {
-            if ( key.getRecordType() == StoredConfigItemKey.RecordType.SETTING )
-            {
-                returnSet.add( key.toPwmSetting() );
-            }
-        }
-        return returnSet;
-    }
-
-    public CertificateMatchingMode readCertificateMatchingMode()
-    {
-        final CertificateMatchingMode mode = readSettingAsEnum( PwmSetting.CERTIFICATE_VALIDATION_MODE, CertificateMatchingMode.class );
-        return mode == null
-                ? CertificateMatchingMode.CA_ONLY
-                : mode;
-    }
-
     public Optional<PeopleSearchProfile> getPublicPeopleSearchProfile()
     {
         if ( readSettingAsBoolean( PwmSetting.PEOPLE_SEARCH_ENABLE_PUBLIC ) )

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

@@ -52,14 +52,13 @@ public enum PwmSetting
     TEMPLATE_STORAGE(
             "template.storage", PwmSettingSyntax.SELECT, PwmSettingCategory.TEMPLATES ),
 
-
     // notes
     NOTES(
             "notes.noteText", PwmSettingSyntax.TEXT_AREA, PwmSettingCategory.NOTES ),
 
     // domains
     DOMAIN_LIST(
-            "domain.list", PwmSettingSyntax.PROFILE, PwmSettingCategory.DOMAINS ),
+            "domain.list", PwmSettingSyntax.STRING_ARRAY, PwmSettingCategory.DOMAINS ),
 
     // application settings
     APP_PROPERTY_OVERRIDES(
@@ -67,7 +66,7 @@ public enum PwmSetting
 
     // domain settings
     PWM_SITE_URL(
-            "pwm.selfURL", PwmSettingSyntax.STRING, PwmSettingCategory.GENERAL ),
+            "pwm.selfURL", PwmSettingSyntax.STRING, PwmSettingCategory.APPLICATION ),
     PUBLISH_STATS_ENABLE(
             "pwm.publishStats.enable", PwmSettingSyntax.BOOLEAN, PwmSettingCategory.TELEMETRY ),
     PUBLISH_STATS_SITE_DESCRIPTION(
@@ -90,9 +89,9 @@ public enum PwmSetting
             "locale.cookie.age", PwmSettingSyntax.DURATION, PwmSettingCategory.LOCALIZATION ),
 
     // clustering
-    CLUSTER_ENABLED(
+    NODE_SERVICE_ENABLED(
             "nodeService.enable", PwmSettingSyntax.BOOLEAN, PwmSettingCategory.NODES ),
-    CLUSTER_STORAGE_MODE(
+    NODE_SERVICE_STORAGE_MODE(
             "nodeService.storageMode", PwmSettingSyntax.SELECT, PwmSettingCategory.NODES ),
     SECURITY_LOGIN_SESSION_MODE(
             "security.loginSession.mode", PwmSettingSyntax.SELECT, PwmSettingCategory.NODES ),
@@ -309,7 +308,7 @@ public enum PwmSetting
 
     // New multiple email settings
     EMAIL_SERVERS(
-            "email.profile.list", PwmSettingSyntax.PROFILE, PwmSettingCategory.INTERNAL ),
+            "email.profile.list", PwmSettingSyntax.PROFILE, PwmSettingCategory.APPLICATION ),
     EMAIL_SERVER_ADDRESS(
             "email.smtp.address", PwmSettingSyntax.STRING, PwmSettingCategory.EMAIL_SERVERS ),
     EMAIL_SERVER_TYPE(
@@ -324,15 +323,14 @@ public enum PwmSetting
             "email.smtp.userpassword", PwmSettingSyntax.PASSWORD, PwmSettingCategory.EMAIL_SERVERS ),
 
     // system wide email settings
-
-    EMAIL_DEFAULT_FROM_ADDRESS(
-            "email.default.fromAddress", PwmSettingSyntax.STRING, PwmSettingCategory.EMAIL_SETTINGS ),
     EMAIL_MAX_QUEUE_AGE(
             "email.queueMaxAge", PwmSettingSyntax.DURATION, PwmSettingCategory.EMAIL_SETTINGS ),
     EMAIL_ADVANCED_SETTINGS(
             "email.smtp.advancedSettings", PwmSettingSyntax.STRING_ARRAY, PwmSettingCategory.EMAIL_SETTINGS ),
 
     // email template
+    EMAIL_DEFAULT_FROM_ADDRESS(
+            "email.default.fromAddress", PwmSettingSyntax.STRING, PwmSettingCategory.EMAIL_TEMPLATES ),
     EMAIL_CHANGEPASSWORD(
             "email.changePassword", PwmSettingSyntax.EMAIL, PwmSettingCategory.EMAIL_TEMPLATES ),
     EMAIL_CHANGEPASSWORD_HELPDESK(

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

@@ -53,6 +53,8 @@ public enum PwmSettingCategory
     MODULES_PUBLIC( MODULES ),
     MODULES_PRIVATE( MODULES ),
 
+    SYSTEM( SETTINGS ),
+
     LDAP_PROFILE( LDAP ),
     LDAP_BASE( LDAP_PROFILE ),
     LDAP_LOGIN( LDAP_PROFILE ),
@@ -71,9 +73,11 @@ public enum PwmSettingCategory
 
     APPLICATION( SETTINGS ),
 
+
+    NODES( SYSTEM ),
+
     DOMAIN( SETTINGS ),
     GENERAL( DOMAIN ),
-    NODES( DOMAIN ),
     LOCALIZATION( DOMAIN ),
     TELEMETRY( DOMAIN ),
 

+ 51 - 8
server/src/main/java/password/pwm/config/SettingReader.java

@@ -21,20 +21,27 @@
 package password.pwm.config;
 
 import password.pwm.bean.DomainID;
+import password.pwm.bean.EmailItemBean;
 import password.pwm.bean.PrivateKeyCertificate;
 import password.pwm.config.profile.Profile;
 import password.pwm.config.profile.ProfileDefinition;
-import password.pwm.config.stored.StoredConfigItemKey;
+import password.pwm.config.stored.StoredConfigKey;
 import password.pwm.config.stored.StoredConfiguration;
 import password.pwm.config.stored.StoredConfigurationUtil;
+import password.pwm.config.value.FileValue;
 import password.pwm.config.value.StoredValue;
 import password.pwm.config.value.ValueTypeConverter;
+import password.pwm.config.value.VerificationMethodValue;
 import password.pwm.config.value.data.ActionConfiguration;
+import password.pwm.config.value.data.ChallengeItemConfiguration;
 import password.pwm.config.value.data.FormConfiguration;
 import password.pwm.config.value.data.NamedSecretData;
 import password.pwm.config.value.data.RemoteWebServiceConfiguration;
 import password.pwm.config.value.data.UserPermission;
+import password.pwm.error.PwmError;
+import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.util.PasswordData;
+import password.pwm.util.i18n.LocaleHelper;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.logging.PwmLogger;
 
@@ -87,6 +94,25 @@ public class SettingReader
         return ValueTypeConverter.valueToLocalizedStringArray( readSetting( setting ), locale );
     }
 
+    public Map<FileValue.FileInformation, FileValue.FileContent> readSettingAsFile( final PwmSetting pwmSetting )
+    {
+        return ValueTypeConverter.valueToFile( pwmSetting, readSetting( pwmSetting ) );
+    }
+
+    public List<ChallengeItemConfiguration> readSettingAsChallengeItems( final PwmSetting setting, final Locale locale )
+    {
+        final Map<String, List<ChallengeItemConfiguration>> storedValues = ValueTypeConverter.valueToChallengeItems ( readSetting( setting ) );
+        final Map<Locale, List<ChallengeItemConfiguration>> availableLocaleMap = storedValues.entrySet().stream()
+                .collect( Collectors.toUnmodifiableMap(
+                        entry -> LocaleHelper.parseLocaleString( entry.getKey() ),
+                        Map.Entry::getValue
+                ) );
+
+        final Locale matchedLocale = LocaleHelper.localeResolver( locale, availableLocaleMap.keySet() );
+
+        return availableLocaleMap.get( matchedLocale );
+    }
+
     public List<FormConfiguration> readSettingAsForm( final PwmSetting setting )
     {
         return ValueTypeConverter.valueToForm( readSetting( setting ) );
@@ -144,17 +170,30 @@ public class SettingReader
 
     public PrivateKeyCertificate readSettingAsPrivateKey( final PwmSetting setting )
     {
+        Objects.requireNonNull( setting );
+
         if ( PwmSettingSyntax.PRIVATE_KEY != setting.getSyntax() )
         {
             throw new IllegalArgumentException( "may not read PRIVATE_KEY value for setting: " + setting.toString() );
         }
-        if ( readSetting( setting ) == null )
-        {
-            return null;
-        }
+
         return ( PrivateKeyCertificate ) readSetting( setting ).toNativeObject();
     }
 
+    public EmailItemBean readSettingAsEmail( final PwmSetting setting, final Locale locale )
+    {
+        final Map<Locale, EmailItemBean> availableLocaleMap = ValueTypeConverter.valueToLocalizedEmail( setting, readSetting( setting ) );
+        final Locale matchedLocale = LocaleHelper.localeResolver( locale, availableLocaleMap.keySet() );
+        return availableLocaleMap.get( matchedLocale );
+    }
+
+    public VerificationMethodValue.VerificationMethodSettings readVerificationMethods( final PwmSetting setting )
+    {
+        final StoredValue configValue = readSetting( setting );
+        return ( VerificationMethodValue.VerificationMethodSettings ) configValue.toNativeObject();
+    }
+
+
     public <T extends Profile> Map<String, T> getProfileMap( final ProfileDefinition profileDefinition )
     {
         return profileReader.getProfileMap( profileDefinition );
@@ -231,7 +270,9 @@ public class SettingReader
             if ( setting.getCategory().getScope() == PwmSettingScope.DOMAIN )
             {
                 final String msg = "attempt to read DOMAIN scope setting '" + setting.toMenuLocationDebug( profileID, null ) + "' via system scope";
-                LOGGER.warn( () -> msg );
+                final PwmUnrecoverableException pwmUnrecoverableException = PwmUnrecoverableException.newException( PwmError.ERROR_INTERNAL, msg );
+                LOGGER.error( () -> pwmUnrecoverableException.getErrorInformation().toDebugStr(), pwmUnrecoverableException );
+                //throw new IllegalStateException( msg );
             }
         }
         else
@@ -239,7 +280,9 @@ public class SettingReader
             if ( setting.getCategory().getScope() == PwmSettingScope.SYSTEM )
             {
                 final String msg = "attempt to read SYSTEM scope setting '" + setting.toMenuLocationDebug( profileID, null ) + "' via domain scope";
-                LOGGER.warn( () -> msg );
+                final PwmUnrecoverableException pwmUnrecoverableException = PwmUnrecoverableException.newException( PwmError.ERROR_INTERNAL, msg );
+                LOGGER.error( () -> pwmUnrecoverableException.getErrorInformation().toDebugStr(), pwmUnrecoverableException );
+                //throw new IllegalStateException( msg );
             }
         }
 
@@ -263,7 +306,7 @@ public class SettingReader
             }
         }
 
-        final StoredConfigItemKey key = StoredConfigItemKey.fromSetting( setting, profileID, domainID );
+        final StoredConfigKey key = StoredConfigKey.forSetting( setting, profileID, domainID );
         return StoredConfigurationUtil.getValueOrDefault( storedConfiguration, key );
     }
 }

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

@@ -20,6 +20,7 @@
 
 package password.pwm.config;
 
+import password.pwm.config.stored.StoredConfigKey;
 import password.pwm.config.stored.StoredConfigurationModifier;
 import password.pwm.http.PwmRequest;
 
@@ -30,8 +31,7 @@ public interface SettingUIFunction
     Serializable provideFunction(
             PwmRequest pwmRequest,
             StoredConfigurationModifier modifier,
-            PwmSetting setting,
-            String profile,
+            StoredConfigKey key,
             String extraData
     )
             throws Exception;

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

@@ -23,8 +23,8 @@ package password.pwm.config.function;
 import password.pwm.bean.UserIdentity;
 import password.pwm.config.AppConfig;
 import password.pwm.config.DomainConfig;
-import password.pwm.config.PwmSetting;
 import password.pwm.config.SettingUIFunction;
+import password.pwm.config.stored.StoredConfigKey;
 import password.pwm.config.stored.StoredConfigurationModifier;
 import password.pwm.config.value.X509CertificateValue;
 import password.pwm.error.ErrorInformation;
@@ -47,15 +47,14 @@ abstract class AbstractUriCertImportFunction implements SettingUIFunction
     public String provideFunction(
             final PwmRequest pwmRequest,
             final StoredConfigurationModifier modifier,
-            final PwmSetting setting,
-            final String profile,
+            final StoredConfigKey key,
             final String extraData )
             throws PwmOperationalException, PwmUnrecoverableException
     {
         final PwmSession pwmSession = pwmRequest.getPwmSession();
         final List<X509Certificate> certs;
 
-        final String urlString = getUri( modifier, setting, profile, extraData );
+        final String urlString = getUri( modifier, key, extraData );
         try
         {
             final URI uri = URI.create( urlString );
@@ -81,7 +80,7 @@ abstract class AbstractUriCertImportFunction implements SettingUIFunction
 
 
         final UserIdentity userIdentity = pwmSession.isAuthenticated() ? pwmSession.getUserInfo().getUserIdentity() : null;
-        store( certs, modifier, setting, profile, extraData, userIdentity );
+        store( certs, modifier, key, extraData, userIdentity );
 
         final StringBuilder returnStr = new StringBuilder();
         for ( final X509Certificate loopCert : certs )
@@ -94,8 +93,7 @@ abstract class AbstractUriCertImportFunction implements SettingUIFunction
 
     abstract String getUri(
             StoredConfigurationModifier modifier,
-            PwmSetting pwmSetting,
-            String profile,
+            StoredConfigKey key,
             String extraData
     )
             throws PwmOperationalException, PwmUnrecoverableException;
@@ -103,15 +101,14 @@ abstract class AbstractUriCertImportFunction implements SettingUIFunction
 
     void store(
             final List<X509Certificate> certs,
-            final StoredConfigurationModifier storedConfiguration,
-            final PwmSetting pwmSetting,
-            final String profile,
+            final StoredConfigurationModifier modifier,
+            final StoredConfigKey key,
             final String extraData,
             final UserIdentity userIdentity
     )
             throws PwmOperationalException, PwmUnrecoverableException
     {
-        storedConfiguration.writeSetting( pwmSetting, profile, X509CertificateValue.fromX509( certs ), userIdentity );
+        modifier.writeSetting( key, X509CertificateValue.fromX509( certs ), userIdentity );
     }
 
 

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

@@ -23,7 +23,9 @@ package password.pwm.config.function;
 import com.google.gson.reflect.TypeToken;
 import password.pwm.bean.UserIdentity;
 import password.pwm.config.PwmSetting;
+import password.pwm.config.stored.StoredConfigKey;
 import password.pwm.config.stored.StoredConfigurationModifier;
+import password.pwm.config.stored.StoredConfigurationUtil;
 import password.pwm.config.value.ActionValue;
 import password.pwm.config.value.StoredValue;
 import password.pwm.config.value.ValueTypeConverter;
@@ -47,8 +49,7 @@ public class ActionCertImportFunction extends AbstractUriCertImportFunction
     @Override
     String getUri(
             final StoredConfigurationModifier modifier,
-            final PwmSetting pwmSetting,
-            final String profile,
+            final StoredConfigKey key,
             final String extraData
     )
             throws PwmOperationalException, PwmUnrecoverableException
@@ -57,8 +58,10 @@ public class ActionCertImportFunction extends AbstractUriCertImportFunction
         {
         } );
 
-        final StoredValue actionValue = modifier.newStoredConfiguration().readSetting( pwmSetting, profile );
-        final List<ActionConfiguration> actionConfigurations = ValueTypeConverter.valueToAction( pwmSetting, actionValue );
+        final PwmSetting pwmSetting = key.toPwmSetting();
+
+        final StoredValue actionValue = StoredConfigurationUtil.getValueOrDefault( modifier.newStoredConfiguration(), key );
+        final List<ActionConfiguration> actionConfigurations = ValueTypeConverter.valueToAction( key.toPwmSetting(), actionValue );
         final ActionConfiguration action = actionConfigurations.get( extraDataMap.get( KEY_ITERATION ) );
         final ActionConfiguration.WebAction webAction = action.getWebActions().get( extraDataMap.get( KEY_WEB_ACTION_ITERATION ) );
 
@@ -68,7 +71,7 @@ public class ActionCertImportFunction extends AbstractUriCertImportFunction
         {
             final ErrorInformation errorInformation = new ErrorInformation(
                     PwmError.CONFIG_FORMAT_ERROR,
-                    "Setting " + pwmSetting.toMenuLocationDebug( profile, null )
+                    "Setting " + pwmSetting.toMenuLocationDebug( key.getProfileID(), null )
                             + " action URL must first be configured" );
             throw new PwmOperationalException( errorInformation );
         }
@@ -80,7 +83,7 @@ public class ActionCertImportFunction extends AbstractUriCertImportFunction
         {
             final ErrorInformation errorInformation = new ErrorInformation(
                     PwmError.CONFIG_FORMAT_ERROR, "Setting "
-                    + pwmSetting.toMenuLocationDebug( profile, null ) + " action URL has an invalid URL syntax" );
+                    + pwmSetting.toMenuLocationDebug( key.getProfileID(), null ) + " action URL has an invalid URL syntax" );
             throw new PwmOperationalException( errorInformation );
         }
         return uriString;
@@ -89,9 +92,8 @@ public class ActionCertImportFunction extends AbstractUriCertImportFunction
     @Override
     void store(
             final List<X509Certificate> certs,
-            final StoredConfigurationModifier storedConfiguration,
-            final PwmSetting pwmSetting,
-            final String profile,
+            final StoredConfigurationModifier modifier,
+            final StoredConfigKey key,
             final String extraData,
             final UserIdentity userIdentity
     )
@@ -101,8 +103,8 @@ public class ActionCertImportFunction extends AbstractUriCertImportFunction
         {
         } );
 
-        final StoredValue actionValue = storedConfiguration.newStoredConfiguration().readSetting( pwmSetting, profile );
-        final List<ActionConfiguration> actionConfigurations = ValueTypeConverter.valueToAction( pwmSetting, actionValue );
+        final StoredValue actionValue = StoredConfigurationUtil.getValueOrDefault( modifier.newStoredConfiguration(), key );
+        final List<ActionConfiguration> actionConfigurations = ValueTypeConverter.valueToAction( key.toPwmSetting(), actionValue );
         final ActionConfiguration action = actionConfigurations.get( extraDataMap.get( KEY_ITERATION ) );
         final ActionConfiguration.WebAction webAction = action.getWebActions().get( extraDataMap.get( KEY_WEB_ACTION_ITERATION ) );
 
@@ -113,6 +115,6 @@ public class ActionCertImportFunction extends AbstractUriCertImportFunction
         action.getWebActions().set( extraDataMap.get( KEY_WEB_ACTION_ITERATION ), clonedAction );
 
         final ActionValue newActionValue = new ActionValue( actionConfigurations );
-        storedConfiguration.writeSetting( pwmSetting, profile, newActionValue, userIdentity );
+        modifier.writeSetting( key, newActionValue, userIdentity );
     }
 }

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

@@ -22,9 +22,10 @@ package password.pwm.config.function;
 
 import password.pwm.PwmDomain;
 import password.pwm.bean.UserIdentity;
-import password.pwm.config.PwmSetting;
 import password.pwm.config.SettingUIFunction;
+import password.pwm.config.stored.StoredConfigKey;
 import password.pwm.config.stored.StoredConfigurationModifier;
+import password.pwm.config.stored.StoredConfigurationUtil;
 import password.pwm.config.value.StringArrayValue;
 import password.pwm.config.value.X509CertificateValue;
 import password.pwm.error.PwmUnrecoverableException;
@@ -44,8 +45,7 @@ public class LdapCertImportFunction implements SettingUIFunction
     public String provideFunction(
             final PwmRequest pwmRequest,
             final StoredConfigurationModifier modifier,
-            final PwmSetting setting,
-            final String profile,
+            final StoredConfigKey key,
             final String extraData
     )
             throws PwmUnrecoverableException
@@ -53,7 +53,7 @@ public class LdapCertImportFunction implements SettingUIFunction
         final PwmDomain pwmDomain = pwmRequest.getPwmDomain();
         final PwmSession pwmSession = pwmRequest.getPwmSession();
 
-        final StringArrayValue ldapUrlsValue = ( StringArrayValue ) modifier.newStoredConfiguration().readSetting( PwmSetting.LDAP_SERVER_URLS, profile );
+        final StringArrayValue ldapUrlsValue = ( StringArrayValue ) StoredConfigurationUtil.getValueOrDefault( modifier.newStoredConfiguration(), key );
         final Set<X509Certificate> resultCertificates = new LinkedHashSet<>();
         if ( ldapUrlsValue != null && ldapUrlsValue.toNativeObject() != null )
         {
@@ -62,7 +62,7 @@ public class LdapCertImportFunction implements SettingUIFunction
         }
 
         final UserIdentity userIdentity = pwmSession.isAuthenticated() ? pwmSession.getUserInfo().getUserIdentity() : null;
-        modifier.writeSetting( setting, profile, X509CertificateValue.fromX509( resultCertificates ), userIdentity );
+        modifier.writeSetting( key, X509CertificateValue.fromX509( resultCertificates ), userIdentity );
         return Message.getLocalizedMessage( pwmSession.getSessionStateBean().getLocale(), Message.Success_Unknown, pwmDomain.getConfig() );
     }
 

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

@@ -22,6 +22,7 @@ package password.pwm.config.function;
 
 import password.pwm.PwmConstants;
 import password.pwm.config.PwmSetting;
+import password.pwm.config.stored.StoredConfigKey;
 import password.pwm.config.stored.StoredConfigurationModifier;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmError;
@@ -36,23 +37,23 @@ public class OAuthCertImportFunction extends AbstractUriCertImportFunction
 
 
     @Override
-    String getUri( final StoredConfigurationModifier modifier, final PwmSetting pwmSetting, final String profile, final String extraData )
+    String getUri( final StoredConfigurationModifier modifier, final StoredConfigKey key, final String extraData )
             throws PwmOperationalException, PwmUnrecoverableException
     {
+        final PwmSetting pwmSetting = key.toPwmSetting();
 
         final String uriString;
         final String menuDebugLocation;
 
+        final PwmSetting urlCertSetting;
         switch ( pwmSetting )
         {
             case OAUTH_ID_CERTIFICATE:
-                uriString = ( String ) modifier.newStoredConfiguration().readSetting( PwmSetting.OAUTH_ID_CODERESOLVE_URL, null ).toNativeObject();
-                menuDebugLocation = PwmSetting.OAUTH_ID_CODERESOLVE_URL.toMenuLocationDebug( null, PwmConstants.DEFAULT_LOCALE );
+                urlCertSetting = PwmSetting.OAUTH_ID_CODERESOLVE_URL;
                 break;
 
             case RECOVERY_OAUTH_ID_CERTIFICATE:
-                uriString = ( String ) modifier.newStoredConfiguration().readSetting( PwmSetting.RECOVERY_OAUTH_ID_CODERESOLVE_URL, profile ).toNativeObject();
-                menuDebugLocation = PwmSetting.RECOVERY_OAUTH_ID_CERTIFICATE.toMenuLocationDebug( profile, PwmConstants.DEFAULT_LOCALE );
+                urlCertSetting = PwmSetting.RECOVERY_OAUTH_ID_CODERESOLVE_URL;
                 break;
 
             default:
@@ -60,6 +61,9 @@ public class OAuthCertImportFunction extends AbstractUriCertImportFunction
                 return null;
         }
 
+        final StoredConfigKey oauthCertKey = StoredConfigKey.forSetting( urlCertSetting, key.getProfileID(), key.getDomainID() );
+        uriString = ( String ) modifier.newStoredConfiguration().readStoredValue( oauthCertKey ).orElseThrow().toNativeObject();
+        menuDebugLocation = urlCertSetting.toMenuLocationDebug( null, PwmConstants.DEFAULT_LOCALE );
 
         if ( uriString.isEmpty() )
         {

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

@@ -22,7 +22,9 @@ package password.pwm.config.function;
 
 import password.pwm.bean.UserIdentity;
 import password.pwm.config.PwmSetting;
+import password.pwm.config.stored.StoredConfigKey;
 import password.pwm.config.stored.StoredConfigurationModifier;
+import password.pwm.config.stored.StoredConfigurationUtil;
 import password.pwm.config.value.RemoteWebServiceValue;
 import password.pwm.config.value.data.RemoteWebServiceConfiguration;
 import password.pwm.error.ErrorInformation;
@@ -39,10 +41,13 @@ public class RemoteWebServiceCertImportFunction extends AbstractUriCertImportFun
 {
 
     @Override
-    String getUri( final StoredConfigurationModifier modifier, final PwmSetting pwmSetting, final String profile, final String extraData )
+    String getUri( final StoredConfigurationModifier modifier, final StoredConfigKey key, final String extraData )
             throws PwmOperationalException, PwmUnrecoverableException
     {
-        final RemoteWebServiceValue actionValue = ( RemoteWebServiceValue ) modifier.newStoredConfiguration().readSetting( pwmSetting, profile );
+        final PwmSetting pwmSetting = key.toPwmSetting();
+        final String profile = key.getProfileID();
+
+        final RemoteWebServiceValue actionValue = ( RemoteWebServiceValue ) StoredConfigurationUtil.getValueOrDefault( modifier.newStoredConfiguration(), key );
         final String serviceName = actionNameFromExtraData( extraData );
         final RemoteWebServiceConfiguration action = actionValue.forName( serviceName );
         final String uriString = action.getUrl();
@@ -75,14 +80,13 @@ public class RemoteWebServiceCertImportFunction extends AbstractUriCertImportFun
     void store(
             final List<X509Certificate> certs,
             final StoredConfigurationModifier modifier,
-            final PwmSetting pwmSetting,
-            final String profile,
+            final StoredConfigKey key,
             final String extraData,
             final UserIdentity userIdentity
     )
             throws PwmOperationalException, PwmUnrecoverableException
     {
-        final RemoteWebServiceValue actionValue = ( RemoteWebServiceValue ) modifier.newStoredConfiguration().readSetting( pwmSetting, profile );
+        final RemoteWebServiceValue actionValue = ( RemoteWebServiceValue )  StoredConfigurationUtil.getValueOrDefault( modifier.newStoredConfiguration(), key );
         final String actionName = actionNameFromExtraData( extraData );
         final List<RemoteWebServiceConfiguration> newList = new ArrayList<>();
         for ( final RemoteWebServiceConfiguration loopConfiguration : actionValue.toNativeObject() )
@@ -100,7 +104,7 @@ public class RemoteWebServiceCertImportFunction extends AbstractUriCertImportFun
             }
         }
         final RemoteWebServiceValue newActionValue = new RemoteWebServiceValue( newList );
-        modifier.writeSetting( pwmSetting, profile, newActionValue, userIdentity );
+        modifier.writeSetting( key, newActionValue, userIdentity );
     }
 
 }

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

@@ -22,6 +22,7 @@ package password.pwm.config.function;
 
 import password.pwm.PwmConstants;
 import password.pwm.config.PwmSetting;
+import password.pwm.config.stored.StoredConfigKey;
 import password.pwm.config.stored.StoredConfigurationModifier;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmError;
@@ -33,13 +34,14 @@ import java.net.URI;
 public class SMSGatewayCertImportFunction extends AbstractUriCertImportFunction
 {
     @Override
-    String getUri( final StoredConfigurationModifier modifier, final PwmSetting pwmSetting, final String profile, final String extraData )
+    String getUri( final StoredConfigurationModifier modifier, final StoredConfigKey key, final String extraData )
             throws PwmOperationalException, PwmUnrecoverableException
     {
         final String uriString;
         final String menuDebugLocation;
 
-        uriString = ( String ) modifier.newStoredConfiguration().readSetting( PwmSetting.SMS_GATEWAY_URL, null ).toNativeObject();
+        final var urlSettingKey = StoredConfigKey.forSetting( PwmSetting.SMS_GATEWAY_URL, key.getProfileID(), key.getDomainID() );
+        uriString = ( String ) modifier.newStoredConfiguration().readStoredValue( urlSettingKey ).orElseThrow().toNativeObject();
         menuDebugLocation = PwmSetting.SMS_GATEWAY_URL.toMenuLocationDebug( null, PwmConstants.DEFAULT_LOCALE );
 
         if ( uriString.isEmpty() )

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

@@ -21,8 +21,8 @@
 package password.pwm.config.function;
 
 import password.pwm.bean.UserIdentity;
-import password.pwm.config.PwmSetting;
 import password.pwm.config.SettingUIFunction;
+import password.pwm.config.stored.StoredConfigKey;
 import password.pwm.config.stored.StoredConfigurationModifier;
 import password.pwm.config.value.X509CertificateValue;
 import password.pwm.error.PwmUnrecoverableException;
@@ -41,19 +41,19 @@ public class SmtpCertImportFunction implements SettingUIFunction
     public String provideFunction(
             final PwmRequest pwmRequest,
             final StoredConfigurationModifier modifier,
-            final PwmSetting setting,
-            final String profile,
+            final StoredConfigKey key,
             final String extraData
     )
             throws PwmUnrecoverableException
     {
         final PwmSession pwmSession = pwmRequest.getPwmSession();
+        final String profile = key.getProfileID();
 
         final List<X509Certificate> certs = EmailServerUtil.readCertificates( pwmRequest.getAppConfig(), profile );
         if ( !JavaHelper.isEmpty( certs ) )
         {
             final UserIdentity userIdentity = pwmSession.isAuthenticated() ? pwmSession.getUserInfo().getUserIdentity() : null;
-            modifier.writeSetting( PwmSetting.EMAIL_SERVER_CERTS, profile, X509CertificateValue.fromX509( certs ), userIdentity );
+            modifier.writeSetting( key, X509CertificateValue.fromX509( certs ), userIdentity );
         }
 
         return Message.getLocalizedMessage( pwmSession.getSessionStateBean().getLocale(), Message.Success_Unknown, pwmRequest.getDomainConfig() );

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

@@ -25,7 +25,9 @@ import password.pwm.bean.UserIdentity;
 import password.pwm.config.AppConfig;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.SettingUIFunction;
+import password.pwm.config.stored.StoredConfigKey;
 import password.pwm.config.stored.StoredConfigurationModifier;
+import password.pwm.config.value.ValueTypeConverter;
 import password.pwm.config.value.X509CertificateValue;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmError;
@@ -36,6 +38,7 @@ import password.pwm.http.PwmRequest;
 import password.pwm.http.PwmSession;
 import password.pwm.i18n.Message;
 import password.pwm.svc.event.SyslogAuditService;
+import password.pwm.util.java.JavaHelper;
 import password.pwm.util.secure.X509Utils;
 
 import java.security.cert.X509Certificate;
@@ -50,8 +53,7 @@ public class SyslogCertImportFunction implements SettingUIFunction
     public String provideFunction(
             final PwmRequest pwmRequest,
             final StoredConfigurationModifier modifier,
-            final PwmSetting setting,
-            final String profile,
+            final StoredConfigKey key,
             final String extraData
     )
             throws PwmOperationalException, PwmUnrecoverableException
@@ -63,8 +65,9 @@ public class SyslogCertImportFunction implements SettingUIFunction
 
         final Set<X509Certificate> resultCertificates = new LinkedHashSet<>();
 
-        final List<String> syslogConfigStrs = ( List<String> ) modifier.newStoredConfiguration().readSetting( PwmSetting.AUDIT_SYSLOG_SERVERS, null ).toNativeObject();
-        if ( syslogConfigStrs != null && !syslogConfigStrs.isEmpty() )
+        final var syslogServerSetting = StoredConfigKey.forSetting( PwmSetting.AUDIT_SYSLOG_SERVERS, key.getProfileID(), key.getDomainID() );
+        final List<String> syslogConfigStrs = ValueTypeConverter.valueToStringArray( modifier.newStoredConfiguration().readStoredValue( syslogServerSetting ).orElseThrow() );
+        if ( !JavaHelper.isEmpty( syslogConfigStrs ) )
         {
             for ( final String entry : syslogConfigStrs )
             {
@@ -99,7 +102,7 @@ public class SyslogCertImportFunction implements SettingUIFunction
         if ( !error )
         {
             final UserIdentity userIdentity = pwmSession.isAuthenticated() ? pwmSession.getUserInfo().getUserIdentity() : null;
-            modifier.writeSetting( setting, null, X509CertificateValue.fromX509( resultCertificates ), userIdentity );
+            modifier.writeSetting( key, X509CertificateValue.fromX509( resultCertificates ), userIdentity );
             return Message.getLocalizedMessage( pwmSession.getSessionStateBean().getLocale(), Message.Success_Unknown, pwmDomain.getConfig() );
         }
         else

+ 7 - 8
server/src/main/java/password/pwm/config/function/UserMatchViewerFunction.java

@@ -31,10 +31,11 @@ import password.pwm.PwmDomain;
 import password.pwm.bean.SessionLabel;
 import password.pwm.bean.UserIdentity;
 import password.pwm.config.AppConfig;
-import password.pwm.config.PwmSetting;
 import password.pwm.config.SettingUIFunction;
+import password.pwm.config.stored.StoredConfigKey;
 import password.pwm.config.stored.StoredConfiguration;
 import password.pwm.config.stored.StoredConfigurationModifier;
+import password.pwm.config.stored.StoredConfigurationUtil;
 import password.pwm.config.value.StoredValue;
 import password.pwm.config.value.ValueTypeConverter;
 import password.pwm.config.value.data.UserPermission;
@@ -65,8 +66,7 @@ public class UserMatchViewerFunction implements SettingUIFunction
     public Serializable provideFunction(
             final PwmRequest pwmRequest,
             final StoredConfigurationModifier storedConfiguration,
-            final PwmSetting setting,
-            final String profile,
+            final StoredConfigKey key,
             final String extraData )
             throws Exception
     {
@@ -74,7 +74,7 @@ public class UserMatchViewerFunction implements SettingUIFunction
 
         final Instant startSearchTime = Instant.now();
         final int maxResultSize = Integer.parseInt( pwmDomain.getConfig().readAppProperty( AppProperty.CONFIG_EDITOR_USER_PERMISSION_MATCH_LIMIT ) );
-        final Collection<UserIdentity> users = discoverMatchingUsers( pwmDomain, maxResultSize, storedConfiguration.newStoredConfiguration(), setting, profile );
+        final Collection<UserIdentity> users = discoverMatchingUsers( pwmDomain, maxResultSize, storedConfiguration.newStoredConfiguration(), key );
         final TimeDuration searchDuration = TimeDuration.fromCurrent( startSearchTime );
 
         final String message = LocaleHelper.getLocalizedMessage(
@@ -95,14 +95,13 @@ public class UserMatchViewerFunction implements SettingUIFunction
             final PwmDomain pwmDomain,
             final int maxResultSize,
             final StoredConfiguration storedConfiguration,
-            final PwmSetting setting,
-            final String profile
+            final StoredConfigKey key
     )
             throws Exception
     {
         final AppConfig config = new AppConfig( storedConfiguration );
-        final PwmApplication tempApplication = PwmApplication.createPwmApplication( pwmDomain.getPwmEnvironment().makeRuntimeInstance( config ) );
-        final StoredValue storedValue = storedConfiguration.readSetting( setting, profile );
+        final PwmApplication tempApplication = PwmApplication.createPwmApplication( pwmDomain.getPwmApplication().getPwmEnvironment().makeRuntimeInstance( config ) );
+        final StoredValue storedValue = StoredConfigurationUtil.getValueOrDefault( storedConfiguration, key );
         final List<UserPermission> permissions = ValueTypeConverter.valueToUserPermissions( storedValue );
 
         validateUserPermissionLdapValues( tempApplication.getDefaultDomain(), permissions );

+ 0 - 9
server/src/main/java/password/pwm/config/option/IdentityVerificationMethod.java

@@ -24,9 +24,6 @@ import password.pwm.config.DomainConfig;
 import password.pwm.i18n.Display;
 
 import java.io.Serializable;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
 import java.util.Locale;
 
 public enum IdentityVerificationMethod implements Serializable, ConfigurationOption
@@ -64,10 +61,4 @@ public enum IdentityVerificationMethod implements Serializable, ConfigurationOpt
     {
         return Display.getLocalizedMessage( locale, this.descriptionKey, domainConfig );
     }
-
-    public static IdentityVerificationMethod[] availableValues( )
-    {
-        final List<IdentityVerificationMethod> values = new ArrayList<>( Arrays.asList( IdentityVerificationMethod.values() ) );
-        return values.toArray( new IdentityVerificationMethod[0] );
-    }
 }

+ 8 - 15
server/src/main/java/password/pwm/config/profile/AbstractProfile.java

@@ -25,7 +25,6 @@ import password.pwm.config.PwmSetting;
 import password.pwm.config.SettingReader;
 import password.pwm.config.option.IdentityVerificationMethod;
 import password.pwm.config.stored.StoredConfiguration;
-import password.pwm.config.value.StoredValue;
 import password.pwm.config.value.VerificationMethodValue;
 import password.pwm.config.value.data.ActionConfiguration;
 import password.pwm.config.value.data.FormConfiguration;
@@ -141,13 +140,17 @@ public abstract class AbstractProfile implements Profile
         return storedConfiguration;
     }
 
+    protected SettingReader getSettingReader()
+    {
+        return settingReader;
+    }
+
     Set<IdentityVerificationMethod> readVerificationMethods( final PwmSetting setting, final VerificationMethodValue.EnabledState enabledState )
     {
         final Set<IdentityVerificationMethod> result = EnumSet.noneOf( IdentityVerificationMethod.class );
-        final StoredValue configValue = readSetting( setting );
-        final VerificationMethodValue.VerificationMethodSettings verificationMethodSettings = ( VerificationMethodValue.VerificationMethodSettings ) configValue.toNativeObject();
+        final VerificationMethodValue.VerificationMethodSettings verificationMethodSettings = settingReader.readVerificationMethods( setting );
 
-        for ( final IdentityVerificationMethod recoveryVerificationMethods : IdentityVerificationMethod.availableValues() )
+        for ( final IdentityVerificationMethod recoveryVerificationMethods : IdentityVerificationMethod.values() )
         {
             if ( verificationMethodSettings.getMethodSettings().containsKey( recoveryVerificationMethods ) )
             {
@@ -157,16 +160,6 @@ public abstract class AbstractProfile implements Profile
                 }
             }
         }
-        return result;
+        return Collections.unmodifiableSet( result );
     }
-
-    protected StoredValue readSetting( final PwmSetting setting )
-    {
-        if ( !setting.getCategory().hasProfiles() )
-        {
-            throw new IllegalStateException( "attempt to read non-profiled setting '" + setting.getKey() + "' via profile" );
-        }
-        return storedConfiguration.readSetting( setting, getIdentifier() );
-    }
-
 }

+ 15 - 46
server/src/main/java/password/pwm/config/profile/ChallengeProfile.java

@@ -26,26 +26,22 @@ import com.novell.ldapchai.cr.Challenge;
 import com.novell.ldapchai.cr.ChallengeSet;
 import com.novell.ldapchai.exception.ChaiValidationException;
 import password.pwm.PwmConstants;
+import password.pwm.bean.DomainID;
 import password.pwm.config.PwmSetting;
-import password.pwm.config.value.StoredValue;
+import password.pwm.config.SettingReader;
 import password.pwm.config.stored.StoredConfiguration;
-import password.pwm.config.value.ChallengeValue;
-import password.pwm.config.value.ValueTypeConverter;
 import password.pwm.config.value.data.ChallengeItemConfiguration;
 import password.pwm.config.value.data.UserPermission;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmOperationalException;
-import password.pwm.util.i18n.LocaleHelper;
 import password.pwm.util.logging.PwmLogger;
 
 import java.io.Serializable;
 import java.util.ArrayList;
 import java.util.Collections;
-import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Locale;
-import java.util.Map;
 
 public class ChallengeProfile implements Profile, Serializable
 {
@@ -79,20 +75,22 @@ public class ChallengeProfile implements Profile, Serializable
     }
 
     public static ChallengeProfile readChallengeProfileFromConfig(
+            final DomainID domainID,
             final String profileID,
             final Locale locale,
             final StoredConfiguration storedConfiguration
     )
     {
-        final int minRandomRequired = ( int ) ValueTypeConverter.valueToLong( storedConfiguration.readSetting( PwmSetting.CHALLENGE_MIN_RANDOM_REQUIRED, profileID ) );
+        final SettingReader settingReader = new SettingReader( storedConfiguration, profileID, domainID );
+
+        final int minRandomRequired = Math.toIntExact( settingReader.readSettingAsLong( PwmSetting.CHALLENGE_MIN_RANDOM_REQUIRED ) );
 
         ChallengeSet readChallengeSet = null;
         try
         {
             readChallengeSet = readChallengeSet(
-                    profileID,
+                    settingReader,
                     locale,
-                    storedConfiguration,
                     PwmSetting.CHALLENGE_REQUIRED_CHALLENGES,
                     PwmSetting.CHALLENGE_RANDOM_CHALLENGES,
                     minRandomRequired
@@ -107,9 +105,8 @@ public class ChallengeProfile implements Profile, Serializable
         try
         {
             readHelpdeskChallengeSet = readChallengeSet(
-                    profileID,
+                    settingReader,
                     locale,
-                    storedConfiguration,
                     PwmSetting.CHALLENGE_HELPDESK_REQUIRED_CHALLENGES,
                     PwmSetting.CHALLENGE_HELPDESK_RANDOM_CHALLENGES,
                     1
@@ -120,15 +117,10 @@ public class ChallengeProfile implements Profile, Serializable
             LOGGER.trace( () -> "discarding configured helpdesk challengeSet for profile '" + profileID + "' issue: " + e.getMessage() );
         }
 
-        final int minRandomSetup = ( int ) ValueTypeConverter.valueToLong( storedConfiguration.readSetting( PwmSetting.CHALLENGE_MIN_RANDOM_SETUP, profileID ) );
-        final int minHelpdeskRandomSetup = ( int ) ValueTypeConverter.valueToLong( storedConfiguration.readSetting(
-                PwmSetting.CHALLENGE_HELPDESK_MIN_RANDOM_SETUP,
-                profileID
-        ) );
-        final List<UserPermission> userPermissions = ( List<UserPermission> ) storedConfiguration.readSetting(
-                PwmSetting.CHALLENGE_POLICY_QUERY_MATCH,
-                profileID
-        ).toNativeObject();
+        final int minRandomSetup = Math.toIntExact( settingReader.readSettingAsLong( PwmSetting.CHALLENGE_MIN_RANDOM_SETUP ) );
+        final int minHelpdeskRandomSetup = Math.toIntExact( settingReader.readSettingAsLong( PwmSetting.CHALLENGE_HELPDESK_MIN_RANDOM_SETUP ) );
+
+        final List<UserPermission> userPermissions = settingReader.readSettingAsUserPermission( PwmSetting.CHALLENGE_POLICY_QUERY_MATCH );
 
         return new ChallengeProfile( profileID, locale, readChallengeSet, readHelpdeskChallengeSet, minRandomSetup, minHelpdeskRandomSetup, userPermissions );
     }
@@ -188,19 +180,16 @@ public class ChallengeProfile implements Profile, Serializable
     }
 
     private static ChallengeSet readChallengeSet(
-            final String profileID,
+            final SettingReader settingReader,
             final Locale locale,
-            final StoredConfiguration storedConfiguration,
             final PwmSetting requiredChallenges,
             final PwmSetting randomChallenges,
             final int minimumRands
     )
             throws PwmOperationalException
     {
-        final List<ChallengeItemConfiguration> requiredQuestions = valueToChallengeItemArray(
-                storedConfiguration.readSetting( requiredChallenges, profileID ), locale );
-        final List<ChallengeItemConfiguration> randomQuestions = valueToChallengeItemArray(
-                storedConfiguration.readSetting( randomChallenges, profileID ), locale );
+        final List<ChallengeItemConfiguration> requiredQuestions = settingReader.readSettingAsChallengeItems( requiredChallenges, locale );
+        final List<ChallengeItemConfiguration> randomQuestions = settingReader.readSettingAsChallengeItems( randomChallenges, locale );
 
         final List<Challenge> challenges = new ArrayList<>();
         int randoms = minimumRands;
@@ -264,26 +253,6 @@ public class ChallengeProfile implements Profile, Serializable
         }
     }
 
-    static List<ChallengeItemConfiguration> valueToChallengeItemArray(
-            final StoredValue value,
-            final Locale locale
-    )
-    {
-        if ( !( value instanceof ChallengeValue ) )
-        {
-            throw new IllegalArgumentException( "may not read ChallengeValue value" );
-        }
-        final Map<String, List<ChallengeItemConfiguration>> storedValues = ( Map<String, List<ChallengeItemConfiguration>> ) value.toNativeObject();
-        final Map<Locale, List<ChallengeItemConfiguration>> availableLocaleMap = new LinkedHashMap<>();
-        for ( final Map.Entry<String, List<ChallengeItemConfiguration>> entry : storedValues.entrySet() )
-        {
-            final String localeStr = entry.getKey();
-            availableLocaleMap.put( LocaleHelper.parseLocaleString( localeStr ), entry.getValue() );
-        }
-        final Locale matchedLocale = LocaleHelper.localeResolver( locale, availableLocaleMap.keySet() );
-
-        return availableLocaleMap.get( matchedLocale );
-    }
 
     @Override
     public ProfileDefinition profileType( )

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

@@ -20,7 +20,6 @@
 
 package password.pwm.config.profile;
 
-import password.pwm.config.PwmSetting;
 import password.pwm.config.stored.StoredConfiguration;
 
 import java.util.Locale;
@@ -44,8 +43,7 @@ public class EmailServerProfile extends AbstractProfile
     @Override
     public String getDisplayName( final Locale locale )
     {
-        final String value = this.readSettingAsLocalizedString( PwmSetting.EMAIL_SERVERS, locale );
-        return value != null && !value.isEmpty() ? value : this.getIdentifier();
+        return this.getIdentifier();
     }
 
     public static class EmailServerProfileFactory implements ProfileFactory

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

@@ -21,7 +21,6 @@
 package password.pwm.config.profile;
 
 import password.pwm.config.PwmSetting;
-import password.pwm.config.value.StoredValue;
 import password.pwm.config.option.IdentityVerificationMethod;
 import password.pwm.config.stored.StoredConfiguration;
 import password.pwm.config.value.VerificationMethodValue;
@@ -72,8 +71,8 @@ public class ForgottenPasswordProfile extends AbstractProfile
 
     public int getMinOptionalRequired( )
     {
-        final StoredValue configValue = getStoredConfiguration().readSetting( PwmSetting.RECOVERY_VERIFICATION_METHODS, getIdentifier() );
-        final VerificationMethodValue.VerificationMethodSettings verificationMethodSettings = ( VerificationMethodValue.VerificationMethodSettings ) configValue.toNativeObject();
+        final VerificationMethodValue.VerificationMethodSettings verificationMethodSettings = getSettingReader().readVerificationMethods(
+                PwmSetting.RECOVERY_VERIFICATION_METHODS );
         return verificationMethodSettings.getMinOptionalRequired();
     }
 

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

@@ -205,7 +205,7 @@ public class LdapProfile extends AbstractProfile implements Profile
 
         if ( !StringUtil.isEmpty( testUserDN ) )
         {
-            return UserIdentity.createUserIdentity( testUserDN, this.getIdentifier(), pwmDomain.getDomainID() ).canonicalized( pwmDomain.getPwmApplication() );
+            return UserIdentity.create( testUserDN, this.getIdentifier(), pwmDomain.getDomainID() ).canonicalized( pwmDomain.getPwmApplication() );
         }
 
         return null;

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

@@ -132,7 +132,7 @@ public class NewUserProfile extends AbstractProfile implements Profile
                 {
                     final ChaiProvider chaiProvider = pwmDomain.getProxyChaiProvider( ldapProfile.getIdentifier() );
                     final ChaiUser chaiUser = chaiProvider.getEntryFactory().newChaiUser( lookupDN );
-                    final UserIdentity userIdentity = UserIdentity.createUserIdentity( lookupDN, ldapProfile.getIdentifier(), pwmDomain.getDomainID() );
+                    final UserIdentity userIdentity = UserIdentity.create( lookupDN, ldapProfile.getIdentifier(), pwmDomain.getDomainID() );
                     thePolicy = PasswordUtility.readPasswordPolicyForUser( pwmDomain, null, userIdentity, chaiUser );
                 }
                 catch ( final ChaiUnavailableException e )

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

@@ -298,7 +298,7 @@ public class PwmPasswordPolicy implements Profile, Serializable
         }
 
         final Locale resolvedLocale = LocaleHelper.localeResolver( locale, policyMetaData.getChangePasswordText().keySet() );
-        return Optional.of( policyMetaData.getChangePasswordText().get( resolvedLocale ) );
+        return Optional.ofNullable( policyMetaData.getChangePasswordText().get( resolvedLocale ) );
     }
 
     public Optional<String> getRuleText( final Locale locale )

+ 176 - 0
server/src/main/java/password/pwm/config/stored/ConfigSearchMachine.java

@@ -0,0 +1,176 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2020 The PWM Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package password.pwm.config.stored;
+
+import password.pwm.bean.DomainID;
+import password.pwm.config.PwmSetting;
+import password.pwm.config.PwmSettingSyntax;
+import password.pwm.config.value.StoredValue;
+import password.pwm.util.java.JavaHelper;
+import password.pwm.util.java.StringUtil;
+import password.pwm.util.logging.PwmLogger;
+
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.Locale;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+public class ConfigSearchMachine
+{
+    private static final PwmLogger LOGGER = PwmLogger.forClass( ConfigSearchMachine.class );
+
+    private final StoredConfiguration storedConfiguration;
+    private final Locale locale;
+
+    public ConfigSearchMachine(
+            final StoredConfiguration storedConfiguration,
+            final Locale locale
+    )
+    {
+        this.storedConfiguration = storedConfiguration;
+        this.locale = locale;
+    }
+
+    public static boolean matchSetting(
+            final StoredConfiguration storedConfiguration,
+            final PwmSetting setting,
+            final StoredValue storedValue,
+            final String term,
+            final Locale defaultLocale )
+    {
+        return new ConfigSearchMachine( storedConfiguration, defaultLocale ).matchSetting( setting, storedValue, term );
+    }
+
+    public Set<StoredConfigKey> search( final String searchTerm, final Set<DomainID> domainScope )
+    {
+        if ( StringUtil.isEmpty( searchTerm ) )
+        {
+            return Collections.emptySet();
+        }
+
+        return StoredConfigurationUtil.allPossibleSettingKeysForConfiguration( storedConfiguration )
+                .parallelStream()
+                .filter( k -> k.getRecordType() == StoredConfigKey.RecordType.SETTING )
+                .filter( k -> JavaHelper.isEmpty( domainScope ) || domainScope.contains( k.getDomainID() ) )
+                .filter( k -> matchSetting( k, searchTerm ) )
+                .sorted()
+                .collect( Collectors.toCollection( LinkedHashSet::new ) );
+    }
+
+    private boolean matchSetting(
+            final StoredConfigKey storedConfigKey,
+            final String searchTerm
+    )
+    {
+        final PwmSetting pwmSetting = storedConfigKey.toPwmSetting();
+        final Optional<StoredValue> value = storedConfiguration.readStoredValue( storedConfigKey );
+
+        if ( value.isEmpty() )
+        {
+            return false;
+        }
+
+        return StringUtil.whitespaceSplit( searchTerm )
+                .parallelStream()
+                .allMatch( s -> matchSetting( pwmSetting, value.get(), 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;
+    }
+}

+ 65 - 64
server/src/main/java/password/pwm/config/stored/ConfigurationCleaner.java

@@ -21,9 +21,8 @@
 package password.pwm.config.stored;
 
 import password.pwm.PwmConstants;
+import password.pwm.bean.DomainID;
 import password.pwm.bean.UserIdentity;
-import password.pwm.config.AppConfig;
-import password.pwm.config.DomainConfig;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.option.ADPolicyComplexity;
 import password.pwm.config.option.RecoveryMinLifetimeOption;
@@ -31,6 +30,7 @@ import password.pwm.config.option.WebServiceUsage;
 import password.pwm.config.value.OptionListValue;
 import password.pwm.config.value.StoredValue;
 import password.pwm.config.value.StringValue;
+import password.pwm.config.value.ValueFactory;
 import password.pwm.config.value.ValueTypeConverter;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.util.java.PwmExceptionLoggingConsumer;
@@ -68,32 +68,21 @@ class ConfigurationCleaner
                 throws PwmUnrecoverableException
         {
             final StoredConfiguration existingConfig = modifier.newStoredConfiguration();
-            final AppConfig appConfig = new AppConfig( existingConfig );
 
-            for ( final DomainConfig domainConfig : appConfig.getDomainConfigs().values() )
-            {
-                for ( final String profileID : domainConfig.getPasswordProfileIDs() )
-                {
-                    final StoredConfigItemKey key = StoredConfigItemKey.fromSetting( PwmSetting.PASSWORD_POLICY_AD_COMPLEXITY, profileID, domainConfig.getDomainID() );
-
-                    existingConfig.readStoredValue( key ).ifPresent( ( storedValue ) ->
-                    {
-                        if ( !existingConfig.isDefaultValue( PwmSetting.PASSWORD_POLICY_AD_COMPLEXITY, profileID ) )
-                        {
-                            doConversion( existingConfig, key, storedValue, modifier );
-                        }
-                    } );
-                }
-            }
+            modifier.newStoredConfiguration().keys()
+                    .filter( key -> key.isRecordType( StoredConfigKey.RecordType.SETTING ) )
+                    .filter( key -> key.toPwmSetting() == PwmSetting.PASSWORD_POLICY_AD_COMPLEXITY )
+                    .forEach( key -> doConversion( existingConfig, key, modifier ) );
         }
 
         private static void doConversion(
                 final StoredConfiguration existingConfig,
-                final StoredConfigItemKey key,
-                final StoredValue storedValue,
+                final StoredConfigKey key,
                 final StoredConfigurationModifier modifier
         )
         {
+            final StoredValue storedValue = existingConfig.readStoredValue( key ).orElseThrow();
+
             final boolean ad2003Enabled = ValueTypeConverter.valueToBoolean( storedValue );
             final StoredValue value;
             if ( ad2003Enabled )
@@ -112,12 +101,12 @@ class ConfigurationCleaner
                     + " to replacement setting "
                     + PwmSetting.PASSWORD_POLICY_AD_COMPLEXITY_LEVEL + ", value="
                     + ValueTypeConverter.valueToString( value ) );
-            final Optional<ValueMetaData> valueMetaData = existingConfig.readMetaData(
-                    StoredConfigItemKey.fromSetting( PwmSetting.PASSWORD_POLICY_AD_COMPLEXITY, profileID ) );
+            final Optional<ValueMetaData> valueMetaData = existingConfig.readMetaData( key );
             final UserIdentity userIdentity = valueMetaData.map( ValueMetaData::getUserIdentity ).orElse( null );
             try
             {
-                modifier.writeSetting( PwmSetting.PASSWORD_POLICY_AD_COMPLEXITY_LEVEL, profileID, value, userIdentity );
+                final StoredConfigKey writeKey = StoredConfigKey.forSetting( PwmSetting.PASSWORD_POLICY_AD_COMPLEXITY_LEVEL, profileID, key.getDomainID() );
+                modifier.writeSetting( writeKey, value, userIdentity );
             }
             catch ( final PwmUnrecoverableException e )
             {
@@ -133,24 +122,29 @@ class ConfigurationCleaner
                 throws PwmUnrecoverableException
         {
             final StoredConfiguration oldConfig = modifier.newStoredConfiguration();
-            for ( final String profileID : oldConfig.profilesForSetting( PwmSetting.RECOVERY_ENFORCE_MINIMUM_PASSWORD_LIFETIME ) )
+            for ( final DomainID domainID : StoredConfigurationUtil.domainList( oldConfig ) )
             {
-                if ( !oldConfig.isDefaultValue( PwmSetting.RECOVERY_ENFORCE_MINIMUM_PASSWORD_LIFETIME, profileID ) )
+                for ( final String profileID : StoredConfigurationUtil.profilesForSetting( PwmSetting.RECOVERY_ENFORCE_MINIMUM_PASSWORD_LIFETIME, oldConfig ) )
                 {
-                    final boolean enforceEnabled = ValueTypeConverter.valueToBoolean( oldConfig.readSetting( PwmSetting.RECOVERY_ENFORCE_MINIMUM_PASSWORD_LIFETIME, profileID ) );
-                    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="
-                            + ValueTypeConverter.valueToString( value ) );
-                    modifier.writeSetting( PwmSetting.RECOVERY_MINIMUM_PASSWORD_LIFETIME_OPTIONS, profileID, value, newActor );
+                    final StoredConfigKey key = StoredConfigKey.forSetting( PwmSetting.RECOVERY_ENFORCE_MINIMUM_PASSWORD_LIFETIME, profileID, domainID );
+                    final Optional<StoredValue> oldValue = oldConfig.readStoredValue( key );
+                    if ( oldValue.isPresent() && !ValueFactory.isDefaultValue( oldConfig.getTemplateSet(), PwmSetting.RECOVERY_ENFORCE_MINIMUM_PASSWORD_LIFETIME, oldValue.get() ) )
+                    {
+                        final boolean enforceEnabled = ValueTypeConverter.valueToBoolean( oldValue.get() );
+                        final StoredValue value = enforceEnabled
+                                ? new StringValue( RecoveryMinLifetimeOption.NONE.name() )
+                                : new StringValue( RecoveryMinLifetimeOption.ALLOW.name() );
+                        final Optional<ValueMetaData> existingData = oldConfig.readSettingMetadata( key );
+                        final UserIdentity newActor = existingData.map( ValueMetaData::getUserIdentity ).orElse( 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="
+                                + ValueTypeConverter.valueToString( value ) );
+
+                        final StoredConfigKey destKey = StoredConfigKey.forSetting( PwmSetting.RECOVERY_ENFORCE_MINIMUM_PASSWORD_LIFETIME, profileID, domainID );
+                        modifier.writeSetting( destKey, value, newActor );
+                    }
                 }
             }
         }
@@ -163,21 +157,28 @@ class ConfigurationCleaner
                 throws PwmUnrecoverableException
         {
             final StoredConfiguration oldConfig = modifier.newStoredConfiguration();
-            if ( !oldConfig.isDefaultValue( PwmSetting.PUBLIC_HEALTH_STATS_WEBSERVICES, null ) )
+            for ( final DomainID domainID : StoredConfigurationUtil.domainList( oldConfig ) )
             {
-                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 StoredConfigKey existingPubWebservicesKey = StoredConfigKey.forSetting( PwmSetting.PUBLIC_HEALTH_STATS_WEBSERVICES, null, domainID );
+                if ( oldConfig.readStoredValue( existingPubWebservicesKey ).isPresent() )
+                {
+                    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 StoredConfigKey existingPubEnableKey = StoredConfigKey.forSetting( PwmSetting.WEBSERVICES_PUBLIC_ENABLE, null, domainID );
+                    final StoredValue existingStoredValue = StoredConfigurationUtil.getValueOrDefault( oldConfig, existingPubEnableKey );
+                    final Set<String> existingValues =  ( Set<String> ) existingStoredValue.toNativeObject();
+                    final Set<String> newValues = new LinkedHashSet<>( existingValues );
+                    newValues.add( WebServiceUsage.Health.name() );
+                    newValues.add( WebServiceUsage.Statistics.name() );
+
+                    final Optional<ValueMetaData> valueMetaData = oldConfig.readMetaData( existingPubWebservicesKey );
+                    final UserIdentity userIdentity = valueMetaData.map( ValueMetaData::getUserIdentity ).orElse( null );
 
-                final Optional<ValueMetaData> valueMetaData = oldConfig.readMetaData(
-                        StoredConfigItemKey.fromSetting( PwmSetting.PUBLIC_HEALTH_STATS_WEBSERVICES, null ) );
-                final UserIdentity userIdentity = valueMetaData.map( ValueMetaData::getUserIdentity ).orElse( null );
+                    final StoredConfigKey destKey = StoredConfigKey.forSetting( PwmSetting.WEBSERVICES_PUBLIC_ENABLE, null, domainID );
+                    modifier.writeSetting( destKey, new OptionListValue( newValues ), userIdentity );
 
-                modifier.writeSetting( PwmSetting.WEBSERVICES_PUBLIC_ENABLE, null, new OptionListValue( newValues ), userIdentity );
+                }
             }
         }
     }
@@ -189,9 +190,9 @@ class ConfigurationCleaner
                 throws PwmUnrecoverableException
         {
             final StoredConfiguration inputConfig = modifier.newStoredConfiguration();
-            inputConfig.modifiedItems()
-                    .parallelStream()
-                    .filter( ( key ) -> StoredConfigItemKey.RecordType.SETTING.equals( key.getRecordType() ) )
+            inputConfig.keys()
+                    .parallel()
+                    .filter( ( key ) -> StoredConfigKey.RecordType.SETTING.equals( key.getRecordType() ) )
                     .filter( ( key ) -> key.toPwmSetting().getCategory().hasProfiles() )
                     .filter( ( key ) -> StringUtil.isEmpty( key.getProfileID() ) )
                     .forEach( ( key ) -> convertSetting( inputConfig, modifier, key ) );
@@ -200,12 +201,12 @@ class ConfigurationCleaner
         private void convertSetting(
                 final StoredConfiguration inputConfig,
                 final StoredConfigurationModifier modifier,
-                final StoredConfigItemKey key )
+                final StoredConfigKey key )
         {
             final PwmSetting pwmSetting = key.toPwmSetting();
 
-            final List<String> targetProfiles = inputConfig.profilesForSetting( pwmSetting );
-            final StoredValue value = inputConfig.readSetting( pwmSetting, null );
+            final List<String> targetProfiles = StoredConfigurationUtil.profilesForSetting( pwmSetting, inputConfig );
+            final StoredValue value = inputConfig.readStoredValue( key ).orElseThrow();
             final Optional<ValueMetaData> valueMetaData = inputConfig.readMetaData( key );
 
             for ( final String destProfile : targetProfiles )
@@ -214,7 +215,8 @@ class ConfigurationCleaner
                 {
                     try
                     {
-                        modifier.writeSettingAndMetaData( pwmSetting, destProfile, value, valueMetaData.orElse( null ) );
+                        final var newKey = StoredConfigKey.forSetting( pwmSetting, destProfile, key.getDomainID() );
+                        modifier.writeSettingAndMetaData( newKey, value, valueMetaData.orElse( null ) );
                     }
                     catch ( final PwmUnrecoverableException e )
                     {
@@ -243,23 +245,22 @@ class ConfigurationCleaner
                 throws PwmUnrecoverableException
         {
             final StoredConfiguration inputConfig = modifier.newStoredConfiguration();
-            inputConfig.modifiedItems()
-                    .stream()
-                    .filter( ( key ) -> StoredConfigItemKey.RecordType.SETTING.equals( key.getRecordType() ) )
+            inputConfig.keys()
+                    .filter( ( key ) -> StoredConfigKey.RecordType.SETTING.equals( key.getRecordType() ) )
                     .filter( ( key ) -> key.toPwmSetting().getCategory().hasProfiles() )
                     .filter( ( key ) -> verifyProfileIsValid( key, inputConfig ) )
                     .forEach( ( key ) -> removeSuperfluousProfile( key, modifier ) );
         }
 
-        boolean verifyProfileIsValid( final StoredConfigItemKey key, final StoredConfiguration inputConfig )
+        boolean verifyProfileIsValid( final StoredConfigKey key, final StoredConfiguration inputConfig )
         {
             final PwmSetting pwmSetting = key.toPwmSetting();
             final String recordID = key.getProfileID();
-            final List<String> profiles = inputConfig.profilesForSetting( pwmSetting );
+            final List<String> profiles = StoredConfigurationUtil.profilesForSetting( pwmSetting, inputConfig );
             return !profiles.contains( recordID );
         }
 
-        void removeSuperfluousProfile( final StoredConfigItemKey key, final StoredConfigurationModifier modifier )
+        void removeSuperfluousProfile( final StoredConfigKey key, final StoredConfigurationModifier modifier )
         {
             try
             {

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

@@ -208,7 +208,7 @@ public class ConfigurationReader
             final String backupDirSetting = domainConfig.readAppProperty( AppProperty.BACKUP_LOCATION );
             if ( backupDirSetting != null && backupDirSetting.length() > 0 )
             {
-                final File pwmPath = pwmDomain.getPwmEnvironment().getApplicationPath();
+                final File pwmPath = pwmDomain.getPwmApplication().getPwmEnvironment().getApplicationPath();
                 backupDirectory = FileSystemUtility.figureFilepath( backupDirSetting, pwmPath );
             }
             backupRotations = Integer.parseInt( domainConfig.readAppProperty( AppProperty.BACKUP_CONFIG_COUNT ) );
@@ -261,12 +261,12 @@ public class ConfigurationReader
     private static void auditModifiedSettings( final PwmDomain pwmDomain, final StoredConfiguration newConfig, final SessionLabel sessionLabel )
             throws PwmUnrecoverableException
     {
-        final Set<StoredConfigItemKey> changedKeys = StoredConfigurationUtil.changedValues( newConfig, pwmDomain.getConfig().getStoredConfiguration() );
+        final Set<StoredConfigKey> changedKeys = StoredConfigurationUtil.changedValues( newConfig, pwmDomain.getConfig().getStoredConfiguration() );
 
-        for ( final StoredConfigItemKey key : changedKeys )
+        for ( final StoredConfigKey key : changedKeys )
         {
-            if ( key.getRecordType() == StoredConfigItemKey.RecordType.SETTING
-                    || key.getRecordType() == StoredConfigItemKey.RecordType.LOCALE_BUNDLE )
+            if ( key.getRecordType() == StoredConfigKey.RecordType.SETTING
+                    || key.getRecordType() == StoredConfigKey.RecordType.LOCALE_BUNDLE )
             {
                 final Optional<StoredValue> storedValue = newConfig.readStoredValue( key );
                 if ( storedValue.isPresent() )
@@ -308,8 +308,8 @@ public class ConfigurationReader
         LOGGER.info( () -> "saved configuration", () -> TimeDuration.fromCurrent( saveFileStartTime ) );
         if ( pwmDomain != null )
         {
-            final String actualChecksum = storedConfiguration.valueHash();
-            pwmDomain.writeAppAttribute( AppAttribute.CONFIG_HASH, actualChecksum );
+            final String actualChecksum = StoredConfigurationUtil.valueHash( storedConfiguration );
+            pwmDomain.getPwmApplication().writeAppAttribute( AppAttribute.CONFIG_HASH, actualChecksum );
         }
 
         LOGGER.trace( () -> "renaming file " + tempWriteFile.getAbsolutePath() + " to " + configFile.getAbsolutePath() );

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

@@ -41,27 +41,27 @@ class StoredConfigData
     private Instant modifyTime = Instant.now();
 
     @Singular
-    private Map<StoredConfigItemKey, StoredValue> storedValues;
+    private Map<StoredConfigKey, StoredValue> storedValues;
 
     @Singular
-    private Map<StoredConfigItemKey, ValueMetaData> metaDatas;
+    private Map<StoredConfigKey, ValueMetaData> metaDatas;
 
     @Value
     static class ValueAndMetaCarrier
     {
-        private final StoredConfigItemKey key;
+        private final StoredConfigKey key;
         private final StoredValue value;
         private final ValueMetaData metaData;
     }
 
-    static Map<StoredConfigItemKey, ValueMetaData> carrierAsMetaDataMap( final Collection<ValueAndMetaCarrier> input )
+    static Map<StoredConfigKey, 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 )
+    static Map<StoredConfigKey, StoredValue> carrierAsStoredValueMap( final Collection<ValueAndMetaCarrier> input )
     {
         return input.stream()
                 .filter( ( t ) -> t.getKey() != null && t.getValue() != null )

+ 42 - 44
server/src/main/java/password/pwm/config/stored/StoredConfigItemKey.java → server/src/main/java/password/pwm/config/stored/StoredConfigKey.java

@@ -32,16 +32,14 @@ import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.StringUtil;
 
 import java.io.Serializable;
-import java.util.Collections;
 import java.util.Comparator;
 import java.util.Locale;
 import java.util.Objects;
-import java.util.Set;
-import java.util.stream.Collectors;
+import java.util.stream.Stream;
 
-public class StoredConfigItemKey implements Serializable, Comparable<StoredConfigItemKey>
+public class StoredConfigKey implements Serializable, Comparable<StoredConfigKey>
 {
-    private static final Comparator<StoredConfigItemKey> COMPARATOR = makeComparator();
+    private static final Comparator<StoredConfigKey> COMPARATOR = makeComparator();
 
     public enum RecordType
     {
@@ -69,7 +67,7 @@ public class StoredConfigItemKey implements Serializable, Comparable<StoredConfi
 
     private static final long serialVersionUID = 1L;
 
-    private StoredConfigItemKey(
+    private StoredConfigKey(
             final RecordType recordType,
             final DomainID domainID,
             final String recordID,
@@ -103,24 +101,19 @@ public class StoredConfigItemKey implements Serializable, Comparable<StoredConfi
         return profileID;
     }
 
-    public static StoredConfigItemKey fromSetting( final PwmSetting pwmSetting, final String profileID )
+    public static StoredConfigKey forSetting( final PwmSetting pwmSetting, final String profileID, final DomainID domainID )
     {
-        return new StoredConfigItemKey( RecordType.SETTING, PwmConstants.DOMAIN_ID_PLACEHOLDER, pwmSetting.getKey(), profileID );
+        return new StoredConfigKey( RecordType.SETTING, domainID, pwmSetting.getKey(), profileID );
     }
 
-    public static StoredConfigItemKey fromSetting( final PwmSetting pwmSetting, final String profileID, final DomainID domainID )
+    static StoredConfigKey forLocaleBundle( final PwmLocaleBundle localeBundle, final String key )
     {
-        return new StoredConfigItemKey( RecordType.SETTING, domainID, pwmSetting.getKey(), profileID );
+        return new StoredConfigKey( RecordType.LOCALE_BUNDLE, PwmConstants.DOMAIN_ID_PLACEHOLDER, localeBundle.getKey(), key );
     }
 
-    static StoredConfigItemKey fromLocaleBundle( final PwmLocaleBundle localeBundle, final String key )
+    static StoredConfigKey forConfigurationProperty( final ConfigurationProperty configurationProperty )
     {
-        return new StoredConfigItemKey( RecordType.LOCALE_BUNDLE, PwmConstants.DOMAIN_ID_PLACEHOLDER, localeBundle.getKey(), key );
-    }
-
-    static StoredConfigItemKey fromConfigurationProperty( final ConfigurationProperty configurationProperty )
-    {
-        return new StoredConfigItemKey( RecordType.PROPERTY, DomainID.systemId(), configurationProperty.getKey(), null );
+        return new StoredConfigKey( RecordType.PROPERTY, DomainID.systemId(), configurationProperty.getKey(), null );
     }
 
     public boolean isRecordType( final RecordType recordType )
@@ -184,17 +177,21 @@ public class StoredConfigItemKey implements Serializable, Comparable<StoredConfi
     {
         final String separator = LocaleHelper.getLocalizedMessage( locale, Config.Display_SettingNavigationSeparator, null );
 
+        final String prefix = "[Domain: " + domainID + "]" + separator + recordType.getLabel() + separator;
+
         switch ( recordType )
         {
             case SETTING:
-                return recordType.getLabel() + separator + toPwmSetting().toMenuLocationDebug( profileID, locale );
+                final boolean correctProfile = toPwmSetting().getCategory().hasProfiles() && !StringUtil.isEmpty( profileID );
+                return correctProfile
+                        ? prefix + toPwmSetting().toMenuLocationDebug( profileID, locale )
+                        : prefix + toPwmSetting().toMenuLocationDebug( null, locale ) + separator + "[profile: " + profileID + "]";
 
             case PROPERTY:
-                return recordType.getLabel() + separator + this.getRecordID();
+                return prefix + this.getRecordID();
 
             case LOCALE_BUNDLE:
-                return recordType.getLabel()
-                        + separator
+                return prefix
                         + this.getRecordID()
                         + separator
                         + this.getProfileID();
@@ -239,7 +236,7 @@ public class StoredConfigItemKey implements Serializable, Comparable<StoredConfi
     }
 
     @Override
-    public int compareTo( @NotNull final StoredConfigItemKey o )
+    public int compareTo( @NotNull final StoredConfigKey o )
     {
         return COMPARATOR.compare( this, o );
     }
@@ -255,7 +252,7 @@ public class StoredConfigItemKey implements Serializable, Comparable<StoredConfi
         {
             return false;
         }
-        final StoredConfigItemKey that = ( StoredConfigItemKey ) o;
+        final StoredConfigKey that = ( StoredConfigKey ) o;
         return Objects.equals( recordType, that.recordType )
                 //&& Objects.equals( domainID, that.domainID )
                 && Objects.equals( recordID, that.recordID )
@@ -294,44 +291,45 @@ public class StoredConfigItemKey implements Serializable, Comparable<StoredConfi
         }
     }
 
-    public static Set<StoredConfigItemKey> filterBySettingSyntax( final PwmSettingSyntax pwmSettingSyntax, final Set<StoredConfigItemKey> input )
+    public static Stream<StoredConfigKey> filterBySettingSyntax( final PwmSettingSyntax pwmSettingSyntax, final Stream<StoredConfigKey> input )
     {
+
+        if ( input == null )
+        {
+            return Stream.empty();
+        }
+
         return filterByType( RecordType.SETTING, input )
-                .stream()
-                .filter( ( k ) -> k.toPwmSetting().getSyntax() == pwmSettingSyntax )
-                .collect( Collectors.toUnmodifiableSet() );
+                .filter( ( k ) -> k.toPwmSetting().getSyntax() == pwmSettingSyntax );
     }
 
-    public static Set<StoredConfigItemKey> filterByType( final RecordType recordType, final Set<StoredConfigItemKey> input )
+    public static Stream<StoredConfigKey> filterByType( final RecordType recordType, final Stream<StoredConfigKey> input )
     {
-        if ( JavaHelper.isEmpty( input ) )
+        if ( input == null )
         {
-            return Collections.emptySet();
+            return Stream.empty();
         }
 
-        return input.stream()
-                .filter( ( k ) -> k.isRecordType( recordType ) )
-                .collect( Collectors.toUnmodifiableSet() );
+        return input.filter( ( k ) -> k.isRecordType( recordType ) );
     }
 
-    public static Comparator<StoredConfigItemKey> comparator()
+    public static Comparator<StoredConfigKey> comparator()
     {
         return COMPARATOR;
     }
 
-    private static Comparator<StoredConfigItemKey> makeComparator()
+    private static Comparator<StoredConfigKey> makeComparator()
     {
-        final Comparator<StoredConfigItemKey> typeComparator = Comparator.comparing(
-                StoredConfigItemKey::getRecordType,
+        final Comparator<StoredConfigKey> typeComparator = Comparator.comparing(
+                StoredConfigKey::getRecordType,
                 Comparator.nullsLast( Comparator.naturalOrder() ) );
 
         /*
-        final Comparator<StoredConfigItemKey> domainComparator = Comparator.comparing( StoredConfigItemKey::getDomainID,
+        final Comparator<StoredConfigKey> domainComparator = Comparator.comparing( StoredConfigKey::getDomainID,
                 Comparator.nullsLast( Comparator.naturalOrder() ) );
+        */
 
-         */
-
-        final Comparator<StoredConfigItemKey> recordComparator = ( o1, o2 ) ->
+        final Comparator<StoredConfigKey> recordComparator = ( o1, o2 ) ->
         {
             if ( Objects.equals( o1.getRecordType(), o2.getRecordType() )
                     && o1.isRecordType( RecordType.SETTING ) )
@@ -345,12 +343,12 @@ public class StoredConfigItemKey implements Serializable, Comparable<StoredConfi
             }
         };
 
-        final Comparator<StoredConfigItemKey> profileComparator = Comparator.comparing(
-                StoredConfigItemKey::getProfileID,
+        final Comparator<StoredConfigKey> profileComparator = Comparator.comparing(
+                StoredConfigKey::getProfileID,
                 Comparator.nullsLast( Comparator.naturalOrder() ) );
 
         return typeComparator
-              //  .thenComparing( domainComparator )
+                //.thenComparing( domainComparator )
                 .thenComparing( recordComparator )
                 .thenComparing( profileComparator );
     }

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

@@ -26,7 +26,6 @@ import password.pwm.bean.UserIdentity;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.PwmSettingFlag;
 import password.pwm.config.PwmSettingScope;
-import password.pwm.config.PwmSettingTemplate;
 import password.pwm.config.value.LocalizedStringValue;
 import password.pwm.config.value.StoredValue;
 import password.pwm.config.value.StoredValueEncoder;
@@ -87,7 +86,6 @@ public class StoredConfigXmlSerializer implements StoredConfigSerializer
         XmlCleaner.preProcessXml( xmlDocument );
         perfLog( "startPreProcessXml", startPreProcessXml );
 
-
         final XmlInputDocumentReader xmlInputDocumentReader = new XmlInputDocumentReader( xmlDocument );
         final StoredConfigData storedConfigData = xmlInputDocumentReader.getStoredConfigData();
         final StoredConfiguration storedConfiguration = new StoredConfigurationImpl( storedConfigData );
@@ -167,7 +165,7 @@ public class StoredConfigXmlSerializer implements StoredConfigSerializer
             {
                 xpathForConfigProperty( configurationProperty ).ifPresent( propertyElement -> propertyElement.getText().ifPresent( propertyText ->
                 {
-                    final StoredConfigItemKey key = StoredConfigItemKey.fromConfigurationProperty( configurationProperty );
+                    final StoredConfigKey key = StoredConfigKey.forConfigurationProperty( configurationProperty );
                     final StoredValue storedValue = new StringValue( propertyText );
                     final ValueMetaData metaData = readMetaDataFromXmlElement( key, propertyElement ).orElse( null );
                     valueAndMetaWrapper.add( new StoredConfigData.ValueAndMetaCarrier( key, storedValue, metaData ) );
@@ -217,7 +215,7 @@ public class StoredConfigXmlSerializer implements StoredConfigSerializer
                     final PwmSetting pwmSetting = optionalPwmSetting.get();
                     final boolean defaultValueSaved = settingElement.getChild( StoredConfigXmlConstants.XML_ELEMENT_DEFAULT ).isPresent();
                     final DomainID domainID = domainIdForSetting( settingElement, pwmSetting );
-                    final StoredConfigItemKey key = StoredConfigItemKey.fromSetting( pwmSetting, profileID.orElse( null ), domainID );
+                    final StoredConfigKey key = StoredConfigKey.forSetting( pwmSetting, profileID.orElse( null ), domainID );
                     final ValueMetaData metaData = readMetaDataFromXmlElement( key, settingElement ).orElse( null );
 
                     final StoredValue storedValue = defaultValueSaved
@@ -273,24 +271,6 @@ public class StoredConfigXmlSerializer implements StoredConfigSerializer
             return null;
         }
 
-        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 List<StoredConfigData.ValueAndMetaCarrier> readLocaleBundles()
         {
             final Instant startReadLocaleBundles = Instant.now();
@@ -315,10 +295,10 @@ public class StoredConfigXmlSerializer implements StoredConfigSerializer
                                 }
                                 if ( !bundleMap.isEmpty() )
                                 {
-                                    final StoredConfigItemKey storedConfigItemKey = StoredConfigItemKey.fromLocaleBundle( pwmLocaleBundle.get(), key.get() );
+                                    final StoredConfigKey storedConfigKey = StoredConfigKey.forLocaleBundle( pwmLocaleBundle.get(), key.get() );
                                     final StoredValue storedValue = new LocalizedStringValue( bundleMap );
-                                    final ValueMetaData metaData = readMetaDataFromXmlElement( storedConfigItemKey, xmlElement ).orElse( null );
-                                    return Stream.of( new StoredConfigData.ValueAndMetaCarrier( storedConfigItemKey, storedValue, metaData ) );
+                                    final ValueMetaData metaData = readMetaDataFromXmlElement( storedConfigKey, xmlElement ).orElse( null );
+                                    return Stream.of( new StoredConfigData.ValueAndMetaCarrier( storedConfigKey, storedValue, metaData ) );
                                 }
                             }
                         }
@@ -336,7 +316,7 @@ public class StoredConfigXmlSerializer implements StoredConfigSerializer
             return results;
         }
 
-        private Optional<ValueMetaData> readMetaDataFromXmlElement( final StoredConfigItemKey key, final XmlElement xmlElement )
+        private Optional<ValueMetaData> readMetaDataFromXmlElement( final StoredConfigKey key, final XmlElement xmlElement )
         {
             Instant instant = null;
             {
@@ -355,6 +335,8 @@ public class StoredConfigXmlSerializer implements StoredConfigSerializer
             }
 
             UserIdentity userIdentity = null;
+
+            // oldStyle modifyUser attribute
             {
                 final Optional<String> modifyUserValue = xmlElement.getAttributeValue( StoredConfigXmlConstants.XML_ATTRIBUTE_MODIFY_USER );
                 if ( modifyUserValue.isPresent() )
@@ -365,7 +347,23 @@ public class StoredConfigXmlSerializer implements StoredConfigSerializer
                     }
                     catch ( final DateTimeParseException | PwmUnrecoverableException e )
                     {
-                        LOGGER.trace( () -> "unable to parse userIdentity metadata for key " + key.toString() );
+                        LOGGER.trace( () -> "unable to parse userIdentity attribute metadata for key " + key.toString() );
+                    }
+                }
+            }
+
+            // newstyle modifyUser xml value
+            {
+                final Optional<XmlElement> metaElement = xmlElement.getChild( StoredConfigXmlConstants.XML_ATTRIBUTE_MODIFY_USER );
+                if ( metaElement.isPresent() )
+                {
+                    try
+                    {
+                        userIdentity = UserIdentity.fromDelimitedKey( metaElement.get().getText().orElse( "" ) );
+                    }
+                    catch ( final DateTimeParseException | PwmUnrecoverableException e )
+                    {
+                        LOGGER.trace( () -> "unable to parse userIdentity element for key " + key.toString() );
                     }
                 }
             }
@@ -465,21 +463,19 @@ public class StoredConfigXmlSerializer implements StoredConfigSerializer
                     .storedValueEncoderMode( figureEncoderMode( storedConfiguration, outputSettings ) )
                     .build();
 
-            final Consumer<StoredConfigItemKey> xmlSettingWriter = storedConfigItemKey ->
+            final Consumer<StoredConfigKey> xmlSettingWriter = storedConfigItemKey ->
             {
-                final PwmSetting pwmSetting = storedConfigItemKey.toPwmSetting();
-                final String profileID = storedConfigItemKey.getProfileID();
                 storedConfiguration.readStoredValue( storedConfigItemKey ).ifPresent( ( storedValue ->
                 {
                     final XmlElement settingElement = makeSettingXmlElement( storedConfiguration, storedConfigItemKey, storedValue, xmlOutputProcessData );
-                    decorateElementWithMetaData( storedConfiguration, StoredConfigItemKey.fromSetting( pwmSetting, profileID ), settingElement );
+                    decorateElementWithMetaData( storedConfiguration, storedConfigItemKey, settingElement );
                     settingsElement.addContent( settingElement );
                 } ) );
             };
 
             StoredConfigurationUtil.allPossibleSettingKeysForConfiguration( storedConfiguration )
                     .stream()
-                    .filter( ( key ) -> StoredConfigItemKey.RecordType.SETTING.equals( key.getRecordType() ) )
+                    .filter( ( key ) -> StoredConfigKey.RecordType.SETTING.equals( key.getRecordType() ) )
                     .filter( ( key ) -> !key.toPwmSetting().getFlags().contains( PwmSettingFlag.Deprecated ) )
                     .sorted()
                     .forEachOrdered( xmlSettingWriter );
@@ -514,7 +510,7 @@ public class StoredConfigXmlSerializer implements StoredConfigSerializer
 
         static XmlElement makeSettingXmlElement(
                 final StoredConfiguration storedConfiguration,
-                final StoredConfigItemKey key,
+                final StoredConfigKey key,
                 final StoredValue storedValue,
                 final XmlOutputProcessData xmlOutputProcessData
         )
@@ -545,7 +541,7 @@ public class StoredConfigXmlSerializer implements StoredConfigSerializer
             }
 
             final List<XmlElement> valueElements = new ArrayList<>(  );
-            if ( storedConfiguration != null && storedConfiguration.isDefaultValue( pwmSetting, profileID ) )
+            if ( storedConfiguration != null && ValueFactory.isDefaultValue( storedConfiguration.getTemplateSet(), pwmSetting, storedValue ) )
             {
                 final XmlElement defaultValue = xmlFactory.newElement( StoredConfigXmlConstants.XML_ELEMENT_DEFAULT );
                 valueElements.add( defaultValue );
@@ -563,7 +559,7 @@ public class StoredConfigXmlSerializer implements StoredConfigSerializer
 
         private static void decorateElementWithDomain(
                 final StoredConfiguration storedConfiguration,
-                final StoredConfigItemKey key,
+                final StoredConfigKey key,
                 final XmlElement xmlElement
         )
         {
@@ -576,7 +572,7 @@ public class StoredConfigXmlSerializer implements StoredConfigSerializer
 
         private static void decorateElementWithMetaData(
                 final StoredConfiguration storedConfiguration,
-                final StoredConfigItemKey key,
+                final StoredConfigKey key,
                 final XmlElement xmlElement
         )
         {
@@ -586,7 +582,9 @@ public class StoredConfigXmlSerializer implements StoredConfigSerializer
             {
                 if ( valueMetaData.get().getUserIdentity() != null )
                 {
-                    xmlElement.setAttribute( StoredConfigXmlConstants.XML_ATTRIBUTE_MODIFY_USER, valueMetaData.get().getUserIdentity().toDelimitedKey() );
+                    final XmlElement metaElement = XmlFactory.getFactory().newElement( StoredConfigXmlConstants.XML_ATTRIBUTE_MODIFY_USER );
+                    metaElement.addText( valueMetaData.get().getUserIdentity().toDelimitedKey() );
+                    xmlElement.addContent( metaElement );
                 }
 
                 if ( valueMetaData.get().getModifyDate() != null )
@@ -609,7 +607,7 @@ public class StoredConfigXmlSerializer implements StoredConfigSerializer
                             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 );
+                            decorateElementWithMetaData( storedConfiguration, StoredConfigKey.forConfigurationProperty( configurationProperty ), propertyElement );
                             propertiesElement.addContent( propertyElement );
                         }
                 );
@@ -645,7 +643,7 @@ public class StoredConfigXmlSerializer implements StoredConfigSerializer
                             localeBundleElement.addContent( valueElement );
                         }
 
-                        decorateElementWithMetaData( storedConfiguration, StoredConfigItemKey.fromLocaleBundle( pwmLocaleBundle, key ), localeBundleElement );
+                        decorateElementWithMetaData( storedConfiguration, StoredConfigKey.forLocaleBundle( pwmLocaleBundle, key ), localeBundleElement );
                         returnList.add( localeBundleElement );
                     }
                 }
@@ -792,7 +790,7 @@ public class StoredConfigXmlSerializer implements StoredConfigSerializer
 
                 final PwmSecurityKey pwmSecurityKey = inputDocumentReader.getKey();
 
-                final StoredConfigItemKey key = StoredConfigItemKey.fromSetting( pwmSetting, null, PwmConstants.DOMAIN_ID_DEFAULT );
+                final StoredConfigKey key = StoredConfigKey.forSetting( pwmSetting, null, PwmConstants.DOMAIN_ID_DEFAULT );
 
                 final XmlElement settingElement = StoredConfigXmlSerializer.XmlOutputHandler.makeSettingXmlElement(
                         null,
@@ -867,7 +865,7 @@ public class StoredConfigXmlSerializer implements StoredConfigSerializer
 
                 final PwmSecurityKey pwmSecurityKey = inputDocumentReader.getKey();
 
-                final StoredConfigItemKey key = StoredConfigItemKey.fromSetting( PwmSetting.APP_PROPERTY_OVERRIDES, null, PwmConstants.DOMAIN_ID_DEFAULT );
+                final StoredConfigKey key = StoredConfigKey.forSetting( PwmSetting.APP_PROPERTY_OVERRIDES, null, PwmConstants.DOMAIN_ID_DEFAULT );
 
                 final XmlElement settingElement = StoredConfigXmlSerializer.XmlOutputHandler.makeSettingXmlElement(
                         null,

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

@@ -47,6 +47,7 @@ import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Optional;
+import java.util.stream.Collectors;
 import java.util.zip.ZipEntry;
 import java.util.zip.ZipInputStream;
 import java.util.zip.ZipOutputStream;
@@ -66,14 +67,14 @@ public class StoredConfigZipJsonSerializer implements StoredConfigSerializer
     {
         final IntermediateRepresentation intermediateRepresentation = readIntermediateRep( inputStream );
 
-        final Map<StoredConfigItemKey, StoredValue> storedValueMap = new HashMap<>();
+        final Map<StoredConfigKey, StoredValue> storedValueMap = new HashMap<>();
         for ( final SerializedValue serializedValue : intermediateRepresentation.getSerializedValues() )
         {
             try
             {
-                final StoredConfigItemKey key = serializedValue.getKey();
+                final StoredConfigKey key = serializedValue.getKey();
                 System.out.println( key.toString() );
-                if ( key.getRecordType() == StoredConfigItemKey.RecordType.SETTING )
+                if ( key.getRecordType() == StoredConfigKey.RecordType.SETTING )
                 {
                     System.out.println( key.toPwmSetting().getSyntax() );
                     System.out.println( key.toPwmSetting().getKey() );
@@ -82,7 +83,7 @@ public class StoredConfigZipJsonSerializer implements StoredConfigSerializer
                 final PwmSettingSyntax syntax = key.getSyntax();
                 final StoredValue storedValue;
                 if (
-                        StoredConfigItemKey.RecordType.SETTING.equals( key.getRecordType() )
+                        StoredConfigKey.RecordType.SETTING.equals( key.getRecordType() )
                                 && key.toPwmSetting().getSyntax().equals( PwmSettingSyntax.FILE )
                 )
                 {
@@ -114,7 +115,7 @@ public class StoredConfigZipJsonSerializer implements StoredConfigSerializer
             }
         }
 
-        final Map<StoredConfigItemKey, ValueMetaData> valueMetaDataMap = new HashMap<>();
+        final Map<StoredConfigKey, ValueMetaData> valueMetaDataMap = new HashMap<>();
         for ( final SerializedMetaValue serializedMetaValue : intermediateRepresentation.serializedMetaValues )
         {
             valueMetaDataMap.put( serializedMetaValue.getKey(), serializedMetaValue.getValueMetaData() );
@@ -220,7 +221,7 @@ public class StoredConfigZipJsonSerializer implements StoredConfigSerializer
     {
         final Map<String, ImmutableByteArray> exRefs = new LinkedHashMap<>();
         final List<SerializedValue> serializedValues = new ArrayList<>();
-        for ( final StoredConfigItemKey key : storedConfiguration.modifiedItems() )
+        for ( final StoredConfigKey key : storedConfiguration.keys().collect( Collectors.toList() ) )
         {
             final Optional<StoredValue> storedValue = storedConfiguration.readStoredValue( key );
             if ( storedValue.isPresent() )
@@ -228,7 +229,7 @@ public class StoredConfigZipJsonSerializer implements StoredConfigSerializer
                 final PwmSettingSyntax syntax;
                 final StoredValue value;
                 if (
-                        StoredConfigItemKey.RecordType.SETTING.equals( key.getRecordType() )
+                        StoredConfigKey.RecordType.SETTING.equals( key.getRecordType() )
                                 && key.toPwmSetting().getSyntax().equals( PwmSettingSyntax.FILE )
                 )
                 {
@@ -258,7 +259,7 @@ public class StoredConfigZipJsonSerializer implements StoredConfigSerializer
         }
 
         final List<SerializedMetaValue> metaValues = new ArrayList<>();
-        for ( final StoredConfigItemKey key : storedConfiguration.modifiedItems() )
+        for ( final StoredConfigKey key : storedConfiguration.keys().collect( Collectors.toList() ) )
         {
             final Optional<ValueMetaData> valueMetaData = storedConfiguration.readMetaData( key );
             valueMetaData.ifPresent( metaData -> metaValues.add( new SerializedMetaValue( key, metaData ) ) );
@@ -291,7 +292,7 @@ public class StoredConfigZipJsonSerializer implements StoredConfigSerializer
     @Value
     private static class SerializedValue implements Serializable
     {
-        private StoredConfigItemKey key;
+        private StoredConfigKey key;
         private PwmSettingSyntax syntax;
         private String valueData;
     }
@@ -305,7 +306,7 @@ public class StoredConfigZipJsonSerializer implements StoredConfigSerializer
     @Value
     private static class SerializedMetaValue implements Serializable
     {
-        private StoredConfigItemKey key;
+        private StoredConfigKey key;
         private ValueMetaData valueMetaData;
     }
 

+ 11 - 8
server/src/main/java/password/pwm/config/stored/StoredConfigZipXmlSerializer.java

@@ -24,6 +24,7 @@ import password.pwm.PwmConstants;
 import password.pwm.config.PwmSettingSyntax;
 import password.pwm.config.value.FileValue;
 import password.pwm.config.value.StoredValue;
+import password.pwm.config.value.ValueTypeConverter;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.http.bean.ImmutableByteArray;
 import password.pwm.util.java.JavaHelper;
@@ -37,6 +38,7 @@ import java.io.OutputStream;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.Optional;
+import java.util.stream.Collectors;
 import java.util.zip.ZipEntry;
 import java.util.zip.ZipInputStream;
 import java.util.zip.ZipOutputStream;
@@ -117,10 +119,10 @@ public class StoredConfigZipXmlSerializer implements StoredConfigSerializer
             throws PwmUnrecoverableException
     {
         final StoredConfiguration inputConfig = modifier.newStoredConfiguration();
-        for ( final StoredConfigItemKey key : inputConfig.modifiedItems() )
+        for ( final StoredConfigKey key : inputConfig.keys().collect( Collectors.toList() ) )
         {
             if (
-                    StoredConfigItemKey.RecordType.SETTING.equals( key.getRecordType() )
+                    StoredConfigKey.RecordType.SETTING.equals( key.getRecordType() )
                             && key.toPwmSetting().getSyntax().equals( PwmSettingSyntax.FILE )
             )
             {
@@ -148,7 +150,7 @@ public class StoredConfigZipXmlSerializer implements StoredConfigSerializer
                     if ( !stripedValues.isEmpty() )
                     {
                         final FileValue strippedFileValue = new FileValue( stripedValues );
-                        modifier.writeSetting( key.toPwmSetting(), key.getProfileID(),  strippedFileValue, null );
+                        modifier.writeSetting( key,  strippedFileValue, null );
                     }
                 }
             }
@@ -156,14 +158,15 @@ public class StoredConfigZipXmlSerializer implements StoredConfigSerializer
         return modifier.newStoredConfiguration();
     }
 
-    private Map<String, ImmutableByteArray> extractExRefs( final StoredConfigurationModifier modifier ) throws PwmUnrecoverableException
+    private Map<String, ImmutableByteArray> extractExRefs( final StoredConfigurationModifier modifier )
+            throws PwmUnrecoverableException
     {
         final Map<String, ImmutableByteArray> returnObj = new HashMap<>();
         final StoredConfiguration inputConfig = modifier.newStoredConfiguration();
-        for ( final StoredConfigItemKey key : inputConfig.modifiedItems() )
+        for ( final StoredConfigKey key : inputConfig.keys().collect( Collectors.toList() ) )
         {
             if (
-                    StoredConfigItemKey.RecordType.SETTING.equals( key.getRecordType() )
+                    StoredConfigKey.RecordType.SETTING.equals( key.getRecordType() )
                             && key.toPwmSetting().getSyntax().equals( PwmSettingSyntax.FILE )
             )
             {
@@ -171,7 +174,7 @@ public class StoredConfigZipXmlSerializer implements StoredConfigSerializer
                 if ( optionalStoredValue.isPresent() )
                 {
                     final FileValue fileValue = ( FileValue ) optionalStoredValue.get();
-                    final Map<FileValue.FileInformation, FileValue.FileContent> values = ( Map ) fileValue.toNativeObject();
+                    final Map<FileValue.FileInformation, FileValue.FileContent> values = ValueTypeConverter.valueToFile( key.toPwmSetting(), fileValue );
                     final Map<FileValue.FileInformation, FileValue.FileContent> stripedValues = new HashMap<>();
 
                     for ( final Map.Entry<FileValue.FileInformation, FileValue.FileContent> entry : values.entrySet() )
@@ -184,7 +187,7 @@ public class StoredConfigZipXmlSerializer implements StoredConfigSerializer
                         stripedValues.put( info, fileContentWithHash );
                     }
                     final FileValue strippedFileValue = new FileValue( stripedValues );
-                    modifier.writeSetting( key.toPwmSetting(), key.getProfileID(),  strippedFileValue, null );
+                    modifier.writeSetting( key,  strippedFileValue, null );
                 }
             }
         }

+ 5 - 15
server/src/main/java/password/pwm/config/stored/StoredConfiguration.java

@@ -20,7 +20,6 @@
 
 package password.pwm.config.stored;
 
-import password.pwm.config.PwmSetting;
 import password.pwm.config.PwmSettingTemplateSet;
 import password.pwm.config.value.StoredValue;
 import password.pwm.error.PwmUnrecoverableException;
@@ -28,10 +27,9 @@ import password.pwm.i18n.PwmLocaleBundle;
 import password.pwm.util.secure.PwmSecurityKey;
 
 import java.time.Instant;
-import java.util.List;
 import java.util.Map;
 import java.util.Optional;
-import java.util.Set;
+import java.util.stream.Stream;
 
 public interface StoredConfiguration
 {
@@ -45,23 +43,15 @@ public interface StoredConfiguration
 
     PwmSettingTemplateSet getTemplateSet();
 
-    List<String> profilesForSetting( PwmSetting pwmSetting );
-
-    ValueMetaData readSettingMetadata( PwmSetting setting, String profileID );
+    Optional<ValueMetaData> readSettingMetadata( StoredConfigKey storedConfigKey );
 
     Map<String, String> readLocaleBundleMap( PwmLocaleBundle bundleName, String keyName );
 
-    StoredValue readSetting( PwmSetting setting, String profileID );
-
-    boolean isDefaultValue( PwmSetting setting, String profileID );
-
-    String valueHash();
-
-    Set<StoredConfigItemKey> modifiedItems();
+    Stream<StoredConfigKey> keys();
 
-    Optional<ValueMetaData> readMetaData( StoredConfigItemKey storedConfigItemKey );
+    Optional<ValueMetaData> readMetaData( StoredConfigKey storedConfigKey );
 
-    Optional<StoredValue> readStoredValue( StoredConfigItemKey storedConfigItemKey );
+    Optional<StoredValue> readStoredValue( StoredConfigKey storedConfigKey );
 
     StoredConfiguration copy();
 }

+ 23 - 85
server/src/main/java/password/pwm/config/stored/StoredConfigurationImpl.java

@@ -20,33 +20,29 @@
 
 package password.pwm.config.stored;
 
+import password.pwm.PwmConstants;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.PwmSettingTemplate;
 import password.pwm.config.PwmSettingTemplateSet;
 import password.pwm.config.value.LocalizedStringValue;
 import password.pwm.config.value.StoredValue;
 import password.pwm.config.value.StringValue;
-import password.pwm.config.value.ValueTypeConverter;
 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.logging.PwmLogger;
-import password.pwm.util.secure.HmacAlgorithm;
 import password.pwm.util.secure.PwmSecurityKey;
-import password.pwm.util.secure.SecureEngine;
 
 import java.time.Instant;
 import java.util.Collections;
 import java.util.EnumSet;
-import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Optional;
 import java.util.Set;
 import java.util.function.Predicate;
-import java.util.function.Supplier;
 import java.util.stream.Collectors;
+import java.util.stream.Stream;
 
 /**
  * Immutable in-memory configuration.
@@ -57,12 +53,10 @@ 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 Map<StoredConfigKey, StoredValue> storedValues;
+    private final Map<StoredConfigKey, ValueMetaData> metaValues;
     private final PwmSettingTemplateSet templateSet;
 
-    private final transient Supplier<String> valueHashSupplier = new LazySupplier<>( this::valueHashImpl );
-
     private static final PwmLogger LOGGER = PwmLogger.forClass( StoredConfigurationImpl.class );
 
     StoredConfigurationImpl( final StoredConfigData storedConfigData )
@@ -72,7 +66,7 @@ public class StoredConfigurationImpl implements StoredConfiguration
         this.metaValues = Map.copyOf( storedConfigData.getMetaDatas() );
         this.templateSet = readTemplateSet( storedConfigData.getStoredValues() );
 
-        final Map<StoredConfigItemKey, StoredValue> tempMap = removeDefaultSettingValues( storedConfigData.getStoredValues(), templateSet );
+        final Map<StoredConfigKey, StoredValue> tempMap = removeDefaultSettingValues( storedConfigData.getStoredValues(), templateSet );
         this.storedValues = Map.copyOf( tempMap );
     }
 
@@ -85,14 +79,14 @@ public class StoredConfigurationImpl implements StoredConfiguration
         this.templateSet = readTemplateSet( Collections.emptyMap() );
     }
 
-    private static Map<StoredConfigItemKey, StoredValue> removeDefaultSettingValues(
-            final Map<StoredConfigItemKey, StoredValue> valueMap,
+    private static Map<StoredConfigKey, StoredValue> removeDefaultSettingValues(
+            final Map<StoredConfigKey, StoredValue> valueMap,
             final PwmSettingTemplateSet pwmSettingTemplateSet
     )
     {
-        final Predicate<Map.Entry<StoredConfigItemKey, StoredValue>> checkIfValueIsDefault = entry ->
+        final Predicate<Map.Entry<StoredConfigKey, StoredValue>> checkIfValueIsDefault = entry ->
         {
-            if ( StoredConfigItemKey.RecordType.SETTING.equals( entry.getKey().getRecordType() ) )
+            if ( StoredConfigKey.RecordType.SETTING.equals( entry.getKey().getRecordType() ) )
             {
                 final String loopHash = entry.getValue().valueHash();
                 final String defaultHash = entry.getKey().toPwmSetting().getDefaultValue( pwmSettingTemplateSet ).valueHash();
@@ -101,7 +95,7 @@ public class StoredConfigurationImpl implements StoredConfiguration
             return true;
         };
 
-        final Map<StoredConfigItemKey, StoredValue> results = valueMap.entrySet()
+        final Map<StoredConfigKey, StoredValue> results = valueMap.entrySet()
                 .parallelStream()
                 .filter( checkIfValueIsDefault )
                 .collect( Collectors.toMap( Map.Entry::getKey, Map.Entry::getValue ) );
@@ -125,7 +119,7 @@ public class StoredConfigurationImpl implements StoredConfiguration
     @Override
     public Optional<String> readConfigProperty( final ConfigurationProperty propertyName )
     {
-        final StoredConfigItemKey key = StoredConfigItemKey.fromConfigurationProperty( propertyName );
+        final StoredConfigKey key = StoredConfigKey.forConfigurationProperty( propertyName );
         final StoredValue storedValue = storedValues.get( key );
         if ( storedValue != null )
         {
@@ -134,17 +128,10 @@ public class StoredConfigurationImpl implements StoredConfiguration
         return Optional.empty();
     }
 
-    @Override
-    public boolean isDefaultValue( final PwmSetting setting, final String profileID )
-    {
-        final StoredConfigItemKey key = StoredConfigItemKey.fromSetting( setting, profileID );
-        return !storedValues.containsKey( key );
-    }
-
     @Override
     public Map<String, String> readLocaleBundleMap( final PwmLocaleBundle pwmLocaleBundle, final String keyName )
     {
-        final StoredConfigItemKey key = StoredConfigItemKey.fromLocaleBundle( pwmLocaleBundle, keyName );
+        final StoredConfigKey key = StoredConfigKey.forLocaleBundle( pwmLocaleBundle, keyName );
         final StoredValue value = storedValues.get( key );
         if ( value != null )
         {
@@ -159,7 +146,7 @@ public class StoredConfigurationImpl implements StoredConfiguration
         return templateSet;
     }
 
-    private static PwmSettingTemplateSet readTemplateSet( final Map<StoredConfigItemKey, StoredValue> valueMap )
+    private static PwmSettingTemplateSet readTemplateSet( final Map<StoredConfigKey, StoredValue> valueMap )
     {
         final Set<PwmSettingTemplate> templates = EnumSet.noneOf( PwmSettingTemplate.class );
         readTemplateValue( valueMap, PwmSetting.TEMPLATE_LDAP ).ifPresent( templates::add );
@@ -168,9 +155,9 @@ public class StoredConfigurationImpl implements StoredConfiguration
         return new PwmSettingTemplateSet( templates );
     }
 
-    private static Optional<PwmSettingTemplate> readTemplateValue( final Map<StoredConfigItemKey, StoredValue> valueMap, final PwmSetting pwmSetting )
+    private static Optional<PwmSettingTemplate> readTemplateValue( final Map<StoredConfigKey, StoredValue> valueMap, final PwmSetting pwmSetting )
     {
-        final StoredConfigItemKey key = StoredConfigItemKey.fromSetting( pwmSetting, null );
+        final StoredConfigKey key = StoredConfigKey.forSetting( pwmSetting, null, PwmConstants.DOMAIN_ID_PLACEHOLDER );
         final StoredValue storedValue = valueMap.get( key );
 
         if ( storedValue != null )
@@ -189,65 +176,16 @@ public class StoredConfigurationImpl implements StoredConfiguration
         return Optional.empty();
     }
 
-    public String toString( final PwmSetting setting, final String profileID )
-    {
-        final StoredValue storedValue = readSetting( setting, profileID );
-        return setting.getKey() + "=" + storedValue.toDebugString( null );
-    }
-
-    @Override
-    public Set<StoredConfigItemKey> modifiedItems()
-    {
-        return Collections.unmodifiableSet( storedValues.keySet() );
-    }
-
-    @Override
-    public List<String> profilesForSetting( final PwmSetting pwmSetting )
-    {
-        final Optional<PwmSetting> profileSetting = pwmSetting.getCategory().getProfileSetting();
-        return profileSetting.map( setting -> ValueTypeConverter.valueToProfileID( setting, readSetting( setting, null ) ) )
-                .orElse( Collections.emptyList() );
-    }
-
     @Override
-    public ValueMetaData readSettingMetadata( final PwmSetting setting, final String profileID )
+    public Stream<StoredConfigKey> keys()
     {
-        final StoredConfigItemKey key = StoredConfigItemKey.fromSetting( setting, profileID );
-        return metaValues.get( key );
+        return storedValues.keySet().stream();
     }
 
     @Override
-    public StoredValue readSetting( final PwmSetting setting, final String profileID )
+    public Optional<ValueMetaData> readSettingMetadata( final StoredConfigKey key )
     {
-        final StoredConfigItemKey key = StoredConfigItemKey.fromSetting( setting, profileID );
-        return StoredConfigurationUtil.getValueOrDefault( this, key );
-    }
-
-    @Override
-    public String valueHash()
-    {
-        return valueHashSupplier.get();
-    }
-
-    private String valueHashImpl()
-    {
-        final Set<StoredConfigItemKey> modifiedSettings = modifiedItems();
-        final StringBuilder sb = new StringBuilder();
-
-        for ( final StoredConfigItemKey storedConfigItemKey : modifiedSettings )
-        {
-            final StoredValue storedValue = storedValues.get( storedConfigItemKey );
-            sb.append( storedValue.valueHash() );
-        }
-
-        try
-        {
-            return SecureEngine.hmac( HmacAlgorithm.HMAC_SHA_512, getKey(), sb.toString() );
-        }
-        catch ( final PwmUnrecoverableException e )
-        {
-            throw new IllegalStateException( e );
-        }
+        return Optional.ofNullable( metaValues.get( key ) );
     }
 
     private PwmSecurityKey cachedKey;
@@ -275,14 +213,14 @@ public class StoredConfigurationImpl implements StoredConfiguration
     }
 
     @Override
-    public Optional<ValueMetaData> readMetaData( final StoredConfigItemKey storedConfigItemKey )
+    public Optional<ValueMetaData> readMetaData( final StoredConfigKey storedConfigKey )
     {
-        return Optional.ofNullable( metaValues.get( storedConfigItemKey ) );
+        return Optional.ofNullable( metaValues.get( storedConfigKey ) );
     }
 
     @Override
-    public Optional<StoredValue> readStoredValue( final StoredConfigItemKey storedConfigItemKey )
+    public Optional<StoredValue> readStoredValue( final StoredConfigKey storedConfigKey )
     {
-        return Optional.ofNullable( storedValues.get( storedConfigItemKey ) );
+        return Optional.ofNullable( storedValues.get( storedConfigKey ) );
     }
 }

+ 25 - 96
server/src/main/java/password/pwm/config/stored/StoredConfigurationModifier.java

@@ -22,25 +22,20 @@ package password.pwm.config.stored;
 
 import password.pwm.bean.UserIdentity;
 import password.pwm.config.PwmSetting;
-import password.pwm.config.PwmSettingCategory;
-import password.pwm.config.value.StoredValue;
 import password.pwm.config.value.LocalizedStringValue;
-import password.pwm.config.value.StringArrayValue;
+import password.pwm.config.value.StoredValue;
 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.JavaHelper;
 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.LinkedHashMap;
-import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 import java.util.concurrent.atomic.AtomicReference;
@@ -65,27 +60,28 @@ public class StoredConfigurationModifier
     }
 
     public void writeSetting(
-            final PwmSetting setting,
-            final String profileID,
+            final StoredConfigKey key,
             final StoredValue value,
             final UserIdentity userIdentity
     )
             throws PwmUnrecoverableException
     {
-        writeSettingAndMetaData( setting, profileID, value, new ValueMetaData( Instant.now(), userIdentity ) );
+        writeSettingAndMetaData( key, value, new ValueMetaData( Instant.now(), userIdentity ) );
     }
 
     void writeSettingAndMetaData(
-            final PwmSetting setting,
-            final String profileID,
+            final StoredConfigKey key,
             final StoredValue value,
             final ValueMetaData valueMetaData
     )
             throws PwmUnrecoverableException
     {
-        Objects.requireNonNull( setting );
+        Objects.requireNonNull( key );
         Objects.requireNonNull( value );
 
+        final String profileID = key.getProfileID();
+        final PwmSetting setting = key.toPwmSetting();
+
         update( ( storedConfigData ) ->
         {
             if ( StringUtil.isEmpty( profileID ) && setting.getCategory().hasProfiles() )
@@ -97,9 +93,6 @@ public class StoredConfigurationModifier
                 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, valueMetaData )
@@ -115,8 +108,8 @@ public class StoredConfigurationModifier
     {
         update( ( storedConfigData ) ->
         {
-            final StoredConfigItemKey key = StoredConfigItemKey.fromConfigurationProperty( propertyName );
-            final Map<StoredConfigItemKey, StoredValue> existingStoredValues = new HashMap<>( storedConfigData.getStoredValues() );
+            final StoredConfigKey key = StoredConfigKey.forConfigurationProperty( propertyName );
+            final Map<StoredConfigKey, StoredValue> existingStoredValues = new HashMap<>( storedConfigData.getStoredValues() );
 
             if ( StringUtil.isEmpty( value ) )
             {
@@ -140,9 +133,9 @@ public class StoredConfigurationModifier
     {
         update( ( storedConfigData ) ->
         {
-            final Map<StoredConfigItemKey, StoredValue> existingStoredValues = new HashMap<>( storedConfigData.getStoredValues() );
+            final Map<StoredConfigKey, StoredValue> existingStoredValues = new HashMap<>( storedConfigData.getStoredValues() );
 
-            final StoredConfigItemKey key = StoredConfigItemKey.fromLocaleBundle( pwmLocaleBundle, keyName );
+            final StoredConfigKey key = StoredConfigKey.forLocaleBundle( pwmLocaleBundle, keyName );
             existingStoredValues.remove( key );
 
             return storedConfigData.toBuilder()
@@ -152,14 +145,12 @@ public class StoredConfigurationModifier
         } );
     }
 
-    public void resetSetting( final PwmSetting setting, final String profileID, final UserIdentity userIdentity )
+    public void resetSetting( final StoredConfigKey key, final UserIdentity userIdentity )
             throws PwmUnrecoverableException
     {
         update( ( storedConfigData ) ->
         {
-            final Map<StoredConfigItemKey, StoredValue> existingStoredValues = new HashMap<>( storedConfigData.getStoredValues() );
-
-            final StoredConfigItemKey key = StoredConfigItemKey.fromSetting( setting, profileID );
+            final Map<StoredConfigKey, StoredValue> existingStoredValues = new HashMap<>( storedConfigData.getStoredValues() );
             existingStoredValues.remove( key );
 
             return storedConfigData.toBuilder()
@@ -170,13 +161,13 @@ public class StoredConfigurationModifier
         } );
     }
 
-    public void deleteKey( final StoredConfigItemKey key )
+    public void deleteKey( final StoredConfigKey key )
             throws PwmUnrecoverableException
     {
         update( ( storedConfigData ) ->
         {
-            final Map<StoredConfigItemKey, StoredValue> existingStoredValues = new HashMap<>( storedConfigData.getStoredValues() );
-            final Map<StoredConfigItemKey, ValueMetaData> existingMetaValues = new HashMap<>( storedConfigData.getMetaDatas() );
+            final Map<StoredConfigKey, StoredValue> existingStoredValues = new HashMap<>( storedConfigData.getStoredValues() );
+            final Map<StoredConfigKey, ValueMetaData> existingMetaValues = new HashMap<>( storedConfigData.getMetaDatas() );
 
             existingStoredValues.remove( key );
             existingMetaValues.remove( key );
@@ -198,7 +189,7 @@ public class StoredConfigurationModifier
     {
         update( ( storedConfigData ) ->
         {
-            final StoredConfigItemKey key = StoredConfigItemKey.fromLocaleBundle( pwmLocaleBundle, keyName );
+            final StoredConfigKey key = StoredConfigKey.forLocaleBundle( pwmLocaleBundle, keyName );
             final StoredValue value = new LocalizedStringValue( localeMap );
 
             return storedConfigData.toBuilder()
@@ -207,73 +198,6 @@ public class StoredConfigurationModifier
         } );
     }
 
-    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 PwmSetting profileSetting = category.getProfileSetting().orElseThrow( IllegalStateException::new );
-            final List<String> existingProfiles = StoredConfigurationUtil.profilesForSetting( profileSetting, oldStoredConfiguration );
-            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 );
-            final Map<StoredConfigItemKey, StoredValue> newValues = new LinkedHashMap<>();
-
-            for ( final PwmSettingCategory interestedCategory : interestedCategories )
-            {
-                for ( final PwmSetting pwmSetting : interestedCategory.getSettings() )
-                {
-                    if ( !oldStoredConfiguration.isDefaultValue( pwmSetting, sourceID ) )
-                    {
-                        final StoredValue value = oldStoredConfiguration.readSetting( pwmSetting, sourceID );
-                        final StoredConfigItemKey key = StoredConfigItemKey.fromSetting( pwmSetting, destinationID );
-                        newValues.put( key, value );
-                    }
-                }
-            }
-
-            {
-                final List<String> newProfileIDList = new ArrayList<>( existingProfiles );
-                newProfileIDList.add( destinationID );
-                final StoredConfigItemKey key = StoredConfigItemKey.fromSetting( profileSetting, null );
-                final StoredValue value = new StringArrayValue( newProfileIDList );
-                newValues.put( key, value );
-            }
-
-            final StoredConfigItemKey key = StoredConfigItemKey.fromSetting( category.getProfileSetting().orElseThrow( IllegalStateException::new ), null );
-            final ValueMetaData valueMetaData = new ValueMetaData( Instant.now(), userIdentity );
-
-            return storedConfigData.toBuilder()
-                    .storedValues( newValues )
-                    .metaData( key, valueMetaData )
-                    .build();
-
-        } );
-    }
 
     public void setPassword( final String password )
             throws PwmOperationalException, PwmUnrecoverableException
@@ -319,7 +243,12 @@ public class StoredConfigurationModifier
         }
         catch ( final RuntimeException e )
         {
-            throw ( PwmUnrecoverableException ) e.getCause();
+            if ( e.getCause() != null && e.getCause().getClass().equals( PwmUnrecoverableException.class ) )
+            {
+                throw ( PwmUnrecoverableException ) e.getCause();
+            }
+            final String errorMsg = "unexpected error modifying storedConfiguration: " + JavaHelper.readHostileExceptionMessage( e );
+            throw PwmUnrecoverableException.newException( PwmError.ERROR_INTERNAL, errorMsg );
         }
         ref.updateAndGet( storedConfigData -> storedConfigData.toBuilder().modifyTime( Instant.now() ).build() );
     }

+ 173 - 179
server/src/main/java/password/pwm/config/stored/StoredConfigurationUtil.java

@@ -31,7 +31,9 @@ import password.pwm.config.PwmSettingTemplateSet;
 import password.pwm.config.value.LocalizedStringArrayValue;
 import password.pwm.config.value.PasswordValue;
 import password.pwm.config.value.StoredValue;
+import password.pwm.config.value.StringArrayValue;
 import password.pwm.config.value.StringValue;
+import password.pwm.config.value.ValueFactory;
 import password.pwm.config.value.ValueTypeConverter;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmError;
@@ -44,19 +46,19 @@ import password.pwm.util.java.StringUtil;
 import password.pwm.util.java.TimeDuration;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.secure.BCrypt;
+import password.pwm.util.secure.HmacAlgorithm;
 import password.pwm.util.secure.PwmRandom;
+import password.pwm.util.secure.SecureEngine;
 
 import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
-import java.util.HashSet;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Optional;
 import java.util.Set;
-import java.util.TreeSet;
 import java.util.function.Consumer;
 import java.util.function.Function;
 import java.util.stream.Collectors;
@@ -103,11 +105,12 @@ public abstract class StoredConfigurationUtil
             final StoredConfiguration storedConfiguration
     )
     {
-        final StoredValue storedValue = storedConfiguration.readSetting( profileSetting, null );
+        final StoredConfigKey key = StoredConfigKey.forSetting( profileSetting, null, PwmConstants.DOMAIN_ID_PLACEHOLDER );
+        final StoredValue storedValue = StoredConfigurationUtil.getValueOrDefault( storedConfiguration, key );
         final List<String> settingValues = ValueTypeConverter.valueToStringArray( storedValue );
-        final List<String> profiles = new ArrayList<>( settingValues );
-        profiles.removeIf( StringUtil::isEmpty );
-        return Collections.unmodifiableList( profiles );
+        return settingValues.stream()
+                .filter( value -> !StringUtil.isEmpty( value ) )
+                .collect( Collectors.toUnmodifiableList() );
     }
 
     public static StoredConfiguration copyConfigAndBlankAllPasswords( final StoredConfiguration storedConfig )
@@ -115,24 +118,24 @@ public abstract class StoredConfigurationUtil
     {
         final StoredConfigurationModifier modifier = StoredConfigurationModifier.newModifier( storedConfig );
 
-        final Consumer<StoredConfigItemKey> valueModifier = PwmExceptionLoggingConsumer.wrapConsumer( storedConfigItemKey ->
+        final Consumer<StoredConfigKey> valueModifier = PwmExceptionLoggingConsumer.wrapConsumer( storedConfigItemKey ->
         {
-            if ( storedConfigItemKey.getRecordType() == StoredConfigItemKey.RecordType.SETTING )
+            if ( storedConfigItemKey.getRecordType() == StoredConfigKey.RecordType.SETTING )
             {
                 final PwmSetting pwmSetting = storedConfigItemKey.toPwmSetting();
                 if ( pwmSetting.getSyntax() == PwmSettingSyntax.PASSWORD )
                 {
-                    final ValueMetaData valueMetaData = storedConfig.readSettingMetadata( pwmSetting, storedConfigItemKey.getProfileID() );
-                    final UserIdentity userIdentity = valueMetaData == null ? null : valueMetaData.getUserIdentity();
+                    final Optional<ValueMetaData> valueMetaData = storedConfig.readSettingMetadata( storedConfigItemKey );
+                    final UserIdentity userIdentity = valueMetaData.map( ValueMetaData::getUserIdentity ).orElse( null );
                     final PasswordValue passwordValue = new PasswordValue( new PasswordData( PwmConstants.LOG_REMOVED_VALUE_REPLACEMENT ) );
-                    modifier.writeSetting( pwmSetting, storedConfigItemKey.getProfileID(), passwordValue, userIdentity );
+                    modifier.writeSetting( storedConfigItemKey, passwordValue, userIdentity );
                 }
             }
         } );
 
-        storedConfig.modifiedItems()
-                .parallelStream()
-                .filter( ( key ) -> StoredConfigItemKey.RecordType.SETTING.equals( key.getRecordType() ) )
+        storedConfig.keys()
+                .parallel()
+                .filter( ( key ) -> StoredConfigKey.RecordType.SETTING.equals( key.getRecordType() ) )
                 .forEach( valueModifier );
 
         final Optional<String> pwdHash = storedConfig.readConfigProperty( ConfigurationProperty.PASSWORD_HASH );
@@ -146,33 +149,36 @@ public abstract class StoredConfigurationUtil
 
     public static List<String> validateValues( final StoredConfiguration storedConfiguration )
     {
-        final Function<StoredConfigItemKey, Stream<String>> validateSettingFunction = storedConfigItemKey ->
+        final Function<StoredConfigKey, Stream<String>> validateSettingFunction = storedConfigItemKey ->
         {
             final PwmSetting pwmSetting = storedConfigItemKey.toPwmSetting();
             final String profileID = storedConfigItemKey.getProfileID();
-            final StoredValue loopValue = storedConfiguration.readSetting( pwmSetting, profileID );
+            final Optional<StoredValue> loopValue = storedConfiguration.readStoredValue( storedConfigItemKey );
 
-            try
+            if ( loopValue.isPresent() )
             {
-                final List<String> errors = loopValue.validateValue( pwmSetting );
-                for ( final String loopError : errors )
+                try
                 {
-                    return Stream.of( pwmSetting.toMenuLocationDebug( storedConfigItemKey.getProfileID(), PwmConstants.DEFAULT_LOCALE ) + " - " + loopError );
+                    final List<String> errors = loopValue.get().validateValue( pwmSetting );
+                    for ( final String loopError : errors )
+                    {
+                        return Stream.of( pwmSetting.toMenuLocationDebug( storedConfigItemKey.getProfileID(), PwmConstants.DEFAULT_LOCALE ) + " - " + loopError );
+                    }
+                }
+                catch ( final Exception e )
+                {
+                    LOGGER.error( () -> "unexpected error during validate value for "
+                            + pwmSetting.toMenuLocationDebug( profileID, PwmConstants.DEFAULT_LOCALE ) + ", error: "
+                            + e.getMessage(), e );
                 }
-            }
-            catch ( final Exception e )
-            {
-                LOGGER.error( () -> "unexpected error during validate value for "
-                        + pwmSetting.toMenuLocationDebug( profileID, PwmConstants.DEFAULT_LOCALE ) + ", error: "
-                        + e.getMessage(), e );
             }
             return Stream.empty();
         };
 
         final Instant startTime = Instant.now();
-        final List<String> errorStrings = storedConfiguration.modifiedItems()
-                .parallelStream()
-                .filter( key -> StoredConfigItemKey.RecordType.SETTING.equals( key.getRecordType() ) )
+        final List<String> errorStrings = storedConfiguration.keys()
+                .parallel()
+                .filter( key -> key.isRecordType( StoredConfigKey.RecordType.SETTING ) )
                 .flatMap( validateSettingFunction )
                 .collect( Collectors.toList() );
 
@@ -181,136 +187,6 @@ public abstract class StoredConfigurationUtil
         return Collections.unmodifiableList( 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;
-        }
-    }
-
     public static boolean verifyPassword( final StoredConfiguration storedConfiguration, final String password )
     {
         if ( !hasPassword( storedConfiguration ) )
@@ -355,13 +231,15 @@ public abstract class StoredConfigurationUtil
     public static void initNewRandomSecurityKey( final StoredConfigurationModifier modifier )
             throws PwmUnrecoverableException
     {
-        if ( !modifier.newStoredConfiguration().isDefaultValue( PwmSetting.PWM_SECURITY_KEY, null ) )
+        final StoredConfigKey key = StoredConfigKey.forSetting( PwmSetting.PWM_SECURITY_KEY, null, DomainID.systemId() );
+
+        if ( !isDefaultValue( modifier.newStoredConfiguration(), key ) )
         {
             return;
         }
 
         modifier.writeSetting(
-                PwmSetting.PWM_SECURITY_KEY, null,
+                key,
                 new PasswordValue( new PasswordData( PwmRandom.getInstance().alphaNumericString( 1024 ) ) ),
                 null
         );
@@ -371,56 +249,73 @@ public abstract class StoredConfigurationUtil
 
     public static Map<String, String> makeDebugMap(
             final StoredConfiguration storedConfiguration,
-            final Collection<StoredConfigItemKey> interestedItems,
+            final Stream<StoredConfigKey> interestedItems,
             final Locale locale
     )
     {
-        return interestedItems.stream()
-                .filter( ( key ) -> !key.isRecordType( StoredConfigItemKey.RecordType.PROPERTY ) )
+        return interestedItems
+                .filter( ( key ) -> !key.isRecordType( StoredConfigKey.RecordType.PROPERTY ) )
                 .collect( Collectors.toUnmodifiableMap(
                         ( key ) -> key.getLabel( locale ),
                         ( key ) -> StoredConfigurationUtil.getValueOrDefault( storedConfiguration, key ).toDebugString( locale )
                 ) );
     }
 
-    public static Set<StoredConfigItemKey> allPossibleSettingKeysForConfiguration(
+    public static Set<StoredConfigKey> allPossibleSettingKeysForConfiguration(
             final StoredConfiguration storedConfiguration
     )
     {
-        final Function<PwmSetting, Stream<StoredConfigItemKey>> function = loopSetting ->
+        final List<DomainID> allDomainIds = new ArrayList<>( StoredConfigurationUtil.domainList( storedConfiguration ) );
+        allDomainIds.add( DomainID.systemId() );
+
+        return allDomainIds.stream()
+                .parallel()
+                .flatMap( domainID -> allPossibleSettingKeysForDomain( storedConfiguration, domainID ) )
+                .collect( Collectors.toUnmodifiableSet() );
+    }
+
+    private static Stream<StoredConfigKey> allPossibleSettingKeysForDomain(
+            final StoredConfiguration storedConfiguration,
+            final DomainID domainID
+    )
+    {
+        final Function<PwmSetting, Stream<StoredConfigKey>> function = loopSetting ->
         {
             if ( loopSetting.getCategory().hasProfiles() )
             {
-                return storedConfiguration.profilesForSetting( loopSetting )
+                return StoredConfigurationUtil.profilesForSetting( loopSetting, storedConfiguration )
                         .stream()
-                        .map( profileId -> StoredConfigItemKey.fromSetting( loopSetting, profileId ) )
+                        .map( profileId -> StoredConfigKey.forSetting( loopSetting, profileId, domainID ) )
                         .collect( Collectors.toList() )
                         .stream();
             }
             else
             {
-                return Stream.of( StoredConfigItemKey.fromSetting( loopSetting, null ) );
+                return Stream.of( StoredConfigKey.forSetting( loopSetting, null, domainID ) );
             }
         };
 
         return PwmSetting.sortedValues().stream()
+                .filter( ( setting ) -> domainID.inScope( setting.getCategory().getScope() ) )
                 .parallel()
                 .flatMap( function )
-                .collect( Collectors.toUnmodifiableSet() );
+                .collect( Collectors.toUnmodifiableSet() )
+                .stream();
     }
 
-    public static Set<StoredConfigItemKey> changedValues (
+    public static Set<StoredConfigKey> changedValues (
             final StoredConfiguration originalConfiguration,
             final StoredConfiguration modifiedConfiguration
     )
     {
         final Instant startTime = Instant.now();
 
-        final Set<StoredConfigItemKey> interestedReferences = new HashSet<>( originalConfiguration.modifiedItems() );
-        interestedReferences.addAll( modifiedConfiguration.modifiedItems() );
+        final Stream<StoredConfigKey> interestedReferences = Stream.concat(
+                originalConfiguration.keys(),
+                modifiedConfiguration.keys() ).distinct();
 
-        final Set<StoredConfigItemKey> deltaReferences = interestedReferences
-                .parallelStream()
+        final Set<StoredConfigKey> deltaReferences = interestedReferences
+                .parallel()
                 .filter( reference ->
                         {
                             final Optional<String> hash1 = originalConfiguration.readStoredValue( reference ).map( StoredValue::valueHash );
@@ -436,7 +331,7 @@ public abstract class StoredConfigurationUtil
 
     public static StoredValue getValueOrDefault(
             final StoredConfiguration storedConfiguration,
-            final StoredConfigItemKey key
+            final StoredConfigKey key
     )
     {
         final Optional<StoredValue> storedValue = storedConfiguration.readStoredValue( key );
@@ -469,11 +364,110 @@ public abstract class StoredConfigurationUtil
         throw new IllegalStateException();
     }
 
-    public static List<String> domainList( final StoredConfiguration storedConfiguration )
+    public static List<DomainID> domainList( final StoredConfiguration storedConfiguration )
     {
-        final StoredConfigItemKey domainListKey = StoredConfigItemKey.fromSetting( PwmSetting.DOMAIN_LIST, null, DomainID.systemId() );
+        final StoredConfigKey domainListKey = StoredConfigKey.forSetting( PwmSetting.DOMAIN_LIST, null, DomainID.systemId() );
         final StoredValue storedStringArray = getValueOrDefault( storedConfiguration, domainListKey );
         final List<String> domainList = ValueTypeConverter.valueToStringArray( storedStringArray );
-        return Collections.unmodifiableList( domainList );
+        return domainList.stream().map( DomainID::create ).sorted().collect( Collectors.toUnmodifiableList() );
+    }
+
+    public static StoredConfiguration copyProfileID(
+            final StoredConfiguration oldStoredConfiguration,
+            final DomainID domainID,
+            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 PwmSetting profileSetting = category.getProfileSetting().orElseThrow( IllegalStateException::new );
+        final List<String> existingProfiles = StoredConfigurationUtil.profilesForSetting( profileSetting, oldStoredConfiguration );
+        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 );
+
+        final StoredConfigurationModifier modifier = StoredConfigurationModifier.newModifier( oldStoredConfiguration );
+        for ( final PwmSettingCategory interestedCategory : interestedCategories )
+        {
+            for ( final PwmSetting pwmSetting : interestedCategory.getSettings() )
+            {
+                final StoredConfigKey existingKey = StoredConfigKey.forSetting( pwmSetting, sourceID, domainID );
+                final Optional<StoredValue> existingValue = oldStoredConfiguration.readStoredValue( existingKey );
+                if ( existingValue.isPresent() )
+                {
+                    final StoredConfigKey destinationKey = StoredConfigKey.forSetting( pwmSetting, destinationID, domainID );
+                    modifier.writeSetting( destinationKey, existingValue.get(), userIdentity );
+                }
+            }
+        }
+
+        {
+            final List<String> newProfileIDList = new ArrayList<>( existingProfiles );
+            newProfileIDList.add( destinationID );
+            final StoredConfigKey key = StoredConfigKey.forSetting( profileSetting, null, domainID );
+            final StoredValue value = new StringArrayValue( newProfileIDList );
+            modifier.writeSetting( key, value, userIdentity );
+        }
+
+        return modifier.newStoredConfiguration();
+    }
+
+    public static String valueHash( final StoredConfiguration storedConfiguration )
+    {
+        final Instant startTime = Instant.now();
+        final StringBuilder sb = new StringBuilder();
+
+        storedConfiguration.keys()
+                .map( storedConfiguration::readStoredValue )
+                .flatMap( Optional::stream )
+                .forEach( v -> sb.append( v.valueHash() ) );
+
+        final String output;
+        try
+        {
+            output = SecureEngine.hmac( HmacAlgorithm.HMAC_SHA_512, storedConfiguration.getKey(), sb.toString() );
+        }
+        catch ( final PwmUnrecoverableException e )
+        {
+            throw new IllegalStateException( e );
+        }
+
+        LOGGER.trace( () -> "calculated StoredConfiguration hash: " + output, () -> TimeDuration.fromCurrent( startTime ) );
+        return output;
+    }
+
+    public static boolean isDefaultValue( final StoredConfiguration storedConfiguration, final StoredConfigKey key )
+    {
+        if ( !key.isRecordType( StoredConfigKey.RecordType.SETTING ) )
+        {
+            throw new IllegalArgumentException( "key must be for SETTING type" );
+        }
+
+        final Optional<StoredValue> existingValue = storedConfiguration.readStoredValue( key );
+        if ( existingValue.isEmpty() )
+        {
+            return false;
+        }
+
+        return ValueFactory.isDefaultValue( storedConfiguration.getTemplateSet(), key.toPwmSetting(), existingValue.get() );
     }
 }

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

@@ -161,7 +161,7 @@ public class FormValue extends AbstractValue implements StoredValue
     @Override
     public String toDebugString( final Locale locale )
     {
-        if ( values != null && !values.isEmpty() )
+        if ( !JavaHelper.isEmpty( values ) )
         {
             final StringBuilder sb = new StringBuilder();
             for ( final FormConfiguration formRow : values )
@@ -186,15 +186,13 @@ public class FormValue extends AbstractValue implements StoredValue
                 if ( !StringUtil.isEmpty( formRow.getRegex() ) )
                 {
                     sb.append( " Regex:" ).append( formRow.getRegex() )
-                            .append( " Regex Error:" ).append( JsonUtil.serializeMap( formRow.getRegexErrors() ) )
-                            .append( "\n" );
+                            .append( " Regex Error:" ).append( JsonUtil.serializeMap( formRow.getRegexErrors() ) );
                 }
                 if ( formRow.getType() == FormConfiguration.Type.photo )
                 {
                     sb.append( " MimeTypes: " ).append( StringUtil.collectionToString( formRow.getMimeTypes() ) ).append( "\n" );
                     sb.append( " MaxSize: " ).append( formRow.getMaximumSize() ).append( "\n" );
                 }
-
             }
             return sb.toString();
         }

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

@@ -24,6 +24,7 @@ import com.google.gson.reflect.TypeToken;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.stored.XmlOutputProcessData;
 import password.pwm.util.i18n.LocaleHelper;
+import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.JsonUtil;
 import password.pwm.util.java.XmlElement;
 import password.pwm.util.java.XmlFactory;
@@ -31,6 +32,7 @@ import password.pwm.util.secure.PwmSecurityKey;
 
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.Iterator;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
@@ -145,10 +147,11 @@ public class LocalizedStringArrayValue extends AbstractValue implements StoredVa
     @Override
     public String toDebugString( final Locale locale )
     {
-        if ( values == null )
+        if ( JavaHelper.isEmpty( values ) )
         {
             return "";
         }
+
         final StringBuilder sb = new StringBuilder();
         for ( final Map.Entry<String, List<String>> entry : values.entrySet() )
         {
@@ -156,9 +159,14 @@ public class LocalizedStringArrayValue extends AbstractValue implements StoredVa
             if ( !values.get( localeKey ).isEmpty() )
             {
                 sb.append( "Locale: " ).append( LocaleHelper.debugLabel( LocaleHelper.parseLocaleString( localeKey ) ) ).append( "\n" );
-                for ( final String value : entry.getValue() )
+                for ( final Iterator<String> iterator = entry.getValue().iterator(); iterator.hasNext(); )
                 {
-                    sb.append( "  " ).append( value ).append( "\n" );
+                    final String value = iterator.next();
+                    sb.append( "  " ).append( value );
+                    if ( iterator.hasNext() )
+                    {
+                        sb.append( "\n" );
+                    }
                 }
             }
         }

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

@@ -21,6 +21,7 @@
 package password.pwm.config.value;
 
 import password.pwm.config.PwmSetting;
+import password.pwm.config.PwmSettingTemplateSet;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmOperationalException;
@@ -28,6 +29,8 @@ import password.pwm.util.java.XmlElement;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.secure.PwmSecurityKey;
 
+import java.util.Objects;
+
 public class ValueFactory
 {
 
@@ -73,5 +76,16 @@ public class ValueFactory
             throw new IllegalStateException( "unable to read xml element '" + settingElement.getName() + "' from setting '" + setting.getKey() + "' error: " + e.getMessage(), e );
         }
     }
+
+    public static boolean isDefaultValue( final PwmSettingTemplateSet templateSet, final PwmSetting pwmSetting, final StoredValue storedValue )
+    {
+        if ( storedValue == null )
+        {
+            return false;
+        }
+
+        final StoredValue defaultValue = pwmSetting.getDefaultValue( templateSet );
+        return Objects.equals( storedValue, defaultValue );
+    }
 }
 

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

@@ -25,6 +25,7 @@ import password.pwm.bean.EmailItemBean;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.PwmSettingSyntax;
 import password.pwm.config.value.data.ActionConfiguration;
+import password.pwm.config.value.data.ChallengeItemConfiguration;
 import password.pwm.config.value.data.FormConfiguration;
 import password.pwm.config.value.data.NamedSecretData;
 import password.pwm.config.value.data.RemoteWebServiceConfiguration;
@@ -203,10 +204,8 @@ public final class ValueTypeConverter
 
     public static List<UserPermission> valueToUserPermissions( final StoredValue value )
     {
-        if ( value == null )
-        {
-            return Collections.emptyList();
-        }
+
+        Objects.requireNonNull( value );
 
         if ( !( value instanceof UserPermissionValue ) )
         {
@@ -225,6 +224,18 @@ public final class ValueTypeConverter
         return results;
     }
 
+    public static Map<String, List<ChallengeItemConfiguration>> valueToChallengeItems( final StoredValue value )
+    {
+        Objects.requireNonNull( value );
+
+        if ( !( value instanceof ChallengeValue ) )
+        {
+            throw new IllegalArgumentException( "setting value is not readable as challenge items" );
+        }
+
+        return ( Map<String, List<ChallengeItemConfiguration>> ) value.toNativeObject();
+    }
+
     public static boolean valueToBoolean( final StoredValue value )
     {
         if ( !( value instanceof BooleanValue ) )

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

@@ -109,7 +109,7 @@ public class VerificationMethodValue extends AbstractValue implements StoredValu
                 input.getMethodSettings(),
                 IdentityVerificationMethod.class );
 
-        for ( final IdentityVerificationMethod recoveryVerificationMethods : IdentityVerificationMethod.availableValues() )
+        for ( final IdentityVerificationMethod recoveryVerificationMethods : IdentityVerificationMethod.values() )
         {
             if ( !tempMap.containsKey( recoveryVerificationMethods ) )
             {
@@ -117,7 +117,7 @@ public class VerificationMethodValue extends AbstractValue implements StoredValu
             }
         }
 
-        return new VerificationMethodSettings( tempMap, input.getMinOptionalRequired() );
+        return new VerificationMethodSettings( Collections.unmodifiableMap( tempMap ), input.getMinOptionalRequired() );
     }
 
     public static StoredValueFactory factory( )

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

@@ -23,6 +23,7 @@ package password.pwm.health;
 import org.apache.commons.io.FileUtils;
 import password.pwm.PwmDomain;
 import password.pwm.PwmEnvironment;
+import password.pwm.bean.DomainID;
 import password.pwm.bean.SessionLabel;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmError;
@@ -59,7 +60,7 @@ public class ApplianceStatusChecker implements HealthChecker
     @Override
     public List<HealthRecord> doHealthCheck( final PwmDomain pwmDomain )
     {
-        final boolean isApplianceAvailable = pwmDomain.getPwmEnvironment().getFlags().contains( PwmEnvironment.ApplicationFlag.Appliance );
+        final boolean isApplianceAvailable = pwmDomain.getPwmApplication().getPwmEnvironment().getFlags().contains( PwmEnvironment.ApplicationFlag.Appliance );
 
         if ( !isApplianceAvailable )
         {
@@ -108,26 +109,26 @@ public class ApplianceStatusChecker implements HealthChecker
 
         if ( updateStatus.pendingInstallation )
         {
-            healthRecords.add( HealthRecord.forMessage( HealthMessage.Appliance_PendingUpdates ) );
+            healthRecords.add( HealthRecord.forMessage( DomainID.systemId(), HealthMessage.Appliance_PendingUpdates ) );
         }
 
         if ( !updateStatus.autoUpdatesEnabled )
         {
-            healthRecords.add( HealthRecord.forMessage( HealthMessage.Appliance_UpdatesNotEnabled ) );
+            healthRecords.add( HealthRecord.forMessage( DomainID.systemId(), HealthMessage.Appliance_UpdatesNotEnabled ) );
         }
 
         if ( !updateStatus.updateServiceConfigured )
         {
-            healthRecords.add( HealthRecord.forMessage( HealthMessage.Appliance_UpdateServiceNotConfigured ) );
+            healthRecords.add( HealthRecord.forMessage( DomainID.systemId(), HealthMessage.Appliance_UpdateServiceNotConfigured ) );
         }
 
         return Collections.unmodifiableList( healthRecords );
 
     }
 
-    private String getApplianceAccessToken( final PwmDomain pwmDomain ) throws IOException, PwmOperationalException
+    private String getApplianceAccessToken( final PwmDomain pwmDomain ) throws PwmOperationalException
     {
-        final String tokenFile = pwmDomain.getPwmEnvironment().getParameters().get( PwmEnvironment.ApplicationParameter.ApplianceTokenFile );
+        final String tokenFile = pwmDomain.getPwmApplication().getPwmEnvironment().getParameters().get( PwmEnvironment.ApplicationParameter.ApplianceTokenFile );
         if ( StringUtil.isEmpty( tokenFile ) )
         {
             final String msg = "unable to determine appliance token, token file environment param "
@@ -142,9 +143,9 @@ public class ApplianceStatusChecker implements HealthChecker
         return "";
     }
 
-    private String figureUrl( final PwmDomain pwmDomain ) throws IOException, PwmOperationalException
+    private String figureUrl( final PwmDomain pwmDomain ) throws PwmOperationalException
     {
-        final String hostnameFile = pwmDomain.getPwmEnvironment().getParameters().get( PwmEnvironment.ApplicationParameter.ApplianceHostnameFile );
+        final String hostnameFile = pwmDomain.getPwmApplication().getPwmEnvironment().getParameters().get( PwmEnvironment.ApplicationParameter.ApplianceHostnameFile );
         if ( StringUtil.isEmpty( hostnameFile ) )
         {
             final String msg = "unable to determine appliance hostname, hostname file environment param "
@@ -153,7 +154,7 @@ public class ApplianceStatusChecker implements HealthChecker
         }
 
         final String hostname = readFileContents( hostnameFile );
-        final String port = pwmDomain.getPwmEnvironment().getParameters().get( PwmEnvironment.ApplicationParameter.AppliancePort );
+        final String port = pwmDomain.getPwmApplication().getPwmEnvironment().getParameters().get( PwmEnvironment.ApplicationParameter.AppliancePort );
 
         final String url = "https://" + hostname + ":" + port + "/sspr/appliance-update-status";
         LOGGER.trace( SessionLabel.HEALTH_SESSION_LABEL, () -> "calculated appliance host url as: " + url );

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

@@ -21,23 +21,23 @@
 package password.pwm.health;
 
 import password.pwm.AppProperty;
-import password.pwm.PwmDomain;
+import password.pwm.PwmApplication;
 import password.pwm.PwmConstants;
-import password.pwm.config.DomainConfig;
-import password.pwm.config.PwmSetting;
+import password.pwm.PwmDomain;
+import password.pwm.config.AppConfig;
 import password.pwm.config.PwmSettingSyntax;
-import password.pwm.config.profile.LdapProfile;
-import password.pwm.config.stored.StoredConfigItemKey;
+import password.pwm.config.stored.StoredConfigKey;
 import password.pwm.config.stored.StoredConfiguration;
-import password.pwm.config.value.ActionValue;
+import password.pwm.config.value.StoredValue;
+import password.pwm.config.value.ValueTypeConverter;
 import password.pwm.config.value.data.ActionConfiguration;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmOperationalException;
-import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.TimeDuration;
 import password.pwm.util.logging.PwmLogger;
+import password.pwm.util.secure.X509Utils;
 
 import java.security.cert.CertificateException;
 import java.security.cert.X509Certificate;
@@ -45,95 +45,102 @@ import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
-import java.util.Optional;
-import java.util.Set;
+import java.util.function.Supplier;
 
-public class CertificateChecker implements HealthChecker
+public class CertificateChecker implements HealthSupplier, HealthChecker
 {
     private static final PwmLogger LOGGER = PwmLogger.forClass( CertificateChecker.class );
 
     @Override
     public List<HealthRecord> doHealthCheck( final PwmDomain pwmDomain )
     {
-        final List<HealthRecord> records = new ArrayList<>( doHealthCheck( pwmDomain.getConfig() ) );
-        try
-        {
-            records.addAll( doActionHealthCheck( pwmDomain.getConfig() ) );
-        }
-        catch ( final PwmUnrecoverableException e )
-        {
-            LOGGER.error( () -> "error while checking action certificates: " + e.getMessage(), e );
-        }
-        return records;
+        final CertificateCheckJob job = new CertificateCheckJob( pwmDomain.getPwmApplication().getConfig() );
+        return Collections.unmodifiableList( job.get() );
     }
 
-    private static List<HealthRecord> doHealthCheck( final DomainConfig domainConfig )
+    public List<Supplier<List<HealthRecord>>> jobs( final PwmApplication pwmApplication )
     {
-        final List<HealthRecord> returnList = new ArrayList<>();
-        for ( final PwmSetting setting : PwmSetting.values() )
+        return Collections.singletonList( new CertificateCheckJob( pwmApplication.getConfig() ) );
+    }
+
+    public static class CertificateCheckJob implements Supplier<List<HealthRecord>>
+    {
+        private final AppConfig appConfig;
+
+        public CertificateCheckJob( final AppConfig appConfig )
         {
-            if ( setting.getSyntax() == PwmSettingSyntax.X509CERT && !setting.getCategory().hasProfiles() )
-            {
-                if ( setting != PwmSetting.LDAP_SERVER_CERTS )
-                {
-                    final List<X509Certificate> certs = domainConfig.readSettingAsCertificate( setting );
-                    returnList.addAll( doHealthCheck( domainConfig, setting, null, certs ) );
-                }
-            }
+            this.appConfig = appConfig;
         }
-        for ( final LdapProfile ldapProfile : domainConfig.getLdapProfiles().values() )
+
+        @Override
+        public List<HealthRecord> get()
         {
-            final List<X509Certificate> certificates = domainConfig.getLdapProfiles().get( ldapProfile.getIdentifier() ).readSettingAsCertificate( PwmSetting.LDAP_SERVER_CERTS );
-            returnList.addAll( doHealthCheck( domainConfig, PwmSetting.LDAP_SERVER_CERTS, ldapProfile.getIdentifier(), certificates ) );
+            return checkImpl( appConfig );
         }
-        return Collections.unmodifiableList( returnList );
     }
 
-    private static List<HealthRecord> doActionHealthCheck( final DomainConfig domainConfig ) throws PwmUnrecoverableException
+    private static List<HealthRecord> checkImpl( final AppConfig appConfig )
     {
+        final TimeDuration warnDuration = TimeDuration.of(
+                Long.parseLong( appConfig.readAppProperty( AppProperty.HEALTH_CERTIFICATE_WARN_SECONDS ) ),
+                TimeDuration.Unit.SECONDS );
 
-        final StoredConfiguration storedConfiguration = domainConfig.getStoredConfiguration();
+        final List<HealthRecord> records = new ArrayList<>();
+        appConfig.getStoredConfiguration().keys()
+                .filter( k -> k.isRecordType( StoredConfigKey.RecordType.SETTING ) )
+                .forEach( k ->
+                {
+                    final PwmSettingSyntax syntax = k.getSyntax();
+                    if ( syntax == PwmSettingSyntax.X509CERT )
+                    {
+                        records.addAll( checkX509Setting( appConfig.getStoredConfiguration(), k, warnDuration ) );
+                    }
+                    else if ( syntax == PwmSettingSyntax.ACTION )
+                    {
+                        records.addAll( checkActionSetting( appConfig.getStoredConfiguration(), k, warnDuration ) );
+                    }
+                } );
+        return Collections.unmodifiableList( records );
+    }
 
-        final List<HealthRecord> returnList = new ArrayList<>();
-        final Set<StoredConfigItemKey> modifiedReferences = storedConfiguration.modifiedItems();
+    private static List<HealthRecord> checkX509Setting(
+            final StoredConfiguration storedConfiguration,
+            final StoredConfigKey key,
+            final TimeDuration timeDuration
+    )
+    {
+        final StoredValue storedValue = storedConfiguration.readStoredValue( key ).orElseThrow();
+        final List<X509Certificate> certs = ValueTypeConverter.valueToX509Certificates( key.toPwmSetting(), storedValue );
+        return doHealthCheck( key, certs, timeDuration );
+    }
 
-        for ( final StoredConfigItemKey storedConfigItemKey : modifiedReferences )
+    private static List<HealthRecord> checkActionSetting(
+            final StoredConfiguration storedConfiguration,
+            final StoredConfigKey key,
+            final TimeDuration timeDuration
+    )
+    {
+        final StoredValue storedValue = storedConfiguration.readStoredValue( key ).orElseThrow();
+        final List<ActionConfiguration> actionConfigurations = ValueTypeConverter.valueToAction( key.toPwmSetting(), storedValue );
+        final List<HealthRecord> returnList = new ArrayList<>();
+        for ( final ActionConfiguration actionConfiguration : actionConfigurations )
         {
-            if ( storedConfigItemKey.getRecordType() == StoredConfigItemKey.RecordType.SETTING )
+            for ( final ActionConfiguration.WebAction webAction : actionConfiguration.getWebActions() )
             {
-                final Optional<PwmSetting> optionalPwmSetting = PwmSetting.forKey( storedConfigItemKey.getRecordID() );
-                optionalPwmSetting.ifPresent( pwmSetting ->
-                {
-                    if ( pwmSetting.getSyntax() == PwmSettingSyntax.ACTION )
-                    {
-                        final ActionValue value = ( ActionValue ) storedConfiguration.readSetting( pwmSetting, storedConfigItemKey.getProfileID() );
-                        for ( final ActionConfiguration actionConfiguration : value.toNativeObject() )
-                        {
-                            for ( final ActionConfiguration.WebAction webAction : actionConfiguration.getWebActions() )
-                            {
-                                final List<X509Certificate> certificates = webAction.getCertificates();
-                                returnList.addAll( doHealthCheck( domainConfig, pwmSetting, storedConfigItemKey.getProfileID(), certificates ) );
-                            }
-                        }
-                    }
-                }
-                );
+                final List<X509Certificate> certificates = webAction.getCertificates();
+                returnList.addAll( doHealthCheck( key, certificates, timeDuration ) );
             }
         }
         return Collections.unmodifiableList( returnList );
     }
 
+
     private static List<HealthRecord> doHealthCheck(
-            final DomainConfig domainConfig,
-            final PwmSetting setting,
-            final String profileID,
-            final List<X509Certificate> certificates
+            final StoredConfigKey storedConfigKey,
+            final List<X509Certificate> certificates,
+            final TimeDuration warnDuration
     )
     {
-        final TimeDuration warnDuration = TimeDuration.of(
-                Long.parseLong( domainConfig.readAppProperty( AppProperty.HEALTH_CERTIFICATE_WARN_SECONDS ) ),
-                TimeDuration.Unit.SECONDS );
-
         if ( certificates != null )
         {
             final List<HealthRecord> returnList = new ArrayList<>();
@@ -147,8 +154,10 @@ public class CertificateChecker implements HealthChecker
                 catch ( final PwmOperationalException e )
                 {
                     final String errorDetail = e.getErrorInformation().getDetailedErrorMsg();
-                    final HealthRecord record = HealthRecord.forMessage( HealthMessage.Config_Certificate,
-                            setting.toMenuLocationDebug( profileID, PwmConstants.DEFAULT_LOCALE ),
+                    final HealthRecord record = HealthRecord.forMessage(
+                            storedConfigKey.getDomainID(),
+                            HealthMessage.Config_Certificate,
+                            storedConfigKey.toPwmSetting().toMenuLocationDebug( storedConfigKey.getProfileID(), PwmConstants.DEFAULT_LOCALE ),
                             errorDetail
                     );
                     returnList.add( record );
@@ -181,27 +190,46 @@ public class CertificateChecker implements HealthChecker
             final ErrorInformation errorInformation = new ErrorInformation( PwmError.ERROR_CERTIFICATE_ERROR, errorMsg.toString(), new String[]
                     {
                             errorMsg.toString(),
-                            }
+                    }
             );
             throw new PwmOperationalException( errorInformation );
         }
 
-        final Instant expireDate = certificate.getNotAfter().toInstant();
-        final TimeDuration durationUntilExpire = TimeDuration.fromCurrent( expireDate );
-        if ( durationUntilExpire.isShorterThan( warnDuration ) )
         {
-            final StringBuilder errorMsg = new StringBuilder();
-            errorMsg.append( "certificate for subject " );
-            errorMsg.append( certificate.getSubjectDN().getName() );
-            errorMsg.append( " will expire on: " );
-            errorMsg.append( JavaHelper.toIsoDate( expireDate ) );
-            errorMsg.append( " (" ).append( durationUntilExpire.asCompactString() ).append( " from now)" );
-            final ErrorInformation errorInformation = new ErrorInformation( PwmError.ERROR_CERTIFICATE_ERROR, errorMsg.toString(), new String[]
-                    {
-                            errorMsg.toString(),
-                            }
-            );
-            throw new PwmOperationalException( errorInformation );
+            final Instant issueDate = certificate.getNotBefore().toInstant();
+            if ( issueDate.isAfter( Instant.now() ) )
+            {
+                final String errorMsg = "certificate " + X509Utils.makeDebugText( certificate )
+                        + " issue date of '" + JavaHelper.toIsoDate( issueDate ) + "' "
+                        + " is prior to current time.";
+
+                final ErrorInformation errorInformation = new ErrorInformation( PwmError.ERROR_CERTIFICATE_ERROR, errorMsg, new String[]
+                        {
+                                errorMsg,
+                        }
+                );
+                throw new PwmOperationalException( errorInformation );
+            }
+        }
+
+        {
+            final Instant expireDate = certificate.getNotAfter().toInstant();
+            final TimeDuration durationUntilExpire = TimeDuration.fromCurrent( expireDate );
+            if ( durationUntilExpire.isShorterThan( warnDuration ) )
+            {
+                final StringBuilder errorMsg = new StringBuilder();
+                errorMsg.append( "certificate for subject " );
+                errorMsg.append( certificate.getSubjectDN().getName() );
+                errorMsg.append( " will expire on: " );
+                errorMsg.append( JavaHelper.toIsoDate( expireDate ) );
+                errorMsg.append( " (" ).append( durationUntilExpire.asCompactString() ).append( " from now)" );
+                final ErrorInformation errorInformation = new ErrorInformation( PwmError.ERROR_CERTIFICATE_ERROR, errorMsg.toString(), new String[]
+                        {
+                                errorMsg.toString(),
+                        }
+                );
+                throw new PwmOperationalException( errorInformation );
+            }
         }
     }
 }

+ 98 - 78
server/src/main/java/password/pwm/health/ConfigurationChecker.java

@@ -36,7 +36,7 @@ import password.pwm.config.profile.HelpdeskProfile;
 import password.pwm.config.profile.LdapProfile;
 import password.pwm.config.profile.NewUserProfile;
 import password.pwm.config.profile.PwmPasswordPolicy;
-import password.pwm.config.stored.StoredConfigItemKey;
+import password.pwm.config.stored.StoredConfigKey;
 import password.pwm.config.stored.StoredConfiguration;
 import password.pwm.config.value.StoredValue;
 import password.pwm.config.value.ValueTypeConverter;
@@ -62,6 +62,7 @@ import java.util.Locale;
 import java.util.NoSuchElementException;
 import java.util.Set;
 import java.util.stream.Collectors;
+import java.util.stream.Stream;
 
 public class ConfigurationChecker implements HealthChecker
 {
@@ -93,7 +94,9 @@ public class ConfigurationChecker implements HealthChecker
 
         if ( pwmDomain.getApplicationMode() == PwmApplicationMode.CONFIGURATION )
         {
-            records.add( HealthRecord.forMessage( HealthMessage.Config_ConfigMode ) );
+            records.add( HealthRecord.forMessage(
+                    config.getDomainID(),
+                    HealthMessage.Config_ConfigMode ) );
         }
 
         if ( config.readSettingAsBoolean( PwmSetting.NEWUSER_ENABLE ) )
@@ -107,6 +110,7 @@ public class ConfigurationChecker implements HealthChecker
                 catch ( final PwmUnrecoverableException e )
                 {
                     records.add( HealthRecord.forMessage(
+                            config.getDomainID(),
                             HealthMessage.NewUser_PwTemplateBad,
                             PwmSetting.NEWUSER_PASSWORD_POLICY_USER.toMenuLocationDebug( newUserProfile.getIdentifier(), PwmConstants.DEFAULT_LOCALE ),
                             e.getMessage() ) );
@@ -166,30 +170,42 @@ public class ConfigurationChecker implements HealthChecker
             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 ) ) );
+                records.add( HealthRecord.forMessage(
+                        config.getDomainID(),
+                        HealthMessage.Config_NoSiteURL,
+                        PwmSetting.PWM_SITE_URL.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 ) ) );
+                records.add( HealthRecord.forMessage(
+                        config.getDomainID(),
+                        HealthMessage.Config_LDAPWireTrace,
+                        PwmSetting.LDAP_ENABLE_WIRE_TRACE.toMenuLocationDebug( null, locale ) ) );
             }
 
             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 ) );
+                records.add( HealthRecord.forMessage(
+                        config.getDomainID(),
+                        HealthMessage.Config_PromiscuousLDAP,
+                        appPropertyKey ) );
             }
 
-            if ( config.readSettingAsBoolean( PwmSetting.DISPLAY_SHOW_DETAILED_ERRORS ) )
+            if ( config.getAppConfig().readSettingAsBoolean( PwmSetting.DISPLAY_SHOW_DETAILED_ERRORS ) )
             {
-                records.add( HealthRecord.forMessage( HealthMessage.Config_ShowDetailedErrors, PwmSetting.DISPLAY_SHOW_DETAILED_ERRORS.toMenuLocationDebug( null, locale ) ) );
+                records.add( HealthRecord.forMessage(
+                        config.getDomainID(),
+                        HealthMessage.Config_ShowDetailedErrors,
+                        PwmSetting.DISPLAY_SHOW_DETAILED_ERRORS.toMenuLocationDebug( null, locale ) ) );
             }
 
             if ( config.getLdapProfiles().isEmpty() )
             {
-                records.add( HealthRecord.forMessage( HealthMessage.Config_NoLdapProfiles ) );
+                records.add( HealthRecord.forMessage(
+                        config.getDomainID(),
+                        HealthMessage.Config_NoLdapProfiles ) );
             }
 
             for ( final LdapProfile ldapProfile : config.getLdapProfiles().values() )
@@ -198,6 +214,7 @@ public class ConfigurationChecker implements HealthChecker
                 if ( testUserDN == null || testUserDN.length() < 1 )
                 {
                     records.add( HealthRecord.forMessage(
+                            config.getDomainID(),
                             HealthMessage.Config_AddTestUser,
                             PwmSetting.LDAP_TEST_USER_DN.toMenuLocationDebug( ldapProfile.getIdentifier(), locale )
                     ) );
@@ -218,6 +235,7 @@ public class ConfigurationChecker implements HealthChecker
                             if ( !secure )
                             {
                                 records.add( HealthRecord.forMessage(
+                                        config.getDomainID(),
                                         HealthMessage.Config_LDAPUnsecure,
                                         PwmSetting.LDAP_SERVER_URLS.toMenuLocationDebug( ldapProfile.getIdentifier(), locale )
                                 ) );
@@ -225,7 +243,9 @@ public class ConfigurationChecker implements HealthChecker
                         }
                         catch ( final URISyntaxException e )
                         {
-                            records.add( HealthRecord.forMessage( HealthMessage.Config_ParseError,
+                            records.add( HealthRecord.forMessage(
+                                    config.getDomainID(),
+                                    HealthMessage.Config_ParseError,
                                     e.getMessage(),
                                     PwmSetting.LDAP_SERVER_URLS.toMenuLocationDebug( ldapProfile.getIdentifier(), locale ),
                                     urlStringValue
@@ -246,30 +266,32 @@ public class ConfigurationChecker implements HealthChecker
         {
             final List<HealthRecord> records = new ArrayList<>();
 
+            final Stream<StoredConfigKey> interestedKeys = StoredConfigKey.filterBySettingSyntax(
+                    PwmSettingSyntax.PASSWORD,
+                    StoredConfigKey.filterByType(
+                            StoredConfigKey.RecordType.SETTING,
+                            config.getStoredConfiguration().keys() ) );
+
             try
             {
-                for ( final StoredConfigItemKey key : config.getStoredConfiguration().modifiedItems() )
+                for ( final StoredConfigKey key : interestedKeys.collect( Collectors.toList() ) )
                 {
-                    if ( key.getRecordType() == StoredConfigItemKey.RecordType.SETTING )
+                    final PwmSetting pwmSetting = key.toPwmSetting();
+                    final StoredValue storedValue = config.getStoredConfiguration().readStoredValue( key ).orElseThrow();
+                    final PasswordData passwordValue = ValueTypeConverter.valueToPassword( storedValue );
+                    if ( passwordValue != null )
                     {
-                        final PwmSetting pwmSetting = key.toPwmSetting();
-                        if ( pwmSetting.getSyntax() == PwmSettingSyntax.PASSWORD )
+                        final String stringValue = passwordValue.getStringValue();
+
+                        if ( !StringUtil.isEmpty( stringValue ) )
                         {
-                            final StoredValue storedValue = config.getStoredConfiguration().readSetting( pwmSetting, key.getProfileID() );
-                            final PasswordData passwordValue = ( PasswordData ) storedValue.toNativeObject();
-                            if ( passwordValue != null )
+                            final int strength = PasswordUtility.judgePasswordStrength( config, stringValue );
+                            if ( strength < 50 )
                             {
-                                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 ) ) );
-                                    }
-                                }
+                                records.add( HealthRecord.forMessage(
+                                        config.getDomainID(),
+                                        HealthMessage.Config_WeakPassword,
+                                        pwmSetting.toMenuLocationDebug( key.getProfileID(), locale ), String.valueOf( strength ) ) );
                             }
                         }
                     }
@@ -297,7 +319,7 @@ public class ConfigurationChecker implements HealthChecker
                     {
                             PwmSetting.FORGOTTEN_PASSWORD_READ_PREFERENCE,
                             PwmSetting.FORGOTTEN_PASSWORD_WRITE_PREFERENCE,
-                            };
+                    };
             for ( final PwmSetting loopSetting : interestedSettings )
             {
                 if ( config.getResponseStorageLocations( loopSetting ).contains( DataStorageMethod.LDAP ) )
@@ -352,13 +374,17 @@ public class ConfigurationChecker implements HealthChecker
 
                 for ( final PwmSetting setting : causalSettings )
                 {
-                    records.add( HealthRecord.forMessage( HealthMessage.Config_MissingDB, setting.toMenuLocationDebug( null, locale ) ) );
+                    records.add( HealthRecord.forMessage(
+                            config.getDomainID(),
+                            HealthMessage.Config_MissingDB,
+                            setting.toMenuLocationDebug( null, locale ) ) );
                 }
             }
 
             if ( config.getResponseStorageLocations( PwmSetting.FORGOTTEN_PASSWORD_WRITE_PREFERENCE ).contains( DataStorageMethod.LOCALDB ) )
             {
                 records.add( HealthRecord.forMessage(
+                        config.getDomainID(),
                         HealthMessage.Config_UsingLocalDBResponseStorage,
                         PwmSetting.FORGOTTEN_PASSWORD_WRITE_PREFERENCE.toMenuLocationDebug( null, locale ) ) );
             }
@@ -366,6 +392,7 @@ public class ConfigurationChecker implements HealthChecker
             if ( config.getOtpSecretStorageLocations( PwmSetting.OTP_SECRET_WRITE_PREFERENCE ).contains( DataStorageMethod.LOCALDB ) )
             {
                 records.add( HealthRecord.forMessage(
+                        config.getDomainID(),
                         HealthMessage.Config_UsingLocalDBResponseStorage,
                         PwmSetting.OTP_SECRET_WRITE_PREFERENCE.toMenuLocationDebug( null, locale ) ) );
             }
@@ -412,6 +439,7 @@ public class ConfigurationChecker implements HealthChecker
                     if ( ldapProfile == null )
                     {
                         records.add( HealthRecord.forMessage(
+                                config.getDomainID(),
                                 HealthMessage.Config_InvalidLdapProfile,
                                 PwmSetting.NEWUSER_LDAP_PROFILE.toMenuLocationDebug( newUserProfile.getIdentifier(), locale ) ) );
                     }
@@ -428,48 +456,27 @@ public class ConfigurationChecker implements HealthChecker
         {
             final List<HealthRecord> records = new ArrayList<>();
 
-            for ( final PwmSetting loopSetting : PwmSetting.values() )
+            final Stream<StoredConfigKey> interestedKeys = StoredConfigKey.filterBySettingSyntax(
+                    PwmSettingSyntax.FORM, config.getStoredConfiguration().keys() );
+
+            for ( final StoredConfigKey key : interestedKeys.collect( Collectors.toList() ) )
             {
-                if ( loopSetting.getSyntax() == PwmSettingSyntax.FORM )
+                final PwmSetting loopSetting = key.toPwmSetting();
+                final StoredValue storedValue = config.getStoredConfiguration().readStoredValue( key ).orElseThrow();
+                final List<FormConfiguration> forms = ValueTypeConverter.valueToForm( storedValue );
+                for ( final FormConfiguration form : forms )
                 {
-                    if ( loopSetting.getCategory().hasProfiles() )
-                    {
-
-                        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 = ValueTypeConverter.valueToForm( storedValue );
-                            for ( final FormConfiguration form : forms )
-                            {
-                                if ( !StringUtil.isEmpty( form.getJavascript() ) )
-                                {
-                                    records.add( HealthRecord.forMessage(
-                                            HealthMessage.Config_DeprecatedJSForm,
-                                            loopSetting.toMenuLocationDebug( profile, locale ),
-                                            PwmSetting.DISPLAY_CUSTOM_JAVASCRIPT.toMenuLocationDebug( null, locale )
-                                    ) );
-                                }
-                            }
-                        }
-
-                    }
-                    else
+                    if ( !StringUtil.isEmpty( form.getJavascript() ) )
                     {
-                        final List<FormConfiguration> forms = config.readSettingAsForm( loopSetting );
-                        for ( final FormConfiguration form : forms )
-                        {
-                            if ( !StringUtil.isEmpty( form.getJavascript() ) )
-                            {
-                                records.add( HealthRecord.forMessage(
-                                        HealthMessage.Config_DeprecatedJSForm,
-                                        loopSetting.toMenuLocationDebug( null, locale ),
-                                        PwmSetting.DISPLAY_CUSTOM_JAVASCRIPT.toMenuLocationDebug( null, locale )
-                                ) );
-                            }
-                        }
+                        records.add( HealthRecord.forMessage(
+                                config.getDomainID(),
+                                HealthMessage.Config_DeprecatedJSForm,
+                                loopSetting.toMenuLocationDebug( key.getProfileID(), locale ),
+                                PwmSetting.DISPLAY_CUSTOM_JAVASCRIPT.toMenuLocationDebug( null, locale )
+                        ) );
                     }
                 }
+
             }
             return records;
         }
@@ -492,7 +499,9 @@ public class ConfigurationChecker implements HealthChecker
                 final MessageSendMethod method = activationProfile.readSettingAsEnum( PwmSetting.ACTIVATE_TOKEN_SEND_METHOD, MessageSendMethod.class );
                 if ( deprecatedMethods.contains( method ) )
                 {
-                    records.add( HealthRecord.forMessage( HealthMessage.Config_InvalidSendMethod,
+                    records.add( HealthRecord.forMessage(
+                            config.getDomainID(),
+                            HealthMessage.Config_InvalidSendMethod,
                             method.toString(),
                             PwmSetting.ACTIVATE_TOKEN_SEND_METHOD.toMenuLocationDebug( activationProfile.getIdentifier(), locale )
                     ) );
@@ -503,7 +512,9 @@ public class ConfigurationChecker implements HealthChecker
                 final MessageSendMethod method = config.readSettingAsEnum( PwmSetting.FORGOTTEN_USERNAME_SEND_USERNAME_METHOD, MessageSendMethod.class );
                 if ( deprecatedMethods.contains( method ) )
                 {
-                    records.add( HealthRecord.forMessage( HealthMessage.Config_InvalidSendMethod,
+                    records.add( HealthRecord.forMessage(
+                            config.getDomainID(),
+                            HealthMessage.Config_InvalidSendMethod,
                             method.toString(),
                             PwmSetting.FORGOTTEN_USERNAME_SEND_USERNAME_METHOD.toMenuLocationDebug( null, locale )
                     ) );
@@ -516,7 +527,9 @@ public class ConfigurationChecker implements HealthChecker
 
                 if ( deprecatedMethods.contains( method ) )
                 {
-                    records.add( HealthRecord.forMessage( HealthMessage.Config_InvalidSendMethod,
+                    records.add( HealthRecord.forMessage(
+                            config.getDomainID(),
+                            HealthMessage.Config_InvalidSendMethod,
                             method.toString(),
                             PwmSetting.HELPDESK_TOKEN_SEND_METHOD.toMenuLocationDebug( helpdeskProfile.getIdentifier(), locale )
                     ) );
@@ -530,7 +543,9 @@ public class ConfigurationChecker implements HealthChecker
 
                     if ( deprecatedMethods.contains( method ) )
                     {
-                        records.add( HealthRecord.forMessage( HealthMessage.Config_InvalidSendMethod,
+                        records.add( HealthRecord.forMessage(
+                                config.getDomainID(),
+                                HealthMessage.Config_InvalidSendMethod,
                                 method.toString(),
                                 PwmSetting.RECOVERY_SENDNEWPW_METHOD.toMenuLocationDebug( forgottenPasswordProfile.getIdentifier(), locale )
                         ) );
@@ -541,7 +556,9 @@ public class ConfigurationChecker implements HealthChecker
 
                     if ( deprecatedMethods.contains( method ) )
                     {
-                        records.add( HealthRecord.forMessage( HealthMessage.Config_InvalidSendMethod,
+                        records.add( HealthRecord.forMessage(
+                                config.getDomainID(),
+                                HealthMessage.Config_InvalidSendMethod,
                                 method.toString(),
                                 PwmSetting.RECOVERY_TOKEN_SEND_METHOD.toMenuLocationDebug( forgottenPasswordProfile.getIdentifier(), locale )
                         ) );
@@ -571,6 +588,7 @@ public class ConfigurationChecker implements HealthChecker
                             + " > "
                             + " (" + maxValue + ")";
                     records.add( HealthRecord.forMessage(
+                            config.getDomainID(),
                             HealthMessage.Config_ValueConflict,
                             PwmSetting.PASSWORD_SYNC_MAX_WAIT_TIME.toMenuLocationDebug( profileID, locale ),
                             PwmSetting.PASSWORD_SYNC_MIN_WAIT_TIME.toMenuLocationDebug( profileID, locale ),
@@ -592,9 +610,9 @@ public class ConfigurationChecker implements HealthChecker
             final List<HealthRecord> records = new ArrayList<>();
 
             final StoredConfiguration storedConfiguration = config.getStoredConfiguration();
-            for ( final StoredConfigItemKey configItemKey : StoredConfigItemKey.filterBySettingSyntax(
+            for ( final StoredConfigKey configItemKey : StoredConfigKey.filterBySettingSyntax(
                     PwmSettingSyntax.USER_PERMISSION,
-                    storedConfiguration.modifiedItems() ) )
+                    storedConfiguration.keys() ).collect( Collectors.toList() ) )
             {
                 final StoredValue storedValue = storedConfiguration.readStoredValue( configItemKey ).orElseThrow( NoSuchElementException::new );
                 final List<UserPermission> permissions = ValueTypeConverter.valueToUserPermissions( storedValue );
@@ -608,6 +626,7 @@ public class ConfigurationChecker implements HealthChecker
                     {
                         final PwmSetting pwmSetting = configItemKey.toPwmSetting();
                         records.add( HealthRecord.forMessage(
+                                config.getDomainID(),
                                 HealthMessage.Config_SettingIssue,
                                 pwmSetting.toMenuLocationDebug( configItemKey.getProfileID(), locale ),
                                 e.getMessage() ) );
@@ -622,7 +641,7 @@ public class ConfigurationChecker implements HealthChecker
 
         private static List<HealthRecord> checkLdapProfile(
                 final DomainConfig domainConfig,
-                final StoredConfigItemKey storedConfigItemKey,
+                final StoredConfigKey storedConfigKey,
                 final Locale locale,
                 final UserPermission permission
         )
@@ -630,10 +649,11 @@ public class ConfigurationChecker implements HealthChecker
             final List<LdapProfile> ldapProfiles = ldapProfilesForLdapProfileSetting( domainConfig, permission.getLdapProfileID() );
             if ( ldapProfiles.isEmpty()  )
             {
-                final PwmSetting pwmSetting = storedConfigItemKey.toPwmSetting();
+                final PwmSetting pwmSetting = storedConfigKey.toPwmSetting();
                 return Collections.singletonList( HealthRecord.forMessage(
+                        domainConfig.getDomainID(),
                         HealthMessage.Config_ProfileValueValidity,
-                        pwmSetting.toMenuLocationDebug( storedConfigItemKey.getProfileID(), locale ),
+                        pwmSetting.toMenuLocationDebug( storedConfigKey.getProfileID(), locale ),
                         permission.getLdapProfileID() ) );
             }
 

+ 9 - 6
server/src/main/java/password/pwm/health/DatabaseStatusChecker.java

@@ -23,6 +23,7 @@ package password.pwm.health;
 import password.pwm.PwmApplication;
 import password.pwm.PwmDomain;
 import password.pwm.PwmEnvironment;
+import password.pwm.bean.DomainID;
 import password.pwm.config.DomainConfig;
 import password.pwm.error.PwmException;
 import password.pwm.util.db.DatabaseAccessor;
@@ -51,24 +52,26 @@ public class DatabaseStatusChecker implements HealthChecker
     {
         if ( !config.getAppConfig().hasDbConfigured() )
         {
-            return Collections.singletonList( HealthRecord.forMessage( HealthMessage.Database_Error,
-                            "Database not configured" ) );
+            return Collections.singletonList( HealthRecord.forMessage(
+                    DomainID.systemId(),
+                    HealthMessage.Database_Error,
+                    "Database not configured" ) );
         }
 
         PwmApplication runtimeInstance = null;
         try
         {
-            final PwmEnvironment runtimeEnvironment = pwmDomain.getPwmEnvironment().makeRuntimeInstance( config.getAppConfig() );
+            final PwmEnvironment runtimeEnvironment = pwmDomain.getPwmApplication().getPwmEnvironment().makeRuntimeInstance( config.getAppConfig() );
             runtimeInstance = PwmApplication.createPwmApplication( runtimeEnvironment );
-            final DatabaseAccessor accessor = runtimeInstance.getDefaultDomain().getDatabaseService().getAccessor();
+            final DatabaseAccessor accessor = runtimeInstance.getDatabaseService().getAccessor();
             accessor.get( DatabaseTable.PWM_META, "test" );
-            return runtimeInstance.getDefaultDomain().getDatabaseService().healthCheck();
+            return runtimeInstance.getDatabaseService().healthCheck();
         }
         catch ( final PwmException e )
         {
             LOGGER.error( () -> "error during healthcheck: " + e.getMessage() );
             e.printStackTrace();
-            return runtimeInstance.getDefaultDomain().getDatabaseService().healthCheck();
+            return runtimeInstance.getDatabaseService().healthCheck();
         }
         finally
         {

+ 21 - 20
server/src/main/java/password/pwm/health/HealthMonitor.java

@@ -54,6 +54,7 @@ import java.util.EnumSet;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.Set;
 import java.util.TreeSet;
 import java.util.concurrent.ConcurrentHashMap;
@@ -66,19 +67,16 @@ public class HealthMonitor implements PwmService
 {
     private static final PwmLogger LOGGER = PwmLogger.forClass( HealthMonitor.class );
 
-    private static final List<HealthChecker> HEALTH_CHECKERS;
+    private static final List<HealthChecker> HEALTH_CHECKERS = List.of(
+            new LDAPHealthChecker(),
+            new JavaChecker(),
+            new CertificateChecker(),
+            new ConfigurationChecker(),
+            new LocalDBHealthChecker(),
+            new ApplianceStatusChecker() );
 
-    static
-    {
-        final List<HealthChecker> records = new ArrayList<>();
-        records.add( new LDAPHealthChecker() );
-        records.add( new JavaChecker() );
-        records.add( new ConfigurationChecker() );
-        records.add( new LocalDBHealthChecker() );
-        records.add( new CertificateChecker() );
-        records.add( new ApplianceStatusChecker() );
-        HEALTH_CHECKERS = records;
-    }
+    private static final List<HealthSupplier> HEALTH_SUPPLIERS = List.of(
+        new CertificateChecker() );
 
     private ExecutorService executorService;
     private ExecutorService supportZipWriterService;
@@ -89,6 +87,7 @@ public class HealthMonitor implements PwmService
 
     private STATUS status = STATUS.CLOSED;
     private PwmDomain pwmDomain;
+    private PwmApplication pwmApplication;
     private volatile HealthData healthData = emptyHealthData();
 
     enum HealthMonitorFlag
@@ -105,6 +104,7 @@ public class HealthMonitor implements PwmService
     public void init( final PwmApplication pwmApplication, final DomainID domainID )
             throws PwmException
     {
+        this.pwmApplication = Objects.requireNonNull( pwmApplication );
         this.pwmDomain = pwmApplication.getDefaultDomain();
         this.healthData = emptyHealthData();
         settings = HealthMonitorSettings.fromConfiguration( pwmDomain.getConfig() );
@@ -116,8 +116,8 @@ public class HealthMonitor implements PwmService
             return;
         }
 
-        executorService = PwmScheduler.makeBackgroundExecutor( pwmDomain, this.getClass() );
-        supportZipWriterService = PwmScheduler.makeBackgroundExecutor( pwmDomain, this.getClass() );
+        executorService = PwmScheduler.makeBackgroundExecutor( pwmApplication, this.getClass() );
+        supportZipWriterService = PwmScheduler.makeBackgroundExecutor( pwmApplication, this.getClass() );
         scheduleNextZipOutput();
 
         {
@@ -126,7 +126,7 @@ public class HealthMonitor implements PwmService
             if ( threadDumpIntervalSeconds > 0 )
             {
                 final TimeDuration interval =  TimeDuration.of( threadDumpIntervalSeconds, TimeDuration.Unit.SECONDS );
-                pwmDomain.getPwmScheduler().scheduleFixedRateJob( new ThreadDumpLogger(), executorService, TimeDuration.SECOND, interval );
+                pwmApplication.getPwmScheduler().scheduleFixedRateJob( new ThreadDumpLogger(), executorService, TimeDuration.SECOND, interval );
             }
         }
 
@@ -182,18 +182,18 @@ public class HealthMonitor implements PwmService
         {
             final Instant startTime = Instant.now();
             LOGGER.trace( () ->  "begin force immediate check" );
-            final Future future = pwmDomain.getPwmScheduler().scheduleJob( new ImmediateJob(), executorService, TimeDuration.ZERO );
+            final Future future = pwmApplication.getPwmScheduler().scheduleJob( new ImmediateJob(), executorService, TimeDuration.ZERO );
             settings.getMaximumForceCheckWait().pause( future::isDone );
             LOGGER.trace( () ->  "exit force immediate check, done=" + future.isDone(), () -> TimeDuration.fromCurrent( startTime ) );
         }
 
-        pwmDomain.getPwmScheduler().scheduleJob( new UpdateJob(), executorService, settings.getNominalCheckInterval() );
+        pwmApplication.getPwmScheduler().scheduleJob( new UpdateJob(), executorService, settings.getNominalCheckInterval() );
 
         {
             final HealthData localHealthData = this.healthData;
             if ( localHealthData.recordsAreOutdated() )
             {
-                return Collections.singleton( HealthRecord.forMessage( HealthMessage.NoData ) );
+                return Collections.singleton( HealthRecord.forMessage( DomainID.systemId(), HealthMessage.NoData ) );
             }
 
             return localHealthData.getHealthRecords();
@@ -256,6 +256,7 @@ public class HealthMonitor implements PwmService
                 }
             }
         }
+
         for ( final PwmService service : pwmDomain.getPwmServices() )
         {
             try
@@ -343,7 +344,7 @@ public class HealthMonitor implements PwmService
         if ( intervalSeconds > 0 )
         {
             final TimeDuration intervalDuration = TimeDuration.of( intervalSeconds, TimeDuration.Unit.SECONDS );
-            pwmDomain.getPwmScheduler().scheduleJob( new SupportZipFileWriter( pwmDomain ), supportZipWriterService, intervalDuration );
+            pwmApplication.getPwmScheduler().scheduleJob( new SupportZipFileWriter( pwmDomain ), supportZipWriterService, intervalDuration );
         }
     }
 
@@ -374,7 +375,7 @@ public class HealthMonitor implements PwmService
         private void writeSupportZipToAppPath()
                 throws IOException, PwmUnrecoverableException
         {
-            final File appPath = pwmDomain.getPwmEnvironment().getApplicationPath();
+            final File appPath = pwmApplication.getPwmEnvironment().getApplicationPath();
             if ( !appPath.exists() )
             {
                 return;

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

@@ -22,6 +22,8 @@ package password.pwm.health;
 
 import lombok.EqualsAndHashCode;
 import org.jetbrains.annotations.NotNull;
+import password.pwm.PwmConstants;
+import password.pwm.bean.DomainID;
 import password.pwm.config.DomainConfig;
 import password.pwm.ws.server.rest.bean.HealthData;
 
@@ -41,12 +43,16 @@ public class HealthRecord implements Serializable, Comparable<HealthRecord>
 
     // new fields
     private final HealthTopic topic;
+    private final DomainID domainID;
     private final HealthMessage message;
     private final List<String> fields;
 
     private static final Comparator<HealthRecord> COMPARATOR = Comparator.comparing(
-            HealthRecord::getStatus,
+            HealthRecord::getDomainID,
             Comparator.nullsLast( Comparator.naturalOrder() ) )
+            .thenComparing(
+                    HealthRecord::getStatus,
+                    Comparator.nullsLast( Comparator.naturalOrder() ) )
             .thenComparing(
                     healthRecord -> healthRecord.getTopic( null, null ),
                     Comparator.nullsLast( Comparator.naturalOrder() ) )
@@ -56,31 +62,43 @@ public class HealthRecord implements Serializable, Comparable<HealthRecord>
 
 
     private HealthRecord(
+            final DomainID domainID,
             final HealthStatus status,
             final HealthTopic topicEnum,
             final HealthMessage message,
             final String... fields
     )
     {
+        this.domainID = Objects.requireNonNull( domainID );
         this.status = Objects.requireNonNull( status,  "status cannot be null" );
         this.topic = Objects.requireNonNull( topicEnum,  "topic cannot be null" );
         this.message = Objects.requireNonNull( message,  "message cannot be null" );
         this.fields = fields == null ? Collections.emptyList() : List.copyOf( Arrays.asList( fields ) );
     }
 
-    public static HealthRecord forMessage( final HealthMessage message )
+
+
+    public static HealthRecord forMessage( final DomainID domainID, final HealthMessage message )
     {
-        return new HealthRecord( message.getStatus(), message.getTopic(), message );
+        return new HealthRecord( domainID, message.getStatus(), message.getTopic(), message );
     }
 
-    public static HealthRecord forMessage( final HealthMessage message, final String... fields )
+    public static HealthRecord forMessage( final DomainID domainID, final HealthMessage message, final String... fields )
+    {
+        return new HealthRecord( domainID, message.getStatus(), message.getTopic(), message, fields );
+    }
+
+    public static HealthRecord forMessage( final DomainID domainID, final HealthMessage message, final HealthTopic healthTopic, final String... fields )
     {
-        return new HealthRecord( message.getStatus(), message.getTopic(), message, fields );
+        return new HealthRecord( domainID, message.getStatus(), healthTopic, message, fields );
     }
 
-    public static HealthRecord forMessage( final HealthMessage message, final HealthTopic healthTopic, final String... fields )
+    /**
+     * @deprecated Replace with {@link #forMessage(DomainID, HealthMessage, String...)}
+     */
+    public static HealthRecord forMessage( final HealthMessage message, final String... fields )
     {
-        return new HealthRecord( message.getStatus(), healthTopic, message, fields );
+        return new HealthRecord( PwmConstants.DOMAIN_ID_PLACEHOLDER, message.getStatus(), message.getTopic(), message, fields );
     }
 
     public HealthStatus getStatus( )
@@ -88,6 +106,11 @@ public class HealthRecord implements Serializable, Comparable<HealthRecord>
         return status;
     }
 
+    public DomainID getDomainID()
+    {
+        return domainID;
+    }
+
     public String getTopic( final Locale locale, final DomainConfig config )
     {
         if ( topic != null )

+ 31 - 0
server/src/main/java/password/pwm/health/HealthSupplier.java

@@ -0,0 +1,31 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2020 The PWM Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package password.pwm.health;
+
+import password.pwm.PwmApplication;
+
+import java.util.List;
+import java.util.function.Supplier;
+
+public interface HealthSupplier
+{
+    List<Supplier<List<HealthRecord>>> jobs( PwmApplication pwmApplication );
+}

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

@@ -22,6 +22,7 @@ package password.pwm.health;
 
 import password.pwm.AppProperty;
 import password.pwm.PwmDomain;
+import password.pwm.bean.DomainID;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -36,18 +37,18 @@ public class JavaChecker implements HealthChecker
         final int maxActiveThreads = Integer.parseInt( pwmDomain.getConfig().readAppProperty( AppProperty.HEALTH_JAVA_MAX_THREADS ) );
         if ( Thread.activeCount() > maxActiveThreads )
         {
-            records.add( HealthRecord.forMessage( HealthMessage.Java_HighThreads ) );
+            records.add( HealthRecord.forMessage( DomainID.systemId(), HealthMessage.Java_HighThreads ) );
         }
 
         final long minMemory = Long.parseLong( pwmDomain.getConfig().readAppProperty( AppProperty.HEALTH_JAVA_MIN_HEAP_BYTES ) );
         if ( Runtime.getRuntime().maxMemory() <= minMemory )
         {
-            records.add( HealthRecord.forMessage( HealthMessage.Java_SmallHeap ) );
+            records.add( HealthRecord.forMessage( DomainID.systemId(), HealthMessage.Java_SmallHeap ) );
         }
 
         if ( records.isEmpty() )
         {
-            records.add( HealthRecord.forMessage( HealthMessage.Java_OK ) );
+            records.add( HealthRecord.forMessage( DomainID.systemId(), HealthMessage.Java_OK ) );
         }
 
         return records;

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

@@ -33,8 +33,8 @@ import com.novell.ldapchai.provider.DirectoryVendor;
 import com.novell.ldapchai.util.ChaiUtility;
 import password.pwm.AppProperty;
 import password.pwm.PwmApplication;
-import password.pwm.PwmDomain;
 import password.pwm.PwmConstants;
+import password.pwm.PwmDomain;
 import password.pwm.bean.PasswordStatus;
 import password.pwm.bean.SessionLabel;
 import password.pwm.bean.UserIdentity;
@@ -81,11 +81,21 @@ import java.util.Locale;
 import java.util.Map;
 import java.util.Optional;
 import java.util.Set;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
 
-public class LDAPHealthChecker implements HealthChecker
+public class LDAPHealthChecker implements HealthChecker, HealthSupplier
 {
     private static final PwmLogger LOGGER = PwmLogger.forClass( LDAPHealthChecker.class );
 
+    @Override
+    public List<Supplier<List<HealthRecord>>> jobs( final PwmApplication pwmApplication )
+    {
+        return pwmApplication.getDomains().values().stream()
+                .map( domain -> ( Supplier<List<HealthRecord>> ) () -> doHealthCheck( domain ) )
+                .collect( Collectors.toList() );
+    }
+
     @Override
     public List<HealthRecord> doHealthCheck( final PwmDomain pwmDomain )
     {
@@ -107,7 +117,7 @@ public class LDAPHealthChecker implements HealthChecker
 
             if ( profileRecords.isEmpty() )
             {
-                profileRecords.add( HealthRecord.forMessage( HealthMessage.LDAP_OK ) );
+                profileRecords.add( HealthRecord.forMessage( pwmDomain.getDomainID(), HealthMessage.LDAP_OK ) );
                 profileRecords.addAll( doLdapTestUserCheck( config, ldapProfiles.get( profileID ), pwmDomain ) );
             }
             returnRecords.addAll( profileRecords );
@@ -128,6 +138,7 @@ public class LDAPHealthChecker implements HealthChecker
                     final String errorDate = JavaHelper.toIsoDate( errorInfo.getDate() );
                     final String errorMsg = errorInfo.toDebugStr();
                     returnRecords.add( HealthRecord.forMessage(
+                            pwmDomain.getDomainID(),
                             HealthMessage.LDAP_RecentlyUnreachable,
                             ldapProfile.getDisplayName( PwmConstants.DEFAULT_LOCALE ),
                             ageString,
@@ -186,7 +197,7 @@ public class LDAPHealthChecker implements HealthChecker
             final String msgString = e.getMessage();
             LOGGER.trace( SessionLabel.HEALTH_SESSION_LABEL, () -> "unexpected error while testing test user (during object creation): message="
                     + msgString + " debug info: " + JavaHelper.readHostileExceptionMessage( e ) );
-            returnRecords.add( HealthRecord.forMessage( HealthMessage.LDAP_TestUserUnexpected,
+            returnRecords.add( HealthRecord.forMessage( pwmDomain.getDomainID(), HealthMessage.LDAP_TestUserUnexpected,
                     PwmSetting.LDAP_TEST_USER_DN.toMenuLocationDebug( ldapProfile.getIdentifier(), PwmConstants.DEFAULT_LOCALE ),
                     msgString
             ) );
@@ -195,14 +206,16 @@ public class LDAPHealthChecker implements HealthChecker
 
         if ( proxyUserDN.equalsIgnoreCase( testUserDN ) )
         {
-            returnRecords.add( HealthRecord.forMessage( HealthMessage.LDAP_ProxyTestSameUser,
+            returnRecords.add( HealthRecord.forMessage(
+                    pwmDomain.getDomainID(),
+                    HealthMessage.LDAP_ProxyTestSameUser,
                     PwmSetting.LDAP_TEST_USER_DN.toMenuLocationDebug( ldapProfile.getIdentifier(), PwmConstants.DEFAULT_LOCALE ),
                     PwmSetting.LDAP_PROXY_USER_DN.toMenuLocationDebug( ldapProfile.getIdentifier(), PwmConstants.DEFAULT_LOCALE )
             ) );
             return returnRecords;
         }
 
-        ChaiUser theUser = null;
+        final ChaiUser theUser;
         ChaiProvider chaiProvider = null;
 
         try
@@ -224,7 +237,9 @@ public class LDAPHealthChecker implements HealthChecker
             }
             catch ( final ChaiUnavailableException e )
             {
-                returnRecords.add( HealthRecord.forMessage( HealthMessage.LDAP_TestUserUnavailable,
+                returnRecords.add( HealthRecord.forMessage(
+                        pwmDomain.getDomainID(),
+                        HealthMessage.LDAP_TestUserUnavailable,
                         PwmSetting.LDAP_TEST_USER_DN.toMenuLocationDebug( ldapProfile.getIdentifier(), PwmConstants.DEFAULT_LOCALE ),
                         e.getMessage()
                 ) );
@@ -238,7 +253,9 @@ public class LDAPHealthChecker implements HealthChecker
                         () -> "unexpected error while testing test user (during object creation): message="
                                 + msgString + " debug info: " + JavaHelper.readHostileExceptionMessage( e )
                 );
-                returnRecords.add( HealthRecord.forMessage( HealthMessage.LDAP_TestUserUnexpected,
+                returnRecords.add( HealthRecord.forMessage(
+                        pwmDomain.getDomainID(),
+                        HealthMessage.LDAP_TestUserUnexpected,
                         PwmSetting.LDAP_TEST_USER_DN.toMenuLocationDebug( ldapProfile.getIdentifier(), PwmConstants.DEFAULT_LOCALE ),
                         msgString
                 ) );
@@ -251,7 +268,9 @@ public class LDAPHealthChecker implements HealthChecker
             }
             catch ( final ChaiException e )
             {
-                returnRecords.add( HealthRecord.forMessage( HealthMessage.LDAP_TestUserError,
+                returnRecords.add( HealthRecord.forMessage(
+                        pwmDomain.getDomainID(),
+                        HealthMessage.LDAP_TestUserError,
                         PwmSetting.LDAP_TEST_USER_DN.toMenuLocationDebug( ldapProfile.getIdentifier(), PwmConstants.DEFAULT_LOCALE ),
                         e.getMessage()
                 ) );
@@ -277,7 +296,9 @@ public class LDAPHealthChecker implements HealthChecker
                     catch ( final Exception e )
                     {
                         LOGGER.debug( SessionLabel.HEALTH_SESSION_LABEL, () -> "error reading user password from directory " + e.getMessage() );
-                        returnRecords.add( HealthRecord.forMessage( HealthMessage.LDAP_TestUserReadPwError,
+                        returnRecords.add( HealthRecord.forMessage(
+                                pwmDomain.getDomainID(),
+                                HealthMessage.LDAP_TestUserReadPwError,
                                 PwmSetting.EDIRECTORY_READ_USER_PWD.toMenuLocationDebug( null, PwmConstants.DEFAULT_LOCALE ),
                                 PwmSetting.LDAP_TEST_USER_DN.toMenuLocationDebug( ldapProfile.getIdentifier(), PwmConstants.DEFAULT_LOCALE ),
                                 e.getMessage()
@@ -288,7 +309,7 @@ public class LDAPHealthChecker implements HealthChecker
                 else
                 {
                     final Locale locale = PwmConstants.DEFAULT_LOCALE;
-                    final UserIdentity userIdentity = UserIdentity.createUserIdentity( testUserDN, ldapProfile.getIdentifier(), pwmDomain.getDomainID() );
+                    final UserIdentity userIdentity = UserIdentity.create( testUserDN, ldapProfile.getIdentifier(), pwmDomain.getDomainID() );
 
                     final PwmPasswordPolicy passwordPolicy = PasswordUtility.readPasswordPolicyForUser(
                             pwmDomain, null, userIdentity, theUser );
@@ -341,7 +362,9 @@ public class LDAPHealthChecker implements HealthChecker
                         }
                         catch ( final ChaiException e )
                         {
-                            returnRecords.add( HealthRecord.forMessage( HealthMessage.LDAP_TestUserWritePwError,
+                            returnRecords.add( HealthRecord.forMessage(
+                                    pwmDomain.getDomainID(),
+                                    HealthMessage.LDAP_TestUserWritePwError,
                                     PwmSetting.LDAP_TEST_USER_DN.toMenuLocationDebug( ldapProfile.getIdentifier(), PwmConstants.DEFAULT_LOCALE ),
                                     e.getMessage()
                             ) );
@@ -355,7 +378,9 @@ public class LDAPHealthChecker implements HealthChecker
             {
                 final String msg = "error setting test user password: " + JavaHelper.readHostileExceptionMessage( e );
                 LOGGER.error( SessionLabel.HEALTH_SESSION_LABEL, () -> msg, e );
-                returnRecords.add( HealthRecord.forMessage( HealthMessage.LDAP_TestUserUnexpected,
+                returnRecords.add( HealthRecord.forMessage(
+                        pwmDomain.getDomainID(),
+                        HealthMessage.LDAP_TestUserUnexpected,
                         PwmSetting.LDAP_TEST_USER_DN.toMenuLocationDebug( ldapProfile.getIdentifier(), PwmConstants.DEFAULT_LOCALE ),
                         msg
                 ) );
@@ -364,7 +389,7 @@ public class LDAPHealthChecker implements HealthChecker
 
             try
             {
-                final UserIdentity userIdentity = UserIdentity.createUserIdentity( theUser.getEntryDN(), ldapProfile.getIdentifier(), pwmDomain.getDomainID() );
+                final UserIdentity userIdentity = UserIdentity.create( theUser.getEntryDN(), ldapProfile.getIdentifier(), pwmDomain.getDomainID() );
                 final UserInfo userInfo = UserInfoFactory.newUserInfo(
                         pwmDomain,
                         SessionLabel.HEALTH_SESSION_LABEL,
@@ -387,6 +412,7 @@ public class LDAPHealthChecker implements HealthChecker
             catch ( final PwmUnrecoverableException e )
             {
                 returnRecords.add( HealthRecord.forMessage(
+                        pwmDomain.getDomainID(),
                         HealthMessage.LDAP_TestUserError,
                         PwmSetting.LDAP_TEST_USER_DN.toMenuLocationDebug( ldapProfile.getIdentifier(), PwmConstants.DEFAULT_LOCALE ),
                         "unable to read test user data: " + e.getMessage() ) );
@@ -409,7 +435,11 @@ public class LDAPHealthChecker implements HealthChecker
             }
         }
 
-        returnRecords.add( HealthRecord.forMessage( HealthMessage.LDAP_TestUserOK, ldapProfile.getDisplayName( PwmConstants.DEFAULT_LOCALE ) ) );
+        returnRecords.add( HealthRecord.forMessage(
+                pwmDomain.getDomainID(),
+                HealthMessage.LDAP_TestUserOK,
+                ldapProfile.getDisplayName( PwmConstants.DEFAULT_LOCALE ) ) );
+
         return returnRecords;
     }
 
@@ -444,6 +474,7 @@ public class LDAPHealthChecker implements HealthChecker
             {
                 final String errorString = "error connecting to ldap server '" + loopURL + "': " + e.getMessage();
                 returnRecords.add( HealthRecord.forMessage(
+                        pwmDomain.getDomainID(),
                         HealthMessage.LDAP_No_Connection,
                         loopURL,
                         errorString ) );
@@ -487,6 +518,7 @@ public class LDAPHealthChecker implements HealthChecker
                 {
                     final String menuLocationStr = PwmSetting.LDAP_PROXY_USER_DN.toMenuLocationDebug( ldapProfile.getIdentifier(), PwmConstants.DEFAULT_LOCALE );
                     return Collections.singletonList( HealthRecord.forMessage(
+                            pwmDomain.getDomainID(),
                             HealthMessage.LDAP_No_Connection,
                              ldapProfile.getIdentifier(),
                              "Missing Proxy User DN: " + menuLocationStr ) );
@@ -495,6 +527,7 @@ public class LDAPHealthChecker implements HealthChecker
                 {
                     final String menuLocationStr = PwmSetting.LDAP_PROXY_USER_PASSWORD.toMenuLocationDebug( ldapProfile.getIdentifier(), PwmConstants.DEFAULT_LOCALE );
                     return Collections.singletonList( HealthRecord.forMessage(
+                            pwmDomain.getDomainID(),
                             HealthMessage.LDAP_No_Connection,
                             ldapProfile.getIdentifier(),
                             "Missing Proxy User Password: " + menuLocationStr ) );
@@ -514,6 +547,7 @@ public class LDAPHealthChecker implements HealthChecker
                     if ( maxPwExpireTime.isLongerThan( expirationDuration ) )
                     {
                         return Collections.singletonList( HealthRecord.forMessage(
+                                pwmDomain.getDomainID(),
                                 HealthMessage.LDAP_ProxyUserPwExpired,
                                 adminEntry.getEntryDN(),
                                 expirationDuration.asLongString( PwmConstants.DEFAULT_LOCALE )
@@ -541,6 +575,7 @@ public class LDAPHealthChecker implements HealthChecker
                     errorString.append( ")" );
                 }
                 returnRecords.add( HealthRecord.forMessage(
+                        pwmDomain.getDomainID(),
                         HealthMessage.LDAP_No_Connection,
                         ldapProfile.getIdentifier(),
                         errorString.toString() ) );
@@ -551,7 +586,10 @@ public class LDAPHealthChecker implements HealthChecker
             }
             catch ( final Exception e )
             {
-                final HealthRecord record = HealthRecord.forMessage( HealthMessage.LDAP_No_Connection, e.getMessage() );
+                final HealthRecord record = HealthRecord.forMessage(
+                        pwmDomain.getDomainID(),
+                        HealthMessage.LDAP_No_Connection,
+                        e.getMessage() );
                 returnRecords.add( record );
                 pwmDomain.getLdapConnectionService().setLastLdapFailure( ldapProfile,
                         new ErrorInformation( PwmError.ERROR_DIRECTORY_UNAVAILABLE, record.getDetail( PwmConstants.DEFAULT_LOCALE, pwmDomain.getConfig() ) ) );
@@ -578,6 +616,7 @@ public class LDAPHealthChecker implements HealthChecker
                                     + PwmSetting.LDAP_CONTEXTLESS_ROOT.toMenuLocationDebug( ldapProfile.getIdentifier(), PwmConstants.DEFAULT_LOCALE )
                                     + "' value '" + loopContext + "' is not valid";
                             returnRecords.add( HealthRecord.forMessage(
+                                    pwmDomain.getDomainID(),
                                     HealthMessage.LDAP_No_Connection,
                                     ldapProfile.getIdentifier(),
                                     errorString ) );
@@ -589,6 +628,7 @@ public class LDAPHealthChecker implements HealthChecker
                                 + PwmSetting.LDAP_CONTEXTLESS_ROOT.toMenuLocationDebug( ldapProfile.getIdentifier(), PwmConstants.DEFAULT_LOCALE )
                                 + "' value '" + loopContext + "' is not valid: " + e.getMessage();
                         returnRecords.add( HealthRecord.forMessage(
+                                pwmDomain.getDomainID(),
                                 HealthMessage.LDAP_No_Connection,
                                 ldapProfile.getIdentifier(),
                                 errorString ) );
@@ -625,6 +665,7 @@ public class LDAPHealthChecker implements HealthChecker
                 if ( !urlUsingHostname( loopURL ) )
                 {
                     returnList.add( HealthRecord.forMessage(
+                            pwmDomain.getDomainID(),
                             HealthMessage.LDAP_AD_StaticIP,
                             loopURL
                     ) );
@@ -635,6 +676,7 @@ public class LDAPHealthChecker implements HealthChecker
                 if ( "ldap".equalsIgnoreCase( scheme ) )
                 {
                     returnList.add( HealthRecord.forMessage(
+                            pwmDomain.getDomainID(),
                             HealthMessage.LDAP_AD_Unsecure,
                             loopURL
                     ) );
@@ -643,6 +685,7 @@ public class LDAPHealthChecker implements HealthChecker
             catch ( final MalformedURLException | UnknownHostException e )
             {
                 returnList.add( HealthRecord.forMessage(
+                        pwmDomain.getDomainID(),
                         HealthMessage.Config_ParseError,
                         e.getMessage(),
                         PwmSetting.LDAP_SERVER_URLS.toMenuLocationDebug( ldapProfile.getIdentifier(), PwmConstants.DEFAULT_LOCALE ),
@@ -670,7 +713,7 @@ public class LDAPHealthChecker implements HealthChecker
 
     private List<HealthRecord> checkVendorSameness( final PwmDomain pwmDomain )
     {
-        final Map<HealthMonitor.HealthMonitorFlag, Serializable> healthProperties = pwmDomain.getHealthMonitor().getHealthProperties();
+        final Map<HealthMonitor.HealthMonitorFlag, Serializable> healthProperties = pwmDomain.getPwmApplication().getHealthMonitor().getHealthProperties();
         if ( healthProperties.containsKey( HealthMonitor.HealthMonitorFlag.LdapVendorSameCheck ) )
         {
             return ( List<HealthRecord> ) healthProperties.get( HealthMonitor.HealthMonitorFlag.LdapVendorSameCheck );
@@ -718,7 +761,7 @@ public class LDAPHealthChecker implements HealthChecker
                     vendorMsg.append( ", " );
                 }
             }
-            healthRecords.add( HealthRecord.forMessage( HealthMessage.LDAP_VendorsNotSame, vendorMsg.toString() ) );
+            healthRecords.add( HealthRecord.forMessage( pwmDomain.getDomainID(), HealthMessage.LDAP_VendorsNotSame, vendorMsg.toString() ) );
             // cache the error
             healthProperties.put( HealthMonitor.HealthMonitorFlag.LdapVendorSameCheck, healthRecords );
 
@@ -738,17 +781,15 @@ public class LDAPHealthChecker implements HealthChecker
 
     private static List<HealthRecord> checkAdPasswordPolicyApi( final PwmDomain pwmDomain )
     {
-
-
         final boolean passwordPolicyApiEnabled = pwmDomain.getConfig().readSettingAsBoolean( PwmSetting.AD_ENFORCE_PW_HISTORY_ON_SET );
         if ( !passwordPolicyApiEnabled )
         {
             return Collections.emptyList();
         }
 
-        if ( pwmDomain.getHealthMonitor() != null )
+        if ( pwmDomain.getPwmApplication().getHealthMonitor() != null )
         {
-            final Map<HealthMonitor.HealthMonitorFlag, Serializable> healthProperties = pwmDomain.getHealthMonitor().getHealthProperties();
+            final Map<HealthMonitor.HealthMonitorFlag, Serializable> healthProperties = pwmDomain.getPwmApplication().getHealthMonitor().getHealthProperties();
             if ( healthProperties.containsKey( HealthMonitor.HealthMonitorFlag.AdPasswordPolicyApiCheck ) )
             {
                 final List<HealthRecord> healthRecords = ( List<HealthRecord> ) healthProperties.get( HealthMonitor.HealthMonitorFlag.AdPasswordPolicyApiCheck );
@@ -784,6 +825,7 @@ public class LDAPHealthChecker implements HealthChecker
                     {
                         final String url = chaiConfiguration.getSetting( ChaiSetting.BIND_URLS );
                         final HealthRecord record = HealthRecord.forMessage(
+                                pwmDomain.getDomainID(),
                                 HealthMessage.LDAP_Ad_History_Asn_Missing,
                                 PwmSetting.AD_ENFORCE_PW_HISTORY_ON_SET.toMenuLocationDebug( null, PwmConstants.DEFAULT_LOCALE ),
                                 url
@@ -801,9 +843,9 @@ public class LDAPHealthChecker implements HealthChecker
                     () ->  "error during ad api password policy (asn " + PwmConstants.LDAP_AD_PASSWORD_POLICY_CONTROL_ASN + ") check: " + e.getMessage() );
         }
 
-        if ( !errorReachingServer && pwmDomain.getHealthMonitor() != null )
+        if ( !errorReachingServer && pwmDomain.getPwmApplication().getHealthMonitor() != null )
         {
-            final Map<HealthMonitor.HealthMonitorFlag, Serializable> healthProperties = pwmDomain.getHealthMonitor().getHealthProperties();
+            final Map<HealthMonitor.HealthMonitorFlag, Serializable> healthProperties = pwmDomain.getPwmApplication().getHealthMonitor().getHealthProperties();
             healthProperties.put( HealthMonitor.HealthMonitorFlag.AdPasswordPolicyApiCheck, healthRecords );
         }
 
@@ -861,6 +903,7 @@ public class LDAPHealthChecker implements HealthChecker
                             {
                                 final Optional<String> errorMsg = validateDN( pwmDomain, value, profile );
                                 errorMsg.ifPresent( s -> returnList.add( HealthRecord.forMessage(
+                                        pwmDomain.getDomainID(),
                                         HealthMessage.Config_DNValueValidity,
                                         pwmSetting.toMenuLocationDebug( profile, PwmConstants.DEFAULT_LOCALE ), s )
                                 ) );
@@ -875,6 +918,7 @@ public class LDAPHealthChecker implements HealthChecker
                                 {
                                     final Optional<String> errorMsg = validateDN( pwmDomain, value, profile );
                                     errorMsg.ifPresent( s -> returnList.add( HealthRecord.forMessage(
+                                            pwmDomain.getDomainID(),
                                             HealthMessage.Config_DNValueValidity,
                                             pwmSetting.toMenuLocationDebug( profile, PwmConstants.DEFAULT_LOCALE ), s )
                                     ) );
@@ -912,6 +956,7 @@ public class LDAPHealthChecker implements HealthChecker
             {
                 return Collections.singletonList(
                         HealthRecord.forMessage(
+                                pwmDomain.getDomainID(),
                                 HealthMessage.NewUser_PwTemplateBad,
                                 PwmSetting.NEWUSER_PASSWORD_POLICY_USER.toMenuLocationDebug( newUserProfile.getIdentifier(), locale ),
                                 "Value missing"
@@ -931,7 +976,7 @@ public class LDAPHealthChecker implements HealthChecker
                     }
                 }
 
-                final UserIdentity newUserTemplateIdentity = UserIdentity.createUserIdentity( policyUserStr, ldapProfile.getIdentifier(), pwmDomain.getDomainID() );
+                final UserIdentity newUserTemplateIdentity = UserIdentity.create( policyUserStr, ldapProfile.getIdentifier(), pwmDomain.getDomainID() );
 
                 final ChaiUser chaiUser = pwmDomain.getProxiedChaiUser( newUserTemplateIdentity );
 
@@ -941,6 +986,7 @@ public class LDAPHealthChecker implements HealthChecker
                     {
                         return Collections.singletonList(
                                 HealthRecord.forMessage(
+                                        pwmDomain.getDomainID(),
                                         HealthMessage.NewUser_PwTemplateBad,
                                         PwmSetting.NEWUSER_PASSWORD_POLICY_USER.toMenuLocationDebug( newUserProfile.getIdentifier(), locale ),
                                         "userDN value is not valid"
@@ -988,7 +1034,9 @@ public class LDAPHealthChecker implements HealthChecker
         catch ( final Exception e )
         {
             return Collections.singletonList(
-                    HealthRecord.forMessage( HealthMessage.LDAP_SearchFailure,
+                    HealthRecord.forMessage(
+                            pwmDomain.getDomainID(),
+                            HealthMessage.LDAP_SearchFailure,
                             e.getMessage()
                     ) );
         }
@@ -998,7 +1046,9 @@ public class LDAPHealthChecker implements HealthChecker
         if ( timeDuration.isLongerThan( warnDuration ) )
         {
             return Collections.singletonList(
-                    HealthRecord.forMessage( HealthMessage.LDAP_SearchFailure,
+                    HealthRecord.forMessage(
+                            pwmDomain.getDomainID(),
+                            HealthMessage.LDAP_SearchFailure,
                             "user search time of " + timeDuration.asLongString() + " exceeded ideal of " + warnDuration.asLongString(  )
                     ) );
         }
@@ -1032,7 +1082,9 @@ public class LDAPHealthChecker implements HealthChecker
                 else
                 {
                     return Collections.singletonList(
-                            HealthRecord.forMessage( HealthMessage.Config_UserPermissionValidity,
+                            HealthRecord.forMessage(
+                                    pwmDomain.getDomainID(),
+                                    HealthMessage.Config_UserPermissionValidity,
                                     settingDebugName,
                                     "specified ldap profile ID invalid: " + configuredLdapProfileID
                             ) );
@@ -1054,6 +1106,7 @@ public class LDAPHealthChecker implements HealthChecker
                     {
                         final Optional<String> errorMsg = validateDN( pwmDomain, userDN, ldapProfileID );
                         errorMsg.ifPresent( s -> returnList.add( HealthRecord.forMessage(
+                                pwmDomain.getDomainID(),
                                 HealthMessage.Config_UserPermissionValidity,
                                 settingDebugName, "userDN: " + s ) ) );
                     }
@@ -1067,8 +1120,10 @@ public class LDAPHealthChecker implements HealthChecker
                     {
                         final Optional<String> errorMsg = validateDN( pwmDomain, groupDN, ldapProfileID );
                         errorMsg.ifPresent( s -> returnList.add( HealthRecord.forMessage(
+                                pwmDomain.getDomainID(),
                                 HealthMessage.Config_UserPermissionValidity,
-                                settingDebugName, "groupDN: " + s ) ) );
+                                settingDebugName,
+                                "groupDN: " + s ) ) );
                     }
                 }
                 break;
@@ -1080,8 +1135,10 @@ public class LDAPHealthChecker implements HealthChecker
                     {
                         final Optional<String> errorMsg = validateDN( pwmDomain, baseDN, ldapProfileID );
                         errorMsg.ifPresent( s -> returnList.add( HealthRecord.forMessage(
+                                pwmDomain.getDomainID(),
                                 HealthMessage.Config_UserPermissionValidity,
-                                settingDebugName, "baseDN: " + s ) ) );
+                                settingDebugName,
+                                "baseDN: " + s ) ) );
                     }
                 }
                 break;
@@ -1170,7 +1227,8 @@ public class LDAPHealthChecker implements HealthChecker
     )
             throws PwmUnrecoverableException
     {
-        final PwmApplication tempApplication = PwmApplication.createPwmApplication( pwmDomain.getPwmEnvironment().makeRuntimeInstance( config.getAppConfig() ) );
+        final PwmApplication tempApplication = PwmApplication.createPwmApplication(
+                pwmDomain.getPwmApplication().getPwmEnvironment().makeRuntimeInstance( config.getAppConfig() ) );
         final LDAPHealthChecker ldapHealthChecker = new LDAPHealthChecker();
 
         final LdapProfile ldapProfile = config.getLdapProfiles().get( profileID );
@@ -1184,7 +1242,7 @@ public class LDAPHealthChecker implements HealthChecker
 
         if ( profileRecords.isEmpty() )
         {
-            profileRecords.add( HealthRecord.forMessage( HealthMessage.LDAP_OK ) );
+            profileRecords.add( HealthRecord.forMessage( config.getDomainID(), HealthMessage.LDAP_OK ) );
         }
 
         if ( fullTest )

+ 8 - 7
server/src/main/java/password/pwm/health/LocalDBHealthChecker.java

@@ -22,6 +22,7 @@ package password.pwm.health;
 
 import password.pwm.AppProperty;
 import password.pwm.PwmDomain;
+import password.pwm.bean.DomainID;
 import password.pwm.config.DomainConfig;
 import password.pwm.util.java.FileSystemUtility;
 import password.pwm.util.java.JavaHelper;
@@ -44,24 +45,24 @@ public class LocalDBHealthChecker implements HealthChecker
 
         final List<HealthRecord> healthRecords = new ArrayList<>();
 
-        final LocalDB localDB = pwmDomain.getLocalDB();
+        final LocalDB localDB = pwmDomain.getPwmApplication().getLocalDB();
 
         if ( localDB == null )
         {
             final String detailedError = pwmDomain.getLastLocalDBFailure() == null ? "unknown, check logs" : pwmDomain.getLastLocalDBFailure().toDebugStr();
-            healthRecords.add( HealthRecord.forMessage( HealthMessage.LocalDB_BAD, detailedError ) );
+            healthRecords.add( HealthRecord.forMessage( DomainID.systemId(), HealthMessage.LocalDB_BAD, detailedError ) );
             return healthRecords;
         }
 
         if ( LocalDB.Status.NEW == localDB.status() )
         {
-            healthRecords.add( HealthRecord.forMessage( HealthMessage.LocalDB_NEW ) );
+            healthRecords.add( HealthRecord.forMessage( DomainID.systemId(), HealthMessage.LocalDB_NEW ) );
             return healthRecords;
         }
 
         if ( LocalDB.Status.CLOSED == localDB.status() )
         {
-            healthRecords.add( HealthRecord.forMessage( HealthMessage.LocalDB_CLOSED ) );
+            healthRecords.add( HealthRecord.forMessage( DomainID.systemId(), HealthMessage.LocalDB_CLOSED ) );
             return healthRecords;
         }
 
@@ -69,7 +70,7 @@ public class LocalDBHealthChecker implements HealthChecker
 
         if ( healthRecords.isEmpty() )
         {
-            healthRecords.add( HealthRecord.forMessage( HealthMessage.LocalDB_OK ) );
+            healthRecords.add( HealthRecord.forMessage( DomainID.systemId(), HealthMessage.LocalDB_OK ) );
         }
 
         return healthRecords;
@@ -79,12 +80,12 @@ public class LocalDBHealthChecker implements HealthChecker
     {
         final DomainConfig domainConfig = pwmDomain.getConfig();
         final long minFreeSpace = JavaHelper.silentParseLong( domainConfig.readAppProperty( AppProperty.HEALTH_DISK_MIN_FREE_WARNING ), 500_000_000 );
-        final long freeSpace = FileSystemUtility.diskSpaceRemaining( pwmDomain.getLocalDB().getFileLocation() );
+        final long freeSpace = FileSystemUtility.diskSpaceRemaining( pwmDomain.getPwmApplication().getLocalDB().getFileLocation() );
 
         if ( freeSpace < minFreeSpace )
         {
             final String spaceValue = StringUtil.formatDiskSizeforDebug( freeSpace );
-            return Collections.singletonList( HealthRecord.forMessage( HealthMessage.LocalDB_LowDiskSpace, spaceValue ) );
+            return Collections.singletonList( HealthRecord.forMessage( DomainID.systemId(), HealthMessage.LocalDB_LowDiskSpace, spaceValue ) );
         }
 
         return Collections.emptyList();

+ 30 - 13
server/src/main/java/password/pwm/http/ContextManager.java

@@ -25,6 +25,7 @@ import password.pwm.PwmApplication;
 import password.pwm.PwmApplicationMode;
 import password.pwm.PwmConstants;
 import password.pwm.PwmEnvironment;
+import password.pwm.bean.DomainID;
 import password.pwm.bean.SessionLabel;
 import password.pwm.config.AppConfig;
 import password.pwm.config.DomainConfig;
@@ -32,8 +33,10 @@ import password.pwm.config.PwmSetting;
 import password.pwm.config.profile.LdapProfile;
 import password.pwm.config.stored.ConfigurationProperty;
 import password.pwm.config.stored.ConfigurationReader;
+import password.pwm.config.stored.StoredConfigKey;
 import password.pwm.config.stored.StoredConfiguration;
 import password.pwm.config.stored.StoredConfigurationModifier;
+import password.pwm.config.stored.StoredConfigurationUtil;
 import password.pwm.config.value.StoredValue;
 import password.pwm.config.value.X509CertificateValue;
 import password.pwm.error.ErrorInformation;
@@ -52,6 +55,7 @@ import password.pwm.util.secure.X509Utils;
 import javax.servlet.ServletContext;
 import javax.servlet.ServletRequest;
 import javax.servlet.http.HttpSession;
+import java.io.ByteArrayOutputStream;
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.IOException;
@@ -153,6 +157,16 @@ public class ContextManager implements Serializable
         return ( ContextManager ) theManager;
     }
 
+    public static String readEulaText( final ContextManager contextManager, final String filename )
+            throws IOException
+    {
+        final String path = PwmConstants.URL_PREFIX_PUBLIC + "/resources/text/" + filename;
+        final InputStream inputStream = contextManager.getResourceAsStream( path );
+        final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+        JavaHelper.copyWhilePredicate( inputStream, byteArrayOutputStream, o -> true );
+        return byteArrayOutputStream.toString( PwmConstants.DEFAULT_CHARSET.name() );
+    }
+
     public PwmApplication getPwmApplication( )
             throws PwmUnrecoverableException
     {
@@ -289,7 +303,7 @@ public class ContextManager implements Serializable
 
         taskMaster = Executors.newSingleThreadScheduledExecutor(
                 PwmScheduler.makePwmThreadFactory(
-                        PwmScheduler.makeThreadName( pwmApplication.getDomains().get( PwmConstants.DOMAIN_ID_PLACEHOLDER ), this.getClass() ) + "-",
+                        PwmScheduler.makeThreadName( pwmApplication, this.getClass() ) + "-",
                         true
                 ) );
 
@@ -729,24 +743,27 @@ public class ContextManager implements Serializable
             final StoredConfigurationModifier modifiedConfig = StoredConfigurationModifier.newModifier( configReader.getStoredConfiguration() );
 
             int importedCerts = 0;
-            for ( final LdapProfile ldapProfile : domainConfig.getLdapProfiles().values() )
+            for ( final DomainID domainID : StoredConfigurationUtil.domainList( configReader.getStoredConfiguration() ) )
             {
-                final List<String> ldapUrls = ldapProfile.getLdapUrls();
-                if ( !JavaHelper.isEmpty( ldapUrls ) )
+                for ( final LdapProfile ldapProfile : domainConfig.getLdapProfiles().values() )
                 {
-                    final Set<X509Certificate> certs = X509Utils.readCertsForListOfLdapUrls( ldapUrls, domainConfig );
-                    if ( !JavaHelper.isEmpty( certs ) )
+                    final List<String> ldapUrls = ldapProfile.getLdapUrls();
+                    if ( !JavaHelper.isEmpty( ldapUrls ) )
                     {
-                        importedCerts += certs.size();
-                        for ( final X509Certificate cert : certs )
+                        final Set<X509Certificate> certs = X509Utils.readCertsForListOfLdapUrls( ldapUrls, domainConfig );
+                        if ( !JavaHelper.isEmpty( certs ) )
                         {
-                            LOGGER.trace( SESSION_LABEL, () -> "imported cert: " + X509Utils.makeDebugText( cert ) );
+                            importedCerts += certs.size();
+                            for ( final X509Certificate cert : certs )
+                            {
+                                LOGGER.trace( SESSION_LABEL, () -> "imported cert: " + X509Utils.makeDebugText( cert ) );
+                            }
+                            final StoredValue storedValue = X509CertificateValue.fromX509( certs );
+
+                            final StoredConfigKey key = StoredConfigKey.forSetting( PwmSetting.LDAP_SERVER_CERTS, ldapProfile.getIdentifier(), domainID );
+                            modifiedConfig.writeSetting( key, storedValue, null );
                         }
-                        final StoredValue storedValue = X509CertificateValue.fromX509( certs );
-
-                        modifiedConfig.writeSetting( PwmSetting.LDAP_SERVER_CERTS, ldapProfile.getIdentifier(), storedValue, null );
                     }
-
                 }
             }
 

+ 4 - 16
server/src/main/java/password/pwm/http/HttpHeader.java

@@ -22,7 +22,8 @@ package password.pwm.http;
 
 import password.pwm.PwmConstants;
 import password.pwm.util.java.JavaHelper;
-import password.pwm.util.java.StringUtil;
+
+import java.util.Optional;
 
 public enum HttpHeader
 {
@@ -85,21 +86,8 @@ public enum HttpHeader
         return JavaHelper.enumArrayContainsValue( properties, Property.Sensitive );
     }
 
-    public static HttpHeader forHttpHeader( final String header )
+    public static Optional<HttpHeader> forHttpHeader( final String header )
     {
-        if ( StringUtil.isEmpty( header ) )
-        {
-            return null;
-        }
-
-        for ( final HttpHeader httpHeader : values() )
-        {
-            if ( header.equals( httpHeader.getHttpName() ) )
-            {
-                return httpHeader;
-            }
-        }
-
-        return null;
+        return JavaHelper.readEnumFromPredicate( HttpHeader.class, loopHeader -> header.equals( loopHeader.getHttpName() ) );
     }
 }

+ 35 - 35
server/src/main/java/password/pwm/http/PwmHttpRequestWrapper.java

@@ -23,7 +23,7 @@ package password.pwm.http;
 import com.google.gson.JsonParseException;
 import password.pwm.AppProperty;
 import password.pwm.PwmConstants;
-import password.pwm.config.DomainConfig;
+import password.pwm.config.AppConfig;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.util.PasswordData;
@@ -51,7 +51,7 @@ import java.util.Set;
 public class PwmHttpRequestWrapper
 {
     private final HttpServletRequest httpServletRequest;
-    private final DomainConfig domainConfig;
+    private final AppConfig appConfig;
 
     private static final Set<String> HTTP_PARAM_DEBUG_STRIP_VALUES = Set.of(
             "password",
@@ -66,10 +66,10 @@ public class PwmHttpRequestWrapper
         BypassValidation
     }
 
-    public PwmHttpRequestWrapper( final HttpServletRequest request, final DomainConfig domainConfig )
+    public PwmHttpRequestWrapper( final HttpServletRequest request, final AppConfig appConfig )
     {
         this.httpServletRequest = request;
-        this.domainConfig = domainConfig;
+        this.appConfig = appConfig;
     }
 
     public HttpServletRequest getHttpServletRequest( )
@@ -97,7 +97,7 @@ public class PwmHttpRequestWrapper
     public String readRequestBodyAsString( )
             throws IOException, PwmUnrecoverableException
     {
-        final int maxChars = Integer.parseInt( domainConfig.readAppProperty( AppProperty.HTTP_BODY_MAXREAD_LENGTH ) );
+        final int maxChars = Integer.parseInt( appConfig.readAppProperty( AppProperty.HTTP_BODY_MAXREAD_LENGTH ) );
         return readRequestBodyAsString( maxChars );
     }
 
@@ -114,9 +114,9 @@ public class PwmHttpRequestWrapper
         final String bodyString = readRequestBodyAsString();
         final Map<String, String> inputMap = JsonUtil.deserializeStringMap( bodyString );
 
-        final boolean trim = Boolean.parseBoolean( domainConfig.readAppProperty( AppProperty.SECURITY_INPUT_TRIM ) );
-        final boolean passwordTrim = Boolean.parseBoolean( domainConfig.readAppProperty( AppProperty.SECURITY_INPUT_PASSWORD_TRIM ) );
-        final int maxLength = Integer.parseInt( domainConfig.readAppProperty( AppProperty.HTTP_PARAM_MAX_READ_LENGTH ) );
+        final boolean trim = Boolean.parseBoolean( appConfig.readAppProperty( AppProperty.SECURITY_INPUT_TRIM ) );
+        final boolean passwordTrim = Boolean.parseBoolean( appConfig.readAppProperty( AppProperty.SECURITY_INPUT_PASSWORD_TRIM ) );
+        final int maxLength = Integer.parseInt( appConfig.readAppProperty( AppProperty.HTTP_PARAM_MAX_READ_LENGTH ) );
 
         final Map<String, String> outputMap = new LinkedHashMap<>();
         if ( inputMap != null )
@@ -130,11 +130,11 @@ public class PwmHttpRequestWrapper
                     String value;
                     value = bypassInputValidation
                             ? entry.getValue()
-                            : Validator.sanitizeInputValue( domainConfig, entry.getValue(), maxLength );
+                            : Validator.sanitizeInputValue( appConfig, entry.getValue(), maxLength );
                     value = passwordType && passwordTrim ? value.trim() : value;
                     value = !passwordType && trim ? value.trim() : value;
 
-                    final String sanitizedName = Validator.sanitizeInputValue( domainConfig, key, maxLength );
+                    final String sanitizedName = Validator.sanitizeInputValue( appConfig, key, maxLength );
                     outputMap.put( sanitizedName, value );
                 }
             }
@@ -150,9 +150,9 @@ public class PwmHttpRequestWrapper
         final String bodyString = readRequestBodyAsString();
         final Map<String, Object> inputMap = JsonUtil.deserializeMap( bodyString );
 
-        final boolean trim = Boolean.parseBoolean( domainConfig.readAppProperty( AppProperty.SECURITY_INPUT_TRIM ) );
-        final boolean passwordTrim = Boolean.parseBoolean( domainConfig.readAppProperty( AppProperty.SECURITY_INPUT_PASSWORD_TRIM ) );
-        final int maxLength = Integer.parseInt( domainConfig.readAppProperty( AppProperty.HTTP_PARAM_MAX_READ_LENGTH ) );
+        final boolean trim = Boolean.parseBoolean( appConfig.readAppProperty( AppProperty.SECURITY_INPUT_TRIM ) );
+        final boolean passwordTrim = Boolean.parseBoolean( appConfig.readAppProperty( AppProperty.SECURITY_INPUT_PASSWORD_TRIM ) );
+        final int maxLength = Integer.parseInt( appConfig.readAppProperty( AppProperty.HTTP_PARAM_MAX_READ_LENGTH ) );
 
         final Map<String, Object> outputMap = new LinkedHashMap<>();
         if ( inputMap != null )
@@ -168,7 +168,7 @@ public class PwmHttpRequestWrapper
                     {
                         String stringValue = bypassInputValidation
                                 ? ( String ) entry.getValue()
-                                : Validator.sanitizeInputValue( domainConfig, ( String ) entry.getValue(), maxLength );
+                                : Validator.sanitizeInputValue( appConfig, ( String ) entry.getValue(), maxLength );
                         stringValue = passwordType && passwordTrim ? stringValue.trim() : stringValue;
                         stringValue = !passwordType && trim ? stringValue.trim() : stringValue;
                         value = stringValue;
@@ -178,7 +178,7 @@ public class PwmHttpRequestWrapper
                         value = entry.getValue();
                     }
 
-                    final String sanitizedName = Validator.sanitizeInputValue( domainConfig, key, maxLength );
+                    final String sanitizedName = Validator.sanitizeInputValue( appConfig, key, maxLength );
                     outputMap.put( sanitizedName, value );
                 }
             }
@@ -190,14 +190,14 @@ public class PwmHttpRequestWrapper
     public PasswordData readParameterAsPassword( final String name )
             throws PwmUnrecoverableException
     {
-        final int maxLength = Integer.parseInt( domainConfig.readAppProperty( AppProperty.HTTP_PARAM_MAX_READ_LENGTH ) );
-        final boolean trim = Boolean.parseBoolean( domainConfig.readAppProperty( AppProperty.SECURITY_INPUT_PASSWORD_TRIM ) );
+        final int maxLength = Integer.parseInt( appConfig.readAppProperty( AppProperty.HTTP_PARAM_MAX_READ_LENGTH ) );
+        final boolean trim = Boolean.parseBoolean( appConfig.readAppProperty( AppProperty.SECURITY_INPUT_PASSWORD_TRIM ) );
 
         final String rawValue = httpServletRequest.getParameter( name );
         if ( rawValue != null && !rawValue.isEmpty() )
         {
             final String decodedValue = decodeStringToDefaultCharSet( rawValue );
-            final String sanitizedValue = Validator.sanitizeInputValue( domainConfig, decodedValue, maxLength );
+            final String sanitizedValue = Validator.sanitizeInputValue( appConfig, decodedValue, maxLength );
             if ( sanitizedValue != null )
             {
                 final String trimmedVale = trim ? sanitizedValue.trim() : sanitizedValue;
@@ -222,7 +222,7 @@ public class PwmHttpRequestWrapper
     public String readParameterAsString( final String name, final String valueIfNotPresent )
             throws PwmUnrecoverableException
     {
-        final int maxLength = Integer.parseInt( domainConfig.readAppProperty( AppProperty.HTTP_PARAM_MAX_READ_LENGTH ) );
+        final int maxLength = Integer.parseInt( appConfig.readAppProperty( AppProperty.HTTP_PARAM_MAX_READ_LENGTH ) );
         final String returnValue = readParameterAsString( name, maxLength );
         return returnValue == null || returnValue.isEmpty() ? valueIfNotPresent : returnValue;
     }
@@ -236,7 +236,7 @@ public class PwmHttpRequestWrapper
     public String readParameterAsString( final String name, final Flag... flags )
             throws PwmUnrecoverableException
     {
-        final int maxLength = Integer.parseInt( domainConfig.readAppProperty( AppProperty.HTTP_PARAM_MAX_READ_LENGTH ) );
+        final int maxLength = Integer.parseInt( appConfig.readAppProperty( AppProperty.HTTP_PARAM_MAX_READ_LENGTH ) );
         return readParameterAsString( name, maxLength, flags );
     }
 
@@ -277,7 +277,7 @@ public class PwmHttpRequestWrapper
     {
         final boolean bypassInputValidation = flags != null && Arrays.asList( flags ).contains( Flag.BypassValidation );
         final HttpServletRequest req = this.getHttpServletRequest();
-        final boolean trim = Boolean.parseBoolean( domainConfig.readAppProperty( AppProperty.SECURITY_INPUT_TRIM ) );
+        final boolean trim = Boolean.parseBoolean( appConfig.readAppProperty( AppProperty.SECURITY_INPUT_TRIM ) );
         final String[] rawValues = req.getParameterValues( name );
         if ( rawValues == null || rawValues.length == 0 )
         {
@@ -290,7 +290,7 @@ public class PwmHttpRequestWrapper
             final String decodedValue = decodeStringToDefaultCharSet( rawValue );
             final String sanitizedValue = bypassInputValidation
                     ? decodedValue
-                    : Validator.sanitizeInputValue( domainConfig, decodedValue, maxLength );
+                    : Validator.sanitizeInputValue( appConfig, decodedValue, maxLength );
 
             if ( sanitizedValue.length() > 0 )
             {
@@ -308,16 +308,16 @@ public class PwmHttpRequestWrapper
 
     public String readHeaderValueAsString( final String headerName )
     {
-        final int maxChars = Integer.parseInt( domainConfig.readAppProperty( AppProperty.HTTP_PARAM_MAX_READ_LENGTH ) );
+        final int maxChars = Integer.parseInt( appConfig.readAppProperty( AppProperty.HTTP_PARAM_MAX_READ_LENGTH ) );
         final HttpServletRequest req = this.getHttpServletRequest();
         final String rawValue = req.getHeader( headerName );
-        final String sanitizedInputValue = Validator.sanitizeInputValue( domainConfig, rawValue, maxChars );
-        return Validator.sanitizeHeaderValue( domainConfig, sanitizedInputValue );
+        final String sanitizedInputValue = Validator.sanitizeInputValue( appConfig, rawValue, maxChars );
+        return Validator.sanitizeHeaderValue( appConfig, sanitizedInputValue );
     }
 
     public Map<String, List<String>> readHeaderValuesMap( )
     {
-        final int maxChars = Integer.parseInt( domainConfig.readAppProperty( AppProperty.HTTP_PARAM_MAX_READ_LENGTH ) );
+        final int maxChars = Integer.parseInt( appConfig.readAppProperty( AppProperty.HTTP_PARAM_MAX_READ_LENGTH ) );
         final HttpServletRequest req = this.getHttpServletRequest();
         final Map<String, List<String>> returnObj = new LinkedHashMap<>();
 
@@ -328,8 +328,8 @@ public class PwmHttpRequestWrapper
             for ( final Enumeration<String> headerValueEnum = req.getHeaders( headerName ); headerValueEnum.hasMoreElements(); )
             {
                 final String headerValue = headerValueEnum.nextElement();
-                final String sanitizedInputValue = Validator.sanitizeInputValue( domainConfig, headerValue, maxChars );
-                final String sanitizedHeaderValue = Validator.sanitizeHeaderValue( domainConfig, sanitizedInputValue );
+                final String sanitizedInputValue = Validator.sanitizeInputValue( appConfig, headerValue, maxChars );
+                final String sanitizedHeaderValue = Validator.sanitizeHeaderValue( appConfig, sanitizedInputValue );
                 if ( sanitizedHeaderValue != null && !sanitizedHeaderValue.isEmpty() )
                 {
                     valueList.add( sanitizedHeaderValue );
@@ -345,12 +345,12 @@ public class PwmHttpRequestWrapper
 
     public List<String> parameterNames( )
     {
-        final int maxChars = Integer.parseInt( domainConfig.readAppProperty( AppProperty.HTTP_PARAM_MAX_READ_LENGTH ) );
+        final int maxChars = Integer.parseInt( appConfig.readAppProperty( AppProperty.HTTP_PARAM_MAX_READ_LENGTH ) );
         final List<String> returnObj = new ArrayList();
         for ( final Enumeration nameEnum = getHttpServletRequest().getParameterNames(); nameEnum.hasMoreElements(); )
         {
             final String paramName = nameEnum.nextElement().toString();
-            final String returnName = Validator.sanitizeInputValue( domainConfig, paramName, maxChars );
+            final String returnName = Validator.sanitizeInputValue( appConfig, paramName, maxChars );
             returnObj.add( returnName );
         }
         return Collections.unmodifiableList( returnObj );
@@ -371,7 +371,7 @@ public class PwmHttpRequestWrapper
     public Map<String, List<String>> readMultiParametersAsMap( )
             throws PwmUnrecoverableException
     {
-        final int maxLength = Integer.parseInt( domainConfig.readAppProperty( AppProperty.HTTP_PARAM_MAX_READ_LENGTH ) );
+        final int maxLength = Integer.parseInt( appConfig.readAppProperty( AppProperty.HTTP_PARAM_MAX_READ_LENGTH ) );
         final Map<String, List<String>> returnObj = new HashMap<>();
         for ( final String paramName : parameterNames() )
         {
@@ -383,7 +383,7 @@ public class PwmHttpRequestWrapper
 
     public String readCookie( final String cookieName )
     {
-        final int maxChars = Integer.parseInt( domainConfig.readAppProperty( AppProperty.HTTP_COOKIE_MAX_READ_LENGTH ) );
+        final int maxChars = Integer.parseInt( appConfig.readAppProperty( AppProperty.HTTP_COOKIE_MAX_READ_LENGTH ) );
         final Cookie[] cookies = this.getHttpServletRequest().getCookies();
         if ( cookies != null )
         {
@@ -393,7 +393,7 @@ public class PwmHttpRequestWrapper
                 {
                     final String rawCookieValue = cookie.getValue();
                     final String decodedCookieValue = StringUtil.urlDecode( rawCookieValue );
-                    return Validator.sanitizeInputValue( domainConfig, decodedCookieValue, maxChars );
+                    return Validator.sanitizeInputValue( appConfig, decodedCookieValue, maxChars );
                 }
             }
         }
@@ -412,9 +412,9 @@ public class PwmHttpRequestWrapper
                 .orElseThrow( () -> new IllegalStateException( "http method not registered" ) );
     }
 
-    public DomainConfig getDomainConfig( )
+    public AppConfig getAppConfig( )
     {
-        return domainConfig;
+        return appConfig;
     }
 
     public String getURLwithoutQueryString( )

+ 11 - 11
server/src/main/java/password/pwm/http/PwmHttpResponseWrapper.java

@@ -21,9 +21,9 @@
 package password.pwm.http;
 
 import password.pwm.AppProperty;
-import password.pwm.PwmDomain;
 import password.pwm.PwmConstants;
-import password.pwm.config.DomainConfig;
+import password.pwm.PwmDomain;
+import password.pwm.config.AppConfig;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.http.filter.CookieManagementFilter;
 import password.pwm.util.Validator;
@@ -44,7 +44,7 @@ public class PwmHttpResponseWrapper
 
     private final HttpServletRequest httpServletRequest;
     private final HttpServletResponse httpServletResponse;
-    private final DomainConfig domainConfig;
+    private final AppConfig appConfig;
 
     public enum CookiePath
     {
@@ -85,12 +85,12 @@ public class PwmHttpResponseWrapper
     protected PwmHttpResponseWrapper(
             final HttpServletRequest request,
             final HttpServletResponse response,
-            final DomainConfig domainConfig
+            final AppConfig appConfig
     )
     {
         this.httpServletRequest = request;
         this.httpServletResponse = response;
-        this.domainConfig = domainConfig;
+        this.appConfig = appConfig;
     }
 
     public HttpServletResponse getHttpServletResponse( )
@@ -101,7 +101,7 @@ public class PwmHttpResponseWrapper
     public void sendRedirect( final String url )
             throws IOException
     {
-        this.httpServletResponse.sendRedirect( Validator.sanitizeHeaderValue( domainConfig, url ) );
+        this.httpServletResponse.sendRedirect( Validator.sanitizeHeaderValue( appConfig, url ) );
     }
 
     public boolean isCommitted( )
@@ -112,8 +112,8 @@ public class PwmHttpResponseWrapper
     public void setHeader( final HttpHeader headerName, final String value )
     {
         this.httpServletResponse.setHeader(
-                Validator.sanitizeHeaderValue( domainConfig, headerName.getHttpName() ),
-                Validator.sanitizeHeaderValue( domainConfig, value )
+                Validator.sanitizeHeaderValue( appConfig, headerName.getHttpName() ),
+                Validator.sanitizeHeaderValue( appConfig, value )
         );
     }
 
@@ -164,7 +164,7 @@ public class PwmHttpResponseWrapper
 
         final boolean secureFlag;
         {
-            final String configValue = domainConfig.readAppProperty( AppProperty.HTTP_COOKIE_DEFAULT_SECURE_FLAG );
+            final String configValue = appConfig.readAppProperty( AppProperty.HTTP_COOKIE_DEFAULT_SECURE_FLAG );
             if ( configValue == null || "auto".equalsIgnoreCase( configValue ) )
             {
                 secureFlag = this.httpServletRequest.isSecure();
@@ -175,7 +175,7 @@ public class PwmHttpResponseWrapper
             }
         }
 
-        final boolean httpOnlyEnabled = Boolean.parseBoolean( domainConfig.readAppProperty( AppProperty.HTTP_COOKIE_HTTPONLY_ENABLE ) );
+        final boolean httpOnlyEnabled = Boolean.parseBoolean( appConfig.readAppProperty( AppProperty.HTTP_COOKIE_HTTPONLY_ENABLE ) );
         final boolean httpOnly = httpOnlyEnabled && !JavaHelper.enumArrayContainsValue( flags, Flag.NonHttpOnly );
 
         final String value;
@@ -193,7 +193,7 @@ public class PwmHttpResponseWrapper
                 else
                 {
                     value = StringUtil.urlEncode(
-                            Validator.sanitizeHeaderValue( domainConfig, cookieValue )
+                            Validator.sanitizeHeaderValue( appConfig, cookieValue )
                     );
                 }
             }

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

@@ -27,15 +27,15 @@ import org.apache.commons.fileupload.servlet.ServletFileUpload;
 import org.apache.commons.io.IOUtils;
 import password.pwm.AppProperty;
 import password.pwm.PwmApplication;
-import password.pwm.PwmDomain;
 import password.pwm.PwmApplicationMode;
 import password.pwm.PwmConstants;
+import password.pwm.PwmDomain;
 import password.pwm.bean.DomainID;
 import password.pwm.bean.LocalSessionStateBean;
 import password.pwm.bean.LoginInfoBean;
 import password.pwm.bean.SessionLabel;
 import password.pwm.bean.UserIdentity;
-import password.pwm.config.AppConfig;
+import password.pwm.config.DomainConfig;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.value.data.FormConfiguration;
 import password.pwm.error.ErrorInformation;
@@ -71,6 +71,7 @@ import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
+import java.util.Optional;
 import java.util.Set;
 import java.util.concurrent.locks.Lock;
 import java.util.concurrent.locks.ReentrantLock;
@@ -117,7 +118,7 @@ public class PwmRequest extends PwmHttpRequestWrapper
     )
             throws PwmUnrecoverableException
     {
-        super( httpServletRequest, pwmDomain.getConfig() );
+        super( httpServletRequest, pwmDomain.getConfig().getAppConfig() );
         this.pwmRequestID = PwmRequestID.next();
         this.pwmResponse = new PwmResponse( httpServletResponse, this, pwmDomain.getConfig() );
         this.pwmSession = pwmSession;
@@ -302,11 +303,6 @@ public class PwmRequest extends PwmHttpRequestWrapper
         return Collections.unmodifiableMap( returnObj );
     }
 
-    public AppConfig getAppConfig()
-    {
-        return pwmApplication.getConfig();
-    }
-
     @Value
     public static class FileUploadItem
     {
@@ -474,7 +470,7 @@ public class PwmRequest extends PwmHttpRequestWrapper
     {
         final LocalSessionStateBean ssBean = this.getPwmSession().getSessionStateBean();
         final String redirectURL = ssBean.getForwardURL();
-        return !( ( redirectURL == null || redirectURL.isEmpty() ) && this.getDomainConfig().isDefaultValue( PwmSetting.URL_FORWARD ) );
+        return !StringUtil.isEmpty( redirectURL );
     }
 
     public String getForwardUrl( )
@@ -526,18 +522,19 @@ public class PwmRequest extends PwmHttpRequestWrapper
         }
     }
 
-    public <T extends Serializable> T readEncryptedCookie( final String cookieName, final Class<T> returnClass )
+    public <T extends Serializable> Optional<T> readEncryptedCookie( final String cookieName, final Class<T> returnClass )
             throws PwmUnrecoverableException
     {
         final String strValue = this.readCookie( cookieName );
 
-        if ( strValue != null && !strValue.isEmpty() )
+        if ( StringUtil.isEmpty( strValue ) )
         {
-            final PwmSecurityKey pwmSecurityKey = pwmSession.getSecurityKey( this );
-            return getPwmDomain().getSecureService().decryptObject( strValue, pwmSecurityKey, returnClass );
+            return Optional.empty();
         }
 
-        return null;
+        final PwmSecurityKey pwmSecurityKey = pwmSession.getSecurityKey( this );
+        final T t = getPwmDomain().getSecureService().decryptObject( strValue, pwmSecurityKey, returnClass );
+        return Optional.of( t );
     }
 
     @Override
@@ -626,6 +623,11 @@ public class PwmRequest extends PwmHttpRequestWrapper
         return PwmConstants.DOMAIN_ID_PLACEHOLDER;
     }
 
+    public DomainConfig getDomainConfig()
+    {
+        return getPwmDomain().getConfig();
+    }
+
     public PwmApplication getPwmApplication()
     {
         return pwmApplication;

+ 3 - 0
server/src/main/java/password/pwm/http/PwmRequestAttribute.java

@@ -66,6 +66,9 @@ public enum PwmRequestAttribute
     ConfigEnablePersistentLogin,
     ApplicationPath,
 
+    DomainId,
+    DomainList,
+
     ConfigHasCertificates,
 
     CaptchaClientUrl,

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

@@ -84,7 +84,7 @@ public class PwmResponse extends PwmHttpResponseWrapper
             final DomainConfig domainConfig
     )
     {
-        super( pwmRequest.getHttpServletRequest(), response, domainConfig );
+        super( pwmRequest.getHttpServletRequest(), response, pwmRequest.getAppConfig() );
         this.pwmRequest = pwmRequest;
     }
 

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

@@ -34,6 +34,7 @@ import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
+import java.util.Optional;
 import java.util.regex.Pattern;
 
 public class PwmURL
@@ -227,16 +228,16 @@ public class PwmURL
         return isPwmServletURL( PwmServletDefinition.UpdateProfile );
     }
 
-    public PwmServletDefinition forServletDefinition()
+    public Optional<PwmServletDefinition> forServletDefinition()
     {
         for ( final PwmServletDefinition pwmServletDefinition : PwmServletDefinition.values() )
         {
             if ( isPwmServletURL( pwmServletDefinition ) )
             {
-                return pwmServletDefinition;
+                return Optional.of( pwmServletDefinition );
             }
         }
-        return null;
+        return Optional.empty();
     }
 
     public boolean isLocalizable( )

+ 16 - 13
server/src/main/java/password/pwm/http/filter/AuthenticationFilter.java

@@ -20,9 +20,9 @@
 
 package password.pwm.http.filter;
 
-import password.pwm.PwmDomain;
 import password.pwm.PwmApplicationMode;
 import password.pwm.PwmConstants;
+import password.pwm.PwmDomain;
 import password.pwm.bean.LoginInfoBean;
 import password.pwm.config.PwmSetting;
 import password.pwm.error.ErrorInformation;
@@ -50,6 +50,7 @@ import password.pwm.util.logging.PwmLogger;
 import javax.servlet.ServletException;
 import javax.servlet.http.HttpServletRequest;
 import java.io.IOException;
+import java.util.Optional;
 
 /**
  * Authentication servlet filter.  This filter wraps all servlet requests and requests direct to *.jsp
@@ -189,20 +190,22 @@ public class AuthenticationFilter extends AbstractPwmFilter
 
         if ( pwmSession.getSessionManager().isAuthenticatedWithoutPasswordAndBind() )
         {
-            final PwmServletDefinition pwmServletDefinition = pwmRequest.getURL().forServletDefinition();
-            if ( pwmServletDefinition != null
-                    && pwmServletDefinition.getFlags().contains( PwmServletDefinition.Flag.RequiresUserPasswordAndBind ) )
+            final Optional<PwmServletDefinition> pwmServletDefinition = pwmRequest.getURL().forServletDefinition();
+            if ( pwmServletDefinition.isPresent() )
             {
-                try
-                {
-                    LOGGER.debug( pwmRequest, () -> "user is authenticated without a password, but module " + pwmServletDefinition.name()
-                            +  " requires user connection, redirecting to login page" );
-                    LoginServlet.redirectToLoginServlet( pwmRequest );
-                    return;
-                }
-                catch ( final Throwable e1 )
+                if ( pwmServletDefinition.get().getFlags().contains( PwmServletDefinition.Flag.RequiresUserPasswordAndBind ) )
                 {
-                    LOGGER.error( () -> "error while marking pre-login url:" + e1.getMessage() );
+                    try
+                    {
+                        LOGGER.debug( pwmRequest, () -> "user is authenticated without a password, but module " + pwmServletDefinition.get().name()
+                                + " requires user connection, redirecting to login page" );
+                        LoginServlet.redirectToLoginServlet( pwmRequest );
+                        return;
+                    }
+                    catch ( final Throwable e1 )
+                    {
+                        LOGGER.error( () -> "error while marking pre-login url:" + e1.getMessage() );
+                    }
                 }
             }
         }

+ 17 - 17
server/src/main/java/password/pwm/http/filter/RequestInitializationFilter.java

@@ -23,10 +23,10 @@ package password.pwm.http.filter;
 import org.apache.commons.validator.routines.InetAddressValidator;
 import password.pwm.AppProperty;
 import password.pwm.PwmApplication;
-import password.pwm.PwmDomain;
 import password.pwm.PwmApplicationMode;
 import password.pwm.PwmConstants;
 import password.pwm.bean.LocalSessionStateBean;
+import password.pwm.config.AppConfig;
 import password.pwm.config.DomainConfig;
 import password.pwm.config.PwmSetting;
 import password.pwm.error.ErrorInformation;
@@ -319,9 +319,9 @@ public class RequestInitializationFilter implements Filter
         {
             return;
         }
-        final PwmDomain pwmDomain = pwmRequest.getPwmDomain();
+        final PwmApplication pwmApplication = pwmRequest.getPwmApplication();
         final PwmSession pwmSession = pwmRequest.getPwmSession();
-        final DomainConfig config = pwmDomain.getConfig();
+        final AppConfig config = pwmApplication.getConfig();
         final PwmResponse resp = pwmRequest.getPwmResponse();
 
         if ( resp.isCommitted() )
@@ -341,7 +341,7 @@ public class RequestInitializationFilter implements Filter
             resp.setHeader( HttpHeader.ContentLanguage, pwmRequest.getLocale().toLanguageTag() );
         }
 
-        addStaticResponseHeaders( pwmDomain, resp.getHttpServletResponse() );
+        addStaticResponseHeaders( pwmApplication, resp.getHttpServletResponse() );
 
 
         if ( pwmSession != null )
@@ -367,12 +367,12 @@ public class RequestInitializationFilter implements Filter
     }
 
     public static void addStaticResponseHeaders(
-            final PwmDomain pwmDomain,
+            final PwmApplication pwmApplication,
             final HttpServletResponse resp
     )
             throws PwmUnrecoverableException
     {
-        final DomainConfig config = pwmDomain.getConfig();
+        final AppConfig config = pwmApplication.getConfig();
 
         final String serverHeader = config.readAppProperty( AppProperty.HTTP_HEADER_SERVER );
         final boolean includeXInstance = Boolean.parseBoolean( config.readAppProperty( AppProperty.HTTP_HEADER_SEND_XINSTANCE ) );
@@ -383,7 +383,7 @@ public class RequestInitializationFilter implements Filter
         final boolean includeXAmb = Boolean.parseBoolean( config.readAppProperty( AppProperty.HTTP_HEADER_SEND_XAMB ) );
 
         {
-            final String noiseHeader = makeNoiseHeader( pwmDomain, config );
+            final String noiseHeader = makeNoiseHeader( pwmApplication, config );
             if ( noiseHeader != null )
             {
                 resp.setHeader( HttpHeader.XNoise.getHttpName(), noiseHeader );
@@ -407,12 +407,12 @@ public class RequestInitializationFilter implements Filter
 
         if ( includeXInstance )
         {
-            resp.setHeader( HttpHeader.XInstance.getHttpName(), String.valueOf( pwmDomain.getInstanceID() ) );
+            resp.setHeader( HttpHeader.XInstance.getHttpName(), String.valueOf( pwmApplication.getInstanceID() ) );
         }
 
         if ( serverHeader != null && !serverHeader.isEmpty() )
         {
-            final String value = MacroRequest.forNonUserSpecific( pwmDomain, null ).expandMacros( serverHeader );
+            final String value = MacroRequest.forNonUserSpecific( pwmApplication.getDefaultDomain(), null ).expandMacros( serverHeader );
             resp.setHeader( HttpHeader.Server.getHttpName(), value );
         }
 
@@ -424,7 +424,7 @@ public class RequestInitializationFilter implements Filter
         if ( includeXAmb )
         {
             resp.setHeader( HttpHeader.XAmb.getHttpName(), PwmConstants.X_AMB_HEADER.get(
-                    pwmDomain.getSecureService().pwmRandom().nextInt( PwmConstants.X_AMB_HEADER.size() )
+                    pwmApplication.getSecureService().pwmRandom().nextInt( PwmConstants.X_AMB_HEADER.size() )
             ) );
         }
 
@@ -434,7 +434,7 @@ public class RequestInitializationFilter implements Filter
 
     public static String readUserHostname( final HttpServletRequest request, final DomainConfig config ) throws PwmUnrecoverableException
     {
-        if ( config != null && !config.readSettingAsBoolean( PwmSetting.REVERSE_DNS_ENABLE ) )
+        if ( config != null && !config.getAppConfig().readSettingAsBoolean( PwmSetting.REVERSE_DNS_ENABLE ) )
         {
             return "";
         }
@@ -622,7 +622,7 @@ public class RequestInitializationFilter implements Filter
 
         if ( ssBean.getSessionCreationTime() != null )
         {
-            final long maxSessionSeconds = pwmRequest.getDomainConfig().readSettingAsLong( PwmSetting.SESSION_MAX_SECONDS );
+            final long maxSessionSeconds = pwmRequest.getAppConfig().readSettingAsLong( PwmSetting.SESSION_MAX_SECONDS );
             final TimeDuration sessionAge = TimeDuration.fromCurrent( ssBean.getSessionCreationTime() );
             final int sessionSecondAge = (int) sessionAge.as( TimeDuration.Unit.SECONDS );
             if ( sessionSecondAge > maxSessionSeconds )
@@ -675,7 +675,7 @@ public class RequestInitializationFilter implements Filter
     private static void checkSourceNetworkAddress( final PwmRequest pwmRequest )
             throws PwmUnrecoverableException
     {
-        final List<String> requiredHeaders = pwmRequest.getDomainConfig().readSettingAsStringArray( PwmSetting.IP_PERMITTED_RANGE );
+        final List<String> requiredHeaders = pwmRequest.getAppConfig().readSettingAsStringArray( PwmSetting.IP_PERMITTED_RANGE );
         if ( requiredHeaders != null && !requiredHeaders.isEmpty() )
         {
             boolean match = false;
@@ -831,14 +831,14 @@ public class RequestInitializationFilter implements Filter
         return StringUtil.mapToString( values );
     }
 
-    private static String makeNoiseHeader( final PwmDomain pwmDomain, final DomainConfig domainConfig )
+    private static String makeNoiseHeader( final PwmApplication pwmApplication, final AppConfig appConfig )
     {
-        final boolean sendNoise = Boolean.parseBoolean( domainConfig.readAppProperty( AppProperty.HTTP_HEADER_SEND_XNOISE ) );
+        final boolean sendNoise = Boolean.parseBoolean( appConfig.readAppProperty( AppProperty.HTTP_HEADER_SEND_XNOISE ) );
 
         if ( sendNoise )
         {
-            final int noiseLength = Integer.parseInt( domainConfig.readAppProperty( AppProperty.HTTP_HEADER_NOISE_LENGTH ) );
-            final PwmRandom pwmRandom = pwmDomain.getSecureService().pwmRandom();
+            final int noiseLength = Integer.parseInt( appConfig.readAppProperty( AppProperty.HTTP_HEADER_NOISE_LENGTH ) );
+            final PwmRandom pwmRandom = pwmApplication.getSecureService().pwmRandom();
             return pwmRandom.alphaNumericString( pwmRandom.nextInt( noiseLength ) + 11 );
         }
 

+ 6 - 2
server/src/main/java/password/pwm/http/filter/SessionFilter.java

@@ -21,12 +21,14 @@
 package password.pwm.http.filter;
 
 import password.pwm.AppProperty;
+import password.pwm.PwmApplication;
 import password.pwm.PwmDomain;
 import password.pwm.PwmApplicationMode;
 import password.pwm.PwmConstants;
 import password.pwm.bean.LocalSessionStateBean;
 import password.pwm.bean.LoginInfoBean;
 import password.pwm.bean.SessionLabel;
+import password.pwm.config.AppConfig;
 import password.pwm.config.DomainConfig;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.option.SessionVerificationMode;
@@ -147,8 +149,10 @@ public class SessionFilter extends AbstractPwmFilter
     )
             throws PwmUnrecoverableException, IOException, ServletException
     {
+        final PwmApplication pwmApplication = pwmRequest.getPwmApplication();
+
         final PwmDomain pwmDomain = pwmRequest.getPwmDomain();
-        final DomainConfig config = pwmRequest.getDomainConfig();
+        final AppConfig config = pwmApplication.getConfig();
 
         final PwmSession pwmSession = pwmRequest.getPwmSession();
         final LocalSessionStateBean ssBean = pwmSession.getSessionStateBean();
@@ -416,7 +420,7 @@ public class SessionFilter extends AbstractPwmFilter
     }
 
 
-    private static boolean checkPageLeaveNotice( final PwmSession pwmSession, final DomainConfig config )
+    private static boolean checkPageLeaveNotice( final PwmSession pwmSession, final AppConfig config )
     {
         final long configuredSeconds = config.readSettingAsLong( PwmSetting.SECURITY_PAGE_LEAVE_NOTICE_TIMEOUT );
         if ( configuredSeconds <= 0 )

+ 2 - 3
server/src/main/java/password/pwm/http/servlet/ClientApiServlet.java

@@ -318,7 +318,7 @@ public class ClientApiServlet extends ControlledPwmServlet
         settingMap.put( "client.locale", LocaleHelper.getBrowserLocaleString( pwmSession.getSessionStateBean().getLocale() ) );
         settingMap.put( "client.pwShowRevertTimeout", Integer.parseInt( config.readAppProperty( AppProperty.CLIENT_PW_SHOW_REVERT_TIMEOUT ) ) );
         settingMap.put( "enableIdleTimeout", config.readSettingAsBoolean( PwmSetting.DISPLAY_IDLE_TIMEOUT ) );
-        settingMap.put( "pageLeaveNotice", config.readSettingAsLong( PwmSetting.SECURITY_PAGE_LEAVE_NOTICE_TIMEOUT ) );
+        settingMap.put( "pageLeaveNotice", config.getAppConfig().readSettingAsLong( PwmSetting.SECURITY_PAGE_LEAVE_NOTICE_TIMEOUT ) );
         settingMap.put( "setting-showHidePasswordFields", pwmDomain.getConfig().readSettingAsBoolean( password.pwm.config.PwmSetting.DISPLAY_SHOW_HIDE_PASSWORD_FIELDS ) );
         settingMap.put( "setting-displayEula", PwmConstants.ENABLE_EULA_DISPLAY );
         settingMap.put( "setting-showStrengthMeter", config.readSettingAsBoolean( PwmSetting.PASSWORD_SHOW_STRENGTH_METER ) );
@@ -442,8 +442,7 @@ public class ClientApiServlet extends ControlledPwmServlet
     )
     {
         final PwmSession pwmSession = pwmRequest.getPwmSession();
-        Class displayClass = LocaleHelper.classForShortName( bundleName );
-        displayClass = displayClass == null ? Display.class : displayClass;
+        final Class displayClass = LocaleHelper.classForShortName( bundleName ).orElse( Display.class );
 
         final Locale userLocale = pwmSession.getSessionStateBean().getLocale();
         final DomainConfig config = pwmDomain.getConfig();

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

@@ -329,7 +329,7 @@ public class ForgottenUsernameServlet extends AbstractPwmServlet
 
         final MacroRequest macroRequest = MacroRequest.forUser( pwmDomain, sessionLabel, userInfo, null );
 
-        pwmDomain.sendSmsUsingQueue( toNumber, smsMessage, sessionLabel, macroRequest );
+        pwmDomain.getPwmApplication().sendSmsUsingQueue( toNumber, smsMessage, sessionLabel, macroRequest );
         return null;
     }
 

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

@@ -465,7 +465,7 @@ public class GuestRegistrationServlet extends AbstractPwmServlet
             LOGGER.info( pwmRequest, () -> "created user object: " + guestUserDN );
 
             final ChaiUser theUser = provider.getEntryFactory().newChaiUser( guestUserDN );
-            final UserIdentity userIdentity = UserIdentity.createUserIdentity(
+            final UserIdentity userIdentity = UserIdentity.create(
                     guestUserDN,
                     pwmSession.getUserInfo().getUserIdentity().getLdapProfileID(),
                     pwmRequest.getDomainID() );

+ 13 - 0
server/src/main/java/password/pwm/http/servlet/LoginServlet.java

@@ -32,6 +32,7 @@ import password.pwm.http.HttpMethod;
 import password.pwm.http.JspUrl;
 import password.pwm.http.ProcessStatus;
 import password.pwm.http.PwmRequest;
+import password.pwm.http.PwmRequestAttribute;
 import password.pwm.http.PwmURL;
 import password.pwm.http.bean.LoginServletBean;
 import password.pwm.ldap.auth.AuthenticationType;
@@ -47,6 +48,7 @@ import javax.servlet.ServletException;
 import javax.servlet.annotation.WebServlet;
 import java.io.IOException;
 import java.net.URI;
+import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
@@ -267,6 +269,17 @@ public class LoginServlet extends ControlledPwmServlet
     )
             throws IOException, ServletException, PwmUnrecoverableException
     {
+        final ArrayList<String> domainIds = new ArrayList<>( pwmRequest.getAppConfig().getDomainIDs() );
+        if ( domainIds.size() > 1 )
+        {
+            pwmRequest.setAttribute( PwmRequestAttribute.DomainList, domainIds );
+        }
+        else
+        {
+            pwmRequest.setAttribute( PwmRequestAttribute.DomainList, new ArrayList<>() );
+        }
+
+
         final JspUrl url = passwordOnly ? JspUrl.LOGIN_PW_ONLY : JspUrl.LOGIN;
         pwmRequest.forwardToJsp( url );
     }

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

@@ -289,7 +289,7 @@ public class SetupOtpServlet extends ControlledPwmServlet
 
         final String bodyString = pwmRequest.readRequestBodyAsString();
         final Map<String, String> clientValues = JsonUtil.deserializeStringMap( bodyString );
-        final String code = Validator.sanitizeInputValue( pwmDomain.getConfig(), clientValues.get( "code" ), 1024 );
+        final String code = Validator.sanitizeInputValue( pwmRequest.getAppConfig(), clientValues.get( "code" ), 1024 );
 
         try
         {

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

@@ -284,7 +284,7 @@ class ActivateUserUtils
             return false;
         }
 
-        pwmDomain.sendSmsUsingQueue(
+        pwmRequest.getPwmApplication().sendSmsUsingQueue(
                 toSmsNumber,
                 message,
                 pwmRequest.getLabel(),

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

@@ -770,7 +770,7 @@ public class AdminServlet extends ControlledPwmServlet
     @ActionHandler( action = "readLogData" )
     public ProcessStatus readLogData( final PwmRequest pwmRequest ) throws IOException, PwmUnrecoverableException
     {
-        final LocalDBLogger localDBLogger = pwmRequest.getPwmDomain().getLocalDBLogger();
+        final LocalDBLogger localDBLogger = pwmRequest.getPwmApplication().getLocalDBLogger();
 
         final LogDisplayType logDisplayType;
         final LocalDBSearchQuery searchParameters;
@@ -839,7 +839,7 @@ public class AdminServlet extends ControlledPwmServlet
     @ActionHandler( action = "downloadLogData" )
     public ProcessStatus downloadLogData( final PwmRequest pwmRequest ) throws IOException, PwmUnrecoverableException
     {
-        final LocalDBLogger localDBLogger = pwmRequest.getPwmDomain().getLocalDBLogger();
+        final LocalDBLogger localDBLogger = pwmRequest.getPwmApplication().getLocalDBLogger();
 
         final LogDownloadType logDownloadType = JavaHelper.readEnumFromString( LogDownloadType.class, LogDownloadType.plain, pwmRequest.readParameterAsString( "downloadType" ) );
 

+ 25 - 25
server/src/main/java/password/pwm/http/servlet/admin/AppDashboardData.java

@@ -35,6 +35,7 @@ import password.pwm.i18n.Admin;
 import password.pwm.i18n.Display;
 import password.pwm.svc.PwmService;
 import password.pwm.svc.node.NodeInfo;
+import password.pwm.svc.node.NodeService;
 import password.pwm.svc.sessiontrack.SessionTrackService;
 import password.pwm.util.i18n.LocaleHelper;
 import password.pwm.util.java.FileSystemUtility;
@@ -44,6 +45,7 @@ import password.pwm.util.java.StringUtil;
 import password.pwm.util.java.TimeDuration;
 import password.pwm.util.localdb.LocalDB;
 import password.pwm.util.localdb.LocalDBException;
+import password.pwm.util.logging.LocalDBLogger;
 import password.pwm.util.logging.PwmLogger;
 
 import java.io.Serializable;
@@ -112,7 +114,7 @@ public class AppDashboardData implements Serializable
     private List<NodeData> nodeData;
     private String nodeSummary;
     private DataStorageMethod nodeStorageMethod;
-    private int ldapConnectionCount;
+    private long ldapConnectionCount;
     private int sessionCount;
     private int requestsInProgress;
 
@@ -152,18 +154,18 @@ public class AppDashboardData implements Serializable
         }
 
         builder.nodeData = makeNodeData( pwmDomain, locale );
-        builder.nodeSummary = pwmDomain.getClusterService().isMaster()
+        builder.nodeSummary = pwmDomain.getPwmApplication().getNodeService().isMaster()
                 ? "This node is the current master"
                 : "This node is not the current master";
         {
-            final Collection<DataStorageMethod> dataStorageMethods = pwmDomain.getClusterService().serviceInfo().getStorageMethods();
+            final Collection<DataStorageMethod> dataStorageMethods = pwmDomain.getPwmApplication().getNodeService().serviceInfo().getStorageMethods();
             if ( !JavaHelper.isEmpty( dataStorageMethods ) )
             {
                 builder.nodeStorageMethod = dataStorageMethods.iterator().next();
             }
         }
 
-        builder.ldapConnectionCount( ldapConnectionCount( pwmDomain ) );
+        builder.ldapConnectionCount( SessionTrackService.totalLdapConnectionCount( pwmDomain.getPwmApplication() ) );
         builder.sessionCount( pwmDomain.getSessionTrackService().sessionCount() );
         builder.requestsInProgress( pwmDomain.getPwmApplication().getActiveServletRequests().get() );
 
@@ -171,11 +173,6 @@ public class AppDashboardData implements Serializable
         return builder.build();
     }
 
-    private static int ldapConnectionCount( final PwmDomain pwmDomain )
-    {
-        return pwmDomain.getLdapConnectionService().connectionCount();
-    }
-
     private static List<DisplayElement> makeAboutData(
             final PwmDomain pwmDomain,
             final ContextManager contextManager,
@@ -225,7 +222,7 @@ public class AppDashboardData implements Serializable
                 "instanceID",
                 DisplayElement.Type.string,
                 l.forKey( "Field_InstanceID" ),
-                pwmDomain.getInstanceID()
+                pwmDomain.getPwmApplication().getInstanceID()
         ), new DisplayElement(
                 "configRestartCounter",
                 DisplayElement.Type.number,
@@ -280,14 +277,14 @@ public class AppDashboardData implements Serializable
                 "worlistSize",
                 DisplayElement.Type.number,
                 "Word List Dictionary Size",
-                numberFormat.format( pwmDomain.getWordlistService().size() )
+                numberFormat.format( pwmDomain.getPwmApplication().getWordlistService().size() )
         ) );
 
         localDbInfo.add( new DisplayElement(
                 "seedlistSize",
                 DisplayElement.Type.number,
                 "Seed List Dictionary Size",
-                numberFormat.format( pwmDomain.getSeedlistManager().size() )
+                numberFormat.format( pwmDomain.getPwmApplication().getSeedlistManager().size() )
         ) );
 
         localDbInfo.add( new DisplayElement(
@@ -318,7 +315,7 @@ public class AppDashboardData implements Serializable
                 "smsQueueSize",
                 DisplayElement.Type.number,
                 "SMS Queue Size",
-                numberFormat.format( pwmDomain.getSmsQueue().queueSize() )
+                numberFormat.format( pwmDomain.getPwmApplication().getSmsQueue().queueSize() )
         ) );
         localDbInfo.add( new DisplayElement(
                 "sharedHistorySize",
@@ -348,11 +345,13 @@ public class AppDashboardData implements Serializable
                 "logEvents",
                 DisplayElement.Type.number,
                 "Log Events",
-                pwmDomain.getLocalDBLogger().sizeToDebugString()
+                pwmDomain.getPwmApplication().getLocalDBLogger().sizeToDebugString()
         ) );
         {
-            final String display = pwmDomain.getLocalDBLogger() != null && pwmDomain.getLocalDBLogger().getTailDate() != null
-                    ? TimeDuration.fromCurrent( pwmDomain.getLocalDBLogger().getTailDate() ).asLongString()
+            final LocalDBLogger localDBLogger = pwmDomain.getPwmApplication().getLocalDBLogger();
+            final String display = localDBLogger != null
+                    && localDBLogger.getTailDate() != null
+                    ? TimeDuration.fromCurrent( localDBLogger.getTailDate() ).asLongString()
                     : notApplicable;
             localDbInfo.add( new DisplayElement(
                     "oldestLogEvents",
@@ -362,12 +361,12 @@ public class AppDashboardData implements Serializable
             ) );
         }
         {
-            final String display = pwmDomain.getLocalDB() == null
+            final String display = pwmDomain.getPwmApplication().getLocalDB() == null
                     ? notApplicable
-                    : pwmDomain.getLocalDB().getFileLocation() == null
+                    : pwmDomain.getPwmApplication().getLocalDB().getFileLocation() == null
                     ? notApplicable
                     : StringUtil.formatDiskSize( FileSystemUtility.getFileDirectorySize(
-                    pwmDomain.getLocalDB().getFileLocation() ) );
+                    pwmDomain.getPwmApplication().getLocalDB().getFileLocation() ) );
             localDbInfo.add( new DisplayElement(
                     "localDbSizeOnDisk",
                     DisplayElement.Type.string,
@@ -376,11 +375,11 @@ public class AppDashboardData implements Serializable
             ) );
         }
         {
-            final String display = pwmDomain.getLocalDB() == null
+            final String display = pwmDomain.getPwmApplication().getLocalDB() == null
                     ? notApplicable
-                    : pwmDomain.getLocalDB().getFileLocation() == null
+                    : pwmDomain.getPwmApplication().getLocalDB().getFileLocation() == null
                     ? notApplicable
-                    : StringUtil.formatDiskSize( FileSystemUtility.diskSpaceRemaining( pwmDomain.getLocalDB().getFileLocation() ) );
+                    : StringUtil.formatDiskSize( FileSystemUtility.diskSpaceRemaining( pwmDomain.getPwmApplication().getLocalDB().getFileLocation() ) );
             localDbInfo.add( new DisplayElement(
                     "localDbFreeSpace",
                     DisplayElement.Type.string,
@@ -398,7 +397,7 @@ public class AppDashboardData implements Serializable
     )
     {
         final Map<LocalDB.DB, String> returnData = new LinkedHashMap<>();
-        final LocalDB localDB = pwmDomain.getLocalDB();
+        final LocalDB localDB = pwmDomain.getPwmApplication().getLocalDB();
         final PwmNumberFormat numberFormat = PwmNumberFormat.forLocale( locale );
         try
         {
@@ -515,7 +514,8 @@ public class AppDashboardData implements Serializable
             final Locale locale
     )
     {
-        if ( pwmDomain.getClusterService().status() != PwmService.STATUS.OPEN )
+        final NodeService nodeService = pwmDomain.getPwmApplication().getNodeService();
+        if ( nodeService.status() != PwmService.STATUS.OPEN )
         {
             return Collections.emptyList();
         }
@@ -525,7 +525,7 @@ public class AppDashboardData implements Serializable
 
         try
         {
-            for ( final NodeInfo nodeInfo : pwmDomain.getClusterService().nodes() )
+            for ( final NodeInfo nodeInfo : nodeService.nodes() )
             {
 
                 final String uptime = nodeInfo.getStartupTime() == null

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

@@ -24,6 +24,7 @@ import lombok.Builder;
 import lombok.Value;
 import password.pwm.AppProperty;
 import password.pwm.PwmConstants;
+import password.pwm.bean.DomainID;
 import password.pwm.bean.EmailItemBean;
 import password.pwm.bean.SmsItemBean;
 import password.pwm.bean.UserIdentity;
@@ -36,7 +37,8 @@ import password.pwm.config.SettingUIFunction;
 import password.pwm.config.profile.EmailServerProfile;
 import password.pwm.config.profile.PwmPasswordPolicy;
 import password.pwm.config.stored.ConfigurationProperty;
-import password.pwm.config.stored.StoredConfigItemKey;
+import password.pwm.config.stored.ConfigSearchMachine;
+import password.pwm.config.stored.StoredConfigKey;
 import password.pwm.config.stored.StoredConfiguration;
 import password.pwm.config.stored.StoredConfigurationModifier;
 import password.pwm.config.stored.StoredConfigurationUtil;
@@ -56,9 +58,11 @@ import password.pwm.http.JspUrl;
 import password.pwm.http.ProcessStatus;
 import password.pwm.http.PwmHttpRequestWrapper;
 import password.pwm.http.PwmRequest;
+import password.pwm.http.PwmRequestAttribute;
 import password.pwm.http.bean.ConfigManagerBean;
 import password.pwm.http.servlet.AbstractPwmServlet;
 import password.pwm.http.servlet.ControlledPwmServlet;
+import password.pwm.http.servlet.PwmServletDefinition;
 import password.pwm.http.servlet.configeditor.data.NavTreeDataMaker;
 import password.pwm.http.servlet.configeditor.data.NavTreeItem;
 import password.pwm.http.servlet.configeditor.data.NavTreeSettings;
@@ -187,10 +191,43 @@ public class ConfigEditorServlet extends ControlledPwmServlet
     @Override
     protected void nextStep( final PwmRequest pwmRequest ) throws PwmUnrecoverableException, IOException, ServletException
     {
+        final DomainStateReader domainStateReader = DomainStateReader.forRequest( pwmRequest );
+        final DomainManageMode mode = domainStateReader.getMode();
+
+        if ( !domainStateReader.isCorrectlyIndicated() )
+        {
+            if ( mode == DomainManageMode.single )
+            {
+                pwmRequest.sendRedirect( PwmServletDefinition.ConfigEditor );
+            }
+            else
+            {
+                pwmRequest.sendRedirect( pwmRequest.getContextPath() + PwmServletDefinition.ConfigEditor.servletUrl() + "/system" );
+            }
+            return;
+        }
+
+        {
+            final String value;
+            switch ( mode )
+            {
+                case system:
+                    value = DomainID.systemId().stringValue();
+                    break;
+                case domain:
+                    value = domainStateReader.getDomainID( PwmSetting.LDAP_PROXY_USER_DN ).stringValue();
+                    break;
+                default:
+                    value = "";
+                    break;
+            }
+            pwmRequest.setAttribute( PwmRequestAttribute.DomainId, value );
+        }
+
         pwmRequest.forwardToJsp( JspUrl.CONFIG_MANAGER_EDITOR );
     }
 
-    private ConfigManagerBean getBean( final PwmRequest pwmRequest ) throws PwmUnrecoverableException
+    static ConfigManagerBean getBean( final PwmRequest pwmRequest ) throws PwmUnrecoverableException
     {
         return pwmRequest.getPwmDomain().getSessionStateService().getBean( pwmRequest, ConfigManagerBean.class );
     }
@@ -208,14 +245,16 @@ public class ConfigEditorServlet extends ControlledPwmServlet
                 .orElseThrow( () -> new IllegalStateException( "invalid setting parameter value" ) );
         final String functionName = requestMap.get( "function" );
         final String profileID = pwmSetting.getCategory().hasProfiles() ? pwmRequest.readParameterAsString( "profile" ) : null;
+        final DomainID domainID = DomainStateReader.forRequest( pwmRequest ).getDomainID( pwmSetting );
         final String extraData = requestMap.get( "extraData" );
 
         try
         {
+            final StoredConfigKey key = StoredConfigKey.forSetting( pwmSetting, profileID, domainID );
             final Class implementingClass = Class.forName( functionName );
             final SettingUIFunction function = ( SettingUIFunction ) implementingClass.getDeclaredConstructor().newInstance();
             final StoredConfigurationModifier modifier = StoredConfigurationModifier.newModifier( configManagerBean.getStoredConfiguration() );
-            final Serializable result = function.provideFunction( pwmRequest, modifier, pwmSetting, profileID, extraData );
+            final Serializable result = function.provideFunction( pwmRequest, modifier, key, extraData );
             configManagerBean.setStoredConfiguration( modifier.newStoredConfiguration() );
             final RestResultBean restResultBean = RestResultBean.forSuccessMessage( result, pwmRequest, Message.Success_Unknown );
             pwmRequest.outputJsonResult( restResultBean );
@@ -238,6 +277,8 @@ public class ConfigEditorServlet extends ControlledPwmServlet
         return ProcessStatus.Halt;
     }
 
+
+
     @ActionHandler( action = "readSetting" )
     private ProcessStatus restReadSetting(
             final PwmRequest pwmRequest
@@ -284,15 +325,15 @@ public class ConfigEditorServlet extends ControlledPwmServlet
     {
         final ConfigManagerBean configManagerBean = getBean( pwmRequest );
         final StoredConfigurationModifier modifier = StoredConfigurationModifier.newModifier( configManagerBean.getStoredConfiguration() );
-        final String key = pwmRequest.readParameterAsString( "key" );
+        final String settingKey = pwmRequest.readParameterAsString( "key" );
         final String bodyString = pwmRequest.readRequestBodyAsString();
         final UserIdentity loggedInUser = pwmRequest.getUserInfoIfLoggedIn();
         final ReadSettingResponse readSettingResponse;
 
         final StoredConfiguration storedConfiguration;
-        if ( key.startsWith( "localeBundle" ) )
+        if ( settingKey.startsWith( "localeBundle" ) )
         {
-            final StringTokenizer st = new StringTokenizer( key, "-" );
+            final StringTokenizer st = new StringTokenizer( settingKey, "-" );
             st.nextToken();
             final PwmLocaleBundle pwmLocaleBundle = PwmLocaleBundle.forKey( st.nextToken() )
                     .orElseThrow( () -> new IllegalArgumentException( "unknown locale bundle name" ) );
@@ -302,24 +343,26 @@ public class ConfigEditorServlet extends ControlledPwmServlet
 
             modifier.writeLocaleBundleMap( pwmLocaleBundle, keyName, outputMap );
             storedConfiguration = modifier.newStoredConfiguration();
-            readSettingResponse = ConfigEditorServletUtils.handleLocaleBundleReadSetting( pwmRequest, storedConfiguration, key );
+            readSettingResponse = ConfigEditorServletUtils.handleLocaleBundleReadSetting( pwmRequest, storedConfiguration, settingKey );
         }
         else
         {
-            final PwmSetting setting = PwmSetting.forKey( key )
+            final PwmSetting setting = PwmSetting.forKey( settingKey )
                     .orElseThrow( () -> new IllegalStateException( "invalid setting parameter value" ) );
+            final DomainID domainID = DomainStateReader.forRequest( pwmRequest ).getDomainID( setting );
             final String profileID = setting.getCategory().hasProfiles() ? pwmRequest.readParameterAsString( "profile" ) : null;
+            final StoredConfigKey key = StoredConfigKey.forSetting( setting, profileID, domainID );
             try
             {
                 final StoredValue storedValue = ValueFactory.fromJson( setting, bodyString );
-                modifier.writeSetting( setting, profileID, storedValue, loggedInUser );
+                modifier.writeSetting( key, storedValue, loggedInUser );
                 storedConfiguration = modifier.newStoredConfiguration();
             }
             catch ( final PwmOperationalException e )
             {
                 throw new PwmUnrecoverableException( e.getErrorInformation() );
             }
-            readSettingResponse = ConfigEditorServletUtils.handleReadSetting( pwmRequest, storedConfiguration, key );
+            readSettingResponse = ConfigEditorServletUtils.handleReadSetting( pwmRequest, storedConfiguration, settingKey );
         }
         configManagerBean.setStoredConfiguration( storedConfiguration );
         pwmRequest.outputJsonResult( RestResultBean.withData( readSettingResponse ) );
@@ -335,11 +378,11 @@ public class ConfigEditorServlet extends ControlledPwmServlet
         final ConfigManagerBean configManagerBean = getBean( pwmRequest );
         final StoredConfigurationModifier modifier = StoredConfigurationModifier.newModifier( configManagerBean.getStoredConfiguration() );
         final UserIdentity loggedInUser = pwmRequest.getUserInfoIfLoggedIn();
-        final String key = pwmRequest.readParameterAsString( "key" );
+        final String settingKey = pwmRequest.readParameterAsString( "key" );
 
-        if ( key.startsWith( "localeBundle" ) )
+        if ( settingKey.startsWith( "localeBundle" ) )
         {
-            final StringTokenizer st = new StringTokenizer( key, "-" );
+            final StringTokenizer st = new StringTokenizer( settingKey, "-" );
             st.nextToken();
             final PwmLocaleBundle pwmLocaleBundle = PwmLocaleBundle.forKey( st.nextToken() )
                     .orElseThrow( () -> new IllegalArgumentException( "unknown locale bundle name" ) );
@@ -348,10 +391,12 @@ public class ConfigEditorServlet extends ControlledPwmServlet
         }
         else
         {
-            final PwmSetting setting = PwmSetting.forKey( key )
+            final PwmSetting setting = PwmSetting.forKey( settingKey )
                     .orElseThrow( () -> new IllegalStateException( "invalid setting parameter value" ) );
+            final DomainID domainID = DomainStateReader.forRequest( pwmRequest ).getDomainID( setting );
             final String profileID = setting.getCategory().hasProfiles() ? pwmRequest.readParameterAsString( "profile" ) : null;
-            modifier.resetSetting( setting, profileID, loggedInUser );
+            final StoredConfigKey key = StoredConfigKey.forSetting( setting, profileID, domainID );
+            modifier.resetSetting( key, loggedInUser );
         }
 
         configManagerBean.setStoredConfiguration( modifier.newStoredConfiguration() );
@@ -516,7 +561,6 @@ public class ConfigEditorServlet extends ControlledPwmServlet
     )
             throws IOException, PwmUnrecoverableException
     {
-        final Instant startTime = Instant.now();
         final ConfigManagerBean configManagerBean = getBean( pwmRequest );
         final String bodyData = pwmRequest.readRequestBodyAsString();
         final Map<String, String> valueMap = JsonUtil.deserializeStringMap( bodyData );
@@ -531,29 +575,26 @@ public class ConfigEditorServlet extends ControlledPwmServlet
             return ProcessStatus.Halt;
         }
 
-        final Set<StoredConfigItemKey> searchResults = StoredConfigurationUtil.search( storedConfiguration, searchTerm, locale );
-        final Map<String, Map<String, SearchResultItem>> returnData = new HashMap<>();
+        final Set<DomainID> searchDomains = DomainStateReader.forRequest( pwmRequest ).searchIDs();
+
+        final Set<StoredConfigKey> searchResults = new ConfigSearchMachine( storedConfiguration, locale ).search( searchTerm, searchDomains );
+
+        final TreeMap<String, Map<String, SearchResultItem>> returnData = new TreeMap<>();
 
         searchResults
                 .stream()
-                .filter( key -> key.getRecordType() == StoredConfigItemKey.RecordType.SETTING )
+                .filter( key -> key.getRecordType() == StoredConfigKey.RecordType.SETTING )
                 .forEach( recordID ->
                 {
                     final SearchResultItem item = SearchResultItem.fromKey( recordID, storedConfiguration, locale );
                     final String returnCategory = item.getNavigation();
 
-                    returnData.putIfAbsent( returnCategory, new ConcurrentHashMap<>() );
+                    returnData.computeIfAbsent( returnCategory, k -> new TreeMap<>() );
                     returnData.get( returnCategory ).put( recordID.getRecordID(), item );
                 } );
 
-        final TreeMap<String, Map<String, SearchResultItem>> outputMap = new TreeMap<>();
-        for ( final Map.Entry<String, Map<String, SearchResultItem>> entry : returnData.entrySet() )
-        {
-            outputMap.put( entry.getKey(), new TreeMap<>( entry.getValue() ) );
-        }
 
-        restResultBean = RestResultBean.withData( outputMap );
-        LOGGER.trace( pwmRequest, () -> "finished search operation with " + returnData.size() + " results", () -> TimeDuration.fromCurrent( startTime ) );
+        restResultBean = RestResultBean.withData( returnData );
         pwmRequest.outputJsonResult( restResultBean );
         return ProcessStatus.Halt;
     }
@@ -609,7 +650,7 @@ public class ConfigEditorServlet extends ControlledPwmServlet
         final StringBuilder output = new StringBuilder();
         output.append( "beginning SMS send process:\n" );
 
-        if ( !SmsQueueManager.smsIsConfigured( config ) )
+        if ( !SmsQueueManager.smsIsConfigured( config.getAppConfig() ) )
         {
             output.append( "SMS not configured." );
         }
@@ -676,7 +717,7 @@ public class ConfigEditorServlet extends ControlledPwmServlet
                 try
                 {
                     EmailService.sendEmailSynchronous( emailServer.get(), testDomainConfig, testEmailItem, macroRequest );
-                   output.append( "message delivered" );
+                    output.append( "message delivered" );
                 }
                 catch ( final PwmException e )
                 {
@@ -703,26 +744,28 @@ public class ConfigEditorServlet extends ControlledPwmServlet
     {
         final ConfigManagerBean configManagerBean = getBean( pwmRequest );
 
-        final String key = pwmRequest.readParameterAsString( "key" );
-        final PwmSetting setting = PwmSetting.forKey( key )
+        final String settingKey = pwmRequest.readParameterAsString( "key" );
+        final PwmSetting pwmSetting = PwmSetting.forKey( settingKey )
                 .orElseThrow( () -> new IllegalStateException( "invalid setting parameter value" ) );
+        final DomainID domainID = DomainStateReader.forRequest( pwmRequest ).getDomainID( pwmSetting );
         final int maxFileSize = Integer.parseInt( pwmRequest.getDomainConfig().readAppProperty( AppProperty.CONFIG_MAX_JDBC_JAR_SIZE ) );
 
-        if ( setting == PwmSetting.HTTPS_CERT )
+        if ( pwmSetting == PwmSetting.HTTPS_CERT )
         {
             ConfigEditorServletUtils.processHttpsCertificateUpload( pwmRequest, configManagerBean );
             return ProcessStatus.Halt;
         }
 
-        final FileValue fileValue = ConfigEditorServletUtils.readFileUploadToSettingValue( pwmRequest, maxFileSize );
-        if ( fileValue != null )
+        final Optional<FileValue> fileValue = ConfigEditorServletUtils.readFileUploadToSettingValue( pwmRequest, maxFileSize );
+        if ( fileValue.isPresent() )
         {
             final UserIdentity userIdentity = pwmRequest.isAuthenticated()
                     ? pwmRequest.getPwmSession().getUserInfo().getUserIdentity()
                     : null;
 
             final StoredConfigurationModifier modifier = StoredConfigurationModifier.newModifier( configManagerBean.getStoredConfiguration() );
-            modifier.writeSetting( setting, null, fileValue, userIdentity );
+            final StoredConfigKey key = StoredConfigKey.forSetting( pwmSetting, null, domainID );
+            modifier.writeSetting( key, fileValue.get(), userIdentity );
             configManagerBean.setStoredConfiguration( modifier.newStoredConfiguration() );
             pwmRequest.outputJsonResult( RestResultBean.forSuccessMessage( pwmRequest, Message.Success_Unknown ) );
         }
@@ -743,9 +786,11 @@ public class ConfigEditorServlet extends ControlledPwmServlet
         final boolean modifiedSettingsOnly = ( boolean ) inputParameters.get( "modifiedSettingsOnly" );
         final int level = ( int ) ( ( double ) inputParameters.get( "level" ) );
         final String filterText = ( String ) inputParameters.get( "text" );
+        final DomainStateReader domainStateReader = DomainStateReader.forRequest( pwmRequest );
 
         final NavTreeSettings navTreeSettings = NavTreeSettings.builder()
                 .modifiedSettingsOnly( modifiedSettingsOnly )
+                .domainManageMode( domainStateReader.getMode() )
                 .level( level )
                 .filterText( filterText )
                 .locale( pwmRequest.getLocale() )
@@ -848,12 +893,12 @@ public class ConfigEditorServlet extends ControlledPwmServlet
             throws IOException, ServletException, PwmUnrecoverableException
     {
         final ConfigManagerBean configManagerBean = getBean( pwmRequest );
-        final StoredConfigurationModifier modifier = StoredConfigurationModifier.newModifier( configManagerBean.getStoredConfiguration() );
         final Map<String, String> inputMap = pwmRequest.readBodyAsJsonStringMap( PwmHttpRequestWrapper.Flag.BypassValidation );
 
         final String settingKey = inputMap.get( "setting" );
         final PwmSetting setting = PwmSetting.forKey( settingKey )
                 .orElseThrow( () -> new IllegalStateException( "invalid setting parameter value" ) );
+        final DomainID domainID = DomainStateReader.forRequest( pwmRequest ).getDomainID( setting );
 
         final PwmSettingCategory category = PwmSettingCategory.forProfileSetting( setting )
                 .orElseThrow( () -> new IllegalStateException( "specified key does not associated with a profile-enabled category" ) );
@@ -863,15 +908,21 @@ public class ConfigEditorServlet extends ControlledPwmServlet
 
         try
         {
-            modifier.copyProfileID( category, sourceID, destinationID, pwmRequest.getUserInfoIfLoggedIn() );
+            final StoredConfiguration newStoredConfig = StoredConfigurationUtil.copyProfileID(
+                    configManagerBean.getStoredConfiguration(),
+                    domainID,
+                    category,
+                    sourceID,
+                    destinationID,
+                    pwmRequest.getUserInfoIfLoggedIn() );
             pwmRequest.outputJsonResult( RestResultBean.forSuccessMessage( pwmRequest, Message.Success_Unknown ) );
+            configManagerBean.setStoredConfiguration( newStoredConfig );
         }
         catch ( final PwmUnrecoverableException e )
         {
             pwmRequest.outputJsonResult( RestResultBean.fromError( e.getErrorInformation(), pwmRequest ) );
         }
 
-        configManagerBean.setStoredConfiguration( modifier.newStoredConfiguration() );
         return ProcessStatus.Halt;
     }
 

+ 58 - 57
server/src/main/java/password/pwm/http/servlet/configeditor/ConfigEditorServletUtils.java

@@ -22,11 +22,12 @@ package password.pwm.http.servlet.configeditor;
 
 import password.pwm.AppProperty;
 import password.pwm.PwmConstants;
+import password.pwm.bean.DomainID;
 import password.pwm.config.AppConfig;
 import password.pwm.config.DomainConfig;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.PwmSettingSyntax;
-import password.pwm.config.stored.StoredConfigItemKey;
+import password.pwm.config.stored.StoredConfigKey;
 import password.pwm.config.stored.StoredConfiguration;
 import password.pwm.config.stored.StoredConfigurationModifier;
 import password.pwm.config.stored.StoredConfigurationUtil;
@@ -63,6 +64,7 @@ import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
+import java.util.Optional;
 import java.util.ResourceBundle;
 import java.util.Set;
 import java.util.StringTokenizer;
@@ -73,7 +75,7 @@ public class ConfigEditorServletUtils
     private static final PwmLogger LOGGER = PwmLogger.forClass( ConfigEditorServletUtils.class );
 
 
-    public static FileValue readFileUploadToSettingValue(
+    public static Optional<FileValue> readFileUploadToSettingValue(
             final PwmRequest pwmRequest,
             final int maxFileSize
     )
@@ -89,14 +91,14 @@ public class ConfigEditorServletUtils
         {
             pwmRequest.outputJsonResult( RestResultBean.fromError( e.getErrorInformation(), pwmRequest ) );
             LOGGER.error( pwmRequest, () -> "error during file upload: " + e.getErrorInformation().toDebugStr() );
-            return null;
+            return Optional.empty();
         }
         catch ( final Throwable e )
         {
             final ErrorInformation errorInformation = new ErrorInformation( PwmError.ERROR_INTERNAL, "error during file upload: " + e.getMessage() );
             pwmRequest.outputJsonResult( RestResultBean.fromError( errorInformation, pwmRequest ) );
             LOGGER.error( pwmRequest, errorInformation );
-            return null;
+            return Optional.empty();
         }
 
         if ( fileUploads.containsKey( PwmConstants.PARAM_FILE_UPLOAD ) )
@@ -106,13 +108,13 @@ public class ConfigEditorServletUtils
             final Map<FileValue.FileInformation, FileValue.FileContent> newFileValueMap = new LinkedHashMap<>();
             newFileValueMap.put( new FileValue.FileInformation( uploadItem.getName(), uploadItem.getType() ), new FileValue.FileContent( uploadItem.getContent() ) );
 
-            return new FileValue( newFileValueMap );
+            return Optional.of( new FileValue( newFileValueMap ) );
         }
 
         final ErrorInformation errorInformation = new ErrorInformation( PwmError.ERROR_INTERNAL, "no file found in upload" );
         pwmRequest.outputJsonResult( RestResultBean.fromError( errorInformation, pwmRequest ) );
         LOGGER.error( pwmRequest, () -> "error during file upload: " + errorInformation.toDebugStr() );
-        return null;
+        return Optional.empty();
     }
 
     static void outputChangeLogData(
@@ -121,35 +123,35 @@ public class ConfigEditorServletUtils
             final Map<String, Object> outputMap
     )
     {
-            final Locale locale = pwmRequest.getLocale();
+        final Locale locale = pwmRequest.getLocale();
 
-            final Set<StoredConfigItemKey> changeLog = StoredConfigurationUtil.changedValues(
-                    pwmRequest.getPwmDomain().getConfig().getStoredConfiguration(),
-                    configManagerBean.getStoredConfiguration() );
+        final Set<StoredConfigKey> changeLog = StoredConfigurationUtil.changedValues(
+                pwmRequest.getPwmDomain().getConfig().getStoredConfiguration(),
+                configManagerBean.getStoredConfiguration() );
 
-            final Map<String, String> changeLogMap = StoredConfigurationUtil.makeDebugMap(
-                    configManagerBean.getStoredConfiguration(),
-                    changeLog,
-                    locale );
+        final Map<String, String> changeLogMap = StoredConfigurationUtil.makeDebugMap(
+                configManagerBean.getStoredConfiguration(),
+                changeLog.stream(),
+                locale );
 
-            final StringBuilder output = new StringBuilder();
-            if ( changeLogMap.isEmpty() )
-            {
-                output.append( "No setting changes." );
-            }
-            else
+        final StringBuilder output = new StringBuilder();
+        if ( changeLogMap.isEmpty() )
+        {
+            output.append( "No setting changes." );
+        }
+        else
+        {
+            for ( final Map.Entry<String, String> entry : changeLogMap.entrySet() )
             {
-                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>" );
-                }
+                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() );
+        }
+        outputMap.put( "html", output.toString() );
+        outputMap.put( "modified", !changeLog.isEmpty() );
 
     }
 
@@ -235,70 +237,69 @@ public class ConfigEditorServletUtils
     static ConfigEditorServlet.ReadSettingResponse handleReadSetting(
             final PwmRequest pwmRequest,
             final StoredConfiguration storedConfig,
-            final String key
+            final String settingKey
     )
             throws PwmUnrecoverableException
     {
         final ConfigEditorServlet.ReadSettingResponse.ReadSettingResponseBuilder builder = ConfigEditorServlet.ReadSettingResponse.builder();
-        final PwmSetting theSetting = PwmSetting.forKey( key )
+        final PwmSetting pwmSetting = PwmSetting.forKey( settingKey )
                 .orElseThrow( () -> new IllegalStateException( "invalid setting parameter value" ) );
 
         final Object returnValue;
-        final String profile = theSetting.getCategory().hasProfiles() ? pwmRequest.readParameterAsString( "profile" ) : null;
-        switch ( theSetting.getSyntax() )
+        final String profile = pwmSetting.getCategory().hasProfiles() ? pwmRequest.readParameterAsString( "profile" ) : null;
+        final DomainID domainID = DomainStateReader.forRequest( pwmRequest ).getDomainID( pwmSetting );
+
+        final StoredConfigKey key = StoredConfigKey.forSetting( pwmSetting, profile, domainID );
+        final boolean isDefault = StoredConfigurationUtil.isDefaultValue( storedConfig, key );
+
+        switch ( pwmSetting.getSyntax() )
         {
             case PASSWORD:
-                returnValue = Collections.singletonMap( "isDefault", storedConfig.isDefaultValue( theSetting, profile ) );
+                returnValue = Collections.singletonMap( "isDefault", isDefault );
                 break;
 
             case X509CERT:
-                returnValue = ( ( X509CertificateValue ) storedConfig.readSetting( theSetting, profile ) ).toInfoMap( true );
+                returnValue = ( ( X509CertificateValue ) StoredConfigurationUtil.getValueOrDefault( storedConfig, key ) ).toInfoMap( true );
                 break;
 
             case PRIVATE_KEY:
-                returnValue = ( ( PrivateKeyValue ) storedConfig.readSetting( theSetting, profile ) ).toInfoMap( true );
+                returnValue = ( ( PrivateKeyValue ) StoredConfigurationUtil.getValueOrDefault( storedConfig, key ) ).toInfoMap( true );
                 break;
 
             case ACTION:
-                returnValue = ( ( ActionValue ) storedConfig.readSetting( theSetting, profile ) ).toInfoMap();
+                returnValue = ( ( ActionValue ) StoredConfigurationUtil.getValueOrDefault( storedConfig, key ) ).toInfoMap();
                 break;
 
             case REMOTE_WEB_SERVICE:
-                returnValue = ( ( RemoteWebServiceValue ) storedConfig.readSetting( theSetting, profile ) ).toInfoMap();
+                returnValue = ( ( RemoteWebServiceValue ) StoredConfigurationUtil.getValueOrDefault( storedConfig, key ) ).toInfoMap();
                 break;
 
             case FILE:
-                returnValue = ( ( FileValue ) storedConfig.readSetting( theSetting, profile ) ).toInfoMap();
+                returnValue = ( ( FileValue ) StoredConfigurationUtil.getValueOrDefault( storedConfig, key ) ).toInfoMap();
                 break;
 
             default:
-                returnValue = storedConfig.readSetting( theSetting, profile ).toNativeObject();
+                returnValue = StoredConfigurationUtil.getValueOrDefault( storedConfig, key ).toNativeObject();
 
         }
         builder.value( returnValue );
 
-        builder.isDefault( storedConfig.isDefaultValue( theSetting, profile ) );
-        if ( theSetting.getSyntax() == PwmSettingSyntax.SELECT )
+        builder.isDefault( isDefault );
+        if ( pwmSetting.getSyntax() == PwmSettingSyntax.SELECT )
         {
-            builder.options( theSetting.getOptions() );
+            builder.options( pwmSetting.getOptions() );
         }
         {
-            final ValueMetaData settingMetaData = storedConfig.readSettingMetadata( theSetting, profile );
-            if ( settingMetaData != null )
+            final Optional<ValueMetaData> settingMetaData = storedConfig.readSettingMetadata( key );
+            if ( settingMetaData.isPresent() )
             {
-                if ( settingMetaData.getModifyDate() != null )
-                {
-                    builder.modifyTime( settingMetaData.getModifyDate() );
-                }
-                if ( settingMetaData.getUserIdentity() != null )
-                {
-                    builder.modifyUser( settingMetaData.getUserIdentity() );
-                }
+                builder.modifyTime( settingMetaData.map( ValueMetaData::getModifyDate ).orElse( null ) );
+                builder.modifyUser( settingMetaData.map( ValueMetaData::getUserIdentity ).orElse( null ) );
             }
         }
-        builder.key( key );
-        builder.category( theSetting.getCategory().toString() );
-        builder.syntax( theSetting.getSyntax().toString() );
+        builder.key( settingKey );
+        builder.category( pwmSetting.getCategory().toString() );
+        builder.syntax( pwmSetting.getSyntax().toString() );
         return builder.build();
     }
 

+ 28 - 0
server/src/main/java/password/pwm/http/servlet/configeditor/DomainManageMode.java

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

+ 160 - 0
server/src/main/java/password/pwm/http/servlet/configeditor/DomainStateReader.java

@@ -0,0 +1,160 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2020 The PWM Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package password.pwm.http.servlet.configeditor;
+
+import password.pwm.bean.DomainID;
+import password.pwm.config.AppConfig;
+import password.pwm.config.PwmSetting;
+import password.pwm.config.PwmSettingScope;
+import password.pwm.config.stored.StoredConfiguration;
+import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.http.PwmRequest;
+import password.pwm.http.PwmURL;
+import password.pwm.http.bean.ConfigManagerBean;
+import password.pwm.http.servlet.PwmServletDefinition;
+
+import java.util.Optional;
+import java.util.Set;
+
+class DomainStateReader
+{
+    private final PwmRequest pwmRequest;
+
+    private DomainStateReader( final PwmRequest pwmRequest )
+    {
+        this.pwmRequest = pwmRequest;
+    }
+
+    public static DomainStateReader forRequest( final PwmRequest pwmRequest )
+    {
+        return new DomainStateReader( pwmRequest );
+    }
+
+    public boolean isCorrectlyIndicated()
+            throws PwmUnrecoverableException
+    {
+        final DomainManageMode mode = getMode();
+        if ( mode == DomainManageMode.single )
+        {
+            return true;
+        }
+
+        return readDomainIdFromRequest().isPresent();
+    }
+
+    private AppConfig getAppConfig()
+            throws PwmUnrecoverableException
+    {
+        final ConfigManagerBean configManagerBean = ConfigEditorServlet.getBean( pwmRequest );
+        final StoredConfiguration storedConfiguration = configManagerBean.getStoredConfiguration();
+        return new AppConfig( storedConfiguration );
+    }
+
+    public DomainManageMode getMode()
+            throws PwmUnrecoverableException
+    {
+        if ( getAppConfig().getDomainIDs().size() < 2 )
+        {
+            return DomainManageMode.single;
+        }
+
+        final Optional<DomainID> optionalDomainID = readDomainIdFromRequest();
+        if ( optionalDomainID.isEmpty() )
+        {
+            return DomainManageMode.system;
+        }
+
+        if ( optionalDomainID.get().isSystem() )
+        {
+            return DomainManageMode.system;
+        }
+
+        return DomainManageMode.domain;
+    }
+
+    public DomainID getDomainID( final PwmSetting pwmSetting )
+            throws PwmUnrecoverableException
+    {
+        final DomainManageMode mode = getMode();
+        if ( mode == DomainManageMode.system )
+        {
+            return DomainID.systemId();
+        }
+
+        final Optional<DomainID> optionalDomainID = readDomainIdFromRequest();
+        if ( mode == DomainManageMode.domain )
+        {
+            if ( optionalDomainID.isPresent() )
+            {
+                return optionalDomainID.get();
+            }
+            throw new IllegalStateException( "invalid domain" );
+        }
+
+        if ( pwmSetting.getCategory().getScope() == PwmSettingScope.SYSTEM )
+        {
+            return DomainID.systemId();
+        }
+        return DomainID.create( pwmRequest.getAppConfig().getDomainIDs().stream().findFirst().orElseThrow() );
+    }
+
+    public Set<DomainID> searchIDs()
+            throws PwmUnrecoverableException
+    {
+        final DomainManageMode mode = getMode();
+        if ( mode == DomainManageMode.single )
+        {
+            return Set.of(
+                    DomainID.systemId(),
+                    DomainID.create( pwmRequest.getAppConfig().getDomainIDs().stream().findFirst().orElseThrow() ) );
+        }
+
+        if ( mode == DomainManageMode.system )
+        {
+            return Set.of( DomainID.systemId() );
+        }
+
+        return Set.of( readDomainIdFromRequest().orElseThrow() );
+    }
+
+    private Optional<DomainID> readDomainIdFromRequest()
+            throws PwmUnrecoverableException
+    {
+        final PwmURL pwmURL = pwmRequest.getURL();
+        String postPath = pwmURL.getPostServletPath( PwmServletDefinition.ConfigEditor );
+
+        while ( postPath.startsWith( "/" ) )
+        {
+            postPath = postPath.substring( 1 );
+        }
+
+        if ( DomainID.systemId().stringValue().equals( postPath ) )
+        {
+            return Optional.of( DomainID.systemId() );
+        }
+
+        if ( getAppConfig().getDomainIDs().contains( postPath ) )
+        {
+            return Optional.of( DomainID.create( postPath ) );
+        }
+        return Optional.empty();
+    }
+}

+ 5 - 4
server/src/main/java/password/pwm/http/servlet/configeditor/SearchResultItem.java

@@ -23,8 +23,9 @@ package password.pwm.http.servlet.configeditor;
 import com.google.gson.annotations.SerializedName;
 import lombok.Value;
 import password.pwm.config.PwmSetting;
-import password.pwm.config.stored.StoredConfigItemKey;
+import password.pwm.config.stored.StoredConfigKey;
 import password.pwm.config.stored.StoredConfiguration;
+import password.pwm.config.stored.StoredConfigurationUtil;
 
 import java.io.Serializable;
 import java.util.Locale;
@@ -41,16 +42,16 @@ class SearchResultItem implements Serializable
     private final String profile;
 
     static SearchResultItem fromKey(
-            final StoredConfigItemKey key,
+            final StoredConfigKey key,
             final StoredConfiguration storedConfiguration,
             final Locale locale )
     {
         final PwmSetting setting = key.toPwmSetting();
         return new SearchResultItem(
                 setting.getCategory().toString(),
-                storedConfiguration.readSetting( setting, key.getProfileID() ).toDebugString( locale ),
+                storedConfiguration.readStoredValue( key ).orElseThrow().toDebugString( locale ),
                 setting.getCategory().toMenuLocationDebug( key.getProfileID(), locale ),
-                storedConfiguration.isDefaultValue( setting, key.getProfileID() ),
+                StoredConfigurationUtil.isDefaultValue( storedConfiguration, key ),
                 key.getProfileID()
         );
     }

+ 41 - 21
server/src/main/java/password/pwm/http/servlet/configeditor/data/NavTreeDataMaker.java

@@ -20,15 +20,19 @@
 
 package password.pwm.http.servlet.configeditor.data;
 
-import password.pwm.PwmDomain;
 import password.pwm.PwmConstants;
+import password.pwm.PwmDomain;
 import password.pwm.PwmEnvironment;
 import password.pwm.config.DomainConfig;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.PwmSettingCategory;
+import password.pwm.config.PwmSettingScope;
+import password.pwm.config.stored.ConfigSearchMachine;
+import password.pwm.config.stored.StoredConfigKey;
 import password.pwm.config.stored.StoredConfiguration;
 import password.pwm.config.stored.StoredConfigurationUtil;
 import password.pwm.config.value.StoredValue;
+import password.pwm.http.servlet.configeditor.DomainManageMode;
 import password.pwm.i18n.Config;
 import password.pwm.i18n.PwmLocaleBundle;
 import password.pwm.util.i18n.LocaleHelper;
@@ -54,6 +58,9 @@ public class NavTreeDataMaker
     private static final PwmLogger LOGGER = PwmLogger.forClass( NavTreeDataMaker.class );
     private static final String ROOT_NODE_ID = "ROOT";
 
+    private static final String DISPLAY_TEXT_ID = "DISPLAY_TEXT";
+    private static final String DISPLAY_TEXT_NAME = "Display Text";
+
     public static List<NavTreeItem> makeNavTreeItems(
             final PwmDomain pwmDomain,
             final StoredConfiguration storedConfiguration,
@@ -75,8 +82,8 @@ public class NavTreeDataMaker
         NavTreeDataMaker.moveNavItemToTopOfList( PwmSettingCategory.NOTES.toString(), navigationData );
         NavTreeDataMaker.moveNavItemToTopOfList( PwmSettingCategory.TEMPLATES.toString(), navigationData );
         LOGGER.trace( () -> "generated " + navigationData.size()
-                + " navTreeItems for display menu with settings"
-                + JsonUtil.serialize( navTreeSettings ),
+                        + " navTreeItems for display menu with settings"
+                        + JsonUtil.serialize( navTreeSettings ),
                 () -> TimeDuration.fromCurrent( startTime ) );
         return Collections.unmodifiableList( navigationData );
     }
@@ -100,6 +107,11 @@ public class NavTreeDataMaker
         final int level = navTreeSettings.getLevel();
         final boolean modifiedSettingsOnly = navTreeSettings.isModifiedSettingsOnly();
 
+        if ( navTreeSettings.getDomainManageMode() != DomainManageMode.domain )
+        {
+            return Collections.emptyList();
+        }
+
         boolean includeDisplayText = false;
         if ( level >= 1 )
         {
@@ -121,7 +133,7 @@ public class NavTreeDataMaker
                         final NavTreeItem categoryInfo = NavTreeItem.builder()
                                 .id( localeBundle.toString() )
                                 .name( localeBundle.getTheClass().getSimpleName() )
-                                .parent( "DISPLAY_TEXT" )
+                                .parent( DISPLAY_TEXT_ID )
                                 .type ( NavTreeItem.NavItemType.displayText )
                                 .keys( outputKeys )
                                 .build();
@@ -135,8 +147,8 @@ public class NavTreeDataMaker
         if ( includeDisplayText )
         {
             final NavTreeItem categoryInfo = NavTreeItem.builder()
-                    .id( "DISPLAY_TEXT" )
-                    .name( "Display Text" )
+                    .id( DISPLAY_TEXT_ID )
+                    .name( DISPLAY_TEXT_NAME )
                     .type( NavTreeItem.NavItemType.navigation )
                     .parent( ROOT_NODE_ID )
                     .build();
@@ -287,14 +299,9 @@ public class NavTreeDataMaker
             final NavTreeSettings navTreeSettings
     )
     {
-        if ( category.isHidden() )
-        {
-            return false;
-        }
-
         if ( category == PwmSettingCategory.HTTPS_SERVER )
         {
-            if ( !pwmDomain.getPwmEnvironment().getFlags().contains( PwmEnvironment.ApplicationFlag.ManageHttps ) )
+            if ( !pwmDomain.getPwmApplication().getPwmEnvironment().getFlags().contains( PwmEnvironment.ApplicationFlag.ManageHttps ) )
             {
                 return false;
             }
@@ -310,7 +317,7 @@ public class NavTreeDataMaker
 
         for ( final PwmSetting setting : category.getSettings() )
         {
-            if ( settingMatcher( storedConfiguration, setting, profile, navTreeSettings ) )
+            if ( settingMatcher( pwmDomain, storedConfiguration, setting, profile, navTreeSettings ) )
             {
                 return true;
             }
@@ -320,23 +327,36 @@ public class NavTreeDataMaker
     }
 
     private static boolean settingMatcher(
+            final PwmDomain pwmDomain,
             final StoredConfiguration storedConfiguration,
             final PwmSetting setting,
             final String profileID,
             final NavTreeSettings navTreeSettings
     )
     {
-        if ( setting.isHidden() )
+        final StoredConfigKey storedConfigKey = StoredConfigKey.forSetting( setting, profileID, pwmDomain.getDomainID() );
+        final boolean valueIsDefault = StoredConfigurationUtil.isDefaultValue( storedConfiguration, storedConfigKey );
+
+        if ( setting.isHidden() && !valueIsDefault )
         {
             return false;
         }
 
-        if ( navTreeSettings.isModifiedSettingsOnly() )
+        final PwmSettingCategory settingCategory = setting.getCategory();
+        if ( navTreeSettings.getDomainManageMode() == DomainManageMode.system
+                && settingCategory.getScope() != PwmSettingScope.SYSTEM )
         {
-            if ( storedConfiguration.isDefaultValue( setting, profileID ) )
-            {
-                return false;
-            }
+            return false;
+        }
+        else if ( navTreeSettings.getDomainManageMode() == DomainManageMode.domain
+                && settingCategory.getScope() != PwmSettingScope.DOMAIN )
+        {
+            return false;
+        }
+
+        if ( navTreeSettings.isModifiedSettingsOnly() && valueIsDefault )
+        {
+            return false;
         }
 
         final int level = navTreeSettings.getLevel();
@@ -351,10 +371,10 @@ public class NavTreeDataMaker
         }
         else
         {
-            final StoredValue storedValue = storedConfiguration.readSetting( setting, profileID );
+            final StoredValue storedValue = storedConfiguration.readStoredValue( storedConfigKey ).orElseThrow();
             for ( final String term : StringUtil.whitespaceSplit( navTreeSettings.getFilterText() ) )
             {
-                if ( StoredConfigurationUtil.matchSetting( storedConfiguration, setting, storedValue, term, PwmConstants.DEFAULT_LOCALE ) )
+                if ( ConfigSearchMachine.matchSetting( storedConfiguration, setting, storedValue, term, PwmConstants.DEFAULT_LOCALE ) )
                 {
                     return true;
                 }

+ 3 - 0
server/src/main/java/password/pwm/http/servlet/configeditor/data/NavTreeSettings.java

@@ -23,6 +23,7 @@ package password.pwm.http.servlet.configeditor.data;
 import lombok.Builder;
 import lombok.Value;
 import password.pwm.PwmConstants;
+import password.pwm.http.servlet.configeditor.DomainManageMode;
 
 import java.io.Serializable;
 import java.util.Locale;
@@ -40,4 +41,6 @@ public class NavTreeSettings implements Serializable
 
     @Builder.Default
     private final Locale locale = PwmConstants.DEFAULT_LOCALE;
+
+    private final DomainManageMode domainManageMode;
 }

+ 3 - 3
server/src/main/java/password/pwm/http/servlet/configeditor/data/SettingDataMaker.java

@@ -21,6 +21,7 @@
 package password.pwm.http.servlet.configeditor.data;
 
 import password.pwm.PwmConstants;
+import password.pwm.bean.DomainID;
 import password.pwm.bean.SessionLabel;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.PwmSettingCategory;
@@ -28,7 +29,6 @@ import password.pwm.config.PwmSettingTemplateSet;
 import password.pwm.config.stored.StoredConfiguration;
 import password.pwm.config.stored.StoredConfigurationFactory;
 import password.pwm.config.stored.StoredConfigurationUtil;
-import password.pwm.config.value.ValueTypeConverter;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.i18n.PwmLocaleBundle;
 import password.pwm.util.java.TimeDuration;
@@ -91,8 +91,8 @@ public class SettingDataMaker
                         LinkedHashMap::new ) ) );
 
         final VarData varMap = VarData.builder()
-                .ldapProfileIds( ValueTypeConverter.valueToStringArray( storedConfiguration.readSetting( PwmSetting.LDAP_PROFILE_LIST, null ) ) )
-                .domainIds( StoredConfigurationUtil.domainList( storedConfiguration ) )
+                .ldapProfileIds( StoredConfigurationUtil.profilesForSetting( PwmSetting.LDAP_PROFILE_LIST, storedConfiguration ) )
+                .domainIds( StoredConfigurationUtil.domainList( storedConfiguration ).stream().map( DomainID::stringValue ).collect( Collectors.toList() ) )
                 .currentTemplate( templateSet )
                 .build();
 

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

@@ -20,8 +20,11 @@
 
 package password.pwm.http.servlet.configguide;
 
+import password.pwm.PwmConstants;
+import password.pwm.bean.DomainID;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.PwmSettingTemplate;
+import password.pwm.config.stored.StoredConfigKey;
 import password.pwm.config.stored.StoredConfiguration;
 import password.pwm.config.stored.StoredConfigurationFactory;
 import password.pwm.config.stored.StoredConfigurationModifier;
@@ -54,6 +57,7 @@ public class ConfigGuideForm
     private static final PwmLogger LOGGER = PwmLogger.forClass( ConfigGuideForm.class );
 
     static final String LDAP_PROFILE_NAME = "default";
+    static final DomainID DOMAIN_ID = PwmConstants.DOMAIN_ID_DEFAULT;
 
     public static Map<ConfigGuideFormField, String> defaultForm( )
     {
@@ -84,7 +88,7 @@ public class ConfigGuideForm
         if ( !StringUtil.isEmpty( formValue ) )
         {
             final PwmSettingTemplate template = PwmSettingTemplate.templateForString( formValue, type );
-            modifier.writeSetting( pwmSetting, null, new StringValue( template.toString() ), null );
+            modifySetting( modifier, pwmSetting, null, new StringValue( template.toString() ) );
         }
     }
 
@@ -95,65 +99,66 @@ public class ConfigGuideForm
     {
 
         final Map<ConfigGuideFormField, String> formData = configGuideBean.getFormData();
-        final StoredConfigurationModifier storedConfiguration = StoredConfigurationModifier.newModifier( StoredConfigurationFactory.newConfig() );
+        final StoredConfigurationModifier modifier = StoredConfigurationModifier.newModifier( StoredConfigurationFactory.newConfig() );
 
         // templates
         updateStoredConfigTemplateValue(
                 formData,
-                storedConfiguration,
+                modifier,
                 PwmSetting.TEMPLATE_LDAP,
                 ConfigGuideFormField.PARAM_TEMPLATE_LDAP,
                 PwmSettingTemplate.Type.LDAP_VENDOR );
 
         updateStoredConfigTemplateValue(
                 formData,
-                storedConfiguration,
+                modifier,
                 PwmSetting.TEMPLATE_STORAGE,
                 ConfigGuideFormField.PARAM_TEMPLATE_STORAGE,
                 PwmSettingTemplate.Type.STORAGE );
 
         updateStoredConfigTemplateValue(
                 formData,
-                storedConfiguration,
+                modifier,
                 PwmSetting.DB_VENDOR_TEMPLATE,
                 ConfigGuideFormField.PARAM_DB_VENDOR,
                 PwmSettingTemplate.Type.DB_VENDOR );
 
         // establish a default ldap profile
-        storedConfiguration.writeSetting( PwmSetting.LDAP_PROFILE_LIST, null, new StringArrayValue(
+
+        modifySetting( modifier, PwmSetting.LDAP_PROFILE_LIST, null, new StringArrayValue(
                 Collections.singletonList( LDAP_PROFILE_NAME )
-        ), null );
+        ) );
 
         {
             final String newLdapURI = figureLdapUrlFromFormConfig( formData );
             final StringArrayValue newValue = new StringArrayValue( Collections.singletonList( newLdapURI ) );
-            storedConfiguration.writeSetting( PwmSetting.LDAP_SERVER_URLS, LDAP_PROFILE_NAME, newValue, null );
+            modifySetting( modifier, PwmSetting.LDAP_SERVER_URLS, LDAP_PROFILE_NAME, newValue );
         }
 
         if ( configGuideBean.isUseConfiguredCerts() && !JavaHelper.isEmpty( configGuideBean.getLdapCertificates() ) )
         {
             final StoredValue newStoredValue = X509CertificateValue.fromX509( configGuideBean.getLdapCertificates() );
-            storedConfiguration.writeSetting( PwmSetting.LDAP_SERVER_CERTS, LDAP_PROFILE_NAME, newStoredValue, null );
+            modifySetting( modifier, PwmSetting.LDAP_SERVER_CERTS, LDAP_PROFILE_NAME, newStoredValue );
         }
 
         {
             // proxy/admin account
             final String ldapAdminDN = formData.get( ConfigGuideFormField.PARAM_LDAP_PROXY_DN );
             final String ldapAdminPW = formData.get( ConfigGuideFormField.PARAM_LDAP_PROXY_PW );
-            storedConfiguration.writeSetting( PwmSetting.LDAP_PROXY_USER_DN, LDAP_PROFILE_NAME, new StringValue( ldapAdminDN ), null );
+            modifySetting( modifier, PwmSetting.LDAP_PROXY_USER_DN, LDAP_PROFILE_NAME, new StringValue( ldapAdminDN ) );
             final PasswordValue passwordValue = new PasswordValue( PasswordData.forStringValue( ldapAdminPW ) );
-            storedConfiguration.writeSetting( PwmSetting.LDAP_PROXY_USER_PASSWORD, LDAP_PROFILE_NAME, passwordValue, null );
+            modifySetting( modifier, PwmSetting.LDAP_PROXY_USER_PASSWORD, LDAP_PROFILE_NAME, passwordValue );
         }
 
-        storedConfiguration.writeSetting( PwmSetting.LDAP_CONTEXTLESS_ROOT, LDAP_PROFILE_NAME, new StringArrayValue(
+        modifySetting( modifier, PwmSetting.LDAP_CONTEXTLESS_ROOT, LDAP_PROFILE_NAME, new StringArrayValue(
                 Collections.singletonList( formData.get( ConfigGuideFormField.PARAM_LDAP_CONTEXT ) )
-        ), null );
+        ) );
 
         {
             final String ldapContext = formData.get( ConfigGuideFormField.PARAM_LDAP_CONTEXT );
-            storedConfiguration.writeSetting( PwmSetting.LDAP_CONTEXTLESS_ROOT, LDAP_PROFILE_NAME, new StringArrayValue(
+            modifySetting( modifier, PwmSetting.LDAP_CONTEXTLESS_ROOT, LDAP_PROFILE_NAME, new StringArrayValue(
                     Collections.singletonList( ldapContext )
-            ), null );
+            ) );
         }
 
         {
@@ -161,11 +166,11 @@ public class ConfigGuideForm
             if ( testuserEnabled )
             {
                 final String ldapTestUserDN = formData.get( ConfigGuideFormField.PARAM_LDAP_TEST_USER );
-                storedConfiguration.writeSetting( PwmSetting.LDAP_TEST_USER_DN, LDAP_PROFILE_NAME, new StringValue( ldapTestUserDN ), null );
+                modifySetting( modifier, PwmSetting.LDAP_TEST_USER_DN, LDAP_PROFILE_NAME, new StringValue( ldapTestUserDN ) );
             }
             else
             {
-                storedConfiguration.resetSetting( PwmSetting.LDAP_TEST_USER_DN, LDAP_PROFILE_NAME, null );
+                modifier.resetSetting( StoredConfigKey.forSetting( PwmSetting.LDAP_TEST_USER_DN, LDAP_PROFILE_NAME, DOMAIN_ID ), null );
             }
         }
 
@@ -176,39 +181,39 @@ public class ConfigGuideForm
                     .type( UserPermissionType.ldapUser )
                     .ldapBase( userDN )
                     .build() );
-            storedConfiguration.writeSetting( PwmSetting.QUERY_MATCH_PWM_ADMIN, null, new UserPermissionValue( userPermissions ), null );
+            modifySetting( modifier, PwmSetting.QUERY_MATCH_PWM_ADMIN, null, new UserPermissionValue( userPermissions ) );
         }
 
         {
             // database
 
             final String dbClass = formData.get( ConfigGuideFormField.PARAM_DB_CLASSNAME );
-            storedConfiguration.writeSetting( PwmSetting.DATABASE_CLASS, null, new StringValue( dbClass ), null );
+            modifySetting( modifier, PwmSetting.DATABASE_CLASS, null, new StringValue( dbClass ) );
 
             final String dbUrl = formData.get( ConfigGuideFormField.PARAM_DB_CONNECT_URL );
-            storedConfiguration.writeSetting( PwmSetting.DATABASE_URL, null, new StringValue( dbUrl ), null );
+            modifySetting( modifier, PwmSetting.DATABASE_URL, null, new StringValue( dbUrl ) );
 
             final String dbUser = formData.get( ConfigGuideFormField.PARAM_DB_USERNAME );
-            storedConfiguration.writeSetting( PwmSetting.DATABASE_USERNAME, null, new StringValue( dbUser ), null );
+            modifySetting( modifier, PwmSetting.DATABASE_USERNAME, null, new StringValue( dbUser ) );
 
             final String dbPassword = formData.get( ConfigGuideFormField.PARAM_DB_PASSWORD );
             final PasswordValue passwordValue = new PasswordValue( PasswordData.forStringValue( dbPassword ) );
-            storedConfiguration.writeSetting( PwmSetting.DATABASE_PASSWORD, null, passwordValue, null );
+            modifySetting( modifier, PwmSetting.DATABASE_PASSWORD, null, passwordValue );
 
             final FileValue jdbcDriver = configGuideBean.getDatabaseDriver();
             if ( jdbcDriver != null )
             {
-                storedConfiguration.writeSetting( PwmSetting.DATABASE_JDBC_DRIVER, null, jdbcDriver, null );
+                modifySetting( modifier, PwmSetting.DATABASE_JDBC_DRIVER, null, jdbcDriver );
             }
         }
 
         {
             //telemetry
             final boolean telemetryEnabled = Boolean.parseBoolean( formData.get( ConfigGuideFormField.PARAM_TELEMETRY_ENABLE ) );
-            storedConfiguration.writeSetting( PwmSetting.PUBLISH_STATS_ENABLE, null, BooleanValue.of( telemetryEnabled ), null );
+            modifySetting( modifier, PwmSetting.PUBLISH_STATS_ENABLE, null, BooleanValue.of( telemetryEnabled ) );
 
             final String siteDescription = formData.get( ConfigGuideFormField.PARAM_TELEMETRY_DESCRIPTION );
-            storedConfiguration.writeSetting( PwmSetting.PUBLISH_STATS_SITE_DESCRIPTION, null, new StringValue( siteDescription ), null );
+            modifySetting( modifier, PwmSetting.PUBLISH_STATS_SITE_DESCRIPTION, null, new StringValue( siteDescription ) );
         }
 
         // cr policy
@@ -216,18 +221,26 @@ public class ConfigGuideForm
         {
             final String stringValue = formData.get( ConfigGuideFormField.CHALLENGE_RESPONSE_DATA );
             final StoredValue challengeValue = ChallengeValue.factory().fromJson( stringValue );
-            storedConfiguration.writeSetting( PwmSetting.CHALLENGE_RANDOM_CHALLENGES, "default", challengeValue, null );
+            modifySetting( modifier, PwmSetting.CHALLENGE_RANDOM_CHALLENGES, "default", challengeValue );
         }
 
         // set site url
-        storedConfiguration.writeSetting( PwmSetting.PWM_SITE_URL, null, new StringValue( formData.get( ConfigGuideFormField.PARAM_APP_SITEURL ) ), null );
+        modifySetting( modifier, PwmSetting.PWM_SITE_URL, null, new StringValue( formData.get( ConfigGuideFormField.PARAM_APP_SITEURL ) ) );
 
         // enable debug mode
-        storedConfiguration.writeSetting( PwmSetting.DISPLAY_SHOW_DETAILED_ERRORS, null, BooleanValue.of( true ), null );
+        modifySetting( modifier, PwmSetting.DISPLAY_SHOW_DETAILED_ERRORS, null, BooleanValue.of( true ) );
+
+        return modifier.newStoredConfiguration();
+    }
 
-        return storedConfiguration.newStoredConfiguration();
+    private static void modifySetting( final StoredConfigurationModifier modifier, final PwmSetting pwmSetting, final String profile, final StoredValue storedValue )
+            throws PwmUnrecoverableException
+    {
+        final StoredConfigKey key = StoredConfigKey.forSetting( pwmSetting, profile, DOMAIN_ID );
+        modifier.writeSetting( key, storedValue, null );
     }
 
+
     static String figureLdapUrlFromFormConfig( final Map<ConfigGuideFormField, String> ldapForm )
     {
         final String ldapServerIP = ldapForm.get( ConfigGuideFormField.PARAM_LDAP_HOST );

+ 37 - 26
server/src/main/java/password/pwm/http/servlet/configguide/ConfigGuideServlet.java

@@ -27,11 +27,13 @@ import password.pwm.PwmApplication;
 import password.pwm.PwmApplicationMode;
 import password.pwm.PwmConstants;
 import password.pwm.PwmDomain;
+import password.pwm.bean.DomainID;
 import password.pwm.config.AppConfig;
 import password.pwm.config.DomainConfig;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.profile.LdapProfile;
 import password.pwm.config.stored.ConfigurationProperty;
+import password.pwm.config.stored.StoredConfigKey;
 import password.pwm.config.stored.StoredConfiguration;
 import password.pwm.config.stored.StoredConfigurationFactory;
 import password.pwm.config.stored.StoredConfigurationModifier;
@@ -85,6 +87,7 @@ import java.util.HashMap;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 
 
 @WebServlet(
@@ -251,13 +254,14 @@ public class ConfigGuideServlet extends ControlledPwmServlet
                 try
                 {
                     ConfigGuideUtils.checkLdapServer( configGuideBean );
-                    records.add( HealthRecord.forMessage( HealthMessage.LDAP_OK ) );
+                    records.add( HealthRecord.forMessage( ConfigGuideForm.DOMAIN_ID, HealthMessage.LDAP_OK ) );
                 }
                 catch ( final Exception e )
                 {
                     final String ldapUrl = ldapProfile.readSettingAsStringArray( PwmSetting.LDAP_SERVER_URLS )
                             .stream().findFirst().orElse( "" );
                     records.add( HealthRecord.forMessage(
+                            ConfigGuideForm.DOMAIN_ID,
                             HealthMessage.LDAP_No_Connection,
                             ldapUrl,
                             e.getMessage() ) );
@@ -271,7 +275,7 @@ public class ConfigGuideServlet extends ControlledPwmServlet
                 records.addAll( ldapHealthChecker.checkBasicLdapConnectivity( tempDomain, tempDomainConfig, ldapProfile, false ) );
                 if ( records.isEmpty() )
                 {
-                    records.add( password.pwm.health.HealthRecord.forMessage( HealthMessage.LDAP_OK ) );
+                    records.add( password.pwm.health.HealthRecord.forMessage( ConfigGuideForm.DOMAIN_ID, HealthMessage.LDAP_OK ) );
                 }
             }
             break;
@@ -281,7 +285,9 @@ public class ConfigGuideServlet extends ControlledPwmServlet
                 records.addAll( ldapHealthChecker.checkBasicLdapConnectivity( tempDomain, tempDomainConfig, ldapProfile, true ) );
                 if ( records.isEmpty() )
                 {
-                    records.add( HealthRecord.forMessage( HealthMessage.Config_SettingOk,
+                    records.add( HealthRecord.forMessage(
+                            DomainID.systemId(),
+                            HealthMessage.Config_SettingOk,
                             PwmSetting.LDAP_CONTEXTLESS_ROOT.getLabel( pwmRequest.getLocale() ) ) );
                 }
             }
@@ -305,6 +311,7 @@ public class ConfigGuideServlet extends ControlledPwmServlet
                 {
                     records.add(
                             HealthRecord.forMessage(
+                                    DomainID.systemId(),
                                     HealthMessage.Config_AddTestUser,
                                     PwmSetting.LDAP_TEST_USER_DN.toMenuLocationDebug( ConfigGuideForm.LDAP_PROFILE_NAME, pwmRequest.getLocale() ) ) );
                 }
@@ -337,14 +344,15 @@ public class ConfigGuideServlet extends ControlledPwmServlet
     )
             throws IOException, PwmUnrecoverableException
     {
+        final DomainID domainID = PwmConstants.DOMAIN_ID_DEFAULT;
         final ConfigGuideBean configGuideBean = getBean( pwmRequest );
 
         StoredConfiguration storedConfiguration = ConfigGuideForm.generateStoredConfig( configGuideBean );
         if ( configGuideBean.getStep() == GuideStep.LDAP_PROXY )
         {
             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 );
+            modifier.resetSetting( StoredConfigKey.forSetting( PwmSetting.LDAP_PROXY_USER_DN, LDAP_PROFILE_KEY, domainID ), null );
+            modifier.resetSetting( StoredConfigKey.forSetting( PwmSetting.LDAP_PROXY_USER_PASSWORD, LDAP_PROFILE_KEY, domainID ), null );
             storedConfiguration = modifier.newStoredConfiguration();
         }
 
@@ -507,8 +515,8 @@ public class ConfigGuideServlet extends ControlledPwmServlet
         {
             final ConfigGuideBean configGuideBean = getBean( pwmRequest );
             final int maxFileSize = Integer.parseInt( pwmRequest.getDomainConfig().readAppProperty( AppProperty.CONFIG_MAX_JDBC_JAR_SIZE ) );
-            final FileValue fileValue = ConfigEditorServletUtils.readFileUploadToSettingValue( pwmRequest, maxFileSize );
-            configGuideBean.setDatabaseDriver( fileValue );
+            final Optional<FileValue> fileValue = ConfigEditorServletUtils.readFileUploadToSettingValue( pwmRequest, maxFileSize );
+            fileValue.ifPresent( configGuideBean::setDatabaseDriver );
             final RestResultBean restResultBean = RestResultBean.forSuccessMessage( pwmRequest, Message.Success_Unknown );
             pwmRequest.getPwmResponse().outputJsonResult( restResultBean );
         }
@@ -552,17 +560,19 @@ public class ConfigGuideServlet extends ControlledPwmServlet
         final ConfigGuideBean configGuideBean = getBean( pwmRequest );
         final StoredConfiguration storedConfiguration = ConfigGuideForm.generateStoredConfig( configGuideBean );
 
-        final String key = pwmRequest.readParameterAsString( "key" );
+        final String settingKey = pwmRequest.readParameterAsString( "key" );
         final LinkedHashMap<String, Object> returnMap = new LinkedHashMap<>();
-        final PwmSetting theSetting = PwmSetting.forKey( key )
+        final PwmSetting pwmSetting = PwmSetting.forKey( settingKey )
                 .orElseThrow( () -> new IllegalStateException( "invalid setting parameter value" ) );
 
+        final StoredConfigKey key = StoredConfigKey.forSetting( pwmSetting, profileID, PwmConstants.DOMAIN_ID_DEFAULT );
+
         final Object returnValue;
-        returnValue = storedConfiguration.readSetting( theSetting, profileID ).toNativeObject();
-        returnMap.put( "isDefault", storedConfiguration.isDefaultValue( theSetting, profileID ) );
-        returnMap.put( "key", key );
-        returnMap.put( "category", theSetting.getCategory().toString() );
-        returnMap.put( "syntax", theSetting.getSyntax().toString() );
+        returnValue = StoredConfigurationUtil.getValueOrDefault( storedConfiguration, key ).toNativeObject();
+        returnMap.put( "isDefault", StoredConfigurationUtil.isDefaultValue( storedConfiguration, key ) );
+        returnMap.put( "key", settingKey );
+        returnMap.put( "category", pwmSetting.getCategory().toString() );
+        returnMap.put( "syntax", pwmSetting.getSyntax().toString() );
 
         returnMap.put( "value", returnValue );
         pwmRequest.outputJsonResult( RestResultBean.withData( returnMap ) );
@@ -575,41 +585,42 @@ public class ConfigGuideServlet extends ControlledPwmServlet
             throws PwmUnrecoverableException, IOException
     {
         final String profileID = "default";
-        final String key = pwmRequest.readParameterAsString( "key" );
+        final String settingKey = pwmRequest.readParameterAsString( "key" );
         final String bodyString = pwmRequest.readRequestBodyAsString();
-        final PwmSetting setting = PwmSetting.forKey( key )
+        final PwmSetting pwmSetting = PwmSetting.forKey( settingKey )
                 .orElseThrow( () -> new IllegalStateException( "invalid setting parameter value" ) );
 
         final ConfigGuideBean configGuideBean = getBean( pwmRequest );
-        final StoredConfiguration storedConfigurationImpl = ConfigGuideForm.generateStoredConfig( configGuideBean );
+        final StoredConfiguration storedConfiguration = ConfigGuideForm.generateStoredConfig( configGuideBean );
 
+        final StoredConfigKey key = StoredConfigKey.forSetting( pwmSetting, profileID, PwmConstants.DOMAIN_ID_DEFAULT );
 
         final LinkedHashMap<String, Object> returnMap = new LinkedHashMap<>();
 
         try
         {
-            final StoredValue storedValue = ValueFactory.fromJson( setting, bodyString );
-            final List<String> errorMsgs = storedValue.validateValue( setting );
+            final StoredValue storedValue = ValueFactory.fromJson( pwmSetting, bodyString );
+            final List<String> errorMsgs = storedValue.validateValue( pwmSetting );
             if ( errorMsgs != null && !errorMsgs.isEmpty() )
             {
-                returnMap.put( "errorMessage", setting.getLabel( pwmRequest.getLocale() ) + ": " + errorMsgs.get( 0 ) );
+                returnMap.put( "errorMessage", pwmSetting.getLabel( pwmRequest.getLocale() ) + ": " + errorMsgs.get( 0 ) );
             }
 
-            if ( setting == PwmSetting.CHALLENGE_RANDOM_CHALLENGES )
+            if ( pwmSetting == PwmSetting.CHALLENGE_RANDOM_CHALLENGES )
             {
                 configGuideBean.getFormData().put( ConfigGuideFormField.CHALLENGE_RESPONSE_DATA, JsonUtil.serialize( (Serializable) storedValue.toNativeObject() ) );
             }
         }
         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 " + pwmSetting.toString() + ", error: " + e.getMessage();
             LOGGER.error( () -> errorMsg, e );
             throw new IllegalStateException( errorMsg, e );
         }
-        returnMap.put( "key", key );
-        returnMap.put( "category", setting.getCategory().toString() );
-        returnMap.put( "syntax", setting.getSyntax().toString() );
-        returnMap.put( "isDefault", storedConfigurationImpl.isDefaultValue( setting, profileID ) );
+        returnMap.put( "key", settingKey );
+        returnMap.put( "category", pwmSetting.getCategory().toString() );
+        returnMap.put( "syntax", pwmSetting.getSyntax().toString() );
+        returnMap.put( "isDefault", StoredConfigurationUtil.isDefaultValue( storedConfiguration, key ) );
         pwmRequest.outputJsonResult( RestResultBean.withData( returnMap ) );
 
 

+ 9 - 7
server/src/main/java/password/pwm/http/servlet/configguide/ConfigGuideUtils.java

@@ -35,6 +35,7 @@ import password.pwm.config.PwmSetting;
 import password.pwm.config.function.UserMatchViewerFunction;
 import password.pwm.config.stored.ConfigurationProperty;
 import password.pwm.config.stored.ConfigurationReader;
+import password.pwm.config.stored.StoredConfigKey;
 import password.pwm.config.stored.StoredConfiguration;
 import password.pwm.config.stored.StoredConfigurationFactory;
 import password.pwm.config.stored.StoredConfigurationModifier;
@@ -179,7 +180,8 @@ public class ConfigGuideUtils
 
         if ( configGuideBean.getStep() == GuideStep.LDAP_PERMISSIONS )
         {
-            final LDAPPermissionCalculator ldapPermissionCalculator = new LDAPPermissionCalculator( ConfigGuideForm.generateStoredConfig( configGuideBean ) );
+            final LDAPPermissionCalculator ldapPermissionCalculator = new LDAPPermissionCalculator(
+                    new AppConfig( ConfigGuideForm.generateStoredConfig( configGuideBean ) ).getDefaultDomainConfig() );
             pwmRequest.setAttribute( PwmRequestAttribute.LdapPermissionItems, ldapPermissionCalculator );
         }
 
@@ -284,32 +286,31 @@ public class ConfigGuideUtils
             final ConfigGuideBean configGuideBean = ConfigGuideServlet.getBean( pwmRequest );
             final Map<ConfigGuideFormField, String> form = configGuideBean.getFormData();
             final PwmApplication tempApplication = PwmApplication.createPwmApplication(
-                    pwmRequest.getPwmDomain().getPwmEnvironment().makeRuntimeInstance( new AppConfig( storedConfiguration ) ) );
+                    pwmRequest.getPwmApplication().getPwmEnvironment().makeRuntimeInstance( new AppConfig( storedConfiguration ) ) );
 
             final String adminDN = form.get( ConfigGuideFormField.PARAM_LDAP_ADMIN_USER );
-            final UserIdentity adminIdentity = UserIdentity.createUserIdentity( adminDN, PwmConstants.PROFILE_ID_DEFAULT, PwmConstants.DOMAIN_ID_PLACEHOLDER );
+            final UserIdentity adminIdentity = UserIdentity.create( adminDN, PwmConstants.PROFILE_ID_DEFAULT, PwmConstants.DOMAIN_ID_PLACEHOLDER );
 
             final UserMatchViewerFunction userMatchViewerFunction = new UserMatchViewerFunction();
             final Collection<UserIdentity> results = userMatchViewerFunction.discoverMatchingUsers(
                     tempApplication.getDefaultDomain(),
                     1,
                     storedConfiguration,
-                    PwmSetting.QUERY_MATCH_PWM_ADMIN,
-                    null
-            );
+                    StoredConfigKey.forSetting( PwmSetting.QUERY_MATCH_PWM_ADMIN, null, PwmConstants.DOMAIN_ID_DEFAULT ) );
 
             if ( !results.isEmpty() )
             {
                 final UserIdentity foundIdentity = results.iterator().next();
                 if ( foundIdentity.canonicalEquals( adminIdentity, tempApplication ) )
                 {
-                    records.add( HealthRecord.forMessage( HealthMessage.LDAP_AdminUserOk ) );
+                    records.add( HealthRecord.forMessage( ConfigGuideForm.DOMAIN_ID, HealthMessage.LDAP_AdminUserOk ) );
                 }
             }
         }
         catch ( final Exception e )
         {
             records.add( HealthRecord.forMessage(
+                    ConfigGuideForm.DOMAIN_ID,
                     HealthMessage.Config_SettingIssue,
                     PwmSetting.LDAP_PROXY_USER_DN.getLabel( pwmRequest.getLocale() ),
                     e.getMessage() ) );
@@ -318,6 +319,7 @@ public class ConfigGuideUtils
         if ( records.isEmpty() )
         {
             records.add( HealthRecord.forMessage(
+                    ConfigGuideForm.DOMAIN_ID,
                     HealthMessage.Config_SettingIssue,
                     PwmSetting.LDAP_PROXY_USER_DN.getLabel( pwmRequest.getLocale() ),
                     "User not found" ) );

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

@@ -26,7 +26,7 @@ import password.pwm.PwmConstants;
 import password.pwm.config.DomainConfig;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.PwmSettingSyntax;
-import password.pwm.config.stored.StoredConfigItemKey;
+import password.pwm.config.stored.StoredConfigKey;
 import password.pwm.config.stored.StoredConfiguration;
 import password.pwm.config.value.StoredValue;
 import password.pwm.config.value.ValueTypeConverter;
@@ -53,7 +53,8 @@ import java.util.Collection;
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.List;
-import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
 
 @WebServlet(
         name = "ConfigManagerCertificateServlet",
@@ -119,32 +120,29 @@ public class ConfigManagerCertificatesServlet extends AbstractPwmServlet
     List<CertificateDebugDataItem> makeCertificateDebugData( final DomainConfig domainConfig ) throws PwmUnrecoverableException
     {
         final StoredConfiguration storedConfiguration = domainConfig.getStoredConfiguration();
-        final Set<StoredConfigItemKey> modifiedSettings = storedConfiguration.modifiedItems();
+        final Stream<StoredConfigKey> modifiedSettings = StoredConfigKey.filterByType( StoredConfigKey.RecordType.SETTING, storedConfiguration.keys() );
 
         final List<CertificateDebugDataItem> certificateDebugDataItems = new ArrayList<>();
 
-        for ( final StoredConfigItemKey ref : modifiedSettings )
+        for ( final StoredConfigKey key : modifiedSettings.collect( Collectors.toList() ) )
         {
-            if ( ref.getRecordType() == StoredConfigItemKey.RecordType.SETTING )
+            final PwmSetting pwmSetting = key.toPwmSetting();
+            if ( pwmSetting.getSyntax() == PwmSettingSyntax.X509CERT )
             {
-                final PwmSetting pwmSetting = ref.toPwmSetting();
-                if ( pwmSetting.getSyntax() == PwmSettingSyntax.X509CERT )
-                {
-                    final StoredValue storedValue = storedConfiguration.readSetting( pwmSetting, ref.getProfileID() );
-                    final List<X509Certificate> certificates = ValueTypeConverter.valueToX509Certificates( pwmSetting, storedValue );
-                    certificateDebugDataItems.addAll( makeItems( pwmSetting, ref.getProfileID(), certificates ) );
-                }
-                else if ( pwmSetting.getSyntax() == PwmSettingSyntax.ACTION )
+                final StoredValue storedValue = storedConfiguration.readStoredValue( key ).orElseThrow();
+                final List<X509Certificate> certificates = ValueTypeConverter.valueToX509Certificates( pwmSetting, storedValue );
+                certificateDebugDataItems.addAll( makeItems( pwmSetting, key.getProfileID(), certificates ) );
+            }
+            else if ( pwmSetting.getSyntax() == PwmSettingSyntax.ACTION )
+            {
+                final StoredValue storedValue = storedConfiguration.readStoredValue( key ).orElseThrow();
+                final List<ActionConfiguration> actionConfigurations = ValueTypeConverter.valueToAction( pwmSetting, storedValue );
+                for ( final ActionConfiguration actionConfiguration : actionConfigurations )
                 {
-                    final StoredValue storedValue = storedConfiguration.readSetting( pwmSetting, ref.getProfileID() );
-                    final List<ActionConfiguration> actionConfigurations = ValueTypeConverter.valueToAction( pwmSetting, storedValue );
-                    for ( final ActionConfiguration actionConfiguration : actionConfigurations )
+                    for ( final ActionConfiguration.WebAction webAction : actionConfiguration.getWebActions() )
                     {
-                        for ( final ActionConfiguration.WebAction webAction : actionConfiguration.getWebActions() )
-                        {
-                            final List<X509Certificate> certificates = webAction.getCertificates();
-                            certificateDebugDataItems.addAll( makeItems( pwmSetting, ref.getProfileID(), certificates ) );
-                        }
+                        final List<X509Certificate> certificates = webAction.getCertificates();
+                        certificateDebugDataItems.addAll( makeItems( pwmSetting, key.getProfileID(), certificates ) );
                     }
                 }
             }

+ 5 - 3
server/src/main/java/password/pwm/http/servlet/configmanager/ConfigManagerLocalDBServlet.java

@@ -22,6 +22,7 @@ package password.pwm.http.servlet.configmanager;
 
 import org.apache.commons.fileupload.servlet.ServletFileUpload;
 import password.pwm.AppProperty;
+import password.pwm.PwmApplication;
 import password.pwm.PwmDomain;
 import password.pwm.PwmApplicationMode;
 import password.pwm.PwmConstants;
@@ -141,7 +142,7 @@ public class ConfigManagerLocalDBServlet extends AbstractPwmServlet
         resp.setHeader( HttpHeader.ContentDisposition, "attachment;filename=" + PwmConstants.PWM_APP_NAME + "-LocalDB.bak" );
         resp.setContentType( HttpContentType.octetstream );
         resp.setHeader( HttpHeader.ContentTransferEncoding, "binary" );
-        final LocalDBUtility localDBUtility = new LocalDBUtility( pwmRequest.getPwmDomain().getLocalDB() );
+        final LocalDBUtility localDBUtility = new LocalDBUtility( pwmRequest.getPwmApplication().getLocalDB() );
         try
         {
             final int bufferSize = Integer.parseInt( pwmRequest.getDomainConfig().readAppProperty( AppProperty.HTTP_DOWNLOAD_BUFFER_SIZE ) );
@@ -159,6 +160,7 @@ public class ConfigManagerLocalDBServlet extends AbstractPwmServlet
             throws IOException, ServletException, PwmUnrecoverableException
 
     {
+        final PwmApplication pwmApplication = pwmRequest.getPwmApplication();
         final PwmDomain pwmDomain = pwmRequest.getPwmDomain();
         final HttpServletRequest req = pwmRequest.getHttpServletRequest();
 
@@ -188,8 +190,8 @@ public class ConfigManagerLocalDBServlet extends AbstractPwmServlet
         LocalDB localDB = null;
         try
         {
-            localDB = pwmDomain.getLocalDB();
-            final File localDBLocation = pwmDomain.getLocalDB().getFileLocation();
+            localDB = pwmApplication.getLocalDB();
+            final File localDBLocation = pwmApplication.getLocalDB().getFileLocation();
             final DomainConfig domainConfig = pwmDomain.getConfig();
             contextManager.shutdown();
 

+ 5 - 6
server/src/main/java/password/pwm/http/servlet/configmanager/ConfigManagerLoginServlet.java

@@ -25,11 +25,10 @@ import com.novell.ldapchai.exception.ChaiUnavailableException;
 import lombok.Value;
 import password.pwm.AppAttribute;
 import password.pwm.AppProperty;
-import password.pwm.PwmDomain;
 import password.pwm.PwmApplicationMode;
 import password.pwm.PwmConstants;
+import password.pwm.PwmDomain;
 import password.pwm.bean.UserIdentity;
-import password.pwm.config.PwmSetting;
 import password.pwm.config.stored.ConfigurationProperty;
 import password.pwm.config.stored.ConfigurationReader;
 import password.pwm.config.stored.StoredConfiguration;
@@ -195,7 +194,7 @@ public class ConfigManagerLoginServlet extends AbstractPwmServlet
 
     private static ConfigLoginHistory readConfigLoginHistory( final PwmRequest pwmRequest )
     {
-       return pwmRequest.getPwmDomain().readAppAttribute( AppAttribute.CONFIG_LOGIN_HISTORY, ConfigLoginHistory.class )
+       return pwmRequest.getPwmApplication().readAppAttribute( AppAttribute.CONFIG_LOGIN_HISTORY, ConfigLoginHistory.class )
                .orElseGet( ConfigLoginHistory::new );
     }
 
@@ -209,7 +208,7 @@ public class ConfigManagerLoginServlet extends AbstractPwmServlet
         );
         final int maxEvents = Integer.parseInt( pwmRequest.getPwmDomain().getConfig().readAppProperty( AppProperty.CONFIG_HISTORY_MAX_ITEMS ) );
         configLoginHistory.addEvent( event, maxEvents, successful );
-        pwmRequest.getPwmDomain().writeAppAttribute( AppAttribute.CONFIG_LOGIN_HISTORY, configLoginHistory );
+        pwmRequest.getPwmApplication().writeAppAttribute( AppAttribute.CONFIG_LOGIN_HISTORY, configLoginHistory );
     }
 
     @Value
@@ -433,14 +432,14 @@ public class ConfigManagerLoginServlet extends AbstractPwmServlet
             return false;
         }
 
-        if ( pwmRequest.getDomainConfig().isDefaultValue( PwmSetting.PWM_SECURITY_KEY ) )
+        if ( pwmRequest.getAppConfig().getSecurityKey() == null )
         {
             LOGGER.debug( pwmRequest, () -> "security key not available, persistent login not possible." );
             return false;
         }
 
         final Optional<String> configPasswordHash = pwmRequest.getDomainConfig().getStoredConfiguration().readConfigProperty( ConfigurationProperty.PASSWORD_HASH );
-        if ( !configPasswordHash.isPresent() )
+        if ( configPasswordHash.isEmpty() )
         {
             LOGGER.debug( pwmRequest, () -> "config password is not present, persistent login not possible." );
             return false;

+ 7 - 5
server/src/main/java/password/pwm/http/servlet/configmanager/ConfigManagerServlet.java

@@ -27,6 +27,7 @@ import password.pwm.AppProperty;
 import password.pwm.Permission;
 import password.pwm.PwmDomain;
 import password.pwm.PwmConstants;
+import password.pwm.config.AppConfig;
 import password.pwm.config.stored.ConfigurationProperty;
 import password.pwm.config.stored.ConfigurationReader;
 import password.pwm.config.stored.StoredConfiguration;
@@ -195,7 +196,7 @@ public class ConfigManagerServlet extends AbstractPwmServlet
     {
         final ConfigurationReader configurationReader = pwmRequest.getContextManager().getConfigReader();
         pwmRequest.setAttribute( PwmRequestAttribute.PageTitle, LocaleHelper.getLocalizedMessage( Config.Title_ConfigManager, pwmRequest ) );
-        pwmRequest.setAttribute( PwmRequestAttribute.ApplicationPath, pwmRequest.getPwmDomain().getPwmEnvironment().getApplicationPath().getAbsolutePath() );
+        pwmRequest.setAttribute( PwmRequestAttribute.ApplicationPath, pwmRequest.getPwmApplication().getPwmEnvironment().getApplicationPath().getAbsolutePath() );
         pwmRequest.setAttribute( PwmRequestAttribute.ConfigFilename, configurationReader.getConfigFile().getAbsolutePath() );
         {
             final Instant lastModifyTime = configurationReader.getStoredConfiguration().modifyTime();
@@ -392,7 +393,7 @@ public class ConfigManagerServlet extends AbstractPwmServlet
             throws IOException, ServletException, PwmUnrecoverableException
     {
         final StoredConfiguration storedConfiguration = readCurrentConfiguration( pwmRequest );
-        final Map<String, String> outputMap = StoredConfigurationUtil.makeDebugMap( storedConfiguration, storedConfiguration.modifiedItems(), pwmRequest.getLocale() );
+        final Map<String, String> outputMap = StoredConfigurationUtil.makeDebugMap( storedConfiguration, storedConfiguration.keys(), pwmRequest.getLocale() );
         pwmRequest.setAttribute( PwmRequestAttribute.ConfigurationSummaryOutput, new LinkedHashMap<>( outputMap ) );
         pwmRequest.forwardToJsp( JspUrl.CONFIG_MANAGER_EDITOR_SUMMARY );
     }
@@ -401,7 +402,8 @@ public class ConfigManagerServlet extends AbstractPwmServlet
             throws IOException, ServletException, PwmUnrecoverableException
     {
         final StoredConfiguration storedConfiguration = readCurrentConfiguration( pwmRequest );
-        final LDAPPermissionCalculator ldapPermissionCalculator = new LDAPPermissionCalculator( storedConfiguration );
+        final AppConfig appConfig = new AppConfig( storedConfiguration );
+        final LDAPPermissionCalculator ldapPermissionCalculator = new LDAPPermissionCalculator( appConfig.getDefaultDomainConfig() );
         pwmRequest.setAttribute( PwmRequestAttribute.LdapPermissionItems, ldapPermissionCalculator );
         pwmRequest.forwardToJsp( JspUrl.CONFIG_MANAGER_PERMISSIONS );
     }
@@ -410,7 +412,7 @@ public class ConfigManagerServlet extends AbstractPwmServlet
     private void downloadPermissionReportCsv(
             final PwmRequest pwmRequest
     )
-            throws PwmUnrecoverableException, IOException, ChaiUnavailableException, ServletException
+            throws IOException, ServletException
     {
         pwmRequest.getPwmResponse().markAsDownload(
                 HttpContentType.csv,
@@ -422,7 +424,7 @@ public class ConfigManagerServlet extends AbstractPwmServlet
         {
 
             final StoredConfiguration storedConfiguration = readCurrentConfiguration( pwmRequest );
-            final LDAPPermissionCalculator ldapPermissionCalculator = new LDAPPermissionCalculator( storedConfiguration );
+            final LDAPPermissionCalculator ldapPermissionCalculator = new LDAPPermissionCalculator( pwmRequest.getDomainConfig() );
 
             for ( final LDAPPermissionCalculator.PermissionRecord permissionRecord : ldapPermissionCalculator.getPermissionRecords() )
             {

+ 4 - 6
server/src/main/java/password/pwm/http/servlet/configmanager/ConfigManagerWordlistServlet.java

@@ -23,7 +23,6 @@ package password.pwm.http.servlet.configmanager;
 import lombok.Builder;
 import lombok.Value;
 import org.apache.commons.fileupload.servlet.ServletFileUpload;
-import password.pwm.PwmDomain;
 import password.pwm.PwmConstants;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmError;
@@ -138,7 +137,6 @@ public class ConfigManagerWordlistServlet extends AbstractPwmServlet
             throws IOException, ServletException, PwmUnrecoverableException
 
     {
-        final PwmDomain pwmDomain = pwmRequest.getPwmDomain();
         final HttpServletRequest req = pwmRequest.getHttpServletRequest();
         final String wordlistTypeParam = pwmRequest.readParameterAsString( "wordlist" );
         final WordlistType wordlistType = WordlistType.valueOf( wordlistTypeParam );
@@ -163,7 +161,7 @@ public class ConfigManagerWordlistServlet extends AbstractPwmServlet
 
         try
         {
-            wordlistType.forType( pwmDomain ).populate( inputStream );
+            wordlistType.forType( pwmRequest.getPwmApplication() ).populate( inputStream );
         }
         catch ( final PwmUnrecoverableException e )
         {
@@ -193,7 +191,7 @@ public class ConfigManagerWordlistServlet extends AbstractPwmServlet
 
         try
         {
-            wordlistType.forType( pwmRequest.getPwmDomain() ).clear();
+            wordlistType.forType( pwmRequest.getPwmApplication() ).clear();
         }
         catch ( final Exception e )
         {
@@ -210,10 +208,10 @@ public class ConfigManagerWordlistServlet extends AbstractPwmServlet
 
         for ( final WordlistType wordlistType : WordlistType.values() )
         {
-            final Wordlist wordlist = wordlistType.forType( pwmRequest.getPwmDomain() );
+            final Wordlist wordlist = wordlistType.forType( pwmRequest.getPwmApplication() );
             final WordlistStatus wordlistStatus = wordlist.readWordlistStatus();
             final Wordlist.Activity activity = wordlist.getActivity();
-            final WordlistConfiguration wordlistConfiguration = wordlistType.forType( pwmRequest.getPwmDomain() ).getConfiguration();
+            final WordlistConfiguration wordlistConfiguration = wordlistType.forType( pwmRequest.getPwmApplication() ).getConfiguration();
 
             final WordlistDataBean.WordlistDataBeanBuilder builder = WordlistDataBean.builder();
             {

+ 43 - 45
server/src/main/java/password/pwm/http/servlet/configmanager/DebugItemGenerator.java

@@ -26,13 +26,14 @@ import org.apache.commons.csv.CSVPrinter;
 import org.apache.commons.io.output.CountingOutputStream;
 import password.pwm.AppProperty;
 import password.pwm.PwmAboutProperty;
+import password.pwm.PwmApplication;
 import password.pwm.PwmConstants;
 import password.pwm.PwmDomain;
 import password.pwm.bean.SessionLabel;
 import password.pwm.bean.UserIdentity;
 import password.pwm.config.AppConfig;
 import password.pwm.config.DomainConfig;
-import password.pwm.config.stored.StoredConfigItemKey;
+import password.pwm.config.stored.StoredConfigKey;
 import password.pwm.config.stored.StoredConfiguration;
 import password.pwm.config.stored.StoredConfigurationFactory;
 import password.pwm.config.stored.StoredConfigurationUtil;
@@ -157,7 +158,7 @@ public class DebugItemGenerator
         final String debugFileName = "zipDebugGeneration.csv";
         final Instant startTime = Instant.now();
         final DebugOutputBuilder debugGeneratorLogFile = new DebugOutputBuilder();
-        final DebugItemInput debugItemInput = new DebugItemInput( pwmDomain, sessionLabel, obfuscatedDomainConfig );
+        final DebugItemInput debugItemInput = new DebugItemInput( pwmDomain.getPwmApplication(), pwmDomain, sessionLabel, obfuscatedDomainConfig );
         debugGeneratorLogFile.appendLine( "beginning debug output" );
         final String pathPrefix = getFilenameBase() + "/";
 
@@ -222,15 +223,14 @@ public class DebugItemGenerator
             final StoredConfiguration storedConfiguration = debugItemInput.getObfuscatedDomainConfig().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 );
-                }
-            }
+            storedConfiguration.keys().filter( k ->  k.getRecordType() == StoredConfigKey.RecordType.SETTING )
+                    .forEach( k ->
+                    {
+                        final String key = k.getLabel( PwmConstants.DEFAULT_LOCALE );
+                        final StoredValue value = storedConfiguration.readStoredValue( k ).orElseThrow();
+                        outputObject.put( key, value );
+                    } );
+
 
             final String jsonOutput = JsonUtil.serializeMap( outputObject, JsonUtil.Flag.PrettyPrint );
             outputStream.write( jsonOutput.getBytes( PwmConstants.DEFAULT_CHARSET ) );
@@ -257,24 +257,20 @@ public class DebugItemGenerator
                     + PwmConstants.SERVLET_VERSION + "\n" );
             writer.write( "Timestamp: " + JavaHelper.toIsoDate( storedConfiguration.modifyTime() ) + "\n" );
             writer.write( "This file is " + PwmConstants.DEFAULT_CHARSET.displayName() + " encoded\n" );
-
             writer.write( "\n" );
-            final Set<StoredConfigItemKey> modifiedSettings = storedConfiguration.modifiedItems();
-
-            for ( final StoredConfigItemKey storedConfigItemKey : modifiedSettings )
-            {
-                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" );
 
-                }
-            }
+            storedConfiguration.keys()
+                    .filter( k -> k.isRecordType( StoredConfigKey.RecordType.SETTING  ) )
+                    .forEach( storedConfigKey ->
+                    {
+                        final String key = storedConfigKey.toPwmSetting().toMenuLocationDebug( storedConfigKey.getProfileID(), locale );
+                        final String value = storedConfiguration.readStoredValue( storedConfigKey ).orElseThrow().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 ) );
         }
@@ -313,7 +309,7 @@ public class DebugItemGenerator
         @Override
         public void outputItem( final DebugItemInput debugItemInput, final OutputStream outputStream ) throws Exception
         {
-            final Properties outputProps = new JavaHelper.SortedProperties();
+            final Properties outputProps = JavaHelper.newSortedProperties();
             final Map<PwmAboutProperty, String> infoBean = PwmAboutProperty.makeInfoBean( debugItemInput.getPwmDomain().getPwmApplication() );
             outputProps.putAll( PwmAboutProperty.toStringMap( infoBean ) );
             outputProps.store( outputStream, JavaHelper.toIsoDate( Instant.now() ) );
@@ -331,7 +327,7 @@ public class DebugItemGenerator
         @Override
         public void outputItem( final DebugItemInput debugItemInput, final OutputStream outputStream ) throws Exception
         {
-            final Properties outputProps = new JavaHelper.SortedProperties();
+            final Properties outputProps = JavaHelper.newSortedProperties();
             outputProps.putAll( System.getenv() );
             outputProps.store( outputStream, JavaHelper.toIsoDate( Instant.now() ) );
         }
@@ -350,7 +346,7 @@ public class DebugItemGenerator
         {
 
             final DomainConfig config = debugItemInput.getObfuscatedDomainConfig();
-            final Properties outputProps = new JavaHelper.SortedProperties();
+            final Properties outputProps = JavaHelper.newSortedProperties();
 
             for ( final AppProperty appProperty : AppProperty.values() )
             {
@@ -409,7 +405,7 @@ public class DebugItemGenerator
         {
             final Locale locale = PwmConstants.DEFAULT_LOCALE;
             final PwmDomain pwmDomain = debugItemInput.getPwmDomain();
-            final Set<HealthRecord> records = pwmDomain.getHealthMonitor().getHealthRecords();
+            final Set<HealthRecord> records = pwmDomain.getPwmApplication().getHealthMonitor().getHealthRecords();
 
             final List<HealthDebugInfo> outputInfos = new ArrayList<>();
             records.forEach( healthRecord -> outputInfos.add( new HealthDebugInfo( healthRecord, healthRecord.getDetail( locale,  debugItemInput.obfuscatedDomainConfig ) ) ) );
@@ -484,15 +480,15 @@ public class DebugItemGenerator
         @Override
         public void outputItem( final DebugItemInput debugItemInput, final OutputStream outputStream ) throws Exception
         {
-            final PwmDomain pwmDomain = debugItemInput.getPwmDomain();
-            final File applicationPath = pwmDomain.getPwmEnvironment().getApplicationPath();
+            final PwmApplication pwmApplication = debugItemInput.getPwmApplication();
+            final File applicationPath = pwmApplication.getPwmEnvironment().getApplicationPath();
             final List<File> interestedFiles = new ArrayList<>(  );
 
-            if ( pwmDomain.getPwmEnvironment().getContextManager() != null )
+            if ( pwmApplication.getPwmEnvironment().getContextManager() != null )
             {
                 try
                 {
-                    final File webInfPath = pwmDomain.getPwmEnvironment().getContextManager().locateWebInfFilePath();
+                    final File webInfPath = pwmApplication.getPwmEnvironment().getContextManager().locateWebInfFilePath();
                     if ( webInfPath != null && webInfPath.exists() )
                     {
                         final File servletRootPath = webInfPath.getParentFile();
@@ -611,7 +607,7 @@ public class DebugItemGenerator
                 .maxQueryTime( TimeDuration.of( maxSeconds, TimeDuration.Unit.SECONDS ) )
                 .build();
 
-        final LocalDBSearchResults searchResults = pwmDomain.getLocalDBLogger().readStoredEvents( searchParameters );
+        final LocalDBSearchResults searchResults = pwmDomain.getPwmApplication().getLocalDBLogger().readStoredEvents( searchParameters );
         final CountingOutputStream countingOutputStream = new CountingOutputStream( outputStream );
 
         final Writer writer = new OutputStreamWriter( countingOutputStream, PwmConstants.DEFAULT_CHARSET );
@@ -643,7 +639,7 @@ public class DebugItemGenerator
         {
 
             final StoredConfiguration storedConfiguration = debugItemInput.getObfuscatedDomainConfig().getStoredConfiguration();
-            final LDAPPermissionCalculator ldapPermissionCalculator = new LDAPPermissionCalculator( storedConfiguration );
+            final LDAPPermissionCalculator ldapPermissionCalculator = new LDAPPermissionCalculator( new AppConfig( storedConfiguration ).getDefaultDomainConfig() );
 
             final CSVPrinter csvPrinter = JavaHelper.makeCsvPrinter( outputStream );
             {
@@ -681,7 +677,8 @@ public class DebugItemGenerator
         @Override
         public void outputItem( final DebugItemInput debugItemInput, final OutputStream outputStream ) throws Exception
         {
-            final LocalDB localDB = debugItemInput.getPwmDomain().getLocalDB();
+            final PwmApplication pwmApplication = debugItemInput.getPwmApplication();
+            final LocalDB localDB = pwmApplication.getLocalDB();
             final Map<String, Serializable> serializableMap = localDB.debugInfo();
             outputStream.write( JsonUtil.serializeMap( serializableMap, JsonUtil.Flag.PrettyPrint ).getBytes( PwmConstants.DEFAULT_CHARSET ) );
         }
@@ -738,14 +735,14 @@ public class DebugItemGenerator
         @Override
         public String getFilename( )
         {
-            return "cluster-info.json";
+            return "node-info.json";
         }
 
         @Override
         public void outputItem( final DebugItemInput debugItemInput, final OutputStream outputStream ) throws Exception
         {
-            final PwmDomain pwmDomain = debugItemInput.getPwmDomain();
-            final NodeService nodeService = pwmDomain.getClusterService();
+            final PwmApplication pwmApplication = debugItemInput.getPwmApplication();
+            final NodeService nodeService = pwmApplication.getNodeService();
 
             final Map<String, Serializable> debugOutput = new LinkedHashMap<>();
             debugOutput.put( "status", nodeService.status() );
@@ -790,10 +787,10 @@ public class DebugItemGenerator
         @Override
         public void outputItem( final DebugItemInput debugItemInput, final OutputStream outputStream ) throws Exception
         {
-            final PwmDomain pwmDomain = debugItemInput.getPwmDomain();
-            final ContextManager contextManager = pwmDomain.getPwmEnvironment().getContextManager();
+            final PwmApplication pwmApplication = debugItemInput.getPwmApplication();
+            final ContextManager contextManager = pwmApplication.getPwmEnvironment().getContextManager();
             final AppDashboardData appDashboardData = AppDashboardData.makeDashboardData(
-                    pwmDomain,
+                    pwmApplication.getDefaultDomain(),
                     contextManager,
                     LOCALE
             );
@@ -894,7 +891,7 @@ public class DebugItemGenerator
         @Override
         public void outputItem( final DebugItemInput debugItemInput, final OutputStream outputStream ) throws Exception
         {
-            final Properties outputProps = new JavaHelper.SortedProperties();
+            final Properties outputProps = JavaHelper.newSortedProperties();
             outputProps.putAll( PwmConstants.BUILD_MANIFEST );
             outputProps.store( outputStream, JavaHelper.toIsoDate( Instant.now() ) );
         }
@@ -957,6 +954,7 @@ public class DebugItemGenerator
     @Value
     private static class DebugItemInput
     {
+        private final PwmApplication pwmApplication;
         private final PwmDomain pwmDomain;
         private final SessionLabel sessionLabel;
         private final DomainConfig obfuscatedDomainConfig;

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

@@ -32,8 +32,8 @@ import com.novell.ldapchai.exception.ChaiOperationException;
 import com.novell.ldapchai.exception.ChaiUnavailableException;
 import com.novell.ldapchai.exception.ChaiValidationException;
 import password.pwm.AppProperty;
-import password.pwm.PwmDomain;
 import password.pwm.PwmConstants;
+import password.pwm.PwmDomain;
 import password.pwm.bean.EmailItemBean;
 import password.pwm.bean.SessionLabel;
 import password.pwm.bean.TokenDestinationItem;
@@ -53,8 +53,8 @@ import password.pwm.error.PwmError;
 import password.pwm.error.PwmException;
 import password.pwm.error.PwmOperationalException;
 import password.pwm.error.PwmUnrecoverableException;
-import password.pwm.http.PwmRequestContext;
 import password.pwm.http.PwmRequest;
+import password.pwm.http.PwmRequestContext;
 import password.pwm.http.auth.HttpAuthRecord;
 import password.pwm.http.bean.ForgottenPasswordBean;
 import password.pwm.i18n.Message;
@@ -234,9 +234,10 @@ public class ForgottenPasswordUtil
                 return false;
             }
 
-            final HttpAuthRecord httpAuthRecord = pwmRequest.readEncryptedCookie( cookieName, HttpAuthRecord.class );
-            if ( httpAuthRecord != null )
+            final Optional<HttpAuthRecord> optionalHttpAuthRecord = pwmRequest.readEncryptedCookie( cookieName, HttpAuthRecord.class );
+            if ( optionalHttpAuthRecord.isPresent() )
             {
+                final HttpAuthRecord httpAuthRecord = optionalHttpAuthRecord.get();
                 if ( httpAuthRecord.getGuid() != null && !httpAuthRecord.getGuid().isEmpty() && httpAuthRecord.getGuid().equals( userGuid ) )
                 {
                     LOGGER.debug( pwmRequest, () -> "auth record cookie validated" );

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

@@ -44,6 +44,7 @@ import java.time.Instant;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Locale;
+import java.util.Optional;
 
 @Value
 @Builder
@@ -84,7 +85,8 @@ public class HelpdeskCardInfoBean implements Serializable
         builder.userKey( userIdentity.toObfuscatedKey( pwmRequest.getPwmApplication() ) );
 
         final PhotoDataReader photoDataReader = HelpdeskServlet.photoDataReader( pwmRequest, helpdeskProfile, userIdentity );
-        builder.photoURL( photoDataReader.figurePhotoURL( ) );
+        final Optional<String> optionalPhotoUrl = photoDataReader.figurePhotoURL();
+        optionalPhotoUrl.ifPresent( builder::photoURL );
 
         builder.displayNames( figureDisplayNames( pwmRequest.getPwmDomain(), helpdeskProfile, pwmRequest.getLabel(), userInfo ) );
 

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

@@ -88,7 +88,7 @@ import password.pwm.util.operations.OtpService;
 import password.pwm.util.password.PasswordUtility;
 import password.pwm.util.operations.otp.OTPUserRecord;
 import password.pwm.util.password.RandomPasswordGenerator;
-import password.pwm.util.secure.SecureService;
+import password.pwm.svc.secure.DomainSecureService;
 import password.pwm.ws.server.RestResultBean;
 import password.pwm.ws.server.rest.RestCheckPasswordServer;
 import password.pwm.ws.server.rest.RestRandomPasswordServer;
@@ -827,8 +827,8 @@ public class HelpdeskServlet extends ControlledPwmServlet
         tokenData.setToken( tokenKey );
         tokenData.setIssueDate( Instant.now() );
 
-        final SecureService secureService = pwmRequest.getPwmDomain().getSecureService();
-        helpdeskVerificationRequestBean.setTokenData( secureService.encryptObjectToString( tokenData ) );
+        final DomainSecureService domainSecureService = pwmRequest.getPwmDomain().getSecureService();
+        helpdeskVerificationRequestBean.setTokenData( domainSecureService.encryptObjectToString( tokenData ) );
 
         final RestResultBean restResultBean = RestResultBean.withData( helpdeskVerificationRequestBean );
         pwmRequest.outputJsonResult( restResultBean );
@@ -855,8 +855,8 @@ public class HelpdeskServlet extends ControlledPwmServlet
         );
         final String token = helpdeskVerificationRequestBean.getCode();
 
-        final SecureService secureService = pwmRequest.getPwmDomain().getSecureService();
-        final HelpdeskVerificationRequestBean.TokenData tokenData = secureService.decryptObject(
+        final DomainSecureService domainSecureService = pwmRequest.getPwmDomain().getSecureService();
+        final HelpdeskVerificationRequestBean.TokenData tokenData = domainSecureService.decryptObject(
                 helpdeskVerificationRequestBean.getTokenData(),
                 HelpdeskVerificationRequestBean.TokenData.class
         );

+ 5 - 5
server/src/main/java/password/pwm/http/servlet/newuser/NewUserFormUtils.java

@@ -35,7 +35,7 @@ import password.pwm.util.PasswordData;
 import password.pwm.util.form.FormUtility;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.logging.PwmLogger;
-import password.pwm.util.secure.SecureService;
+import password.pwm.svc.secure.DomainSecureService;
 
 import java.io.IOException;
 import java.util.HashMap;
@@ -102,7 +102,7 @@ class NewUserFormUtils
     )
             throws PwmOperationalException, PwmUnrecoverableException
     {
-        final SecureService secureService = pwmRequest.getPwmDomain().getSecureService();
+        final DomainSecureService domainSecureService = pwmRequest.getPwmDomain().getSecureService();
 
         final Map<String, String> payloadMap = tokenPayload.getData();
 
@@ -113,7 +113,7 @@ class NewUserFormUtils
 
         final String encryptedTokenData = payloadMap.get( NewUserServlet.TOKEN_PAYLOAD_ATTR );
 
-        return secureService.decryptObject( encryptedTokenData, NewUserTokenData.class );
+        return domainSecureService.decryptObject( encryptedTokenData, NewUserTokenData.class );
     }
 
     static Map<String, String> toTokenPayload(
@@ -129,8 +129,8 @@ class NewUserFormUtils
         newUserTokenData.setCurrentTokenField( newUserBean.getCurrentTokenField() );
         newUserTokenData.setCompletedTokenFields( newUserBean.getCompletedTokenFields() );
 
-        final SecureService secureService = pwmRequest.getPwmDomain().getSecureService();
-        final String encodedTokenData = secureService.encryptObjectToString( newUserTokenData );
+        final DomainSecureService domainSecureService = pwmRequest.getPwmDomain().getSecureService();
+        final String encodedTokenData = domainSecureService.encryptObjectToString( newUserTokenData );
         final Map<String, String> payloadMap = new HashMap<>();
         payloadMap.put( NewUserServlet.TOKEN_PAYLOAD_ATTR, encodedTokenData );
         return payloadMap;

Kaikkia tiedostoja ei voida näyttää, sillä liian monta tiedostoa muuttui tässä diffissä